public class Auditor implements TreeListener {
private Map<String,Date> map = new HashMap<String,Date>();
public void nodeAdded(Node n) {
map.put(n.id(), new Date());
}
public void nodeRemoved(Node n) {
Date d = map.get(n.id());
String s = d.toString(); // <-- NPE is here!!
map.removeKey(n.id());
// some other things ...
}
}
My unit tests told me that the Auditor and Tree class are working fine - the tree fires the correct events and the Auditor reacts correctly. Of course, a Green bar does not guarantee "no bugs" so I double checked the code and it seemed fine.Indeed the problem was elsewhere. Turns out that a bug in my app caused each listener to be registered twice. Auditor was not expecting to be called twice for each event, hence the NPE.
So I fixed the bug. Then I went back to the addListener() method of the Tree class. It is a very simple method:
public class Tree {
private List<TreeListener> listeners
= new ArrayList<TreeListener>();
public void addListener(TreeListener arg) {
listeners.add(arg);
}
// Additional methods ...
}
Looking at the code I tried to think if this method should be changed in order to prevent future occurrences of this bug. As I was thinking about it I realized that this simple method is an excellent illustration to one software's most fundamental truths:
Programming is about making (design) decisions.
I am not the first one to say this (see Joakim Holm's "Programming is all Design" or Alistair Cockburn). Here's the (probably partial) list of decision that a programmer has to make when writing a one-liner method that should simply add a listener to a list.
- Should the listeners field be private? Maybe we want subclasses to manipulate/inspect it in some way, which calls for
protected
visibility. - What is the order of notification? Should we use a "first-registered first-notified" policy? or maybe "last-registered first-notified". Is order important at all? If not then maybe we would like to force the application not to depend on the order by random shuffling?
- Can a listener be registered with more than one Tree object? If so, then the listener interface should probably specify the originating tree, not just the node
- Can a listener be registered more than once with a single Tree object? Usually the answer is no, but there are scenarios where multiple registration does make sense.
- Assuming each listener may be registered exactly once, how do we deal with multiple registrations?
- Throw an exception?
- A standard Java assertion? Could be disabled which may be good or bad depending on context.
- Throw an
AssertionError
? Has a more dramatic effect than a plain Exception. Also will be reported by JUnit as a failure rather than an error. Again, may be either good or bad. - Silently ignore. Return from the method without adding the listener. This seems very natural but it has the drawback that it hides the problem. If I choose to silently ignore then I will get no notification about future problems. How about logging?
- Return a Boolean value indicating whether the listener was already registered. Puts the responsibility on the client code to check this value. Will not always happen.
- Do I need to provide some thread protection?
- Define the method as synchronized?
- Define the listeners field as a ConcurrentLinkedList?
- How should this list of listeners interact with the Garbage Collector? Should a listener that is only accessible from this list be collectible (by storing it as a
WeakReference
)? A "No" answer puts additional burden on client code: must remove listeners from the Tree object when they are no longer needed. If listeners are dynamically added as the app is running this may become a big problem.
Note that choosing weak references is not always a good idea. Some listeners are only accessible from the listened-to object. Take for example a listener that inspects the tree and updates the title of the main window. It is created, pushed onto the listeners list and all other traces to it are lost. A weak reference will make such a listener disappear with the very first collection cycle. - Do I want to refactor the whole "listeners" concern into a dedicated strategy class. This will improve testability and decoupling.
As I said this is a partial list. Point is simple: a programming task requires much more decision (and mental energy) that what initially seems. In other words: If you want to design everything upfront you'll end up programming upfront but without the assistance of tools such as IDE, Unit tests, Refactoring, and the likes. Good luck.
Some decisions totally drain your mental energy.
Over the years I've came up with a number of ways to relax my conscience:
1. I first go with the most minimalistic implementation there is. Promising myself additional refactoring iterations later if I'll feel they are called for. More than often, I later on decide that the code is good enough, and I move on.
2. I document ALL of my concerns in the code's JavaDoc. Preferably with standard annotations (e.g., "@Immutable") This really help alleviating concerns. Even if I won't have time to improve the code later, someone else would have this option.
3. I look up to the Java standard library for exactly this sort of design trade off decisions. If James Gosling choose one way, I know I can't be that off in my choice (hopefully).
Gili Nachum
August 27, 2010 at 1:43 PM