Tuesday, September 06, 2005

Dynamic delegation in Java without ugly InvocationHandlers

I have also seen dynamic proxies (the Proxy and InvocationHandler classes in J2SE) be used for a lot of situations where what the programmer really wants is something more like dynamic delegation.

The design of the InvocationHandler.invoke() works great for when you want to do the same thing no matter what method is being invoked. For instance, logging:


class MyInvocationHandler implements InvocationHandler {

public MyInvocationHandler(Object realObject) {
this.realObject = realObject;
}

private Object realObject;

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// log it
log.debug(m);

// now run it
m.invoke( realObject, args);
}
}


But one often sees these invocation handlers start to grow hair like this:


private Object realObject;

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if( m.getName().equals( "foo" )) {
// .. insert any special logic for foo method here ..

// followed by actually running the method
m.invoke( realObject, args);
} else if (m.getName().equals( "bar" )) {
// swallow the call to bar() and don't pass it on!
} else if (m.getName().equals( "baz" )) {
throw new SomeException("you can't call baz()!");
} else {
// any other method, just call it straight through
m.invoke( realObject, args);
}

}


While the above will get the job done, it is about as anti-OOP, anti-Design Patterns, and just plain anti-Java-esque a method as you could possibly invent. It violates one of the first rules we learned as Java programmers, which is that switch/case statements (of which the above is really an instance, if only Java supported switching on an object) are a relic from procedural languages and should be replaced by more modern constructs. You'll get this advice from Effective Java, you'll get it from Fowler's Refactoring. How can we remove this ugliness?

What is the code actually trying to do? It wants to match the Method object to any custom invocation code that needs to run when that Method is called. This is a mapping operation, hence this block of if/else statements is really trying to be a Map.

Now, a first approximation of our goal might look something like this:


private Map handlersMap = new HashMap();

public MyInvocationHandler(Object realObject) {
this.realObject = realObject;

handlersMap.put("foo", new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// special code for foo()
}
);

// add mappings for bar(), baz(), etc..

}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = (InvocationHandler) handlersMap.get(method.getName());

if( h != null ) {
h.invoke(proxy,method,args);
} else {
method.invoke(realObject,args);
}
}


Big friggin deal, right? So far all we've done is shifted most of the code into the constructor that builds up the map, and made ourselves look smarter by using some anonymous classes. But considering how much reflection we are using already, maybe we could ue just a bit more to get rid of some of those classes without having to re-introduce the old switch block. Maybe the map shouldn't contain a bunch of baby InvocationHandlers, maybe it should contain Methods. Our Methods! Since we are now dealing with methods more directly, we now need to know the signatures of the methods that we want to intercept, as well as their names. For this purpose, let's say Foo takes and int, bar takes a String, and baz is void.


private Map methodsMap = new HashMap();

