Null Object模式

2019-02-02  本文已影响0人  AlgoPeek

对很多面向对象语言,像C++、Java、C#等,对象可能为空,我们在调用对象的方法时,通常会先检查对象是否为null, 然后再调用,否则可能造成崩溃或空指针异常。看看下面的代码:

Employee e = DB.getEmployee("Bob");
if (e != null && e.isTimeToPay(today)) {
    e.pay();
}

我们可能曾经都编写过类似的代码,这是一个惯用法。当雇员Bob不存在时,会返回null值,&&的第一个表达式会被首先求值,当且仅当第一个表达式为true时才会执行第二个表达式。如果忘记对第一个表达式进行检查,可能就会出现各种bug。

Null Object模式主要是消除对null进行检查,并简化代码。

对于上面例子,如果把Employee变成一个抽象接口,EmployeeImpl实现期望的方法,而NullEmployee也实现接口的所有方法,但“什么也没做”,对isTimeToPay方法的实现直接返回false,类结构如下:

使用Null Object模式,上面的代码可以改写成这样:

Employee e = DB.getEmployee("Bob")
if (e.isTimeToPay(today)) {
    e.pay();
}

即使是Bob不存在,也会返回一个NullEmployee的对象,该对象调用isTimeToPay时返回false,符合预期。

但是在实际情况中,虽然Null Object实现的接口“什么也没做”,但Null Object的实现可能并不符合预期的,我们可能需要一个方法(类似empty、valid)来检查这个对象是否是我们期望的对象。看看pugixml的一个例子:

pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_string("<node>hello pugixml</node>");
if (result) {
    pugi::xml_node node = doc.child("node");
    if (!node.empty()) {
        std::cout << node.text().as_string() << std::endl;
    }

    node = doc.child("node_not_exists");
    std::cout << node.text().as_string("please check node exists!") << std::endl;
}

在上面的例子中,我们要获取xml中node结点的值,DOM解析成功后,通过pugi::xml_document的child方法返回node结点,通过empty()方法检查node结点的有效性,再获取node结点的值。

当node结点不存在时返回的是什么呢?实际上就是一个空的xml_node结点(Null Object),我们可以对这个空结点调用所有xml_node的方法,只是这些方法“什么也没做”。像上面例子中,node_not_exists结点并不存在,返回了一个空的xml_node结点,如果我们直接获取其值,将得到一个空字符串,但我们可以指定一个默认值。

像对C++这种支持操作符重载的语言,还可以直接利用语言特性,写出再简洁的代码,上面例子中获取结点值的逻辑可以修改如下:

pugi::xml_node node = doc_child("node");
if (node) {
    std::cout << node.text().as_string() << std::endl;
}

你可能要问,这跟判断对象是否为空有什么区别呢?

在我看来,它们本质上是没什么区别的,但是null是语言层面上对空对象、空指针定义,而Null Object是业务层面对空对象的定义,它们本不是一个维度的东西,只是在实际应用中,我们经常将null作为业务层面的空对象使用。
通过Null Object模式可以规避掉语言层面上对空引用时所引发的异常,并且可以让代码更加简洁和可读。虽然使用Null Object模式需要做一层额外的抽象,但它所来代码的简洁性和收益是值得的。

参考:

  1. 敏捷软件开发,原则、模式与实践
  2. Null object pattern
上一篇下一篇

猜你喜欢

热点阅读