Home > OS >  Visitor Pattern and Double Dispatch
Visitor Pattern and Double Dispatch

Time:02-26

I know this is well trodden territory but I have a specific question... I promise.

Having spent very little time in the statically typed, object oriented world, I recently came across this design pattern while reading Crafting Interpreters. While I understand this pattern allows for extensible behavior (methods) on a set of well defined existing types (classes), I don't quite get the characterization of it as a solution to the double dispatch problem, at least not without some additional assumptions. I see it more as making a tradeoff to the expression problem, where you trade closed types for open methods.

In most of the examples I've seen, you end up with something like this (shamelessly stolen from the awesome Clojure Design Patterns)

public interface Visitor {
  void visit(Activity a);
  void visit(Message m);
}

public class PDFVisitor implements Visitor {
  @Override
  public void visit(Activity a) {
    PDFExporter.export(a);
  }

  @Override
  public void visit(Message m) {
    PDFExporter.export(m);
  }
}

public abstract class Item {
  abstract void accept(Visitor v);
}

class Message extends Item {
  @Override
  void accept(Visitor v) {
    v.visit(this);
  }
}

class Activity extends Item {
  @Override
  void accept(Visitor v) {
    v.visit(this);
  }
}

Item i = new Message();
Visitor v = new PDFVisitor(); 
i.accept(v);

Here we have a set of types (Message and Activity) which are presumably closed or infrequently changing, and a set of methods which we want to be open for extension (the Visitors). Now where I get confused is that in most examples, they will show how you can implement other visitors without touching existing classes, e.g. something like this:

public class XMLVisitor implements Visitor {
  @Override
  public void visit(Activity a) {
    XMLExporter.export(a);
  }

  @Override
  public void visit(Message m) {
    XMLExporter.export(m);
  }
}

and then make some hand-waivy allusion to this being "double dispatch", which it is not. Here accept dynamically dispatches on the subtype of Item, but within accept the visit methods statically dispatch to the passed in visitor via method overloading. So we have single dispatch on Item, and then the "second" static dispatch within accept is really about selecting a behavior (method) to call with that Item type. There is only one "type" being dispatched on, not two - the second is a behavior.

When I think of double dispatch, I think of a function that dispatches on the type of two arguments. One behavior, two types.

export(Activity,XML)
export(Activity,PDF)
export(Message,XML)
export(Message,PDF)

To me this is subtly different to the visitor pattern which allows any set of behaviors to be extended to existing classes, but those behaviors don't necessarily all represent the same behavior like in the four export examples above - they can be anything. If we add another Visitor it may represent exporting, but it could just as well not. From the API layer you're just calling accept methods and trusting that the passed in Visitor does what you want, whatever that may be.

Am I looking at this the wrong way?

CodePudding user response:

The comment from @user207421 is spot on. If a language does not natively support double dispatch, no design pattern can alter the language to make it so. A pattern merely provides an alternative which may solve some of the problems that double dispatch would be applied to in another language.

People learning the Visitor Pattern who already have an understanding of double dispatch may be assisted by explanations such as, "Visitor solves a similar set of problems to those solved by double dispatch". Unfortunately, that explanation is often reduced to, "Visitor implements double dispatch" which is not true.

The fact you've recognized this means you have a solid understanding of both concepts already.

  • Related