methodsMap.put( "foo", getClass().getMethod("foo", new Class[]{ int.class });
methodsMap.put( "bar", getClass().getMethod("bar", new Class[]{ String.class });
methodsMap.put( "bar", getClass().getMethod("baz", new Class[]{ });

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method myMethod = (Method) handlersMap.get(method.getName());

if( myMethod != null ) {
// if it's in the map, run the one from the map
myMethod.invoke(this,args);
} else {
// otherwise just run the method on the original object
method.invoke(realObject,args);
}
}

public int foo(int x) {
// .. any custom stuff goes here ...
return (SomeInterface) realObject.foo(x);
}

public void bar(String s) {
// do nothing! hahaha
}

public boolean baz() {
throw new SomeException("you can't call baz()!");
}


Nice, huh? The above looks nifty, but it isn't quite correct.. the problem is that the SomeInterace/realObject might have multiple methods of the same name which again requires the argument types for disambiguation. So really method's whole signature should be the key to the HashMap. Now, you could just use an ArrayList or something to hold the name and the argument classes, but let's be nice to ourselves and make a helper class for this:


class MethodSig extends HashKey {
public MethodSig(Method m) {
super( m.getName(),
java.util.Arrays.asList( m.getParameterTypes()) );

}
}


MethodSig extends HashKey, which is one of the stupidest classes you could possibly imagine, but which I have actually written for use in real code. Someone please tell me that some new language feature in Tiger or later makes this class unneccessary!


public class HashKey extends AbstractCollection implements Collection, java.io.Serializable {

private Collection keys;

/** Creates a new instance of HashKey */
public HashKey(Object key1) {
this( new Object[]{ key1 });
}

/** Creates a new instance of HashKey */
public HashKey(Object key1, Object key2) {
this( new Object[]{ key1, key2});
}

/** Creates a new instance of HashKey */
public HashKey(Object key1, Object key2, Object key3) {
this( new Object[]{ key1,key2,key3});
}

// repeat constructors for as many different objects as you might want..

/** Creates a new instance of HashKey */
public HashKey(Object[] keys) {
this( Arrays.asList(keys));
}


/** Creates a new instance of HashKey */
public HashKey(Collection keysToAdd) {
keys = keysToAdd;
}

public boolean equals(Object obj) {

if( !(obj instanceof HashKey) )
return false;

HashKey that = (HashKey) obj;

return keys.equals( that.keys);

}

public String toString() {
return keys.toString();
}

public int hashCode() {
return keys.hashCode();
}

public int size() {
return keys.size();
}

public Iterator iterator() {
return keys.iterator();
}

}


Now, using MethodSig, we get this:


public class MyInvocationHandler implements InvocationHandler {
private Map methodsMap = new HashMap();

private void addMethod(Method m) {
methodsMap.put( new MethodSig(m), m);
}

public MyInvocationHandler(Object realObject) {
this.realObject = realObject;

addMethod( getClass().getMethod("foo", new Class[]{ int.class });
addMethod( getClass().getMethod("bar", new Class[]{ String.class });
addMethod( getClass().getMethod("baz", new Class[]{ });
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method myMethod = (Method) handlersMap.get( new MethodSig(method) );

if( myMethod != null ) {
// if it's in the map, run the one from the map
myMethod.invoke(this,args);
} else {
// otherwise just run the method on the original object
method.invoke(realObject,args);
}
}

public int foo(int x) {
// .. any custom stuff goes here ...
return (SomeInterface) realObject.foo(x);
}

public void bar(String s) {
// do nothing! hahaha
}

public boolean baz() {
throw new SomeException("you can't call baz()!");
}
}


That code that repeated lycalls addMethod() is the last vestige or the original if/else block. How can we get rid of it? That's when the bolt of lightning hits you. Duh, these methods are declared right on our own class, we can use reflectioin to find them, and loop over them! Our code now becomes:



public class MyInvocationHandler implements InvocationHandler {
private Map methodsMap = new HashMap();

private void addMethod(Method m) {
methodsMap.put( new MethodSig(m), m);
}

public MyInvocationHandler(Object realObject) {
this.realObject = realObject;

Method[] methods = getClass().getMethods();
for(i =0; i<methods.length; i++) {
addMethod( methods[i]);
}
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method myMethod = (Method) handlersMap.get( new MethodSig(method));

if( myMethod != null ) {
// if it's in the map, run the one from the map
myMethod.invoke(this,args);
} else {
// otherwise just run the method on the original object
method.invoke(realObject,args);
}
}

public int foo(int x) {
// .. any custom stuff goes here ...
return (SomeInterface) realObject.foo(x);
}

public void bar(String s) {
// do nothing! hahaha
}

public boolean baz() {
throw new SomeException("you can't call baz()!");
}
}


Tada! We have essentially achieved the dynamic delegate pattern (which is accomplished in C++ using templates, and in dynamic scripting languages, etc.) using nothing but plain old reflection. No code generation, no bytecode maniplation, no generics required.

Next time, I will show you what the above ended up looking like after I spent several more iterations refactoring it.

Comments: