I came across a cool design problem at work today, so I thought I'd share my approach to it. Let me know what you think!
Names of types have been changed to protect the guilty.
Programmers are often faced with the problem of adding functionality to pre-existing types. This is commonly solved in most OO languages by either
- extending an existing class and adding the behavior (either by calling super or, if the parent class was designed for extension, implementing template methods).
- decorating an instance of the existing type with another object that delegates most method calls to the decorated instance and adds new functionality where appropriate.
We often prefer using decoration/composition over inheritance in these cases because
- the decorator can be instantiated after the object of the existing type has already been instantiated
- the decorator doesn't share any state with the original type, discouraging violations of data hiding
However, decorating objects can prove problematic. Consider the following linked list implementation:
public interface INode { public INode getNext(); public String getValue(); } public class Node implements INode { private INode _next; private String _value; //constructor and next setter omitted for brevity public INode getNext() { return _next; } public String getValue() { return _value; } }Assume we're somehow motivated to add some behavior or state to every item in this list. We could write ourselves a decorator type:
public class DecoratedNode implements INode { public INode getNext() { return _decorated.getNext(); } public String getValue() { return do something funky to _decorated.getValue(); } }
However, this approach leads to trouble when we start using object identity to, for example, index values in a Map. In order for the decorator pattern to work, we always need to create new objects. We could make sure that all of our decoration is done prior to using our objects in Maps. However, that seems very brittle and easy to overlook.
We could patch the above example by adding a method, e.g. getDecoratedNode(), to our INode type:
... in INode ... public INode getDecoratedNode(); ... in Node ... public INode getDecoratedNode() { return this; } ... and in DecoratedNode ... public INode getDecoratedNode() { return _decorated.getDecoratedNode(); }
This approach even solves the problem of multiple decorators effectively. However, it seems like a very inelegant design. If we forget to call getDecoratedNode() even once when keying on our Map, we'll introduce an error that's extremely hard to spot.
However, it seems to me that we could use decoration to solve this problem as well. If our problem is maintaining object identity, then let's separate the object providing identity, which must always stay the same, with the object at the top of the decoration chain, which must change. Let's change the purpose of the Node class from implementing a Node to serving the functionality of being a decorator or wrapper for an implementation type of INode.
public interface INode { public INode getValue(); public INode getNext(); public INode getDelegate(); public setDelegate( INode value ); } public class Node implements INode { private INode _delegate; public INode getValue() { return _delegate.getValue(); } public INode getNext() { return _delegate.getNext(); } public INode getDelegate() { return _delegate; } public setDelegate( INode value ) { _delegate = value; } } // Actual, default implementation. This code used to be in Node public class NodeDelegate implements INode { private String _value; private INode _next; public INode getValue() { return _value; } public INode getNext() { return _next; } public INode getDelegate() { return this; } public setDelegate( INode value ) { } }
Now, when we write our DecoratedNode type, we can inherit from Node to remove the need to type out all of our delegation:
public class DecoratedNode extends Node { @Override public String getValue() { return do something funky to decorated.getValue(); } }
In this fashion, we can always refer to the original Node object for the purposes of indexing in associative arrays. When we need to add a layer of decoration, all we need to do is decorate our Node's delegate:
public void decorateMyNode( INode head ) { DecoratedNode result = new DecoratedNode(); result.delegate = head.delegate; head.delegate = result; }
We're now maintaining object identity totally separately from the object's functionality! This means that we don't ever have to go around updating references in various places when we want to add functionality.
Update: Phil Riley (of RS) has brought to my attention the fact that if your associative data structure supports a real hashing algorithm (like the one in the java collections framework does), then this problem is solved. However, if you're using a data structure like Actionscript's Dictionary (the issue I was facing, despite my examples being in Java), then this is a problem. Thanks, Phil!
No comments:
Post a Comment