Apparently, I've been doing an "unorthodoxed" Visitor pattern my whole programmer life.
Yes, I dispatch to a concrete composite element visit method from the Visitor's Visit
method.
I think this is how I learned it, but now I can't find any examples of it, and the source I learned it from is gone.
Now, faced with overwhelming evidence that the concrete element dispatch goes into the composite element's Accept
method, I'm wondering if the way I had been doing it has at least some advantage. The two advantages that appear to me are:
Visit
to handle them.Here is the basic Composite/Visitor model:
// "Unorthodox" version
public class BaseVisitor
{
public virtual void Visit(CompositeElement e)
{
if(e is Foo)
{
VisitFoo((Foo)e);
}
else if(e is Bar)
{
VisitBar((Bar)e);
}
else
{
VisitUnknown(e);
}
}
protected virtual void VisitFoo(Foo foo) { }
protected virtual void VisitBar(Bar bar) { }
protected virtual void VisitUnknown(CompositeElement e) { }
}
public class CompositeElement
{
public virtual void Accept(BaseVisitor visitor) { }
}
public class Foo : CompositeElement { }
public class Bar : CompositeElement { }
Note the visitor class is now responsible for the 2nd type-based dispatch, instead of the canonical version, where, for example, Foo
would be responsible for it and would have:
// Canonical visitor pattern 2nd dispatch
public override void Accept(BaseVisitor visitor)
{
visitor.VisitFoo(this);
}
Now, for the defense...
Let's say we want to add a new CompositeElement type:
public class Baz : CompositeElement { }
In order to accomodate this new element type in the visitor model, I just need to make changes to the BaseVisitor class:
public class BaseVisitor
{
public virtual void Visit(CompositeElement e)
{
// Existing cases elided...
else if(e is Baz)
{
VisitBaz((Baz)e);
}
}
protected virtual void VisitBaz(Foo foo) { }
}
Admittedly, this is a small issue, but it does appear to simplify maintenance (that is, if you don't mind big if
or switch
statements).
Let's say we want to extend the composite in a separate package. We can accomodate this without modifying BaseVisitor
:
public class ExtendedVisitor : BaseVisitor
{
public override Visit(CompositeElement e)
{
if(e is ExtendedElement)
{
VisitExtended((ExtendedElement)e);
}
else
{
base.Visit(e);
}
}
protected virtual void VisitExtended(ExtendedElement e) { }
}
public class ExtendedCompositeElement : CompositeElement { }
Having this structure allows us to break the dependency of BaseVisitor
needing to have VisitExtended
in order to accomodate extended CompositeElement types.
I haven't implemented Visitor pattern enough or maintained it long enough to have any disadvantages weigh on me at this point. Obviously, maintaining a big switch statement is a pain, and there are performance implications, however I'm not sure they outweigh the flexibility of keeping the BaseVisitor
free of dependences on extensions.
Please weigh in with your thoughts on the downsides.
The main reason the visitor pattern is defined in the GoF book as it is, is that C++ didn't have any form of Run-Time Type Identification (RTTI). They used "double dispatch" to get the target objects to tell them what their type was. A pretty cool, but incredibly hard to describe trick.
The main difference between what you describe and the GoF Visitor pattern (as you mention) is that you have an explicit "dispatch" method - the "visit" method that checks the type of the argument and sends it to the explicit visitFoo, visitBar, etc methods.
The GoF Visitor pattern uses the data objects themselves to do the dispatch by providing an "accept" method that turns around and passes "this" back to the visitor, resolving to the proper method.
To put it all in one place, the basic GoF pattern looks like (I'm a Java guy, so please excuse the Java code instead of C# here)
public interface Visitor {
void visit(Type1 value1);
void visit(Type2 value2);
void visit(Type3 value3);
}
(note that this interface could be a base class with default method implementations if you would like)
and your data objects all need to implement an "accept" method:
public class Type1 {
public void accept(Visitor v) {
v.visit(this);
}
}
Note: The big difference between this and what you mentioned for the GoF version is that we can use method overloading so the "visit" method name stays consistent. This allows every data object to have an identical implementation of "accept", reducing the chance of a typo
Every type needs the exact same method code. The "this" in the accept method causes the compiler to resolve to the correct visit method.
You can then implement the Visitor interface however you would like.
Note that adding a new type (Type4 for example), in the same or different package, would require fewer changes than what you describe. If in the same package, we would add a method to the Visitor interface (and each implementation), but you don't need the "dispatch" method.
That said...
I teach Design Patterns at Johns Hopkins, by the way, and what I like to recommend is a nicely dynamic approach.
Start with a simpler, single-object Visitor interface:
public interface Visitor<T> {
void visit(T type);
}
Then create a VisitorRegistry
public class VisitorRegistry {
private Map<Class<?>, Visitor<?>> visitors = new HashMap<Class<?>, Visitor<?>>();
public <T> void register(Class<T> clazz, Visitor<T> visitor) {
visitors.put(clazz, visitor);
}
public <T> void visit(T thing) {
// needs error checks, and possibly "walk up" to check supertypes if direct type not found
// also -- can provide default action to perform - maybe register using Void.class?
@SuppressWarnings("unchecked")
Visitor<T> visitor = (Visitor<T>) visitors.get(thing.getClass());
visitor.visit(thing);
}
}
You would use this like
VisitorRegistry registry = new VisitorRegistry();
registry.register(Person.class, new Visitor<Person>() {
@Override public void visit(Person person) {
System.out.println("I see " + person.getName());
}});
// register other types similarly
// walk the data however you would...
for (Object thing : things) {
registry.visit(thing);
}
This allows you to now register independent visitors for each type you want to visit, and it won't break existing visitor implementations whenever a new type is added.
You can also re-register (and de-register) different combinations of visitors at runtime, even loading definitions of what to do from some configuration info.
Hope this helps!
Take a look at the acyclic visitor pattern. It also offers the advantages that you listed in your visitor adaptation, without the big switch
statement:
// acyclic version
public interface IBaseVisitor { }
public interface IBaseVisitor<T> : IBaseVisitor where T : CompositeElement {
void Visit(T e) { }
}
public class CompositeElement {
public virtual void Accept(IBaseVisitor visitor) { }
}
public class Foo : CompositeElement {
public override void Accept(IBaseVisitor visitor) {
if (visitor is IBaseVisitor<Foo>) {
((IBaseVisitor<Foo>)visitor).Visit(this);
}
}
}
public class Bar : CompositeElement {
public override void Accept(IBaseVisitor visitor) {
if (visitor is IBaseVisitor<Bar>) {
((IBaseVisitor<Bar>)visitor).Visit(this);
}
}
}
Your real visitors can pick and choose which subclasses they visit:
public class MyVisitor : IBaseVisitor<Foo>, IBaseVisitor<Bar> {
public void Visit(Foo e) { }
public void Visit(Bar e) { }
}
It's "acyclic" because it doesn't have a cyclic dependency between the types in your hierarchy and the methods in the visitor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With