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 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
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 NullObject
s because they did not double dispatch to it.
This was fine for NullObject
s at the edge of the graph,
but was the wrong behaviour for NullObject
s that had been explicitly included in "switch" nodes.
The problem was that our refactoring had changed the way that NullObject
s 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.
- 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)
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.