空对象设计模式(Null Object Design Pattern)


目的
在大多数面向对象的语言中,例如Java或C#,引用可能为null。在调用任何方法之前,需要检查这些引用以确保它们不为null,因为通常无法在空引用上调用方法。不使用空引用来表示对象的缺失(例如,不存在的客户),而是使用实现预期接口但其方法体为空的对象。这种方法优于工作默认实现的优点是Null对象是非常可预测的并且没有副作用:它什么都不做。

结构

参与者:
Client  

  • 需要合作者。

AbstractObject 

  • 声明Client的协作者的接口。

  • 根据需要,为所有类共有的接口实现默认行为。

RealObject

  • 定义AbstractObject的具体子类,其实例提供Client期望的有用行为。

NullObject

  • 提供与AbstractObject相同的接口,以便可以用空对象替换真实对象。

  • 实现其接口以不执行任何操作。什么都不做的确切含义取决于Client期望的行为类型。

  • 当有多个方法可以不做任何事情时,可能需要多个NullObject类。

合作
客户端使用AbstractObject类接口与其协作者进行交互。如果接收者是RealObject,则处理该请求以提供真实行为。如果接收者是NullObject,则通过不执行任何操作或至少提供空结果来处理请求。


源代码
空对象模式用中性对象替换空值。很多时候,这简化了算法,因为不需要额外的空值检查。
在此示例中,我们构建了一个二叉树,其中节点是正常对象或空对象。树中没有使用空值,使遍历变得容易。

步骤1:通过参考类图,让我们创建接口 - 节点接口

/**
 * 
 * Interface for binary tree node.
 *
 */

public interface Node {

  String getName();

  int getTreeSize();

  Node getLeft();

  Node getRight();

  void walk();
}

步骤2:为二叉树的普通节点创建实现。

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();
    }
  }
}

步骤3: 二叉树节点的空对象实现。
实现为Singleton,因为所有NullNodes都是相同的。

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
  }
}

步骤4:让我们使用客户端测试这个Null对象设计模式。

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();
  }
}

输出:

1
11
111
12
122

适用性
使用Null对象模式时希望避免显式空检查并保持算法优雅且易于阅读。