解决问题
之前看设计模式的书并没有看到过Null Object
设计模式, 所谓空对象设计模式,实际上是为了规避客户端获取一个对象后(比如是指针对象),在后面调用的所有地方都要判空,否则调用方法(或者解引用)那可能就有问题了,轻则coredump
重则程序没有挂但是运行是不对的. 下面针对一个比较简单的例子给出场景:
std::shared_ptr<int> n;
int x = *n + 1;
因为n本身没有被初始化,对其接引就会当掉了, 对于shared_ptr<>
解引用实际上就是对存储的指针解引用:
解引用所存储的指针。若存储的指针为空,则行为未定义.
那么对于服务端提供的类对象来说,道理是一样的
ServerClass *obj;
...
obj->dosomething();
这时候,客户端程序员就很没有安全感了, 不知道对于空对象执行成员函数结果是什么样的. 为了解决此问题,客户端不得不在所有用到的地方判空. 麻烦且容易出错. 这时候空对象
设计模式就派上用场了.
原理
先明确要解决的问题,上文中给出了客户端不得不应对处理这些空指针(空对象), 如果这个脏活能拿到服务端类对象里面就好了,毕竟客户端多次调用,但是服务端的类(或者API)只写一次就好了. 所以服务端要做的有两个事情:
- 在类对象构造的时候,区分空对象和可用对象
- 在客户端调用类成员函数的时候,根据对象是否为空,给出不同的行为.
按以上两种处理即可. 实现的方式也有两种,列出如下,实现中分别给出阐述.
- 新定义定义一个空对象类,所有成员函数设置为空(或者定制化)
- 底层使用
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
的出现,最佳实践更加简单,大量减轻客户端负担,优点如下:
- 不需要认为的对对象的合法性进行判断,就可以保证运行时安全. 不依赖client端.
- 对于调用空对象方法的结果, server端控制.
参考
Design Patterns in Modern C++
被遗忘的设计模式-空对象(Null Object Pattern)
Null Object Design Pattern
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。