談 C++17 裡的 Chain of Responsibility 模式

hedzr發表於2021-10-28
責任鏈模式:介紹相關概念並模擬實現一個訊息分發系統。

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>{}),則 message_chain_t 會繼續分發訊息給其它全部接收者。

和訊號槽、observer 模式等的不同之處在於,message_chain_t 是一個 message bumper,而不是釋出訂閱系統,它是泛泛廣播的。

message_chain_t

message_chain_t 是一個可以指定訊息引數包 Messages 以及訊息處理結果 R 的模板。訊息處理結果 R 由 std::optional 打包,所以 message_chain_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 大幅度減少(減少到幾百、幾十的數量級)。

分層級可以通過串聯兩個 message_chain_t 的方法來實現。

receiver_t

你可以向 message_chain_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::resp_chain::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_resp_chain                          ----------
[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_resp_chain                            ----------

其中最後一組資訊是廣播訊息,所以 quit 訊號不會導致終止。

後記

真實的訊息分發,例如 Windows 系統的視窗訊息分發,會在效能和邏輯上繼續深入,而我們的示例程式碼在這個部分比較簡易。

可以很容易修改 message_chain_t 管理一個 tree 結構以應對諸如視窗、對話方塊這樣的 UI 系統模型,但由於多數 GUI 類庫都會自行負責和提供一整套基礎設施,所以本文僅作參考。

物件樹的枝幹可以組成一條鏈

FROM: here

相關文章