Thursday, June 24, 2010

Duck typing in Java

In OO programming duck typing means an object is defined by what it can do, not by what it is. A statement calling a method on an object does not rely on the declared type of an object, only that the object must implement the method called. This concept is a form of inductive reasoning: "when I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck." - James Whitcomb Riley

Several languages advocate it or at least have support for it: Python, Ruby, PHP, C#, etc.. I personally highly prefer static typing for it's better compile-time type safety but duck typing may have justified uses too. The ability to use polymorphism without class inheritence constraints may be handy in certain scenarios. Why not keep that option open in Java too? The idea can be easily illustrated through the following example.

The duck

I expect a duck to be capable of this:
public interface Duck {
    void quack();
}
A few concrete animals (some of them can quack(), but none of them implements the Duck interface):
public class SilentDuck {
    public void quack() {
        System.out.println("quack");
    }
}

public class LoudDuck {
    public void quack() {
        System.out.println("QUACK");
    }
}

public class AverageDog {
    public void bark() {
        System.out.println("bark");
    }
}

public class TalentedDog {
    public void bark() {
        System.out.println("superior bark");
    }

    public void quack() {
        System.out.println("quacklikesound");
    }
}
The expectations

Since SilentDuck, LoudDuck and TalentedDog all can quack(), I should be able to use those objects through the Duck interface - even though their classes don't implement that interface explicitly.
Duck duck1 = attach(Duck.class, new SilentDuck());
Duck duck2 = attach(Duck.class, new LoudDuck());
Duck duckImpersonator = attach(Duck.class, new TalentedDog());

duck1.quack();
duck2.quack();
duckImpersonator.quack();
List<Duck> duck = Arrays.asList(duck1, duck2, duckImpersonator);
AverageDog can't quack(). I expect to get runtime exception when I try to attach it to the Duck interface.
Duck wannabeDuck = attach(Duck.class, new AverageDog()); // throws exception - no quack()
Risks

While duck typing has benefits, it has many drawbacks too. Type checking takes place at runtime instead of compile time. The concrete classes don't refer to a dynamic interface directly, simply renaming a method in them may cause runtime problems in client code unintentionally. Relying on such coding style calls for different, careful practices.

Implementation
I implemented a simple dynamic interface attachment tool to make the above code sample work:
public class DynamicInterface {
    public static <I> I attach(Class<I> i, Object o) {
        try {
            ensureMethodsExist(i, o);
            ensureIsInterface(i);
            return attachInterface(i, o);
        } catch (Exception e) {
            throw new DynamicInterfaceException(e);
        }
    }

    @SuppressWarnings("unchecked")
    private static <I> I attachInterface(Class<I> i, Object o) {
        Object proxy = Proxy.newProxyInstance(i.getClassLoader(), new Class[]{i}, new DynamicInterfaceHandler(o));
        return (I) proxy;
    }

    private static <I> void ensureMethodsExist(Class<I> i, Object o) throws NoSuchMethodException {
        for (Method method : i.getDeclaredMethods()) {
            if (o.getClass().getMethod(method.getName(), method.getParameterTypes()) == null)
                throw new NoSuchMethodException(method.getName());
        }
    }

    private static <I> void ensureIsInterface(Class<I> i) {
        if (!i.isInterface())
            throw new DynamicInterfaceException(i.getName() + " is not an interface");
    }
}

class DynamicInterfaceHandler implements InvocationHandler {
    private Object o;

    DynamicInterfaceHandler(Object o) {
        this.o = o;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method oMethod = o.getClass().getMethod(method.getName(), method.getParameterTypes());
        return oMethod.invoke(o, args);
    }
}

public class DynamicInterfaceException extends RuntimeException {
    public DynamicInterfaceException(Throwable t) {
        super(t);
    }

    public DynamicInterfaceException(String m) {
        super(m);
    }
}

No comments:

Post a Comment