对很多面向对象语言,像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模式需要做一层额外的抽象,但它所来代码的简洁性和收益是值得的。
参考:
网友评论