1

I've been studying design patterns, and I've come across the Visitor Pattern. It's a really strange thing to me; it seems as if its only purpose is to prevent the original classes in the hierarchy from needing to change even when a new responsibility is added to them. This happens simply by delegating those responsibilities to an implementation of an abstract visitor class (or interface) and effectively double dispatching to call the operating function of the correct visitor passing in the correct dynamic type.

I've looked into various other questions discussing when and why to use the Visitor Pattern, and all I've come across is that in OOP, it is expected that your number of classes grow rather than the number of methods per class, which the Visitor Pattern specializes in.

However, is this true? This sounds like a gross oversimplification of OOP to me. For reference, this answer came from here

I also read another (loosely) related question here, the answers to which imply that you should only create more classes and separate responsibilities where it makes sense. Does it really make sense to organize your code such that a "responsibility" of a class is defined merely by a single operation, regardless of the type being operated on (i.e. a visitor is responsible for the implementation of the operation for every relevant type), or does it make much more sense to define a responsibility as a set of operations which can be done on a single type (delegate those responsibilities to the types themselves)?

If the goal is merely to reduce lines of code in each class, why is this a legitimate goal? Line count alone does not have any direct implications regarding encapsulation, modularity, code organization, or general good OOP practices, right?

Edit: I now understand that patterns do not exist to serve OOP, and I now understand that there are cases where the Visitor pattern is the best solution. That being said, does it, or does it not, promote less sensible class hierarchies when used alongside OOP?

Alexander Guyer
  • 2,063
  • 1
  • 14
  • 20

3 Answers3

1

I think you might have misunderstood what the Visitor Pattern, and Patterns in general do and why we sometimes use them.

It isn't because of some OOP thing, some abstract concept or some hidden truth. It's quite the opposite. You are expected to model your problem in a proper object-oriented way, using objects with behavior, hidden state, all that. That is OO in a nutshell. You are not supposed to go out and look for patterns to apply randomly in your code, in fact that would be an anti-pattern in itself :)

Now, some people noticed, that there are certain small problems to which most of us tend to produce similar solutions. These are interesting, not because the solutions are so ground-breaking, or valuable on their own, but because it just spares us some time and brainpower not to solve these over and over again. These are the Patterns.

Back to your question: If you don't quite understand what the Visitor Pattern does or why it does that, or how/why one would use it, don't worry about it. If you come across a problem that needs it, you will notice! That is the situation where nothing else will quite work, and you will essentially have to use it. In all other situations you shouldn't use it!

Summary: No, we don't use the Visitor Pattern because of some OOP thing. Reducing lines it not a legitimate goal in itself. Yes, line count alone doesn't impact encapsulation, modularity, etc.

Robert Bräutigam
  • 7,514
  • 1
  • 20
  • 38
  • +1 This makes a lot of sense; design patterns don't exist to serve the OOP paradigm, but rather simply to solve problems. That being said, generally no single pattern will always break OOP paradigm, as otherwise the pattern would only make sense to use in other paradigms, making it a less generalizable solution. I believe I came up with a use case for the Visitor pattern this morning, but if I hadn't, I would heed your advice and wait for the problem that requires it rather than assuming it is never sensible :) – Alexander Guyer Mar 20 '19 at 15:18
  • On second thought, I'm still confused... Regardless of the actual purpose of the Visitor (and other) design patterns, it undoubtedly favors more modules over larger modules by defining a "responsibility" as a single operation on many possible dynamic types rather than the classical definition of a responsibility as a set of operations on a single type. So regardless of why we use design patterns, does it, or does it not, promote less sensible class hierarchies when used in OOP? – Alexander Guyer Mar 20 '19 at 15:25
  • 1
    Yes, the Visitor Pattern exports some functionality that *normally* would reside in the objects themselves, so it creates more classes instead of less bigger ones. However, if the Visitor Pattern is used, "normal" is not an option. Be it for architectural, organizational or for other reasons. *If* the other option *is* possible, then yes, using the Visitor Pattern is less sensible. – Robert Bräutigam Mar 20 '19 at 15:48
0

Object-oriented software is most maintainable over the long run when designed with SOLID object-oriented principles in mind. Design patterns are just named, reusable applications of these principles.

The Visitor pattern's purpose is NOT to prevent the original classes in the hierarchy from needing to change "even when a new responsibility is added to them." It is, rather, to set those classes up to be extended without being modified, which is a perfect example of the Open/Closed Principle.

But, to answer your specific questions:

  1. Correct. Reducing line count is not the goal. In fact, I can show you a class where using the Single Responsibility Principle will increase the number of lines of code. (See below.)

  2. There is nothing "alongside OOP" about the Visitor pattern. And, no. It does not promote less sensible class hierarchies. It allows you to add behavior to a class without modifying that class, which in the long run makes your software more maintainable.

Here's that example I promised (in Ruby with annotations at the end):

class OrderTotalCaluculator
  attr_reader :order, :tax_calculators

  def initialize(order, tax_calculators = [])
    @order = order
    @tax_calculators = tax_calculators
  end

  def total
    items_subtotal = order.items.sum { |item| item.price * item.quantity }
    taxes_subtotal = tax_calculators.sum { |calculator| calculator.calculate(items_subtotal) }

    items_subtotal + taxes_subtotal
  end
end

class CaliforniaStateSalesTaxCalculator
  def self.calculate(amount)
    amount * 0.075
  end
end

class SanFranciscoCitySalesTaxCalculator
  def self.calculate(amount)
    amount * 0.01
  end
end

total = OrderTotalCalculator.new(
  order, 
  [
    CaliforniaStateSalesTaxCalculator, 
    SanFranciscoCitySalesTaxCalculator
  ]
).total

Overall, this code -- which implements the Strategy pattern -- is pretty good. Every class has a single responsibility. Plus, if the United States were ever to create a national sales tax, the OrderTotalCalculator would not need to change.

But, if we look closer, the OrderTotalCalculator#total method has more than one responsibility. Here's what it would look like if we cleaned that up:

class OrderTotalCaluculator
  attr_reader :order, :tax_calculators

  def initialize(order, tax_calculators = [])
    @order = order
    @tax_calculators = tax_calculators
  end

  def total
    items_subtotal + taxes_subtotal
  end

  def items_subtotal
    @items_subtotal ||= order.items.sum { |item| item.price * item.quantity }
  end

  def taxes_subtotal
    tax_calculators.sum { |calculator| calculator.calculate(items_subtotal) }
  end
end

Now, literally every method has a single, well-defined, well-named responsibility. And, the class actually grew in size. So there you go.

Ruby Annotations:

  • @variable creates an instance variable named variable
  • attr_reader :variable syntactic sugar to create a getter method for the @variable instance variable
  • @variable ||= statement remembers the value of the statement in an instance variable so that the next time the method is called, the statement is not executed again
aridlehoover
  • 3,139
  • 1
  • 26
  • 24
-1

I'm going to start off by saying that I am neither an expert on patterns or OOP dogma. I am simply a developer that use patterns and OOP daily.

I tend to agree with almost any view that is less dogmatic, simply because adhering to dogmas sooner or later tends to land you in shit. However, I do agree with the fact that classes would be the thing to increase, rather than methods. This is of course an oversimplification and saying that a class may not increase methods is simply wrong. The only valid case for a dogmatic approach such as that would be if there could never occur any update to the object you are modeling with your class. Normally though, a class defines an object and the anticipation would be that the class represents a final object, not something that would change.

Chris
  • 89
  • 1
  • 1
  • 10