Паттерн проектирования «Цепочка обязанностей» / «Chain of Responsibility»

    Почитать описание других паттернов.


    Проблема


    Эффективно и компактно реализовать механизм обработки потока событий/запросов/сообщений в системах с потенциально большим количеством обработчиков.

    Описание


    Модель событие/обработчик широко применяется в программных системах из различных областей. В основном, это — графический интерфейс пользователя, где события, генерируемые от действий пользователя различным образом обрабатываются элементами интерфейса. Нельзя так-же забывать про WinAPI, который сплошь и рядом реализует такую модель. В большинстве источников эта модель имеет название Event Loop.


    С одной стороны — все достаточно прозрачно. Есть множество типов сообщений, есть множество обработчиков этих сообщений. Задача — обрабатывать каждое сообщение в потоке ассоциированным с его конкретными типом обработчиком. Проще говоря, необходимо иметь эффективный механизм увязывания типов сообщений и обработчиков.

    В большинстве случаев, с фиксированным и небольшим количеством обработчиков в системе, бывает достаточно реализовать данный механизм на базе конструкции if-then-else. Однако, не всегда все так просто. Во-первых, не всегда заранее известно сколько именно обработчиков необходимо. Во вторых — добавление в систему нового типа сообщения как правило приводит к появлению нового обработчика, внедрение которого уже становится действительно сложной задачей.

    Для решения подобных проблем существует шаблон — “Цепочка обязанностей”.

    Идея шаблона заключается в организации рекуррентного конвейера из обработчиков, в котором каждый обработчик может либо обработать поступившее сообщение (например, только сообщения определенного типа), либо делегировать обработку следующему в конвейере обработчику. Возможен еще вариант обработки и последующей передачи. При этом, клиенту, чтобы инициировать обработку того или иного сообщения достаточно лишь передать его первому в конвейере обработчику.

    Практический пример


    Рассмотрим, пожалуй самый наглядный пример шаблона — компьютерную сеть. Действительно — большое количество обработчиков — узлов сети (компьютеров, серверов, маршрутизаторов) и еще большое количество типов сетевых запросов.

    Пусть в упрощенной и выдуманной сетевой модели есть 4-типа обработчиков — сеть, маршрутизатор, форвардер и сервер. Так-же есть всего один тип запроса — запрос на обработку сервером. Обработчики обладают следующим поведением: сеть — просто предает по своей среде запрос, маршрутизатор — передает запрос из одной сети в другую, форвардер — передает запрос конкретному хосту, сервер — обрабатывает запрос.

    Узлы сети представляют собой конвейер обработчиков. Запрос — конкретное сообщение. Запрос, двигаясь по цепочке обрабатывается (маршрутизируется, форвардится) каждым ее узлом и передается дальше. До тех пор, пока не будет окончательно обработан сервером.

    Диаграмма классов


    Основной момент на который следует обратить внимание — способ организации конвейерной обработки. В данном случае используется следующий подход. Все обработчики реализуют один абстрактный класс — RequestHandled, который содержит ссылку на самого себя (successor) для делегирования обязанностей по обработке следующему обработчику в конвейере. Реализация метода handleRequest() по-умолчанию реализует такую делегацию.


    Реализация на С++


    // Запрос
    #ifndef REQUEST_H
    #define REQUEST_H
    
    #include <string>
    
    using namespace std;
    
    class Request {
    private:
    protected:
    public:
      string requestString;
      Request(string requestString) : requestString(requestString) { }
    };
    #endif
    
    // Обработчик
    #ifndef REQUEST_HANDLER_H
    #define REQUEST_HANDLER_H
    
    #include "Request.h"
    
    class RequestHandler {
    protected:
      RequestHandler *successor;
    public:
      RequestHandler();
      RequestHandler(RequestHandler *successor) : successor(successor) { } 
    
      virtual void handleRequest(const Request& request) {
        successor->handleRequest(request);
      }
    };
    
    RequestHandler::RequestHandler() { }
    
    #endif
    
    // Хост
    #ifndef HOST_H
    #define HOST_H
    
    #include "RequestHandler.h"
    
    class Host : public RequestHandler {
    private:
    protected:
    public:
      Host();
      Host(Host *host);
    };
    
    Host::Host() { }
    Host::Host(Host *host) : RequestHandler((RequestHandler*)host) { }
    
    #endif
    
    // Сеть
    #ifndef NETWORK_H
    #define NETWORK_H
    
    #include "RequestHandler.h"
    #include "Router.h"
    
    class Network : public RequestHandler {
    private:
    protected:
    public:
      Network(Host *host);
    };
    
    Network::Network(Host *host) : RequestHandler((RequestHandler*)host) { }
    #endif
    
    // Роутер
    #ifndef ROUTER_H
    #define ROUTER_H
    
    #include "Host.h"
    #include "Network.h"
    #include "RequestHandler.h"
    #include "Request.h"
    
    #include <cstdlib>
    #include <iostream>
    
    using namespace std;
    
    class Router : public Host {
    private:
      void route(Network *network, const Request& request) {
        if (network != NULL) {
          ((RequestHandler*)network)->handleRequest(request);
        } else {
          cout << "ER: Network is unreachable. Request with string " << request.requestString << " was lost" << endl;
        }
      }
    protected:
    public:
    
      Router(Network *network);
      virtual void handleRequest(const Request& request) {
        route((Network*) successor, request);
      }
    };
    
    Router::Router(Network *network) {
      successor = (RequestHandler*) network;
    }
    
    #endif
    
    // Сервер
    #ifndef SERVER_H
    #define SERVER_H
    
    #include "Host.h"
    #include "Request.h"
    
    #include "string"
    
    using namespace std;
    
    class Server : public Host {
    private:
      void showMessage(const string& msg) {
        cout << msg << endl;
      }
    protected:
    public:
      Server() : Host(NULL) {
        
      }
    
      virtual ~Server();
    
      virtual void handleRequest(const Request& request) {
        string messageStr = "Request received with string: " + request.requestString;
        showMessage(messageStr);
      }
    };
    
    #endif
    
    // Форвардер
    #ifndef FORWARDER_H
    #define FORWARDER_H
    
    #include "Host.h"
    #include "Request.h"
    
    class Forwarder : public Host {
    private:
    protected:
    public:
    
      Forwarder(Host *host) : Host(host) { } 
      virtual ~Forwarder();
    };
    
    #endif
    
    // Клиент
    #include <iostream>
    #include <cstdlib>
    
    using namespace std;
    
    #include "Server.h"
    #include "Router.h"
    #include "Forwarder.h"
    #include "Network.h"
    
    int main(int argc, char *argv[]) {
    
      Server *webServer = new Server();
      Forwarder *fw1 = new Forwarder(webServer);
      Forwarder *fw2 = new Forwarder(fw1);
      Network *network = new Network(fw2);
      Router *router = new Router(network);
    
      const Request *req = new Request("correct request");
      router->handleRequest(*req);
    
      Router *router1 = new Router(NULL);
      Forwarder *fw3 = new Forwarder(router1);
      Forwarder *fw4 = new Forwarder(fw3);
      Network *network2 = new Network(fw4);
      Router *router2 = new Router(network2);
    
      const Request *req1 = new Request("incorrect request");
      router2->handleRequest(*req1);
    
      system("pause");
    }
    


    PS


    Внимательный читатель скажет — “О! Я уже читал статьи этого парня!”. Все верно, я решил продолжить серию статей о шаблонах проектирования и надеюсь на ваши фидбеки.
    Поделиться публикацией

    Комментарии 24

      +4
      Не понял толком отличия forwarder'а и прочих, поэтому повторил как понял.
      Опять же неясно, зачем нужны forwarder'ы, если основная идея в цепочности.
      У меня при возврате Just v сообщение идёт дальше по цепочке, при возврате Nothing цепочка прерывается. А route нужен, чтобы ответвить сообщение в другую цепочку.

      module Event (
          ) where

      import Prelude hiding (id, (.))
      import Control.Category

      data Node a b = Node (a -> IO (Maybe b))

      instance Category Node where
          id = Node $ return . Just
          (Node g) . (Node f) = Node $ \v -> do
              f v >>= maybe (return Nothing) g

      endnode :: (a -> IO ()) -> Node a ()
      node :: (a -> IO ()) -> Node a a
      run :: Node a b -> a -> IO (Maybe b)
      run_ :: Node a b -> a -> IO ()

      endnode f = Node $ \v -> f v >> return Nothing
      node f = Node $ \v -> f v >> return (Just v)
      run (Node f) v = f v
      run_ (Node f) v = f v >> return ()

      server = endnode putStrLn
      route to = node $ run_ to
      forward to = node $ run_ to
      network to = node $ run_ to

      test = run test' "correct" where
          logmsg = node $ \s -> putStrLn ("log: " ++ s)
          test' = logmsg >>> test'' >>> server
          test'' = route (network fw2) where
              fw2 = forward fw1
              fw1 = forward serv
              serv = server
        +17
        За что такая подсветка???
          0
          Отличия в потенциально возможном различном поведении.
          +3
          хаскелл-джедай..? :)

          А вы, собственно, что именно куском кода-то сказать хотели?
            +3
            Си++ код тоже можно записать так же компактно, без иерархии и десятка наследуемых друг от друга классов.
            +3
            Интересный паттерн. Спасибо за пищу для мозга. Но не очень-то наглядный пример, особенно для людей не знающих C++. Комментариев в коде нет, что особенно напрягает. Т.е. суть ясна, а реализация — нет.
              0
              да-да.
              было бы гораздо круче использовать что-нибудь более абстрактно-понятное.
              а начинаешь залипать в код, вместо осмысливания идеи.
                +1
                Плюсую. Хоть и знаю С++, читать все равно напряжко. Голосую за то, чтоб в статье был псевдокод и ссылка на реализацию
                –1
                Хорошо. А что вы хотите видеть в примерах?

                Абстрактный класс Handler, два наследника Handler1, Handler2, сообщение Message которое обрабатывается одним из обработчиков — handle(Message message)? Это ровно четыре строчки кода и скукота. Я пытаюсь дать читатели интересный и сложный пример, поняв который он лучше осознает/запомнит материал.
                  0
                  Давайте еще хэллоуворлды объяснять с помощью 20-ти классов с шаблонами и виртуальным наследованием, а то скукота
                    0
                    Давайте!

                    Раз уж мы изучаем шаблоны проектирования, давайте пытаться думать и изобретать ;)
                  +1
                  Да, C# или Java, например
                    0
                    Да каккая разница какой язык?

                    Я писал примеры к постам на различных языках — на Python, Java, C#, C++ и т.д.

                    Так случилось что этот паттерн я реализовал на C++. Если вы перейдете по первой ссылке, найдете примеры на других языках.
                  +1
                  Хорошее и достаточно детальное описанное применения паттерна (комментариев правда маловато, как выше верно заметили). «Бандитский» пример менеджера справки из книги гораздо более абстрактный — пришлось два раза перечитывать, чтобы понять структуру распределения обязанностей между объектами.
                  Я тоже не так давно применил этот паттерн в разработке одного приложения с использованием SCSF. У меня ситуация была следующая: есть список Workspace вложенных друг в друга. ShellWorkspace с кнопкой Back и Next и Print, в него встраивается RootWorkspace c кнопками категорий. А при нажатии на одну их кнопок категорий в RootWorkspace встраивался ContentWorkspace. Обработку навигации я делегировал специальному менеджеру-сервису а вот обработку кнопки Print реализовал с помощью паттерна Chain of Responsibility. Т.е. команду мог обрабатывать только конкретный ContentWorkspace. Поэтому все вышележашие просто реализовывали так:
                  public bool Print()
                  {
                      IContentView view = this.Workspace as IContentView;
                  
                      return view != null ? view.Print() : false;
                  }
                  


                  И последний в списке экземпляр конкретного ContentView:
                  public bool Print()
                  {
                      bool result = false;
                  
                      // Printing code...
                  
                      return result;
                  }
                  
                    0
                    И разместите пожалуйста в этой статье ссылку на книгу, или оригинального описания этого паттерна. А то непосвященные будут читать и думать, что это вы придумали этот паттерн :) Нехорошо это — на святое замахиваетесь :)
                      0
                      Боже упоси читать статью о паттернах непосвященным людям :) По этой ссылке переходят только те для кого слов GoF не просто бесмысленное сочетание букв.
                        0
                        Ну а если человек никогда об этом не слышал? Надо же дать ему шанс :)
                      +1
                      Наверное на этом можно заканчивать комментарии.
                      ISBN 978-5-469-01136-1
                      всем советую, книга имеет ограничение по возрасту (читать от 3-х лет + одного большого проекта, переписанного второй раз). ;-)
                        0
                        интересно, спасибо. Только вместо диаграммы классов было бы интереснее увидеть диаграмму деятельности
                          –1
                          А поясните, пожалуйста, почему

                          RequestHandler(RequestHandler *successor): successor(successor) { }

                          Насколько я понимаю, тут вызов конструктора этемента successor, но ведь у нас есть только указатель на него (который получаем в сончтруированном виде), не надо ли вместо него написать

                          RequestHandler(RequestHandler *successor) { this.successor = successor}
                            0
                            Не обязательно. Это лишь сокрашенная форма записи.

                            Можно было написать и так — RequestHandler(RequestHandler *successor) { this->successor = successor}
                            +1
                            Стремление внедрять шаблоны это хорошо, главное понимать их правильно ( не как большинство понимает MVC).
                            Так вот:
                            «Возможен еще вариант обработки и последующей передачи. „
                            Это не “возможен» — это главный мотив использовать этот шаблон. Если у вас нет необходимости совместной обработки несколькими обработчиками приходящего сообщения — то его применение проигрывает простому Map [Тип сообщения] -> [Экземпляр класса с интерфейсом обработчика] как по понятности/количеству кода, так и по скорости работы (обычно не так важно)

                              0
                              Отличное замечание!

                              Спс. Я действительно должен был акцентировать на этом внимание.
                              0
                              Спасибо за статью.
                              Хороший пример использования — фрейморк Netty (Java), обработка запроса и ответа.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое