Scenario:
Sometimes, we need to add a behavior to several classes, where we cannot modify the classes. For example, imagine wanting to add prettyToString() to Strings, Numbers, Booleans, Collections and Maps.
A first stab at the code may look like this:
public static String prettyToString(Object input) {
if (input instanceof String) {
return prettyToString((String) input);
} else if (input instanceof Number) {
return prettyToString((Number) input);
} else if (input instanceof Boolean) {
return prettyToString((Boolean) input);
} else if (input instanceof Collection) {
return prettyToString((Collection) input);
} else if (input instanceof Map) {
return prettyToString((Map) input);
}
return "instance of " + input.getClass().getName();
}
private static String prettyToString(String string) { ... }
private static String prettyToString(Number number) { ... }
...
Problem:
The if-else blocks become unwieldy very quickly.
Simple Improvement:
Define a base type for a family of functors. Each concrete functor knows how to handle one of the condition blocks. When the general method is called, it tries to use each of the functors in turn until one formats the object. If they all fail, then the default formatting is performed.
private static final List<PrettyToString> PRETTY_TO_STRING = buildPrettyToStringHandlers();
public static String prettyToString(Object input) {
for (PrettyToString functor : PRETTY_TO_STRING) {
String formatted = functor.handleIfPossible(input);
if (formatted != null) {
return formatted;
}
}
return "instance of " + input.getClass().getName();
}
private static String prettyToString(String string) { ... }
private static String prettyToString(Number number) { ... }
private static List<PrettyToString> buildPrettyToStringHandlers() {
List<PrettyToString> m = new ArrayList<PrettyToString>();
m.add(new PrettyToString<String>(String.class) {
String handle(String string) { return prettyToString(string); }
});
m.add(new PrettyToString<Number>(Number.class) {
String handle(Number number) { return prettyToString(number); }
});
...
return Collections.unmodifiableList(m);
}
private static abstract class PrettyToString<T> {
private final Class<T> _type;
PrettyToString(Class<T> type) {
_type = type;
}
final String handleIfPossible(Object object) {
return _type.isInstance(object) ? handle(_type.cast(object)) : null;
}
abstract String handle(T object);
}
Notes:
This is a well known technique. Looking through the Gang of Four’s behavioral design patterns, I see that this is a variation of a “Chain of Responsibility” pattern.
This approach — and the additional code — becomes defensible when the list of functors is determined at runtime. It does not, however, buy an improvement in code. After all, we’ve removed a single sprawling if-else-if block but added a bunch of inner classes, a static builder method and a static member. Furthermore, we’ve harmed performance.
Part 2 will build on this approach but address the deficiencies.