Visitor pattern vs if-ladder

G

Giovanni Azua

Tom Anderson said:
Having to write an accept method each time specially in larger hierarchies
that statically maps to the right overloading visit counterpart is indeed
error-prone e.g. you just might forget to call visit. It is simply
boilerplate code that you will have to write and maintain. Attempts to make
generic accepts in the Element hierarchy lead to type casts and really
type-unsafe operations.
Not so. Exactly one short method has to be added, and that method is
fundamentally about how the hierarchy interacts, in a generic way, with
other classes, which is very much a concern of the hierarchy itself.
The goal of an Element hierarchy is to provide implementations for well
defined abstractions and not an accept method that might cover a ghost
requirement. The accept method is counter-intuitive and fills only the
artificial need for a double-dispatching capability missing natively in
languages like Java. Besides, in the manual way:

1-. You can not create visitors for Element hierarchies for which you do not
have access to the source code
2-. Adding new Elements in the hierarchy imposes a high maintenance toll in
existing Visitor implementations.
You left out the class which actually does the work:
http://perfectjpattern.sourceforge..../core/behavioral/visitor/AbstractVisitor.html

And which is based on a pretty shocking reflective strategy
That people want to enjoy the Burger does not mean that they want to meet
the Cow.

If we go back to design principles and a client-provider contract, I give
you an interface that offers:

- double dispatch so you can attach externalizable concerns rather than
imposse them to a (maybe existing) Element hierarchy.
- type safety defined as: no matter what parameter combination you try to
attack this interface with, there will be no type-unsafety translated in
potential for a ClassCastException.
- reasonable performance. Note that the "pretty shocking reflective
strategy" does memoization. The matching methods corresponding to the
second dispatch are cached and this may even be faster than the "manual"
implementation. Thanks to this thread I am encouraged to write for the next
release a benchmarking test to try to proof exactly this.
which gives absolutely no type safety
Not true. I encourage you to write a test case that uses this Visitor via
its public API and produces a ClassCastException. I might even pay you for
that :)
and a lot of overhead (doubly so, because it's a bad idea, badly
implemented).
The idea - unfortunately not mine - comes from scientific research in the
field of Software Engineering. The relevant information can be found here:
http://se.ethz.ch/~meyer/publications/computer/visitor.pdf

But then you need a bit of understanding of Eiffel which I don't expect from
a Biologist?

Best regards,
Giovanni
 
G

Giovanni Azua

Joshua Cranmer said:
Several problems I see here:
1. You're not doing it right. If you're going to complain about the
visitor pattern being error-prone, the least you could do is to provide a
*correct* implementation.
I don't see any concrete problem here, only an empty claim.
2. Speed? Reflection is very slow and prohibits any kind of optimization
whatsoever.
Old myths and cliche, another empty claim.
3. Using reflection kills type safety, since you're essentially
prohibiting the compiler from figuring out what's going on. Another way of
putting it is that you've added dynamicism which pushes what should be
compile-time errors to runtime. Which is the wrong direction, in case you
didn't know.
An empty claim again. The IVisitor has a generic parameter that defines
exactly which type of Elements it can visit. If you try to pass a
mismatching argument you will get the acclaimed compilation error:
http://perfectjpattern.sourceforge....ern/core/api/behavioral/visitor/IVisitor.html

I encourage you too to write a test-case of this Visitor via its public API
that leads to a ClassCastException and that would sustain your theory.
or you have a library that needs to be able to inspect running class
methods for various reasons.
I do, in order to implement the double-dispatch :]
The key thing is that reflection should be a tool of last resort, not a
cheap trick
Well the "cheap trick" as you call it comes from scientific research
literature in Software Engineering and was not originally my idea, so I
can't get any credit for it:
http://se.ethz.ch/~meyer/publications/computer/visitor.pdf
you use to get around laziness.
This is very funny. Several years ago when I was majoring in Software
Engineering at ETH Zurich, and while explaining a design in the OOSC course,
Bertrand Meyer himself called me a "Lazy Developer", but he meant it as a
compliment :]

