Multiple dispatch in Java
In this article I'd like to show few examples of code beyond the world of single dispatch. I'll show why method overloading isn't enough, how things may be done in Clojure and that it's quite easy to write a similar code in Java. I hope you'll find this valuable.
Animal sounds
Let's tart with an interface of Animals:
public interface Animal
{
public String doSound();
}
Just by looking at it we know that every single Animal is able to do a sound.
There are two classes of Animals: Cats and Cows respectively:
public class Cat implements Animal
{
public String doSound()
{
return "Meow";
}
}
public class Cow implements Animal
{
public String doSound()
{
return "Mooo";
}
}
Thanks to polymorphism we can take any Animal and make it do a sound:
Animal meower = new Cat("Mr. Meower", 7);
Animal mooer = new Cow("Lady Mooer", 500);
System.out.println(meower.doSound()); // "Meow"
System.out.println(mooer.doSound()); // "Mooo"
It doesn't matter if it's a Cow or a Cat, as the way of asking it to do a sound is the same. Please notice that in this example the compile time type of both the meower and the mooer is an Animal and not a Cat and a Cow. It works as expected only because the actual method that's being called is selected upon the runtime type of the Animal we call the method on.
Cats differ from cows
Let's admit that cats slightly differ from cows:
public class Cat implements Animal
{
public final String name;
public final int remainingLifes;
public Cat(String name, int remainingLifes)
{
this.name = name;
this.remainingLifes = remainingLifes;
}
public String doSound()
{
return "Meow";
}
}
public class Cow implements Animal
{
public final String name;
public final int weight;
public Cow(String name, int weight)
{
this.name = name;
this.weight = weight;
}
public String doSound()
{
return "Mooo";
}
}
Mr. Presenter who introduces animals
What if we would like to introduce Animals to the audience, but we don't want to make them responsible for that? Then we would need a presenter we could ask for introducing them.
Method overloading
The simplest, but not always working solution, could be method overloading.
class Presenter
{
public String introduce(Cat c)
{
return "A kitty called " + c.name + " with " + c.remainingLifes + " lifes.";
}
public String introduce(Cow c)
{
return "A " + c.weight + " kg milk producer named " + c.name + ".";
}
}
Unfortunately method overloading isn't based on runtime types, but on compile time types. It means that even though this works as expected:
Presenter presenter = new Presenter();
Cat meower = new Cat("Mr. Meower", 7);
Cow mooer = new Cow("Lady Mooer", 500);
System.out.println(presenter.introduce(meower));
System.out.println(presenter.introduce(mooer));
This doesn't even compile:
Presenter presenter = new Presenter();
Animal meower = new Cat("Mr. Meower", 7);
Animal mooer = new Cow("Lady Mooer", 500);
System.out.println(presenter.introduce(meower));
System.out.println(presenter.introduce(mooer));
It happens because Java supports only single dispatch. The method that's actually called is selected upon the runtime type of just one variable - the object it is called on. Runtime types of other arguments aren't taken into account.
The visitor pattern
One of the possible solutions is the visitor pattern. We define its interface:
public interface AnimalVisitor<T>
{
public T visit(Cow c);
public T visit(Cat c);
}
And we let it visit animals by modifying the Animal interface to accept a visitor:
public interface Animal
{
public String doSound();
public <T> T accept(AnimalVisitor<T> visitor);
}
Now we need to add the following method to both the Cat and Cow classes:
public <T> T accept(AnimalVisitor<T> visitor)
{
return visitor.visit(this);
}
We're ready to write a presenting visitor:
public class PresentingVisitor implements AnimalVisitor<String>
{
public String visit(Cow c)
{
return "A " + c.weight + " kg milk producer named " + c.name + ".";
}
public String visit(Cat c)
{
return "A kitty called " + c.name + " with " + c.remainingLifes + " lifes.";
}
}
This allows us to forget about differences between cats and cows when we just want to ask a presenter to introduce some animals:
AnimalVisitor<String> presenter = new PresentingVisitor();
Animal meower = new Cat("Mr. Meower", 7);
Animal mooer = new Cow("Lady Mooer", 500);
System.out.println(meower.accept(presenter));
System.out.println(mooer.accept(presenter));
Thanks to applying the visitor pattern, it works as expected. Both Mr. Meower and Lady Mooer are considered simply Animals, not a Cat and a Cow. Because the visit method is actually called by both the Cat and the Cow, the compiler knows which of the AnimalVisitor's methods should be called - the one accepting a Cat and the one accepting a Cow, respectively. We can now write code that is far more generic, for example we can iterate over a collection of Animals and introduce each of them in appropriate way:
AnimalVisitor<String> presenter = new PresentingVisitor();
Animal[] animals = new Animal[]{
new Cat("Mr. Meower", 7),
new Cow("Lady Mooer", 500),
};
for(Animal oneAnimal: animals) {
System.out.println(oneAnimal.accept(presenter));
}
The fact that now every animal must know how to accept a visitor isn't nice, because we need the visitor interface and its support since the very beginning (the first animal implementation). But it's not a high price for that flexibility, as it's always just one method regardless the number of implementations of the visitor interface. However, there's a more serious problem we face when we add or remove Animals.
The visitor pattern is meant to make it easy to add new functions to existing types, but not to add new types. It means that when it comes to adding new types, it doesn't allow us to follow the Open/Closed principle, because we're eventually forced to modify the code of already existing visitors as we keep adding new types.
Try Clojure
Let's try to do something similar in the Clojure programming language. First we define our animals:
(defrecord Cow [name weight])
(defrecord Cat [name remaining-lifes])
(def animals [
(Cow. "Lady Mooer" 500)
(Cat. "Mr. Meower" 7)
])
Now we add a multimethod based on the class of the given object:
(defmulti introduce class)
That allows us to add independent functions that introduce different animals. We can start with cats. It means nothing but adding a method that's called when a Cat is given:
(defmethod introduce Cat [c]
(str "A kitty called " (:name c) " with " (:remaining-lifes c) " lifes.")
)
And then we add support of Cows. There's no need of modifying the code responsible for introducing Cats or Cows themselves:
(defmethod introduce Cow [c]
(str "A " (:weight c) " kg milk producer named " (:name c) ".")
)
Now even if we want to introduce two different animals, we can use the same function. Thanks to polymorphism, the introduce function is able to introduce both types of animals and doesn't require us to think which exact function to pick:
(println (map introduce animals))
Modularity - that's why it's so awesome
But why is it nicer than the visitor pattern? The answer is very simple - modularity!
Adding new types doesn't force us to add new functions
When we add a new type of animal, let's say a Bird, we're not forced to immediately add support of introducing it. Maybe we won't need it at all in our app? Maybe a bird will just be fed, but never introduced? It doesn't matter if the Bird is the first of added animals, or there are already 10 more, if we want to add it's type we can simply write:
(defrecord Bird [name can-fly])
That's all! We've just added a new type without modifying anything and without a need of thinking about already existing types. We can now use Birds. Let's say we want to introduce them. The great thing is there's no need of changing the code that already exists to introduce other animals like Cows, Cats or some other types added in the meantime. We simply add:
(defmethod introduce Bird [b]
(str "A bird named " (:name b) " that " (if (:can-fly b) "can" "can't") " fly.")
)
Perfect! After the need of introducing Birds appeared, not before, we added an independent new function that introduces them. A Bird may be now added to the list of animals to introduce:
(def animals [
(Cow. "Lady Mooer" 500)
(Cat. "Mr. Meower" 7)
(Bird. "Sparrow" true)
])
The final code
(defrecord Cow [name weight])
(defrecord Cat [name remaining-lifes])
(defrecord Bird [name can-fly])
(defmulti introduce class)
(defmethod introduce Cat [c]
(str "A kitty called " (:name c) " with " (:remaining-lifes c) " lifes.")
)
(defmethod introduce Cow [c]
(str "A " (:weight c) " kg milk producer named " (:name c) ".")
)
(defmethod introduce Bird [b]
(str "A bird named " (:name b) " that " (if (:can-fly b) "can" "can't") " fly.")
)
(def animals [
(Cow. "Lady Mooer" 500)
(Cat. "Mr. Meower" 7)
(Bird. "Sparrow" true)
])
(println (map introduce animals))
It all looks great, doesn't it? I've decided to show a Clojure example, because it's really clean in that language. Does it mean that in languages that support only single dispatch, like Java, it cannot be achieved? Of course it can!
One more attempt in Java
Once again - what's our goal?
Just to sum it up - our goal is to have a presenter capable of introducing animals. We don't expect animals themselves to do that job well.
No need of visitors
If there will be a fully separated presenter, we don't need Animals to accept any visitors. We can go back to a very simple Animal interface:
public interface Animal
{
public String doSound();
}
Cats may be now implemented without thinking about any visitors. No need of the accept method.
How to talk to a presenter?
Let's write its interface this way:
public interface Presenter
{
public String introduce(Animal a) throws UnableToIntroduce;
}
An important detail is the fact that it may throw a checked exception, when for some reason it's unable to introduce the given animal. To give you an example from the real world, let's imagine you're asking somebody the way. What if that person is not able to answer? Does the world end? Hopefully not! Can you make it on time? Maybe, maybe not. In the end it depends on you, not only on one person you asked.
So because of this interface, we must be prepared for a situation when Mr. Presenter isn't able to introduce some animal. This exception may be then handled gracefully or cause an application crash. It's the client responsibility to make this decision.
An alternative solution could be to return some default value in case of failure, but from my experience I can tell you that it's easier to add it later than to add proper exception handling if there has been a default value since the very beginning. For example, if there's a suitable default value, we can write a decorator that catches some specified exceptions and returns that value.
Let's write the first presenter - the one that introduces cats:
public class CatPresenter implements Presenter
{
public String introduce(Animal a) throws UnableToIntroduce
{
if (!(a instanceof Cat)) {
throw new UnableToIntroduce();
}
Cat c = (Cat)a;
return "A kitty called " + c.name + " with " + c.remainingLifes + " lifes.";
}
}
Why do we have type checking and casting here? Because even though the interface lets us to introduce any Animal, it also provides us a way of saying "Sorry, I don't know how to do it!". That's why this particular implementation may support only Cats. The interface forces its clients to support both cases - when a presenter is able to introduce an animal and when it's not able to do it. This way we don't break any code that uses the presenter when we change it's implementation, because it's prepared for the fact that sometimes an animal may not be introduced.
Needs from the future don't influence the code now
Even if we know that in the future there will be Cows, we may not need them now. We can already test and deliver part of the functionality, which is full support of Cats or anything that uses Animals and Presenters. Let's use the first presenter.
Presenter presenter = new CatPresenter();
//--------------------
Animal[] animals = new Animal[]{ new Cat("Mr. Meower", 7) };
for(Animal oneAnimal: animals) {
try {
System.out.println(presenter.introduce(oneAnimal));
} catch (UnableToIntroduce e) {
//failure handling code goes here
}
}
The line I drew in the comment represents a very important concept - separation of the code that creates the presenter from the code that uses it. For the sake of simplicity, I'm not showing here a separated object that uses a presenter passed to it as a constructor parameter (or in some special cases, using a setter). However, if we want to achieve modularity, the user of a service cannot be its creator in the same time.
With Cows (sometimes) comes their presenter
We knew this day would come. There are Cows. Moreover, we want to introduce them! So we need a Cow presenter. In order to get more verbose exception messages from the presenter and keep the code simple, we can move the common part into a template method like this:
public abstract class ClassSupportingPresenter implements Presenter
{
public String introduce(Animal a) throws UnableToIntroduce
{
throwExceptionIfUnsupported(a);
return actuallyIntroduce(a);
}
protected void throwExceptionIfUnsupported(Animal a) throws UnableToIntroduce
{
if (!getClassOfSupportedAnimals().isInstance(a)) {
throw new UnableToIntroduce(String.format(
"%s supports only %s, but %s was given",
this.getClass().getCanonicalName(),
getClassOfSupportedAnimals().getCanonicalName(),
a.getClass().getCanonicalName()
));
}
}
protected abstract Class getClassOfSupportedAnimals();
protected abstract String actuallyIntroduce(Animal raw) throws UnableToIntroduce;
}
Extending this abstract class, the CowPresenter looks like this:
public class CowPresenter extends ClassSupportingPresenter
{
@Override
protected Class getClassOfSupportedAnimals()
{
return Cow.class;
}
@Override
protected String actuallyIntroduce(Animal raw) throws UnableToIntroduce
{
Cow c = (Cow)raw;
return "A " + c.weight + " kg milk producer named " + c.name + ".";
}
}
An instance of this class may be used to introduce Cows and only Cows. The problem of having one polymorphic method introduce capable of introducing Animals of any kind hasn't been solved yet.
Dispatcher
We need a construction similar to the previously mentioned Clojure introduce multimethod, that is a connection between a class of Animals and its Presenter.
The Container library
In this example I'm going to use a simple map between classes and objects from the Container library. I do it to avoid writing the same container logic over and over again, what allows to keep the domain specific code simpler. The ObjectToObjectMap fits our needs perfectly:
public interface ObjectToObjectMap<K, V>
{
public V getValueBy(K keyObject) throws UnableToGetValue;
}
Given an object, gives another based on some logic. If nothing is found, a checked exception is thrown.
Presenter composed of many presenters
The complete implementation of the class we need in order to be able to use the CatPresenter, the CowPresenter and any other Presenter in the future is:
public class ComposedPresenter implements Presenter
{
ObjectToObjectMap<Animal, Presenter> actualPresenters;
public ComposedPresenter(ObjectToObjectMap<Animal, Presenter> actualPresenters)
{
this.actualPresenters = actualPresenters;
}
public String introduce(Animal a) throws UnableToIntroduce
{
try {
return this.actualPresenters.getValueBy(a).introduce(a);
} catch (UnableToGetValue e) {
throw new UnableToIntroduce(String.format(
"%s doesn't contain any presenter for %s",
getClass().getCanonicalName(),
a.getClass().getCanonicalName()
));
}
}
}
Let me explain what's going on here. First of all, while building the ComposedPresenter, we provide an ObjectToObjectMap which given an Animal gives a Presenter supposed to introduce it.
Then, when the introduce method is called, we try to call it on the actual presenter fetched from the actualPresenters map. If nothing is found, a suitable exception matching the Presenter interface is thrown.
Now instead of limiting ourselves to just this:
Presenter presenter = new CatPresenter();
Or this:
Presenter presenter = new CowPresenter();
We can get both:
Presenter presenter = new ComposedPresenter(new ExactClassBasedMap<Animal, Presenter>()
.associate(Cat.class, new CatPresenter())
.associate(Cow.class, new CowPresenter())
);
Now the code that uses the Presenter may pass a different Animal to it without using a separated method like introduceCow.
The final code
Presenter presenter = new ComposedPresenter(new ExactClassBasedMap<Animal, Presenter>()
.associate(Cat.class, new CatPresenter())
.associate(Cow.class, new CowPresenter())
);
Animal[] animals = new Animal[]{
new Cat("Mr. Meower", 7),
new Cow("Lady Mooer", 500),
};
for(Animal oneAnimal: animals) {
try {
System.out.println(presenter.introduce(oneAnimal));
} catch (UnableToIntroduce e) {
//failure handling code goes here
}
}
Getting closer to the deadly diamond of death
We've connected the CatPresenter with objects of the Cat class. If there were DomesticCats, they wouldn't be introduced, because the ObjectToObjectMap implementation used in this example - ExactClassBasedMap - doesn't support inheritance. When picking some that does, we need to answer a very important question - which Presenter should be used, if there are few matching the given Animal? The recently associated one? Or maybe the one somehow considered the closest one in the inheritance hierarchy? What if the introduce method looked like this:
public String introduce(
Animal newAnimal,
Animal toAnimal
) throws UnableToIntroduce;
And we had two dispatchers: one for introducing a Cat to a DomesticCat and another for introducing a DomesticCat to a Cat? If we pass two DomesticCats to it, two Presenters match. There's no one correct answer to this question. How do we solve this problem depends on the logic of our application. For example, in Clojure we could use the prefer-method function to tell that the function introducing Cats to DomesticCats is preferred over the one introducing DomesticCats to Cats.
It's not a free lunch
Introducing additional dispatch logic, often based on many parameters, comes with a price in performance. Depending on the exact implementation of the dispatching function, the performance may vary, but it will always be a little bit slower. However, before avoiding this approach in the name of performance, a measurement should be done. Maybe greater modularity and simpler code are worth it?