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

解决问题

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

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

由于n自己没有被初始化,对其接引就会当掉了, 对于shared_ptr<>解引用实际上就是对存储的指针解引用:c++

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

那么对于服务端提供的类对象来讲,道理是同样的设计模式

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

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

原理

先明确要解决的问题,上文中给出了客户端不得不该对处理这些空指针(空对象), 若是这个脏活能拿到服务端类对象里面就行了,毕竟客户端屡次调用,可是服务端的类(或者API)只写一次就行了. 因此服务端要作的有两个事情:ide

  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

相关文章
相关标签/搜索