If you read the link above, the first line of the Abstract says "Abstract:
In software design, laziness is a virtue ...".
Beware of bugs in the above code; I have only proved it correct, not tried
it. -- Donald E. Knuth
Unlike you, I don't do this, I write unit tests that proof the code right,
up front.

Best regards,
Giovanni
 
J

Joshua Cranmer

Giovanni said:
I don't see any concrete problem here, only an empty claim.

You completely ignore the possibility of inheritance. So if I want to
visit Engine with a SoupedUpV8Engine, it will fail--no-op.
Old myths and cliche, another empty claim.

I did a simple microbenchmark. It finds that (avoiding the inlining of
the method call), the time it took to do a regular method call was
approximately 1% the time it took to do a simple reflection call. If I
made the method a no-op, that was cut in half, which leads me to believe
that my computer's granularity in nanoTime() is getting in the way.

In places where the tree is extremely large--say the parse tree of a
compiler--a 100-fold slowdown in dispatch can become noticeable.
An empty claim again. The IVisitor has a generic parameter that defines
exactly which type of Elements it can visit. If you try to pass a
mismatching argument you will get the acclaimed compilation error:
http://perfectjpattern.sourceforge....ern/core/api/behavioral/visitor/IVisitor.html

Suppose I accidentally omit to write the visit for an Engine. The
pattern will turn that call into a no-op--it won't even tell me at all.
In the traditional model, the compiler will most likely inform me that I
forgot to write the visit(Engine) method.
or you have a library that needs to be able to inspect running class
methods for various reasons.
I do, in order to implement the double-dispatch :]

And you can't even do the method lookup correctly.

I wouldn't consider this a valid case: you're trying to do the VM's job
for it instead.
Unlike you, I don't do this, I write unit tests that proof the code right,
up front.

You need to proofread your post....

Perhaps a taste of Dijkstra would help:
"Program testing can be used to show the presence of bugs, but never to
show their absence!"

But in any case, the purpose of my signature (in part, at least) is to
point out that code I present here is not necessarily bug-free and that
any analysis I present is more important than the actual code.
 
D

Daniel Pitts

Philipp said:
Hello,
I think about replacing an if-ladder (with instanceof calls) in my
code with a visitor pattern, but the advantages are not really obvious
to me.
Pros:
- cleaner code (although I think this is debatable)
Cons:
- Needs modification of the original interface and classes (to add the
accept(Visitor) method)
- If new subclasses of the acceptor interface are added, both the
Visitor interface and it's implementation needs change (adding a new
visit(MyNewClass c); to the interface). In the ladder implementation,
you only need to change at one point and add an if(...)
- A bit slower than the ladder implementation (see below)

What advantages am I missing? Why is an if ladder seen as code smell
and not the visitor pattern?


About speed:
I compared the speed of execution of an if-ladder with the visitor
pattern for counting different types of Vehicles.
If ladder:
public void count(Vehicle vehicle){
if (vehicle instanceof Car) {
counts[0]++;
} else if (vehicle instanceof Motorbike) {
counts[1]++;
} else if (vehicle instanceof Bike) {
counts[2]++;
[...]

Visitor pattern:
public void visit(Car car) {
counts[0]++;
}
public void visit(Motorbike motorbike) {
counts[1]++;
}
public void visit(Bike bike) {
counts[2]++;
}
[...]

I ran the counting with 8 types of vehicle on 10^8 randomly produced
vehicles 10 times alternatively (each run is ~16 sec, all 20 runs are
in one JVM execution, so warm up should be accounted for). The ladder
implementation is about 10% faster (mean time per run: ladder 16800ms,
visitor 18380).
I'm not saying that this is a big difference or should influence the
decision very much. It's just FYI.

Phil
Maybe consider adding an enum type-token, and using an
EnumMap<Vehical.Type, CountAccumulator>

where:
public class CountAccumulator {
private int count;
public void increment() { ++count; }
public int get() { return count; }
}

I usually avoid enum for type-tokens, but this seems like an appropriate
place to apply them, since you are looking for a very specific set of
"types". It also would reduce the size of your count method/visitor
dramatically:

Map<Vehical.Type, CountAccumulator> count(Collection<Vehical> vehicals){
Map<Vehical.Type, CountAccumulator> counts =
new EnumMap<Vehical.Type, CountAccumulator>(CarType.class);
for (Vehical.Type type: Vehical.Type.values()) {
counts.put(type, new CountAccumulator());
}
for (Vehical vehical: vehicals) {
counts.get(vehical.getType()).increment();
}
return counts;
}
 
T

Tom Anderson

Maybe consider adding an enum type-token, and using an EnumMap<Vehical.Type,
CountAccumulator>

where:
public class CountAccumulator {
private int count;
public void increment() { ++count; }
public int get() { return count; }
}

I usually avoid enum for type-tokens, but this seems like an appropriate
place to apply them, since you are looking for a very specific set of
"types".

I suspect my discomfort with type tokens is the same as yours, and stems
from the incomplete static typing: there's nothing stopping Car.getType()
returning VehicleType.BIKE.

But is there any clever generic trickery we could do to fix that?

[goes off and has a play]

Well, you can't make enums generic, so no, apparently not. Hmm.
It also would reduce the size of your count method/visitor
dramatically:

Map<Vehical.Type, CountAccumulator> count(Collection<Vehical> vehicals){
Map<Vehical.Type, CountAccumulator> counts =
new EnumMap<Vehical.Type, CountAccumulator>(CarType.class);
for (Vehical.Type type: Vehical.Type.values()) {
counts.put(type, new CountAccumulator());
}
for (Vehical vehical: vehicals) {
counts.get(vehical.getType()).increment();
}
return counts;
}

Good call! I was thinking you'd still have to write a switch on the enum,
but the EnumMap makes things very compact, but still fast.

tom
 
T

Tom Anderson

Having to write an accept method each time specially in larger hierarchies
that statically maps to the right overloading visit counterpart is indeed
error-prone e.g. you just might forget to call visit.

That would be a remarkable thing to forget, given that that call is the
only thing accept has to do. This is accept:

public void accept(Visitor v) {
v.visit(this);
}

So to forget to call visit, you'd have to write this:

public void accept(Visitor v) {
}

Which would be quite a surprising thing to write!

Unless you're thinking of the style of visitor which deals with composite
hierarchies, where elements deal with traversal of their children
(although your own code doesn't do this, so i assume this is not what you
mean), in which case the correct method looks like:

public void accept(Visitor v) {
v.visit(this);
// next bit could be more complicated
for (Element child: getChildren()) {
child.accept(v);
}
}

In which case you might accidentally write:

public void accept(Visitor v) {
// next bit could be more complicated
for (Element child: getChildren()) {
child.accept(v);
}
}

Which would be more understandable, but is still a pretty unlikely error,
i'd say.
It is simply boilerplate code that you will have to write and maintain.

Write, yes, but it'd not something that takes much maintenance, is it?
Attempts to make generic accepts in the Element hierarchy lead to type
casts and really type-unsafe operations.

Agreed - i don't think there's any way around the need to write accept in
each element class.
The goal of an Element hierarchy is to provide implementations for well
defined abstractions and not an accept method that might cover a ghost
requirement.

"Might cover a ghost requirement"! If you're adding visitor support to the
hierarchy, it's because you have a concrete requirement for it. I
certainly don't advocate going round adding visitor support to hierarchies
willy-nilly!
The accept method is counter-intuitive

How so?
and fills only the artificial need for a double-dispatching capability
missing natively in languages like Java.

True. In a language which can do dispatch based on parameter types, you
only need one accept method (although i don't know how that could be
typesafe). But the fact is that java can't, so dismissing it as an
'artificial need' is sophistry.
Besides, in the manual way:

1-. You can not create visitors for Element hierarchies for which you do not
have access to the source code

True. If i couldn't change the hierarchy, i would have to consider an
alternative strategy, and i might well consider one like yours. But when i
can change the hierarchy, the classical visitor pattern is a better
solution.
2-. Adding new Elements in the hierarchy imposes a high maintenance toll in
existing Visitor implementations.

Absolute nonsense! This "high maintenance toll" means adding methods to
deal with the new element types - which you *have to do* to maintain the
correctness of your program! Any way of avoiding this "toll" is simply a
way of writing an incorrect program! That is generally not considered a
good thing!
That people want to enjoy the Burger does not mean that they want to meet
the Cow.

It's not simply the ugliness of the cow that bothers me, it's the fact
that the cow is being raised in unsanitary conditions, and is likely to
give its consumers CJD.
If we go back to design principles and a client-provider contract, I give
you an interface that offers:

- double dispatch so you can attach externalizable concerns rather than
imposse them to a (maybe existing) Element hierarchy.

This is good. It's definitely useful to be able to do this to an existing
hierarchy.
- type safety defined as: no matter what parameter combination you try to
attack this interface with, there will be no type-unsafety translated in
potential for a ClassCastException.

Inadequate. Type safety, in this case, is not just about avoiding
ClassCastExceptions, it's about statically ensuring that all the input
types are dealt with properly. Your code does not do that, and it cannot
be made to do it.
- reasonable performance. Note that the "pretty shocking reflective
strategy" does memoization. The matching methods corresponding to the
second dispatch are cached and this may even be faster than the "manual"
implementation.

It may not involve a reflective lookup, but it still involves a map
lookup. In fact, *three* map lookups, although it could easily be reduced
to one - where you currently do (eliding the details of creating a
visitor):

if (!theLookup.containsKey(myElementClassName)) {
theLookup.put(myElementClassName, createVisitor());
}
if (theLookup.containsKey(myElementClassName)) {
theLookup.get(myElementClassName).visit(anElement);
}

You could do:

IVisitor<E> visitor = theLookup.get(myElementClassName);
if (visitor == null) {
visitor = createVisitor();
if (visitor != null) theLookup.put(myElementClassName, visitor);
}
if (visitor != null) visitor.visit(anElement);

Even if you did that, a map lookup and a reflective invocation is still a
lot slower than two virtual invocations.

It's not just the performance which is shocking, though. There's also the
fact that findVisitorMethod is so unselective - it will pick any method
with the right parameter and return type as a visitor method, which means
that i have to be careful when writing my visitor class not to introduce
any methods which might be mistaken for visitor methods.

A couple of other queries. Firstly, why does VisitorDelegator override
getReturn with an implementation that simply does return
super.getReturn()? That's no different to not overriding it. Secondly, why
does isValidReturn do
aTestMethod.getReturnType().isAssignableFrom(aReturnClass), rather than
the other way round? If the specified return type is, say, String, then
that means that a method which returns Object will be classed as having a
valid return type. And that if the specified return type is Object, then
only methods declared as returning Object will be classed as valid, and
not any subtypes of Object!
Thanks to this thread I am encouraged to write for the next release a
benchmarking test to try to proof exactly this.

Glad to hear it.
Not true. I encourage you to write a test case that uses this Visitor
via its public API and produces a ClassCastException. I might even pay
you for that :)

I don't claim to be able to produce a ClassCastException, but a test case
that breaks your code is easy. Write a new class:

public class Gearbox implements ICarPart {}

Modify Car to add:

private final Gearbox theGearbox = new Gearbox();
public Gearbox getGearbox() {
return theGearbox;
}

Modify PrintVisitor.visitCar to add, at the end:

visit(aCar.getGearbox());

This will compile and run without error. However, there is no visitor
method which takes a Gearbox, so it won't be visited, and that's a bug.
The idea - unfortunately not mine - comes from scientific research in the
field of Software Engineering. The relevant information can be found here:
http://se.ethz.ch/~meyer/publications/computer/visitor.pdf

But then you need a bit of understanding of Eiffel which I don't expect from
a Biologist?

Wow, an argument from authority and an ad hominem attack in the same
sentence. Impressive. Now see if you can get three fallacies in one
sentence!

Bertrand Meyer is a terribly famous guy, but it doesn't change the fact
that he's quite wrong in this case. Amusingly, he tries to spin the safety
hole that i've pointed out as an advantage - if you're not interested in
visiting a given type, you just don't write a method for it! A fine
example of why academics shouldn't try to give advice on the production of
software.

Nonetheless, if you do have a situation where you want to ignore all types
of element except a select few, that's easily dealt with with the
classical visitor. You just define a class NullVisitor or IgnoringVisitor,
which implements the visitor interface with no-op methods. Visitor classes
which want to be selective extend this, and override the methods for the
types they're interested in. Any other types, including any types added
after the visitor was written, will be ignored.

tom
 
R

Roedy Green

I think about replacing an if-ladder (with instanceof calls) in my
code with a visitor pattern, but the advantages are not really obvious
to me.

instanceof ladders are a sign of pathology in Java code. What you do
instead is implement some code in each class to do the custom bit of
logic with the same method name.

See http://mindprod.com/jgloss/delegate.html
--
Roedy Green Canadian Mind Products
http://mindprod.com

"It is not the strongest of the species that survives, nor the most intelligent that survives. It is the one that is the most adaptable to change."
~ Charles Darwin
 
M

Mark Space

Tom said:
That would be a remarkable thing to forget, given that that call is the
only thing accept has to do. This is accept:

public void accept(Visitor v) {
v.visit(this);
}

So to forget to call visit, you'd have to write this:

public void accept(Visitor v) {
}

Which would be quite a surprising thing to write!


I just want to clarify something here. Given that a concrete Element
which already implements the visitor pattern, there's no need for
sub-classes of that concrete element to re-implement the visitor
pattern. At least, none that I see.

interface Element {
void accept( Visitor v );
}


class ConcreteElementA implements Element {

public void accept( Visitor v )
{
v.VisitA( this );
}

}


interface Visitor {
void VisitA( ConcreteElementA e );
void VisitB( ConcreteElementB e );
}


There's no need for any children of ConcreteElementA to reimplement the
visitor pattern. In fact, they can't, since there's no method for them
to call in this Visitor interface.

Now if you add a method :

interface Visitor {
void VisitA( ConcreteElementA e );
void VisitB( ConcreteElementB e );
void VisitSubSubA( ConcreteElementAAA e );}
}

