Next create your copy function taking an Object as the initial parameter and internally coercing to the Readable interface:
Note that not only will this method now work with InputStream and Reader, but with
any other class that has a read method with the same semantics.
Another example where polymorphism is difficult in Java is when using JDK 1.5 Annotations. An annotation is a pseudo-interface that represents meta-data about a class (or elements of a class). However, apart from the implictly extended Annotation interface there is no way to access annotations polymorpically. While this is not a big deal for most people, if you find yourself writing code that processes a lot of annotations, you will find yourself writing a bunch of duplicated code to deal with different annotation types.
However, with Duck Typing it becomes possible to write an annotation processor that
can deal polymorphically with annotations.
Let's suppose we are wanting to write a simple persistence layer for some classes
using annotations. We start by adding the following annotations to classes:
@Persistent
@Immutable
@Transient
We will then create a PersistenceHandler strategy interface that will be used by our annotation processor to work out what to do with these annotations:
interface PersistenceHandler {
void persist(Object object);
}
We define the annotations so they include a default handler implementation.
public @interface Persistent {
Class handler() : default PersistentPersistenceHandler.class;
}
public @interface Immutable {
Class handler() : default ImmutableHandler.class;
}
public @interface Transient {
Class handler() : default TransientPersistenceHandler.class;
}
Next we define the interface we are going to use access the handler attribute of
the annotation:
public interface PersistenceAnnotation {
Class handler();
}
And lastly we write our annotation processor:
void processAnnotations(Object object) throws Exception {
for (Annotation annotation : object.getClass().getAnnotations()) {
DuckType annotationToCoerce = coerce(annotation);
if (annotationToCoerce.quacksLikeA(PersistenceAnnotation.class)) {
Class handler =
annotationToCoerce.to(PersistenceAnnotation.class).handler();
handler.newInstance().persist(object);
}
}
}
The nice thing about this is we have completely decoupled our code from which
particular annotations it can support. We can add new annotations as much as we
like, and providing they support the handler() attribute then we can support them. Also we can add other annotations to our classes that we don't handle and these are simply ignored.
Allowing mocking/testing of legacy classes that don't have interfaces
If you are writing your code using Test Driven Development you'll know about the power of mocking and dependency injection to make your code testable. Using a package such as EasyMock it is very easy to quickly create Mocks/Stubs of classes that would be very difficult to otherwise test.
The way these things work is they generate a Dynamic Proxy for the Interface you want to mock and then allow you to specify your expectations of how that class will be called.
That's all well and good, but it assumes that the things you want to mock are already Interfaces[
3] and not concrete classes that you have no ability to make changes over.
Sounds like a problem for Duck typing. Let's use for example the java.io.File class. File has a large number of methods, but typically we'll only want to be using a couple of them. Let's define the interface that we want to use:
interface DeleteableFile {
String getName();
String exists();
boolean delete();
}
Now the method we want to test should be written as:
public deleteTmpFileIfExists(Object fileToDelete) {
DeletableFile file = coerce(fileToDelete).to(DeletableFile.class);
if (file.exists() &&
Pattern.matches("(\\.tmp|~)$", file.getName()) {
file.delete();
}
}
We can now test this with a mock using the DeletableFile interface or a standard
File object.
Creating an encrypted String class
One of the decisions made by the early Sun engineers was to make the String class final. This was done mainly for security reasons that I won't bore you with here, but suffice it to say it makes it very difficult to do a lot of extended String classes that add more functionality. Let's suppose we have an encrypted String class that apes the standard String interface:
public final class EncryptedString {
...
}
By now the process for writing methods that handle both this and a standard String should be fairly obvious. We create the interface with the methods we care about:
interface StringLike {
boolean startsWith(String prefix);
String subString(int beginIndex);
}
And then we use Duck Typing to refer to either:
String skipLeadingSlash(Object stringLikeThing) {
StringLike s = coerce(stringLikeThing).to(StringLike.class);
if (s.startsWith("/")) {
return s.subString(1);
}
}
Parting words
While Duck Typing is a useful concept, it's not all sweetness and light. You do give up a significant amount of compile-time checking to make the concept work. However, if you are practicing Test Driven Development (and give yourself a slap now if you are not), then with good testing you can make this fact largely irrelevant. What duck typing does give you is an incredibly flexible mechanism for polymorphism that buys out many of the disadvantages of inheritance based approaches. Try it in your own code and see what you can do.
Appendix A: DuckType.java code listing
package example;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Allows "duck typing" or dynamic invocation based on method signature rather
* than type hierarchy. In other words, rather than checking whether something
* IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck.
*
* To use first use the coerce static method to indicate the object you want to
* do Duck Typing for, then specify an interface to the to method which you want
* to coerce the type to, e.g:
*
* public interface Foo {
* void aMethod();
* }
* class Bar {
* ...
* public void aMethod() { ... }
* ...
* }
* Bar bar = ...;
* Foo foo = DuckType.coerce(bar).to(Foo.class);
* foo.aMethod();
*
*
*/
public class DuckType {
private final Object objectToCoerce;
private DuckType(Object objectToCoerce) {
this.objectToCoerce = objectToCoerce;
}
private class CoercedProxy implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Method delegateMethod = findMethodBySignature(method);
assert delegateMethod != null;
return delegateMethod.invoke(DuckType.this.objectToCoerce, args);
}
}
/**
* Specify the duck typed object to coerce.
*
* @param object the object to coerce
* @return
*/
public static DuckType coerce(Object object) {
return new DuckType(object);
}
/**
* Coerce the Duck Typed object to the given interface providing it
* implements all the necessary methods.
*
* @param
* @param iface
* @return an instance of the given interface that wraps the duck typed
* class
* @throws ClassCastException if the object being coerced does not implement
* all the methods in the given interface.
*/
public T to(Class iface) {
assert iface.isInterface() : "cannot coerce object to a class, must be an interface";
if (isA(iface)) {
return iface.cast(objectToCoerce);
}
if (quacksLikeA(iface)) {
return generateProxy(iface);
}
throw new ClassCastException("Could not coerce object of type "
+ objectToCoerce.getClass() + " to " + iface);
}
private boolean isA(Class iface) {
return objectToCoerce.getClass().isInstance(iface);
}
/**
* Determine whether the duck typed object can be used with
* the given interface.
*
* @param Type of the interface to check.
* @param iface Interface class to check
* @return true if the object will support all the methods in the
* interface, false otherwise.
*/
public boolean quacksLikeA(Class iface) {
for (Method method : iface.getMethods()) {
if (findMethodBySignature(method) == null) {
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
private T generateProxy(Class iface) {
return (T) Proxy.newProxyInstance(iface.getClassLoader(),
new Class[] { iface }, new CoercedProxy());
}
private Method findMethodBySignature(Method method) {
try {
return objectToCoerce.getClass().getMethod(method.getName(),
method.getParameterTypes());
} catch (NoSuchMethodException e) {
return null;
}
}
}
Footnotes
[1] Attributed to Alex Martelli in
a message to the comp.lang.python newsgroup [Wikipedia].
[2] Adapted from the example in the [
Wikipedia Duck Typing Article.[3] More recent versions of EasyMock do class instrumentation to overcome this limitation, but we'll ignore that for now.