Now I know why it didn't rely on first class functions.
It wasn't written in Smaltalk. It was written in the intersection of Smaltalk and C++. Sounds like Java, even though it didn't exit yet. Come to think of it, I wonder if they didn't have hints about this upcoming language from Sun. Java came out in 1995, but was in the works since 1991…
So my point stands.
If it were really written in Smaltalk, it wouldn't have needed insanities such as the Visitor pattern to work around the lack of closures. It would have used Smaltalk Blocks directly.
Oh, my. This is even worse than I had thought. The motivating section makes me want to grab my closure hammer, especially the "reuse of iteration code" motivation. I don't know how I would handle it of the top of my head, but the way the Wikipedia article present it feels wrong.
The Java example looks more explicit. Oh, crap. This is Bad, Wrong, Horrible design. Sure, now the compiler can help you not forget cases, but you're still on the wrong side of the Expression Problem: for every single subtype you add, you still need to add a case to each of your visitors.
Is this the kind of price one has to pay for class hierarchies?
For each class, you basically need to write an "accept" method to call the visitor. Fine for now. But then, the visitor specializes depending on the type of the object! Couldn't we just inline the appropriate visit method directly at the call site? It would get rid of all the boilerplate introduced by the visitor pattern, without duplicating more code. It would still suck[1], but it would suck less.
Some algorithms, like collision detection for example, are readily described with double dispatch. So in a language that only offers single dispatch, like C++ or Java for example, the visitor pattern actually leads to more elegant code for such algorithms.
I'm not saying I particularly like this way of doing things (I'd rather the language had direct support for multiple dispatch, really), but it's certainly not a dumb way of doing things as you seem to think. In fact it's rather clever, and in some cases the best option.
Oops.
Still, it makes me feel very uneasy: the accept methods dispatch over behaviours, and the visit methods dispatch over the object. This means highly non-local code.
Collision detection looks like a good example. Okay. Now even with multiple dispatch, I can't suffer the idea of having up to n² methods, for n different classes to dispatch over. In the case of collision detection, I would try very hard to have no more than 3 or 4 types of collision volumes. Just one if I can help it.
Personally, I'm not sure I like multiple dispatch at all. When you need it, you expose yourself to combinatorial explosion. It's only polynomial, but it still scales poorly.
Consider the multimethod approach:
(defgeneric collide? (a b))
(defmethod collide? ((a bus) (b person)) ...)
(defmethod collide? ((a person) (b bus)) ...)
(defmethod collide? ((a bus) (b bus)) ...)
(defmethod collide? ((a person) (b person)) ...)
Versus the pattern-matching approach (abusing CL's case expression for clarity): (defun collide? (a b)
(case (list (type-of a) (type-of b))
((bus person) ...)
((person bus) ...)
((bus bus) ...)
((person person) ...)))
Beside potential performance implications of implementing one way or the other, you also have an open-closed conundrum. Multimethods leave the set of behaviors open, while the pattern-matching leaves it closed. That is, if a library wanted to define a new kind of collidable entity, it's trivial to add with a new library method. Not so easy with the pattern-matching approach. For some applications, this doesn't matter; for some it does.The bigger question is, is it practical at all for everyone to use the same type of collision object?
I once wrote a version of Asteroids in C++ in which every object had a particular bounding shape that was used for the detection. For the logic, I still had n² cases to determine the result of that collision. Looking back, I contend that it would have been cleaner to use the visitor pattern, since all the logic in my game was contained in various game objects except for the collision handling logic. [1]
It's good to be skeptical of design patterns. I've worked at several companies where liberal application of patterns really mucked up the code base. But in some cases, they really can be the best solution -- when used conservatively and carefully.
[1]: Looking back I also would have used an entity system rather than "classic" OOP, but that would not have solved the n² case dilemma.
I've written about how "OO" as is commonly done in Java/C++ is a mistake: http://loup-vaillant.fr/articles/classes-suck I know of the closure/object duality, and have some understanding of the tradeoffs involed, here: http://loup-vaillant.fr/articles/classes-as-syntactic-sugar so my opinion isn't totally uninformed.
So why should I waste my time with yet another OO book? Plus, I know it from reputation, which told me closures greatly simplify or even eliminates many patterns in this book. Languages with a GC and no closures should not exist, and so should books who teach how to work around their absence.
But I have just learned some interesting things about this book right now. Like, it was written in Smaltalk primarily, and merely translated to C++. Before Java came out, no less. So maybe its use isn't limited to class based, single dispatch, statically typed languages that don't have closures nor generics (like the first versions of Java).
But if the book is limited to such braindead languages, then patterns are just a symptom of bad language design, and I'd like to demonstrate that.
But first, I'll read the book. At least, I will know my enemy. I'll probably learn a thing or two along the way. And who knows, maybe I will finally "get OO"?
Why is this a problem? If you have N visitors, that's N different behaviors for every type, and you'll have to define those N behaviors for a new type somewhere. In this case, you do it on each Visitor (since each Visitor defines a behavior). Without using the Visitor pattern, you might put the behaviors somewhere else, but you would put them somewhere.
The obvious way out of this is intermediate representation. Typically, you would have the objects present themselves in the same way to any incoming behaviour. Now the behaviours don't have to know about each and every object. They just need to know about the intermediate representation. And that pattern is trivially solved with first class functions and parametric polymorphism.
It's actually the same amount of code in either case. With N different behaviors and M classes or types, you'll either implement the N behaviors as N methods on a class (and you do this for M classes - still M * N) or you'll have N visitors and add one method to each visitor to define the behaviors for a type (and you'll do this for M types as well - still M * N).
Consider the 2D-CAD example in the Wikipedia article: there are some M shapes and N different file formats to implement. If you add a new shape, you need define how it is represented in all N file formats. You can't get away with anything less than O(M * N) code. All the Visitor pattern does is change where you put it.
Typically, you would have the objects present themselves in the same way to any incoming behaviour. Now the behaviours don't have to know about each and every object.
If this is possible (which is very problem-dependent), then it's just as easily solved with inheritance. Since each subtype would have the same external interface (presenting themselves identically to incoming behaviors), the base class defines the external interface and subclasses override base class methods to define the implementation.
Which cases are you comparing? I was just saying M+N ≠ M×N, and that solutions that forces M×N are bogus.
> If this is possible (which is very problem-dependent), then it's just as easily solved with inheritance.
True. Then again, you don't need the visitor pattern.
Agreed. I was clarifying that converting your code to use visitors does not force an order of magnitude increase in the amount of code. If you start out with O(M+N) without visitors, you'll still have O(M+N) code after converting to visitors. Although, visitors are generally better appreciated when managing O(M*N) code than when managing O(M+N) code.
1: You may convince me. I'm serious. I do not anticipate any strong counter arguments, but if they come, I will listen. (I did listen to your first argument. It troubled me, really, I was certain the book was written in Java. So I checked.)
2: You may show the other readers the error of my ways. Since my arguments are documented and relatively articulate, they will likely sway anyone who isn't committed to a particular position. You wouldn't want me to put false ideas in their heads.