关于c++17:谈-C17-里的-Chain-of-Responsibility-模式

33次阅读

共计 5108 个字符,预计需要花费 13 分钟才能阅读完成。

责任链模式:介绍相干概念并模仿实现一个音讯散发零碎。

Responsibility Chain Pattern

对于本系列文章

这次的 谈 XX 模式 系列,并不会一一全副介绍 GoF 的 23 个模式,也不限于 GoF。有的模式可能是没有模板化复用的必要性的,另外有的模式却并不蕴含在 GoF 中,所以有时候会有注释的补充版本,像上次的 谈 C++17 里的 Observer 模式 – 4 – 信号槽模式 就是如此。

因为本系列的重心在于模板化实作下面,以工程实作为指标,所以咱们并不会像个别的设计模式文章那样规规矩矩地介绍动机、场景什么的(有时候又未必),而是会以咱们的教训和对模式的了解,用本人的话来做论述,我感觉它可能会有点用,当然快消的世界这样做是很愚昧。

这对于咱们来讲,对集体来讲,也是一个扫视和再思考的过程。而对于你来说,换个角度看看别人的了解,说不定其实是有用途的。

形容

责任链模式也是一种行为模式(Behavior Patterns)。它的外围概念在于音讯或者申请沿着一条观察者链条被传递,每个观察者都能够解决申请、或者略过申请,又或者通过信号终止音讯持续向后传递。

音讯散发零碎是它的典型使用场景。

除此之外,在用户身份鉴权与角色赋予环节也是利用责任链的好场景。

Responsibility Chain 和观察者模式的区别在于前者的观察者是顺次解决同一事件且有可能被中断的,观察者们具备一个轮次关系,而后者的观察者们具备普遍意义上的平等性。

实作

咱们会建设一个音讯散发零碎的可复用模板,借助于这个 message_chain_t 能够很容易地建设一套音讯散发机制起来。其特点在于 message_chain_t 负责散发音讯事件,接收者 receivers 会收到所有事件。

  • 所以每个接收者应该判断消息来源以及音讯类别来决定本人是否应该解决一个音讯。
  • 如果接收者生产了某个事件,那么应该返回一个生产后果实体,这个实体由你的音讯协定来决定,能够是一个简略的 bool,或者一个状态码,也能够是一个处理结果包(struct result)。
  • 一个无效的后果实体会令 message_chain_t 完结音讯散发行为。
  • 如果返回空(std::optional<R>{}),则 mediator_t 会持续散发音讯给其它全副接收者。

和信号槽、observer 模式等的不同之处在于,message_chain_t 是一个 message bumper,而不是公布订阅零碎,它是泛泛播送的。

message_chain_t

message_chain_t 是一个能够指定音讯参数包 Messages 以及音讯处理结果 R 的模板。音讯处理结果 R 由 std::optional 打包,所以 mediator_t 依据 std::optional<R>::has_value() 来决定是否持续音讯散发循环。

namespace dp::resp_chain {
  template<typename R, typename... Messages>
  class message_chain_t {
    public:
    using Self = message_chain_t<R, Messages...>;
    using SenderT = sender_t<R, Messages...>;
    using ReceiverT = receiver_t<R, Messages...>;
    using ReceiverSP = std::shared_ptr<ReceiverT>;
    using Receivers = std::vector<ReceiverSP>;

    void add_receiver(ReceiverSP &&o) {_coll.emplace_back(o); }
    template<class T, class... Args>
      void add_receiver(Args &&...args) {_coll.emplace_back(std::make_shared<T>(args...)); }

    std::optional<R> send(SenderT *sender, Messages &&...msgs) {
      std::optional<R> ret;
      for (auto &c : _coll) {ret = c->recv(sender, std::forward<Messages>(msgs)...);
        if (!ret.has_value())
          break;
      }
      return ret;
    }

    protected:
    Receivers _coll;
  };
}

如果接收者成千上万,那么音讯散发循环将会是一个性能瓶颈点。

如果有这样的需要,个别是通过音讯分层分级之后再分组的形式来解决。无论是分层级还是分组的目标都是为了削减一次散发循环所须要遍历的 elements 大幅度缩小(缩小到几百、几十的数量级)。

分层级能够通过串联两个 mediator_t 的办法来实现。

receiver_t

你能够向 mediator_t 增加接收者。接收者须要从 receiver_t 派生,并且实现 on_recv 虚函数。

namespace dp::resp_chain {
  template<typename R, typename... Messages>
  class receiver_t {
    public:
    virtual ~receiver_t() {}
    using SenderT = sender_t<R, Messages...>;
    std::optional<R> recv(SenderT *sender, Messages &&...msgs) {return on_recv(sender, std::forward<Messages>(msgs)...); }

