Questions about the Visitor pattern (sample in Java)
Asked Answered
A

4

9

I'm just trying to understand the main benefits of using the Visitor pattern.

Here's a sample Java implementation

///////////////////////////////////
// Interfaces
interface MamalVisitor {
    void visit(Mammal mammal);
}
interface MammalVisitable {
    public void accept(MamalVisitor visitor);
}
interface Mammal extends MammalVisitable {
    public int getLegsNumber();
}
///////////////////////////////////


///////////////////////////////////
// Model
class Human implements Mammal {
    @Override
    public void accept(MamalVisitor visitor) {  visitor.visit(this);  }
    @Override
    public int getLegsNumber() { return 2; }
}
//PIRATE HAS A WOOD LEG
class Pirate extends Human { 
    @Override
    public int getLegsNumber() { return 1; }
    public int getWoodLegNumber() { return 1; }
}
class Dog implements Mammal {
    @Override
    public void accept(MamalVisitor visitor) {  visitor.visit(this);  }
    @Override
    public int getLegsNumber() { return 4; }
}
///////////////////////////////////


///////////////////////////////////
class LegCounterVisitor implements MamalVisitor {
    private int legNumber = 0;
    @Override
    public void visit(Mammal mammal) {   legNumber += mammal.getLegsNumber();   }
    public int getLegNumber() { return legNumber; }
}
class WoodLegCounterVisitor implements MamalVisitor {
    private int woodLegNumber = 0;
    @Override
    public void visit(Mammal mammal) {   
        // perhaps bad but i'm lazy
        if ( mammal instanceof Pirate ) {
            woodLegNumber += ((Pirate) mammal).getWoodLegNumber();
        }
    }
    public int getWoodLegNumber() { return woodLegNumber; }
}
///////////////////////////////////



///////////////////////////////////
public class Main {
    public static void main(String[] args) {
        // Create a list with 9 mammal legs and 3 pirate woodlegs
        List<Mammal> mammalList = Arrays.asList(
                new Pirate(),
                new Dog(),
                new Human(),
                new Pirate(),
                new Pirate()
        );

        ///////////////////////////////////
        // The visitor method
        LegCounterVisitor legCounterVisitor = new LegCounterVisitor();
        WoodLegCounterVisitor woodLegCounterVisitor = new WoodLegCounterVisitor();
        for ( Mammal mammal : mammalList ) {
            mammal.accept(legCounterVisitor);
            mammal.accept(woodLegCounterVisitor);
            // why not also using:
            // legCounterVisitor.visit(mammal);
            // woodLegCounterVisitor.visit(mammal);
        }
        System.out.println("Number of legs:" + legCounterVisitor.getLegNumber());
        System.out.println("Number of wood legs:" + woodLegCounterVisitor.getWoodLegNumber());

        ///////////////////////////////////
        // The standart method
        int legNumber = 0;
        int woodLegNumber = 0;
        for ( Mammal mammal : mammalList ) {
            legNumber += mammal.getLegsNumber();
            // perhaps bad but i'm lazy
            if ( mammal instanceof Pirate ) {
                woodLegNumber += ((Pirate) mammal).getWoodLegNumber();
            }
        }
        System.out.println("Number of legs:" + legNumber);
        System.out.println("Number of wood legs:" + woodLegNumber);
    }
}
///////////////////////////////////

I just wonder what is the main advantage for this case to use such a pattern. We can also iterate over the collection and get almost the same thing, except we don't have to handle a new interface and add boilerplate code to the model...

With Apache Commons, or a functional language, the classic way seems to do some map/reduce operation (map to the leg numbers and reduce with addition) and it's quite easy...

I also wonder why we use

        mammal.accept(legCounterVisitor);
        mammal.accept(woodLegCounterVisitor);

and not

        legCounterVisitor.visit(mammal);
        woodLegCounterVisitor.visit(mammal);

The 2nd option seems to remove the accept(...) method on the model part.

