Monday, February 20, 2017

Enhancing (extending) a class using dynamic proxy classes in Java

The Decorator Design Pattern is quite simple to understand and implement. Usually, in its simple form, it goes like this:
public interface A {
String foo();
}
view raw A.java hosted with ❤ by GitHub
public class ADecorator implements A {
private final A delegate;
public ADecorator(A a) {
this.delegate = a;
}
public String foo() {
return "*** " + this.delegate.foo() + " ***"; //will return "*** foo ***"
}
}
view raw ADecorator.java hosted with ❤ by GitHub
public class AImpl implements A {
public String foo() {
return "foo";
}
}
view raw AImpl.java hosted with ❤ by GitHub
In this example we have an interface A with a concrete implementation AImpl. We then implement ADecorator, a concrete decorator which wraps an instance of A and decorates the return value of the foo method.

We can also use this pattern not just to decorate but also enhance with additional functionality (like in java.io.Reader implementations). Continuing our example above, with some changes:
public interface AB extends A {
String bar();
}
view raw AB.java hosted with ❤ by GitHub
public class ABDecorator implements AB {
private final A delegate;
public ABDecorator(A a) {
this.delegate = a;
}
public String foo() {
return this.delegate.foo();
}
public String bar() {
return "bar ";
}
}
In this example we did not want to change the return value of foo(), we wanted to enhance the functionality of A, in this case with the additional method bar(). Now, think about what would happen if A had more than one method. You would have to implement all those methods and delegate to the encapsulated instance of A.

Enhancing Using Dynamic Proxy Class

A way to avoid all this work and verbose code is to use Dynamic Proxy Classes:
public abstract class ABFactory {
private ABFactory() { /* prevent instantiation */ }
public static AB createAB(A a) {
B b = new BImpl(a);
return (AB) Proxy.newProxyInstance(a.getClass().getClassLoader(),
new Class[]{AB.class}, (proxy, method, args) -> {
Class<?>[] declaringClassInterfaces = method.getDeclaringClass().getInterfaces();
List<Class<?>> interfaces = new ArrayList<>(Arrays.asList(declaringClassInterfaces));
interfaces.add(method.getDeclaringClass());
if (interfaces.contains(A.class)) {
return method.invoke(a, args);
} else if (interfaces.contains(B.class)) {
return method.invoke(b, args);
} else {
throw new NoSuchMethodException("Could not find proxied method: " +
method.toGenericString());
}
});
}
private static class BImpl implements B {
private final A a;
private BImpl(A a) {
this.a = a;
}
@Override
public String bar() {
return a.foo() + "bar";
}
}
}
view raw ABFactory.java hosted with ❤ by GitHub
A a = new AImpl();
...
AB ab = ABFactory.create(a);
System.out.println(ab.foo()); //prints "foo"
System.out.println(ab.bar()); //prints "foo & bar"

In this example we define BImpl which is a private implementation of B - an interface with the extended functionality. BImpl takes an instance of A and encapsulates it so it can use it for its implementation. The create method creates a proxy object for interface AB (which is A & B). In the proxy, for each method we delegate to the corresponding object according to the origin class of the method.

Limitations


  1. This implementation relies on the knowledge that A and B are distinct - there are no shared methods. Otherwise we might delegate to the wrong object.
  2. Overriding a method from A in the proxy is possible but requires more effort to do it right and robust (mainly to method renaming).