Then you have something to do for grandchildren of ConcreteElementA, but
only if your design actually requires that those classes are treated
differently than their ancestors.

Did I miss something?
 
G

Giovanni Azua

Tom Anderson said:
Write, yes, but it'd not something that takes much maintenance, is it?
How many real-life production code Visitor implementations have you seen and
maintained in your lifetime? I wonder.
Simple. You are superfluously calling a method that won't do anything
i.e.Element.accept(Visitor) accept does not implement any logic. It is
simply a pass-through artifact to achieve double dispatch. What is confusing
is that the Visitor is the one actually doing something with the Element and
that is why it is counter-intuitive. In fact I still remember when I learned
this pattern the first time I had troubles retaining who was accepting who
for this very reason.

If you are going to argue that Visitor.visit(Element) is not simpler than
Element.accept(Visitor) ?

Are you also going to argue that Item.accept(List) is more intuitive than
List.add(Item).
But when i can change the hierarchy, the classical visitor pattern is a
better solution.
Totally disagree. The classical is not just adding a method, it means
complying with a supertype Visitable that does not actually fit in any model
hierarchy. You indeed are suggesting that accept is so natural that Object
probably should then be accepting too ...
correctness of your program! Any way of avoiding this "toll" is simply a
way of writing an incorrect program! That is generally not considered a
good thing!
As far as Software Engineering is concerned there is no global
correct/incorrect ... maybe in other fields this would make more sense. In
SE a program is correct if it satisfies the [correctness] specification it
was proposed to fulfill namely the contracts/assertions or test suites. It
is actually very simple: RED means wrong, GREEN means correct :]

