1

I'm taking another look at the pros and cons of the Visitor design pattern for a talk.

There's obviously the issue that the Visitor has to be aware of every class it has specific handling for, but that's an inherent feature of the pattern.

The bigger problem I have is that, unless I've misunderstood something, every visited class has to be aware of its visitor, because it has to implement:

public void accept(Visitor visitor){
   visitor.visit(this);
}

So while you only need to change each class once to support visitation, this awareness is still an encapsulation leak over simple downcasting within the "no-longer-a-visitor" via, for example, instanceof in Java or dynamic_cast<> in C++; and obviously means you cannot apply the visitor pattern to 'visited' third-party classes that do not support it.

Is there a way to avoid having to make the Visited aware of the visitor? Or am I mistaken in thinking this is a flaw of the pattern?

5
  • Use an interface instead of a concrete class as a parameter to the accept() method? May 26, 2022 at 17:26
  • @RobertHarvey That seems implicit in the pattern (so you can have various visitors), you've still got a requirement on any "visited" to be aware of the interface and its visit() method, that still creates quite a tight coupling between the two halves of the pattern.
    – deworde
    May 26, 2022 at 21:12
  • No, it creates a tight coupling between the class and the visitor interface, which happens all the time in what are considered loosely-coupled architectures. Tightly coupling to an interface is perfectly acceptable; that's the whole point of having interfaces. May 26, 2022 at 21:16
  • Sure, but in a scenario where: 1) The Visitor is aware of every class it might conceivably visit 2) Every visited class must implement the same interface for the visitor to call I'm wondering about the advantages of the pattern over downcasting within the "no-longer-a-visitor".
    – deworde
    May 26, 2022 at 21:21
  • Well, why does a visited class need an accept method at all? All that accept method is doing is passing this back to the visitor. You can visit an object without ever handing the class a Visitor object; just hand the visitor a reference to the object. Of course, the Visitor now has to know something about the object to do anything meaningful, and there has to be some "actor" that knows about both the visitor and the object. A DI container might be of some help here. May 26, 2022 at 21:22

1 Answer 1

3

Visitor provides a way to treat all 'visited' (element) variants transparently in the same way, and the mechanism by which this is achieved requires all of the 'visited' classes to be aware of the abstract Visitor (or the Visitor interface). As with all of the patterns, there's room for variation, but this is a key aspect that enables the pattern to work and be utilized by client code as intended.

Besides being a way to implement double dispatch, Visitor is essentially a way to implement an abstract data type (ADT) in an OO language that happens to lack other, more convenient mechanisms.

Here, I'm using the term ADT in its strict sense. It could be described like this: you have a finite number of distinct data structures (that you expect you won't have to change or extend often), but you want to abstract that fact away and let clients treat them all as a single data type. What's needed is something to represent the type itself (in the Visitor pattern, that is the role of the abstract Element), and a bunch of operations on that type that can internally distinguish the underlying concrete data representations (in Visitor, operations are represented by various concrete Visitors).

It helps if you mentally translate
element.Accept(visitor)

to
abstractElement.Do(operation)

The idea is that client code has an abstract reference to an element (doesn't know its concrete type), and operates on it by selecting and/or instantiating a concrete visitor (passing operation parameters to the constructor). So, in a way, the whole elements + visitors construct is best thought of as representing one thing, one type and its operations. Every element ('visited') class is aware of the abstract Visitor type only, but that's done in order to provide implementation dispatch (the capability to call the correct method) on the received visitor instance (often, this relies on polymorphism + built-in overload resolution capabilities).

ADTs have complementary extensibility characteristics compared to objects of OOP. It's easy to add a new operation (just create a new Visitor derivative), but it's hard to add new representations (if you add a new element type, you have to update every visitor). Whereas in OOP, once you have several derivatives in use, it's hard to extend the interface with new operations, but it's easy to create new representations (you simply create a new derivative).

The different concrete data types relate to the same abstract concept, and are generally designed to work together in some way, and may have a recursive structure. So it might not make too much sense to apply visitors (operations) to arbitrary 3rd-party classes. You might try and create a wrapper, though, but it gets convoluted - and the Visitor pattern is already pretty convoluted to begin with.

You might also try and keep the ADT semantics, but break away from the Visitor pattern, by taking the dispatching responsibility out of the element; e.g., instead of
abstractElement.Do(operation)

you could have
do(abstractElement, operation)

('where do might simply type-check and downcast). Or you might move dispatching into the operations themselves. Note that, in order for client code to use them abstractly, you still need a way to represent all element types, including the 3rd-party ones, as a single abstract type (that is, a way to assign them interchangeably to variables of the same single type).

One thing that Visitor has going for it is that it uses the compiler as a bookkeeping device - for each new derived visitor, inheriting the Visitor interface forces you to implement the operation for all supported concrete element types (whereas if you are type-checking "manually", you have to be careful not to forget some). Also, since operations are represented by object instances (concrete visitors), you can pass them around and do other interesting things with them, but that's less of an advantage since today most OOP languages support closures + lambdas / delegates, and some have first-class functions.

6
  • This is a really good answer, but perhaps might be improved by a direct "yes, they do have to" at the top. I think "so it might not make too much sense to apply visitors (operations) to arbitrary 3rd-party classes" is a good point, although depending on the scenario, the 3rd-party classes might not be arbitrary.
    – deworde
    May 26, 2022 at 21:07
  • I guess one question I have is "what is an OO language that supports inheritance that doesn't support abstract data types?" Also, when you say "the capability to call the correct method", do you mean specifically "the correct method for the concrete type?"
    – deworde
    May 26, 2022 at 21:15
  • @deworde "what is an OO language that supports inheritance that doesn't support abstract data types?" - Basically all mainstream OOP languages don't have great support for ADTs, but they may have some support. I should have included this in the answer, but, I'm using the term ADT in the more strict comp-sci sense, which is not synonymous with with OOP's utilization of messages/inheritance/polymorphism, which is a different form of data abstraction, and not synonymous with classes. See this paper by Cook. 1/2 May 26, 2022 at 21:49
  • 1
    @deworde - '"perhaps might be improved by a direct "yes, they do have to"' - when I wrote the answer, the "interface" qualification was not in the title, so before the edit, I felt I couldn't answer directly without some elaboration. May 26, 2022 at 21:55
  • 1
    @deworde - '"the capability to call the correct method", do you mean specifically "the correct method for the concrete type?' - yes (each visitor provides several implementations of the same abstract operation, one for each concrete element type, so one has to be chosen based on the concrete type of the element for which the operation is invoked). "depending on the scenario, the 3rd-party classes might not be arbitrary" - by "arbitrary", I meant not specifically designed to be a part of your ADT May 26, 2022 at 22:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.