I think I may not have done such a good job explaining @OverrideOnly
and the problems with visitors with the template method pattern a few days ago.
Here’s where the situation comes up: Let’s say you have a binary tree, realized using the union and composite patterns. It can either be a leaf, have one child or two children.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | interface IBinTree { public <R,P> R execute(IBinTreeVisitor visitor<R,P>, P param); } interface IBinTree<R,P> { public R leafCase(Leaf host, P param); public R oneCase(OneNode host, P param); public R twoCase(TwoNode host, P param); } class Leaf implements IBinTree { public <R,P> R execute(IBinTreeVisitor<R,P> visitor, P param) { return visitor.leafCase(this, param); } } class OneNode implements IBinTree { public <R,P> R execute(IBinTreeVisitor<R,P> visitor, P param) { return visitor.oneCase(this, param); } } class TwoNode implements IBinTree { public <R,P> R execute(IBinTreeVisitor<R,P> visitor, P param) { return visitor.twoCase(this, param); } } |
Let’s say we’re now checking a structure like this, and we know we expect a OneNode
, and in all other cases it should throw an exception. This comes up very often, for example when working with Java class files: I know that the referenced item should be a class, but what if it’s not?
Since this is so common, we define an abstract class using the template method pattern:
1 2 3 4 5 6 | abstract class ADefaultBinTreeVisitor<R,P> implements IBinTreeVisitor<R,P> { public R leafCase(Leaf host, P param) { return defaultCase(host, param); } public R oneCase(OneNode host, P param) { return defaultCase(host, param); } public R twoCase(TwoNode host, P param) { return defaultCase(host, param); } public abstract R defaultCase(IBinTree host, P param); } |
This class delegates all calls to a default method that has to be overridden. To do the checking for OneNode
, we would write something like this:
1 2 3 4 5 6 7 8 9 |
Let’s say we’re lazy and we copy and paste this class so that we check for TwoNode
. It’s very easy to incorrectly change the signature of the oneCase
method above:
1 2 3 4 5 6 7 8 9 |
In this anonymous inner class, we have not overridden the twoCase
method as intended; the twoCase
method still calls defaultCase
and throws an exception. Instead, we have added a new method twoCase
that takes a OneNode
as first parameter. This method is useless and will never called, but the compiler does not complain.
If we told the Java compiler or some checking tool that classes extending ADefaultBinTreeVisitor
have to override or implement one of the methods already defined, and that they may not introduce new methods like we accidentally did above, this problem could be avoided:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @OverrideOnly abstract class ADefaultBinTreeVisitor<R,P> implements IBinTreeVisitor<R,P> { public R leafCase(Leaf host, P param) { return defaultCase(host, param); } public R oneCase(OneNode host, P param) { return defaultCase(host, param); } public R twoCase(TwoNode host, P param) { return defaultCase(host, param); } public abstract R defaultCase(IBinTree host, P param); } myTree.execute(new ADefaultBinTreeVisitor<Void,Void>() { public Void defaultCase(IBinTree host, Void param) { throw new RuntimeException("TwoNode expected!"); } // ERROR: this method does not occur in the superclass public Void twoCase(OneNode host, Void param)</font> { // do whatever we have to do return Void.TYPE; } }, Void.TYPE); |
This is the one error that I run into the most often with visitors and the template method pattern, and this would be really easy to check for a compiler.