The specification that was proposed in this case was to allow introducing
new Element types to be Visited without forcing all Visitors to comply with
the new Element, namely breaking [compilation] all Visitors when you
actually want to provide implementation only for a few.
It's not simply the ugliness of the cow that bothers me, it's the fact
that the cow is being raised in unsanitary conditions, and is likely to
give its consumers CJD.
Unsanitary according to what your perception of correctness is.
Inadequate. Type safety, in this case, is not just about avoiding
ClassCastExceptions, it's about statically ensuring that all the input
types are dealt with properly. Your code does not do that, and it cannot
be made to do it.
there is only one definition of type-safety and you can not just improvise
one here :] Type safety is that there will be no error consequencing from
trying to do an operation on some values that is not appropriate to their
data type i.e. ClassCastException. This is no such case. There is no type
error. The no-opp behavior is intended and is part of the correctness
specification namely the existing test cases.
It may not involve a reflective lookup, but it still involves a map
lookup. In fact, *three* map lookups, although it could easily be reduced
to one - where you currently do (eliding the details of creating a
visitor):

It's not just the performance which is shocking, though. There's also the
fact that findVisitorMethod is so unselective - it will pick any method
with the right parameter and return type as a visitor method, which means
that i have to be careful when writing my visitor class not to introduce
any methods which might be mistaken for visitor methods.

