Mixins are powerful programming concept in dynamic languages because they allow you to implements aspects of classes in different places and then "plug" them together. For example, the "tree" aspect of a data structure (something having parents and children) is well understood. A lot of data can be arranged in hierarchic trees. Yet, in many languages, you cannot say:
class FileTreeNode extends File mixin TreeNode
to get a class which gives you access to all file operations and allows to arrange the items in a tree at the same time. This means you can't directly attach it to a tree viewer. In some languages, like Python, this is trivial since you can add methods to a class any time you want. Other languages like C++ have multiple inheritance which allows to do something like this. Alas, not at runtime.
For Java, the Eclipse guys came up with a solution: adapters. It looks like this:
publicT getAdapter (Class desiredType) { ... create an adapter which makes "this" behave like "desiredType" ... }
where "desiredType" is usually an interface of some kind (note: The Eclipse API itself is still Java 1.4, this is the generics version to avoid the otherwise necessary cast).
How can you use this?
In the most simple case, you can just make the class implement the interface and "return this" in the adapter. Not very impressive.
The next step is to create a factory which gets two bits of information: The object you want to wrap and the desired API. On top of that, you can use org.eclipse.core.internal.runtime.AdapterManager which allows to register any number of factories for adapters. Now, we're getting somewhere and the getAdapter() method could look like this:
@SuppressWarnings("unchecked") publicT getAdapter (Class desiredType) { return (T)AdapterManager.getDefault ().getAdapter (this, desiredType); }
This allows me to modify the behavior of my class at runtime more cheaply and safely than using the Reflection API. Best of all, the compiler will complain if you try to call a method that doesn't exist:
ITreeNode node = file.getAdapter(ITreeNode.class); ITreeNode parent = node.getParent(); // okay node.lastModification(); // Sorry, this is a file method
Try this with reflection: Lots of strings and no help. To implement the above example with an object that has no idea about trees but which you want to manage in a tree-like structure, you need this:
- A factory which creates a tree adapter for the object in question.
- The tree adapter is the actual tree data structure. The objects still have no idea they are in a tree. So adding/removing objects will happen in the tree adapter. Things get complicated quickly if you have some of the information you need for the tree in the objects themselves. Think files: You can use listFiles() to get the children. This is nice until you want to notify either side that a file has been created or deleted (and it gets horrible when you must spy on the actual filesystem for changes).
- The factory must interact with the tree adapter in such a way that it can return existing nodes if you ask twice for an adapter for object X. This usually means that you need to have a map to lookup existing nodes.
A very simple example how to use this is to allow to override equals() at runtime. You need an interface:
interface IEquals { public boolean equals (Object other); public int hashCode (); }
Now, you can define one or more adapter classes which implement this interface for your objects. If you register a default implementation for your object class, then you can use this code in your object to compare itself in the way you need at runtime:
public boolean equals (Object obj) { return getAdapter (IEquals.class).equals (obj); }
Note: I suggest to cache the adapter if you don't plan to change it at runtime. This allows you to switch it once at startup and equals() will still be fast. And you should not try to change this adapter when the object is stored as a key in a hashmap ... you will have really strange problems like set.put(obj); ... set.contains(obj) -> false etc.
Or you can define an adapter which looks up a nice icon depending on the class or file type. The best part is that you don't have API cross-pollution. If you have a file, then getParent() will return the parent directory while if you look at the object from the tree API, it will be a tree node. Neither API can "see" the other, so you will never have to rename methods because of name collisions. ITreeNode node = file.getAdapter(ITreeNode.class) also clearly expresses how you look at the object from now on: as a tree. This makes it much more simple to write reliable, reusable code.
1 comment:
This seems to be very similar to COM's QueryInterface method, which all COM interfaces must support. Only, one doesn't have to deal with all the reference counting weirdness. I definitely approve.
Post a Comment