Switch by class type
Imagining a method which receives as a parameter a type Object instance and needs to perform some action according to its type is not difficult. In fact, there are several design patterns that, when used in combination, help solve just that. However, this post is about adding a new switch..case construct to prevent some boiler-plate caused by these patterns.
For example, imagine a simple addition operation, which should behave well for Integer instances and Complex numbers, and throw an exception for any other input:
Object add(Object a, Object b) {
if (a.getClass() != b.getClass())
throw new IllegalArgumentException("Not same type");
if (a.getClass() == Integer.class) {
return ... // do int addition
} else if (a.getClass() == Complex.class) {
return ... // do complex addition
} else {
throw new IllegalArgumentException("Unsupported type");
}
}
Design patterns to the rescue
In order to allow further extensions, an interface for the addition operation can be created:interface Addition {
Object add(Object a, Object b);
}
and a strategy to create the correct instance, which accepts the two parameters, is created as well:
class AdditionStrategy {
static Addition createByType(Class type) { ... }
}
and obviously implement the interface for Complex and Integer instances. This reduces the original code to:
Object add(Object a, Object b) {
if (a.getClass() != b.getClass())
throw new IllegalArgumentException("Not same type");
return AdditionStrategy.createByType(a.getClass()).add(a, b);
}
extension-ability achieved! But how is that strategy implemented? It could be implemented using a bunch of if statements as well, but since we actually have an object to define the action to be taken now, we can use a Map instead:
class AdditionStrategy {
Map<Class , Addition> additions = ...;
static Addition createByType(Class type) {
Addition res = additions.get(type);
if (res == null)
throw new IllegalArgumentException("Unsupported type");
return res;
}
}
This gets a bit complicated if we want to check whether the instance class extends one of the class types in our map (for example, if we wanted the same add operation for anything extending the Number class):
class AdditionStrategy {
Map<Class , Addition> additions = ...;
static Addition createByType(Class type) {
for (Map.Entry<Class , Addition> entry : additions.entrySet()) {
if (entry.getKey().isAssignableFrom(type)) {
return entry.getValue();
}
}
throw new IllegalArgumentException("Unsupported type");
}
}
New construct
So far, so good - and probably nothing new as well. I'm wondering is if even that can be removed, as this is clearly boiler-plate code. The Java language has done a great deal of removing boiler-plate code through means of compiler decisions: enums, strings and for-each loops are just a few of those. We could see something like the following for our factory class:class AdditionStrategy {
static Addition createByType(Class type) {
switch (type) {
case Complex: return new ComplexAddition();
case extends Number: return new NumberAddition();
default: throw new IllegalArgumentException("Unsupported type");
}
}
}
Notice the case extends which queries whether the class extends the Number type, and not just equal to it. This is even better than the Map option, as the developer is given a choice to check for extension in some cases and not to do so in others.
What do you think?
Comments (10)