    protected:
    virtual std::optional<R> on_recv(SenderT *sender, Messages &&...msgs) = 0;
  };
}

sender_t

音讯的生产者须要 sender_t 的帮忙,它的申明如下:

namespace dp::resp_chain {
  template<typename R, typename... Messages>
  class sender_t {
    public:
    virtual ~sender_t() {}

    using ControllerT = message_chain_t<R, Messages...>;
    using ControllerPtr = ControllerT *;
    void controller(ControllerPtr sp) {_controller = sp;}
    ControllerPtr &controller() { return _controller;}

    std::optional<R> send(Messages &&...msgs) {return on_send(std::forward<Messages>(msgs)...); }

    protected:
    virtual std::optional<R> on_send(Messages &&...msgs);

    private:
    ControllerPtr _controller{};};
}

相似地,一个发送者要实现 sender_t::on_send。

测试代码

测试代码有一点复杂度。

StatusCode, A and B

首先是定义相应的对象:

namespace dp::resp_chain::test {

  enum class StatusCode {
    OK,
    BROADCASTING,
  };

  template<typename R, typename... Messages>
  class A : public sender_t<R, Messages...> {
    public:
    A(const char *id = nullptr)
      : _id(id ? id : "") {}
    ~A() override {}
    std::string const &id() const { return _id;}
    using BaseS = sender_t<R, Messages...>;

    private:
    std::string _id;
  };

  template<typename R, typename... Messages>
  class B : public receiver_t<R, Messages...> {
    public:
    B(const char *id = nullptr)
      : _id(id ? id : "") {}
    ~B() override {}
    std::string const &id() const { return _id;}
    using BaseR = receiver_t<R, Messages...>;

    protected:
    virtual std::optional<R> on_recv(typename BaseR::SenderT *, Messages &&...msgs) override {std::cout << '[' << _id << "} received:";
      std::tuple tup{msgs...};
      auto &[v, is_broadcast] = tup;
      if (_id == "bb2" && v == "quit") { // for demo app, we assume "quit" to stop message propagation
        if (is_broadcast) {
          std::cout << v << '' <<'*'<<'\n';
          return R{StatusCode::BROADCASTING};
        }
        std::cout << "QUIT SIGNAL to stop message propagation" << '\n';
        dbg_print("QUIT SIGNAL to stop message propagation");
        return {};}
      std::cout << v << '\n';
      return R{StatusCode::OK};
    }

    private:
    std::string _id;
  };

} // namespace dp::mediator::test

test_resp_chain

在测试代码中,咱们定义了一个音讯组为(Msg,bool)的 message_chain_t。

bool 参数的含意为 is_broadcasting,true 代表着音讯将始终被分发给所有接收者,false 时则恪守 message_chain_t 的默认逻辑,一旦有接收者生产了音讯组的内容,就进行音讯的持续散发。

留神 is_broadcasting = true 时,接收者 A 和 B 都会有相应的条件分支来返回空,从而令 message_chain_t 持续向下散发。

test_resp_chain() 为:

void test_resp_chain() {
  using namespace dp::resp_chain;

  using R = test::StatusCode;
  using Msg = std::string;
  using M = message_chain_t<R, Msg, bool>;
  using AA = test::A<R, Msg, bool>;
  using BB = test::B<R, Msg, bool>;

  M m;

  AA aa{"aa"};
  aa.controller(&m); //

  m.add_receiver<BB>("bb1");
  m.add_receiver<BB>("bb2");
  m.add_receiver<BB>("bb3");

  aa.send("123", false);
  aa.send("456", false);
  aa.send("quit", false);
  aa.send("quit", true);
}

运行后果会是这样:

--- BEGIN OF test_mediator                            ----------
[bb1} received: 123
[bb2} received: 123
[bb3} received: 123
[bb1} received: 456
[bb2} received: 456
[bb3} received: 456
[bb1} received: quit
[bb2} received: QUIT SIGNAL to stop message propagation
[bb1} received: quit
[bb2} received: quit *
[bb3} received: quit
--- END OF test_mediator                              ----------

其中最初一组信息是播送音讯,所以 quit 信号不会导致终止。

后记

实在的音讯散发,例如 Windows 零碎的窗口音讯散发,会在性能和逻辑上持续深刻,而咱们的示例代码在这个局部比拟繁难。

能够很容易批改 message_chain_t 治理一个 tree 构造以应答诸如窗口、对话框这样的 UI 零碎模型,但因为少数 GUI 类库都会自行负责和提供一整套基础设施,所以本文仅作参考。

FROM: here

正文完
 0