A couple of other queries. Firstly, why does VisitorDelegator override
getReturn with an implementation that simply does return
super.getReturn()? That's no different to not overriding it.
This is good feedback indeed it needs improvement I already opened a ticket
to solve this.
This will compile and run without error. However, there is no visitor
method which takes a Gearbox, so it won't be visited, and that's a bug.
Maybe. This Visitor implementation only tackles the so called "Anemic Domain
Models" those that contain attributes but offer no behavior. Since there is
no behavior assumed then there would be nothing to override but only extend
with new attributes and in such case you will need a new visit method to
treat the new Element differently therefore the exact match instead of
matching covariant argument types.

I will improve this too for the next release and avoid using Delegates
altogether but directly use reflection so "Rich Domain Models" with
Overriding type Inheritance will also be supported.

Best regards,
Giovanni

PS: I got nothing against Biologists adventuring into software development
:] but rather against people making bold and technically incorrect claims
like "broken" "type-unsafe" and the like.
 
G

Giovanni Azua

Tom Anderson said:
That would be a remarkable thing to forget, given that that call is the
only thing accept has to do. This is accept:
While implementing the Visitable in the Element model tree and thus
supporting the accept method it is also possible to forget to override the
accept method in a subclass. This will cause the wrong visit method to be
invoked, leading to really hard to spot bugs e.g.

class A implements Visitable {
accept(Visitor aVisitor){
aVisitor.visitA(this);
}}

class B extends A {
// just forgot to override accept
}

what happens when you do?
B myB = new B();
myB.accept(mySomeVisitor);

B will be visited as an A. In large Element models this bug will be really
hard to figure out.
A couple of other queries. Firstly, why does VisitorDelegator override
getReturn with an implementation that simply does return
super.getReturn()? That's no different to not overriding it.
It is different. The access modifier has been changed by overriding from
protected to public. In the super class "getReturn" is protected and thus
can not be accessed from an enclosing class of the subtype namely
AbstractVisitor. In Java it is possible to override and assign a more
unrestrictive access to the field/method, which is exactly what that method
does.

Best regards,
Giovanni
 
G

Giovanni Azua

Joshua Cranmer said:
Suppose I accidentally omit to write the visit for an Engine. The pattern
will turn that call into a no-op--it won't even tell me at all. In the
traditional model, the compiler will most likely inform me that I forgot
to write the visit(Engine) method.
If you look carefully, AbstractVisitor includes a redefinable method called
"deny" which by default does nothing i.e. the defaul no-opp but if you
override it in your concrete Visitor then you can throw the most horrific
Exception you are asking for, it is just that I don't see how that can give
you any added value. Maybe a good test-time assertion would be useful but
the idea of no-opp is to precisely have more flexibility than lots of
empty-do-nothing visit implementations.
I wouldn't consider this a valid case: you're trying to do the VM's job
for it instead.
I am not doing VM's job. VM does not offer double-dispatch, if it did I
would happily use it but it doesn't.
Perhaps a taste of Dijkstra would help:
"Program testing can be used to show the presence of bugs, but never to
show their absence!"
This is a really old quote, according to wikipedia back to the 1972.

Two contrasting points against that slightly outdated quote:
- Test-driven development: test are develop to define "software correctness"
before the software exists and ensure there are no bugs upfront.
- Regression tests: Make sure that defects found and fixed once won't
reappear undetected.

I've heard this quote before, from people where lazyness was not a virtue
but rather a failiure to provide even minimal tests to their code. This is a
bad kind of laziness you won't find in PerfectJPattern.

Best regards,
Giovanni
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,432
Messages
2,571,680
Members
48,796
Latest member
Greg L.

Latest Threads

Top