In many samples i've found, it seems that they don't use a common interface for model objects. I added it because like that i just have to add one visit(Mammal) method, instead of implementing one for each Mammal. Is it good to make all my objects implement Mammal? (i guess sometimes it's just not possible anyway). Is it still a Visitor pattern like that?

So my questions are: - do you see any advantage in my exemple for using visitors? - if not, can you provide some concrete usecases for visitors? - are visitors useful in functional programming languages

The only exemple that i found relevant for this pattern is the case of a pretty printer, where you keep in the visitor's state the offset to use during the visit of different nodes (for displaying an XML tree for exemple)

Atalie answered 6/6, 2011 at 12:58 Comment(1)
I believe it's hard to demonstrate Visitor Pattern with several lines since it's meant for more complicated situation. I provided a high-levle understanding as below and hope it helps for the new to Visitor Pattern, it's not that hard.Hulahula
C
4

Visitor pattern is a fancy switch case / pattern matching system to facilitate graph traversal.

As typical functional languages offer pattern matching and efficient ways to traverse graphs, interest is much more limited.

Even in JAVA, with instanceof or using enum, a visitor is more of a fancy way to perform things than a generic solution as many algorithms will not fit well into it.

Courtship answered 6/6, 2011 at 13:47 Comment(0)
D
7

The visitor pattern is just double dispatch.

I'm not sure I agree with your implementation of a visitor. I'd implement something like this:

interface MammalVisitor {
    void visit(Pirate pirate);
    void visit(Human human);
    void visit(Dog dog);
}

// Basic visitor provides no-op behaviour for everything.
abstract class MammalAdapter implements MammalVisitor {
    void visit(Pirate pirate) {};
    void visit(Human human) {};
    void visit(Dog dog) {};
}

And then the implementation would become cleaner:

// We only want to provide specific behaviour for pirates
class WoodLegCounterVisitor extends MammalAdaptor {
    private int woodLegNumber = 0;
    @Override
    public void visit(Pirate pirate) {   
        woodLegNumber += pirate.getWoodLegNumber();
    }

    public int getWoodLegNumber() { return woodLegNumber; }
}

In answer to your actual question, the main advantage of using the visitor is avoiding the need to do the "instanceof" checks. It gives you the ability to separate out the logic for processing a hierarchy into a separate class. It also gives you the ability to add new behaviour without changing the original classes.

Dross answered 6/6, 2011 at 13:5 Comment(2)
Actually it seems almost the same for me here. I used instanceof but i guess in real complex visitor patterns we'd rather avoid using instanceof...Atalie
Actually i used instanceof because i was lazy, but i could have done the same without any instanceof, in both visitor / standart way...Atalie
C
4

Visitor pattern is a fancy switch case / pattern matching system to facilitate graph traversal.

As typical functional languages offer pattern matching and efficient ways to traverse graphs, interest is much more limited.

Even in JAVA, with instanceof or using enum, a visitor is more of a fancy way to perform things than a generic solution as many algorithms will not fit well into it.

Courtship answered 6/6, 2011 at 13:47 Comment(0)
S
2

The purpose of the Visitor Pattern is to separate the object structure (in your case, Mammal) from the algorithm (in your case, the counter Leg counter algorithm).

The whole idea is that your object (mostly in java, JavaBeans) doesn't change its structure at all, and only a new virtual function is introduced to introduce a new algorithm.

Unlike Jeff Foster's implementation, One can use Generics to make code easier. This brings specificity to your visitor, e.g.:

public interface MammalVisitor<T extends Mammal> {

    public void visit(T mammal);
}

public class LegCounterVisitor implements MamalVisitor<Human> {
    private int legNumber = 0;
    @Override
    public void visit(Human mammal) {   legNumber += mammal.getLegsNumber();   }
    public int getLegNumber() { return legNumber; }
}

public class WoodLegCounterVisitor implements MamalVisitor<Pirate> {
    private int legNumber = 0;
    @Override
    public void visit(Pirate mammal) {legNumber += mammal.getWoodLegNumber();   }
    public int getLegNumber() { return legNumber; }
}
Sore answered 6/6, 2011 at 13:12 Comment(13)
How would you have something that sums wooden legs and normal legs in your example?Dross
@Jeff Forster, Simple, since Pirate class has a getWoodLegNumber() method, the WoodLegCounterVisitor will only visit anything that is a Pirate.Sore
What I was trying to get at was that your visitor implementation cannot have behaviour for both pirates and humans (without type casting) since you only have one visit method. I was under the impression that getting rid of type casting was a good thing.Dross
That is irrelevant as pirates has one leg (from the OP question). So, LegCounterVisitor caters for both humans and pirates (who are humans). No typecasting is needed in this regard. As for your case, your code will have many unimplemented method, which is a waste in my regards.Sore
I'm probably not getting my point across clearly. Your visitor has a single visit method. This is not good because in general you want different behaviour for different objects. Perhaps you're just pointing out a way of shortening the code in this specific case, but in general having a single method for your visitor is not a good thing. I do take your point about many unimplemented methods, that's why typically there is a "VisitorAdapter" class which provides null implementations (similar to the Swing functions)Dross
I did understand your point. What I'm try to say is that this example is a simple case visitor pattern that isn't necessary to incorporate different behaviours in a visitor as each object has its own visitor. One could use Abstract Factory for each mammal object to retrieve an algorithm for calculating legs.Sore
I agree. Anyway i though this was strange, but with the code you give, i get a runtime exception: Exception in thread "main" java.lang.ClassCastException: Dog cannot be cast to Pirate at WoodLegCounterVisitor.visit(Main.java:1) at Dog.accept(Main.java:35) at Main.main(Main.java:76)Atalie
What type of RuntimeException are you getting?Sore
The problem with your code is that i can't iterate with the wood leg visitor over a list of Mammal since only the Pirate is visitable with it... Actually i was also surprized it compile successfully... I guess it has something to do with type erasureAtalie
And what would be the type to use on accept method? accept(MamalVisitor<Mammal> visitor)?Atalie
@Sebastien Lorber, the reason why it compiled is because I set the Generics type T to extend Mammal, and no, for human, you can specify accept(MammalVisitor<Mammal> visitor), it will still be valid passing a pirate since a pirate is a human.Sore
Down voted for the reason Jeff Forster mentioned in his last comment. I'm just learning about Visitors and this is a confusing implementation of this pattern as it only has a single visit method.Luehrmann
Whatever but it seems to be better.Greed
H
1

It seems all the answers above provided a detailed explanation at the data structure/algorithm level, however, a high-level/conceptual understanding is still lacking. And the lacking of conceptual understanding may lead to misunderstanding that Visitor Pattern is nothing more than a fancy style.

IMHO, let's consider that all interactions among your domain entities could be reduced to one-to-one interactions. The Visitor Pattern is suitable for such a situation where one entity maintains a stable type(s), while another entity has multiple specializations and is dynamically expanding. Under these circumstances, the stable entity can be modeled as the Element (or Visitable), and the dynamic one as the Visitor.

For a more detailed explanation regarding Double Dispatch, consider this assumption: there is an interaction between Class A and Class B, with m and n specializations, respectively. We must implement all m*n methods for each combination. Let's assume m < n, so we'll denote Class A as Element and Class B as Visitor.

  • To begin with, we need to design a route that branches into m*n paths. The Element.accept method makes the first selection, while the Visitor.visit method makes the second.
  • It's clear that the Element should be more stable (meaning fewer in number), like it sits at a higher level in the decision tree.
  • In this 3-level tree, the top level represents the starting point, the second level corresponds to each Element specialization, and the third level represents multiple Visitors under each Element implementation. The leaves of the tree symbolize the methods for the corresponding Element and Visitor.
  • To add a new specialization to the Element, every Visitor must implement a new method across different code files, resulting in low cohesion. Conversely, adding a new Visitor only requires implementing all methods in a single file, leaving other files untouched, which is highly decoupled.

I believe that out of these context, any delivery is hard to be judged as whether reasonable Visitor Pattern since any interaction could be implemented as A.accept and B.visit.

A key distinction in the design of the code is that the visitor always knows the exact type of the visitable, but not the other way around.

Hope this helps。

Hulahula answered 19/9, 2023 at 3:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.