That’s a good talk, but unfortunately it’s about Lisp ((which ((is) really)) scary).
I’ll try to explain double/multiple dispatch, but as always, we should start with something you know already: single dispatch.
You have two classes, A and B, and they both have a method called foo:
class A
def foo
print "foo in A"
end
end
class B
def foo
print "foo in B"
end
end
Now if we do:
a = new A
b = new B
a.foo()
b.foo()
You’ll get:
foo in A
foo in B
The computer “knows” that he should execute ‘print “foo in A”’ if we call a.foo(), and ‘print “foo in B”’ if we execute b.foo(). That’s basic, ok?
Now we change the syntax a bit:
class A
class B
def foo(A obj)
print "foo in A"
end
def foo(B obj)
print "foo in B"
end
a = new A
b = new B
foo(a)
foo(b)
First we define that there are two classes: A and B. Then we defined 2 methods: foo(A obj) and foo(B obj). foo(A obj) means that we are defining a function named foo, which takes a parameter obj of type A. So this function will be called if we pass in an object of class A. Same for B. Now we create an A and a B in the variables A and B. Then we call foo(a) and foo(b). The computer knows that we mean ‘print “foo in A”’ if we call foo(a) because the class of a is A. Still clear?
Remember that only the syntax has changed. You may feel different about this code: the method foo(A obj) doesn’t belong to class A, does it?
But what if we use methods with multiple parameters?
class A
class B
def foo(A obj1, B obj2)
print "got an A in obj1 and a B in obj 2"
end
def foo(A obj1, A obj2)
print "got two A's"
end
a1 = new A
a2 = new A
b1 = new B
foo(a1, b1)
foo(a1, a2)
What will this code do? For the first call: foo(a1, b1), your computer will find the corresponding method definition ‘print “got an A in obj1 and a B in obj 2”’. The second call: foo(a1, a2) will print “got two A’s”. Nothing strange about it, the computer just checks two parameters now. What if we call foo(b1, a1)? That will produce a MethodNotDefinedError or something, because there is no corresponding method foo(B obj1, A obj2).
This is called double dispatch.
What does this have to do with visitor?
Let’s look at Zend’s example:
class Binary extends MyListComponent {
//...
function accept(MyListVisitor $visitor) {
$visitor->visitBinary($this);
}
}
class WebText extends MyListComposite {
//...
function accept(MyListVisitor $visitor) {
$visitor->visitWebText($this);
//...
}
}
//...
class SimpleSearchVisitor extends MyListVisitor {
//...
function visitBinary(Binary $comp, $level) {
// don't match binaries
}
function visitWebText(WebText $comp, $level) {
if ($this->testText($comp->getTitle()) || $this->testText($comp->getText())) {
$this->matches[] = $comp;
}
}
}
What happens here?
The classes Binary and Note “tell” the visitor which class they have: a Binary calls $visitor->visitBinary(), and a WebText calls visitWebText. They call methods based on their class, they dispatch based on their class. We want that because they way you search is different for different types of objects. You want to search the text of WebTexts, but you don’t want to search Binaries.
Let’s translate that to our plain-old-function-language:
class Binary extends MyListComponent
class WebText extends MyListComponent
class SimpleSearchVisitor extends MyListVisitor
def accept(Binary binary, MyListVisitor visitor)
visitbinary(visitor, binary) # the visitor visits the binary
end
def accept(WebText webtext, MyListVisitor visitor)
visitwebtext(visitor, webtext)
end
def visitbinary(SimpleSearchVisitor searcher, Binary comp)
# don't match binaries
end
def visitwebtext(SimpleSearchVisitor searcher, WebText comp)
if(testtext(searcher, gettitle(comp)) || testtext(searcher, gettext(comp)))
searcher.matches[] = comp
end
end
Not very clear…yet. But it should work: we call visitwebtext if we have a webtext, and we call visitbinary if we have a binary. The accept method looks very similar, we can create a single accept method for both types:
class Binary extends MyListComponent
class WebText extends MyListComponent
class SimpleSearchVisitor extends MyListVisitor
def accept(Object node, MyListVisitor visitor)
visit(visitor, node)
end
def visit(SimpleSearchVisitor searcher, Binary comp)
# don't match binaries
end
def visit(SimpleSearchVisitor searcher, WebText comp)
if(testtext(searcher, gettitle(comp)) || testtext(searcher, gettext(comp)))
searcher.matches[] = comp
end
end
Now we have one visit method and one accept method. The visit method takes care of the different types, so accept doesn’t have to anymore. The accept method doesn’t do much anymore, so we can remove it.
If we give the methods meaningful names we get this:
class Binary extends MyListComponent
class WebText extends MyListComponent
class SimpleSearcher extends MyListSearcher
def searchwith(SimpleSearcher searcher, Binary comp)
# don't match binaries
end
def searchwith(SimpleSearcher searcher, WebText comp)
if(testtext(searcher, gettitle(comp)) || testtext(searcher, gettext(comp)))
searcher.matches[] = comp
end
end
So we can now do:
searchmethod = new SimpleSearcher("The Search Query")
thingtosearch = new WebText("The Title", "The Text")
searchwith(searchmethod, thingtosearch) # read: search the WebText with SimpleSearcher
I think this is much simpler than the visitor pattern: we just define a method which dispatches on the searchmethod: the way we search the thing: SimpleSearch, AdvancedSearch, etc. and the thingtosearch: if we’re searching a Binary with SimpleSearch, we don’t want to do anything, if we’re searching a WebText with SimpleSearch, we want to search the title and the text.
I hope this makes sense…