以前看设计模式的书并无看到过Null Object
设计模式, 所谓空对象设计模式,其实是为了规避客户端获取一个对象后(好比是指针对象),在后面调用的全部地方都要判空,不然调用方法(或者解引用)那可能就有问题了,轻则coredump
重则程序没有挂可是运行是不对的. 下面针对一个比较简单的例子给出场景:html
std::shared_ptr<int> n; int x = *n + 1;
由于n自己没有被初始化,对其接引就会当掉了, 对于shared_ptr<>
解引用实际上就是对存储的指针解引用:c++
解引用所存储的指针。若存储的指针为空,则行为未定义.程序员
那么对于服务端提供的类对象来讲,道理是同样的设计模式
ServerClass *obj; ... obj->dosomething();
这时候,客户端程序员就很没有安全感了, 不知道对于空对象执行成员函数结果是什么样的. 为了解决此问题,客户端不得不在全部用到的地方判空. 麻烦且容易出错. 这时候空对象
设计模式就派上用场了.安全
先明确要解决的问题,上文中给出了客户端不得不该对处理这些空指针(空对象), 若是这个脏活能拿到服务端类对象里面就行了,毕竟客户端屡次调用,可是服务端的类(或者API)只写一次就行了. 因此服务端要作的有两个事情:ide
按以上两种处理便可. 实现的方式也有两种,列出以下,实现中分别给出阐述.函数
std::optional
包装真正对象,而std::optional
自然能够区分对象是否空对象,未初始化状态就是空对象.场景:假定服务端提供了日志记录的接口,客户端使用日志接口中的info()
功能,客户端多处使用,若是客户端使用空对象,预期的行为是啥也不干,日志类接口以下:设计
struct Logger{ virtual ~Logger() = default; virtual void info(const std::string &s) =0; };
按照原理所述,给用户提供的接口类应该可以包装空对象和实际对象. 调用的时候对对象的存在性进行判断而左右行为. 所以须要新增空对象并对其包装.相关处理以下:指针
// 新增空对象类 struct NullLogger : Logger{ void info(const std::string &s) override { } }; // 对客户端提供接口,内含设计类`impl`和空对象`no_logging` // 能够看出若是实际类对象不存在则调用了这个成员函数`info`也不会有实际行为,客户端程序变安全了. struct OptionalLogger : Logger{ std::shared_ptr<Logger> impl; static std::shared_ptr<Logger> no_logging; OptionalLogger(const std::shared_ptr<Logger> &logger) : impl(logger) {} void info(const std::string &s) override { if(impl) impl->info(s); } };
继续讲第二种方式(c++17std::optional
实现),由于本地没有c++17编译器,所以用boost::optional
来代替. optional
自然就能包装有值的类和未初始化的空对象, 所以不须要额外定义,相对更简单,实现以下:日志
struct OptionalLogger2 : Logger { boost::optional<std::shared_ptr<Logger>> impl; OptionalLogger2(const std::shared_ptr<Logger> &logger) { // 对于对象impl的初始化工做能够自行定义. `shared_ptr<>`能够直接和`nullptr`进行比较 // if(nullptr != logger) impl = logger; } // 直接判断是不是空对象 void info(const std::string &s) override { if(impl) (*impl)->info(s); } };
这样客户端调用的时候就方便多了,判断对象合法性基本不用管,若是调用的成员方法不是必要,那能够安排空对象. 客户端调用方式:
auto log2 = std::make_shared<OptionalLogger2>(nullptr); ... // 安全调用 log2->info("");
与其说这是一个设计模式, 更不如说这是API设计的一个最佳实践,特别是在std::optional
推出以后,就算不考虑面向对象,在函数返回值的策略上也能够变得很易用,不用再用千奇百怪的负值做为非法返回值(-1 -999)了,由于能够用一个std::optional<int>
同时包装调用成功失败的状态和查询到的返回值,这样更加优美了; 回到这个主题,std::optional
的出现,最佳实践更加简单,大量减轻客户端负担,优势以下:
Design Patterns in Modern C++
被遗忘的设计模式-空对象(Null Object Pattern)
Null Object Design Pattern