June 14th, 2008 by Mark | Posted in Code Design, Design Patterns, Java, Lisp | No Comments »
Following from my yakking about the state machine pattern, I’ve run into situations since where I’ve had a class which must react differently to different inputs, and thought how, rather than write a huge switch statement, or huge else-if with lots of instanceof tests, it would instead be nice to write methods which accept different types of object. And even better, to be able to hoist a catch-all method which accept the superclass of these objects into a superclass of my handler class:
public abstract class Handler
{
public void handleInput(Input input)
{
// do nothing - just catch all Inputs not handled by subclass
}
}
public abstract class Input
{}
public class AlphaInput extends Input
{}
public class BetaInput extends Input
{}
public class GammaInput extends Input
{}
public class MyHandler extends Handler
{
public void handleInput(AlphaInput alpha)
{
//handle one kind of input here
}
public void handleInput(BetaInput beta)
{
//handle another kind of input here
}
}
public class Main
{
public static void main(String[] args)
{
Handler handler = new MyHandler();
//handle an AlphaInput
Input input = new AlphaInput();
handler.handleInput(input);
//handle a BetaInput
input = new BetaInput();
handler.handleInput(input);
//handle a GammaInput - it can handle any subclass of Input because of the
//catch-all method in the Handler superclass.
input = new GammaInput();
handler.handleInput(input);
}
}
THIS WILL NOT WORK. What will happen here is that all three inputs will be handled the same way. They will all be swallowed up by the catch-all in the Handler class, which accepts the Input type.
“Why not?”, you might ask. It turns out that Java, and likewise C++, are what are known as single-dispatch languages. A class method can be thought of as a function where the first parameter is the object which owns the method. For example, when we make the method call handler.handleInput(input), this can be thought of as the function
handleInput(handler, input);
Java, as a single dispatch language, will examine at runtime the actual type of object that the first of these parameters is refering to (in this case, MyHandler), but will not do the same for subsequent parameters - it will use the type we referred to them by (in this case Input). Thus when finding the appropriate function to use, Java comes up with:
handleInput(MyHandler handler, Input input);
Java calls the method of MyHandler which accepts an Input - i.e. the one inheritted from Handler which does nothing. Other languages, such as Common Lisp, are known as multiple-dispatch languages because all the parameters’ types are investigated at runtime.
Design patterns such as the Visitor pattern were invented to work around the limitation of single-dispatch in languages like Java. Patterns such as this basically boil down to the object being passed in as the parameter identifying its own type to the receiving object.
For example, the handler example could be improved by altering the code to look like this:
public abstract class Handler
{
public abstract void handleInput(Input input);
public void handleAlphaInput(AlphaInput input)
{
// do nothing
}
public void handleBetaInput(BetaInput input)
{
// do nothing
}
public void handleGammaInput(GammaInput input)
{
// do nothing
}
}
public abstract class Input
{
public abstract void handleMe(Handler handler);
}
public class AlphaInput extends Input
{
public void handleMe(Handler handler)
{
handler.handleAlphaInput(this);
}
}
public class BetaInput extends Input
{
public void handleMe(Handler handler)
{
handler.handleBetaInput(this);
}
}
public class GammaInput extends Input
{
public void handleMe(Handler handler)
{
handler.handleGammaInput(this);
}
}
public class MyHandler extends Handler
{
public void handleInput(Input input)
{
input.handleMe(this);
}
public void handleAlphaInput(AlphaInput alpha)
{
//handle one kind of input here
}
public void handleBetaInput(BetaInput beta)
{
//handle another kind of input here
}
}
public class Main
{
public static void main(String[] args)
{
Handler handler = new MyHandler();
//handle an AlphaInput
Input input = new AlphaInput();
handler.handleInput(input);
//handle a BetaInput
input = new BetaInput();
handler.handleInput(input);
//handle a GammaInput
input = new GammaInput();
handler.handleInput(input);
}
}
We must create separate methods to handle alpha, beta and gamma inputs, and these may be optionally be overridden in subclasses of Handler. To decide which to invoke, each subclass of Handler must implement handleInput(Input input) and call input.handleMe(this) to get the subclass of Input to identify its runtime type, by calling the appropriate method and passing itself in. Its ugly, it means that adding new Input subclasses is extremely awkward, but its the best that Java has to offer without using reflection.