【设计模式】空对象设计模式学习

解决问题

之前看设计模式的书并没有看到过Null Object设计模式, 所谓空对象设计模式,实际上是为了规避客户端获取一个对象后(比如是指针对象),在后面调用的所有地方都要判空,否则调用方法(或者解引用)那可能就有问题了,轻则coredump重则程序没有挂但是运行是不对的. 下面针对一个比较简单的例子给出场景:

std::shared_ptr<int> n;
int x = *n + 1;

因为n本身没有被初始化,对其接引就会当掉了, 对于shared_ptr<>解引用实际上就是对存储的指针解引用:

解引用所存储的指针。若存储的指针为空,则行为未定义.

那么对于服务端提供的类对象来说,道理是一样的

ServerClass  *obj;
...
obj->dosomething();

这时候,客户端程序员就很没有安全感了, 不知道对于空对象执行成员函数结果是什么样的. 为了解决此问题,客户端不得不在所有用到的地方判空. 麻烦且容易出错. 这时候空对象设计模式就派上用场了.

原理

先明确要解决的问题,上文中给出了客户端不得不应对处理这些空指针(空对象), 如果这个脏活能拿到服务端类对象里面就好了,毕竟客户端多次调用,但是服务端的类(或者API)只写一次就好了. 所以服务端要做的有两个事情:

  1. 在类对象构造的时候,区分空对象和可用对象
  2. 在客户端调用类成员函数的时候,根据对象是否为空,给出不同的行为.

按以上两种处理即可. 实现的方式也有两种,列出如下,实现中分别给出阐述.

  1. 新定义定义一个空对象类,所有成员函数设置为空(或者定制化)
  2. 底层使用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

    原文作者:设计模式
    原文地址: https://segmentfault.com/a/1190000011819656
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