When refactoring a design that uses the
NullObject pattern, one must beware of refactorings that change the system to use
NullObject classes to represent domain concepts, rather than being implementation details. This goes against the intent of the
NullObject pattern and will cause headaches at later date.
Here's an example from my own experience with the
SceneBeans framework.
The
SceneBeans framework provides a
SceneGraph data structure that defines a graphical scene as a directed graph of polymorphic scene-graph "nodes" that are processed using the
VisitorPattern. We used the
NullObject pattern to mark the edges of the
SceneGraph, and combined the
NullObjectAndVisitor patterns so that the
NullObject nodes did not
DoubleDispatch to the visitor -- as far as the visitors knew, the
NullObject nodes did not even exist.
One of the scene graph classes was a "switch" node that would contain multiple subgraphs but only draw one at a time. We later needed to selectively show or hide parts of the scene. We realised that we could do this by putting a subgraph
and a
NullObject node into to a switch node -- switching between the subgraph and the
NullObject would have the effect of showing or hiding the subgraph. This only required changing our file format so that files could explicitly create null objects in the graph. Cunning!
Or so we thought... We noticed a problem when we wrote a visitor to write a scene graph into a file in our format. The visitor never wrote the
NullObjects because they did not double dispatch to it.
This was fine for
NullObjects at the edge of the graph,
but was the wrong behaviour for
NullObjects that had been explicitly included in "switch" nodes.
The problem was that our refactoring had changed the way that
NullObjects were used from an internal implementation detail of how the framework marked the edges of the scene graph to explicitly representing concept of "draw nothing" in a user-visible way.
Solutions...
- Add a visitNull method to the visitor interface, and provide a do-nothing default implementation in the abstract base class from which all visitors are derived. (We chose this one)
- Use different classes for Null Objects that mark the edge of the graph from those used to represent "draw nothing". (The most elegant approach, but adds classes to the system)
- Explicitly check for NullObjects in the save-to-file visitor. (A quick-and-dirty fix that we ruled out for obvious reasons)
--
NatPryce
Is solution #2 unacceptable because of FearOfAddingClasses?
Solution #2 is
not unacceptable at all. I would say that it's the ideal solution in most cases. However, we
SceneBeans is a client-side library to be downloaded across the net, and so we are trying to keep download times to an absolute minimum. --
NatPryce
Can anyone explain the conceptual difference between serialization of "edges" and serialization of "show nothing" objects in this example?
The difference is that a NullObject representing the "edge" of a graph is implicitly created by the data structure itself as part of it's internal housekeeping (in constructors, or when removing a child node from it's parent), and is therefore an implementation detail that is invisible to the user.
A "show nothing" object is explicitly created by the user when they want to be able to toggle the visibility of a sub-graph by switching between the sub-graph and the "show-nothing" object.
And doesn't
NullObject always represent some domain concept, which usually is not clearly exposed because of its "non-object" nature? --
NikitaBelenki
It represents the edge of a data structure, but in a way that is invisible to client code. It is therefore an internal implementation detail of the data structure, and so not a domain concept.
CategoryNull