头图
Responsibility chain mode: introduce related concepts and simulate the realization of a message distribution system.

Responsibility Chain Pattern

About this series of articles

In this talk about the XX mode series, I will not introduce all the 23 modes of GoF one by one, nor are they limited to GoF. Some patterns may not be necessary for template reuse, and some patterns are not included in GoF, so sometimes there will be supplementary versions of the text, like the last time talked about Observer in C++17 Mode-4-Signal slot mode is like this.

Because the focus of this series is on template implementation, with engineering practice as the goal, we will not introduce motivations, scenarios or the like (sometimes not necessarily) like general design pattern articles, but will Based on our experience and understanding of the model, to explain it in our own words, I think it might be useful, of course, it is stupid to do so in the fast-moving world.

For us and for individuals, this is also a process of review and rethinking. For you, it might actually be useful to look at other people's understanding from another angle.

describe

The chain of responsibility pattern is also a behavior pattern (Behavior Patterns). Its core concept is that messages or requests are passed along a chain of observers, and each observer can process the request, or skip the request, or terminate the message through a signal to continue the backward transmission.

The message distribution system is its typical application scenario.

In addition, the link of user identity authentication and role assignment is also a good scenario for applying the responsibility chain.

The difference between the Responsibility Chain and the observer mode is that the observers of the former process the same event in turn and may be interrupted. The observers have a round relationship, while the observers of the latter have equality in the general sense.

Implement

We will build a reusable template for the message distribution system. With the help of this message_chain_t, a set of message distribution mechanism can be easily established. Its characteristic is that message_chain_t is responsible for distributing message events, and receivers will receive all events.

  • So each receiver should judge the source of the message and the type of the message to decide whether it should process a message.
  • If the receiver consumes an event, it should return a consumption result entity. This entity is determined by your message protocol. It can be a simple bool, a status code, or a processing result packet (struct result) .
  • A valid result entity will cause message_chain_t to end the message distribution behavior.
  • If it returns empty ( std::optional<R>{} ), then message_chain_t will continue to distribute the message to all other receivers.

The difference from the signal slot, observer mode, etc. is that message_chain_t is a message bumper, not a publish-subscribe system, it is a general broadcast.

message_chain_t

message_chain_t is a template that can specify the message parameter package Messages and the message processing result R. The message processing result R is packaged by std::optional, so message_chain_t decides whether to continue the message distribution cycle 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;
  };
}

If there are thousands of receivers, the message distribution loop will be a performance bottleneck.

If there is such a demand, it is generally solved by grouping messages after hierarchical classification. The purpose of both hierarchical and grouping is to reduce the number of elements that need to be traversed in a distribution cycle.

Hierarchical levels can be achieved by concatenating two message_chain_t methods.

receiver_t

You can add recipients to message_chain_t. The receiver needs to derive from receiver_t and implement the on_recv virtual function.

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

The producer of the message needs the help of sender_t, and its declaration is as follows:

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{};
  };
}

Similarly, a sender needs to implement sender_t::on_send.

Test code

The test code is a little bit complicated.

StatusCode, A and B

The first is to define the corresponding object:

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

In the test code, we define a message_chain_t whose message group is (Msg, bool).

The meaning of the bool parameter is is_broadcasting. True means that the message will always be distributed to all receivers. When it is false, the default logic of message_chain_t is followed. Once a receiver consumes the content of the message group, the message will stop continuing to distribute.

Note that when is_broadcasting = true, both receivers A and B will have corresponding conditional branches to return to empty, so that message_chain_t continues to be distributed downwards.

test_resp_chain() is:

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);
}

The operation result will be like this:

--- 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                            ----------

The last set of information is a broadcast message, so the quit signal will not cause termination.

postscript

The real message distribution, such as the window message distribution of the Windows system, will continue in-depth in terms of performance and logic, and our sample code is relatively simple in this part.

It is easy to modify message_chain_t to manage a tree structure to cope with UI system models such as windows and dialogs. However, since most GUI libraries are responsible for and provide a complete set of infrastructure, this article is only for reference.

对象树的枝干可以组成一条链

FROM: here

:end:


hedzr
95 声望19 粉丝