Null Object Design Pattern

Intent

In most object-oriented languages, such as Java or C#, references may be null. These references need to be checked to ensure they are not null before invoking any methods because methods typically cannot be invoked on null references. Instead of using a null reference to convey the absence of an object (for instance, a non-existent customer), one uses an object which implements the expected interface, but whose method body is empty. The advantage of this approach over a working default implementation is that a Null Object is very predictable and has no side effects: it does nothing.

Structure


Participants

Client 
  • requires a collaborator.
AbstractObject 
  • declares the interface for Client's collaborator.
  • implements default behavior for the interface common to all classes, as appropriate.
RealObject
  • defines a concrete subclass of AbstractObject whose instances provide useful behavior that Client expects.
NullObject
  • provides an interface identical to AbstractObject's so that a null object can be substituted for a real object.
  • implements its interface to do nothing. What exactly it means to do nothing depends on what sort of behavior Client is expecting.
  • when there is more than one way to do nothing, more than one NullObject class may be required.

Collaborations

Clients use the AbstractObject class interface to interact with their collaborators. If the receiver is a RealObject, then the request is handled to provide real behavior. If the receiver is a NullObject, the request is handled by doing nothing or at least providing a null result.

Source code

Null Object pattern replaces null values with neutral objects. Many times this simplifies algorithms since no extra null checks are needed.
In this example, we build a binary tree where the nodes are either normal or Null Objects. No null values are used in the tree making the traversal easy.

Step 1: By referring to the class diagram, let's create the interface - Node interface
/**
 * 
 * Interface for binary tree node.
 *
 */
public interface Node {

  String getName();

  int getTreeSize();

  Node getLeft();

  Node getRight();

  void walk();
}
Step 2: Create Implementation for binary tree's normal nodes.
public class NodeImpl implements Node {

  private static final Logger LOGGER = LoggerFactory.getLogger(NodeImpl.class);

  private final String name;
  private final Node left;
  private final Node right;

  /**
   * Constructor
   */
  public NodeImpl(String name, Node left, Node right) {
    this.name = name;
    this.left = left;
    this.right = right;
  }

  @Override
  public int getTreeSize() {
    return 1 + left.getTreeSize() + right.getTreeSize();
  }

  @Override
  public Node getLeft() {
    return left;
  }

  @Override
  public Node getRight() {
    return right;
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public void walk() {
    LOGGER.info(name);
    if (left.getTreeSize() > 0) {
      left.walk();
    }
    if (right.getTreeSize() > 0) {
      right.walk();
    }
  }
}
Step 3: Null Object implementation for binary tree node.
Implemented as Singleton, since all the NullNodes are the same.
public final class NullNode implements Node {

  private static NullNode instance = new NullNode();

  private NullNode() {}

  public static NullNode getInstance() {
    return instance;
  }

  @Override
  public int getTreeSize() {
    return 0;
  }

  @Override
  public Node getLeft() {
    return null;
  }

  @Override
  public Node getRight() {
    return null;
  }

  @Override
  public String getName() {
    return null;
  }

  @Override
  public void walk() {
    // Do nothing
  }
}
Step 4: Let's test this Null Object design pattern using client.
public class Client{
  /**
   * Program entry point
   * 
   * @param args command line args
   */
  public static void main(String[] args) {

    Node root =
        new NodeImpl("1", new NodeImpl("11", new NodeImpl("111", NullNode.getInstance(),
            NullNode.getInstance()), NullNode.getInstance()), new NodeImpl("12",
            NullNode.getInstance(), new NodeImpl("122", NullNode.getInstance(),
                NullNode.getInstance())));

    root.walk();
  }
}
Output :
1
11
111
12
122

Applicability

Use the Null Object pattern when
  • you want to avoid explicit null checks and keep the algorithm elegant and easy to read.

Credits



Comments