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.
interface IBinTree {
public
}
interface IBinTree
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
return visitor.leafCase(this, param);
}
}
class OneNode implements IBinTree {
public
return visitor.oneCase(this, param);
}
}
class TwoNode implements IBinTree {
public
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:
abstract class ADefaultBinTreeVisitor
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:
myTree.execute(new ADefaultBinTreeVisitor
public Void defaultCase(IBinTree host, Void param) {
throw new RuntimeException("OneNode expected!");
}
public Void oneCase(OneNode host, Void param) {
// do whatever we have to do
return Void.TYPE;
}
}, Void.TYPE);
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:
myTree.execute(new ADefaultBinTreeVisitor
public Void defaultCase(IBinTree host, Void param) {
throw new RuntimeException("TwoNode expected!");
}
public Void twoCase(OneNode host, Void param) {
// do whatever we have to do
return Void.TYPE;
}
}, Void.TYPE);
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:
@OverrideOnly
abstract class ADefaultBinTreeVisitor
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
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) {
// 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.