Wednesday, May 7, 2008

Type registry strategy pattern

One pattern that I have often seen applied in Java, is the one where you plug-in and register a handler for a given type of object to thereby supply a specific behavior for it. For instance, this is often used in order to render, edit and validate elements of various visual components in Swing, such as the JTable. I am unaware of any official name for this mechanism, but it appears to be a combination of Martin Fowlers Registry Pattern and the GoF Strategy pattern so I like to think of it as the Type registry strategy pattern.

Legacy example
Conditional behavior is encapsulated by some common interface, which provides the mechanism for dispatching to elsewhere, responsible for applying a concrete strategy. An example is this format handler, which allows an object to be formatted as a String:


1 interface Handler
2 {
3 String format(Object object);
4 }


And an API, capable of associating handlers for various Object types registered and dispatching to these:


5 class SomeAPI
6 {
7 Map handlers = new HashMap();
8
9 public void installHandler(Class clazz, Handler handler)
10 {
11 handlers.put(clazz, handler);
12 }
13
14 public String format(Object obj)
15 {
16 Handler handler = (Handler)handlers.get( obj.getClass() );
17 if(handler != null)
18 return handlers.format( obj );
19 else
20 return obj.toString();
19 }
20 }



This way, we can supply an API with an implementation (in the following example, an anonymous inner class) of formatting behavior for a given Object type, i.e. a Date:


21 someApiInstance.installHandler(Date.class, new Handler(){
22 public String format(Object obj)
23 {
24 Date date = (Date)obj;
25 return SimpleDateFormat.getInstance().format(date);
26 }
27 });



This works fine, but it's a little verbose and not particular type-safe. I wondered if I could device a generified version which improved on this. After all, we KNOW that the object passed is an instance of Date, we just need to convince Java of this as well.

Generified type strategy
Like many others in the Java space, I am not super comfortable nor experienced with generifying methods and dealing with use-site covariance. So in order to work out the following, I got some much appreciated help from the usenet group comp.lang.java.help.

Take one
First things first, the required changes to the callback interface is pretty obvious. We now simply declare our Handler to be of the type T, for which it has a format method accepting an instance of this type:


28 interface Handler<T>
29 {
30 String format(T object);
31 }



Now comes the tricky part. We need to have the compiler able to infer the type we provide, that is, apply some wildcard capturing magic. Our map now maps unknown Class types to an unknown Handler type. The installHandler method makes use of this, and associates an unknown Class type with a Handler of type T. We capture the type of the Handler in the format method, by casting to any type that is a super type of T:


32 class SomeAPI
33 {
34 Map<Class<?>, Handler<?>> handlers = new HashMap<Class<?>, Handler<?>>();
35
36 public <T> void installHandler(Class clazz<?>, Handler<T> handler>)
37 {
38 handlers.put(clazz, handler);
39 }
40
41 public <T> String format(T obj)
42 {
43 Handler handler<? super T> = getHandler(obj);
44 if(handler != null)
45 return handler.format( obj );
46 else
47 return obj.toString();
48 }
49
50 @SuppressWarnings("unchecked")
51 private <t> Handler<? super T> getHandler(T obj)
52 {
53 return (Handler<? super T>) handlers.get(obj.getClass());
54 }
55 }



This allows the type to flow all the way out through the callback, such that we may implement it as follows:


56 someApiInstance.installHandler(Date.class, new Handler<Date>(){
57 public String format(Date obj){
58 return SimpleDateFormat.getInstance().format(obj);
59 }
60 }



Voila, no more casting needed. :)

Take two
Now, the observant reader will notice that while we got rid of the casts, we still need to supply the type twice, once as class literal (the key of the internal map) and secondly as the type of the Handler (the value of the map). I was not sure if this could be done, given Java's generics by erasure. But it turns out that the type information actually is saved as meta-data, in order for reflection to still be possible:


61 public <T> void installHandler(Handler<T> handler)
62 {
63 callbacks.put(extractGenericType(handler), handler);
64 }
65
66 private <T> Class<T> extractGenericType(Handler<T> handler)
67 {
68 Type[] interfaces = handler.getClass().getGenericInterfaces();
69
70 for(Type type:interfaces)
71 if(type instanceof ParameterizedType)
72 return (Class<T>)
((ParameterizedType)type).getActualTypeArguments()[0];
73
74 throw new IllegalArgumentException ("You must supply a generified Handler
with a single parameterized type");
77 }



It's not pretty and generally makes everything a bit more fragile, but it sure makes for a nice public API for your consumers:


78 someApiInstance.installHandler(new Handler<Date>(){
79 public String format(Date obj){
80 return SimpleDateFormat.getInstance().format(obj);
81 }
82 }



The issue of erasure
While the parameterized type of the Handler can be uptained through reflection, as shown in the previous example, it isn't actually available at the core of the type system. For a demonstration of this, imagine your consumer of SomeAPI tries to implement several formatters:


83 public class Formatter implements Handler<Date>, Handler<Calendar>
84 {
85 public String format(Date obj){
86 return SimpleDateFormat.getInstance().format(obj);
87 }
88
89 public String format(Calendar obj){
90 return SimpleDateFormat.getInstance().format(obj.getTime());
91 }
92 ...



This code won't compile, for the simple reason that during compilation, the parameterized types with Date and Calendar are lost and both resolve to the same Handler interface, and you are of course not allowed to implement the same interface twice (one of the reasons for why delegates exists in C#, but that's another story).

I'll try to make more use of this type-safe way of applying composition to factor away centralized conditional complexity. And it should be interesting having it work together with the service provider pattern.


Update
It turns out there's a trick to get rid of the unchecked cast in the SomeAPI class. The trick is to use a dynamic cast to make up for a limitation in the Java type system and is mentioned in Joshua Bloch's Effective Java Second Edition, item 29.

Post a Comment