Andrey on .NET | Поведенческие шаблоны: Цепочка ответственностей

Поведенческие шаблоны: Цепочка ответственностей

Название шаблона

Цепочка ответственностей (Chain of Responsibility).

Тип

Поведенческий шаблон проектирования (Behavioral).

Описание

Chain of responsibility patternШаблон Цепочка ответственностей предназначен для предотвращения связывания инициатора сообщения с конкретным экземпляром его обработчика. При этом само сообщение передается по цепочке,  в которой каждое звено выбирает между обработкой и пересылкой данных дальше.

Цепочка ответственностей применяется в случаях, если:

  • существует более одного обработчика исходного запроса;
  • (или) обработчики сообщения определяются во время выполнения приложения;
  • (или) необходимо отправить сообщение без явного указания получателя.

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

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

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

В результате применения шаблона:

  • Уменьшается связанность, т.к. отправителю и получателю нет необходимости знать обо всех обработчиках в цепочке, а так же какие именно их них отреагируют на запрос. Более того, сами обработчики могут не знать друг о друге и даже о следующем звене. Наглядным примером может служить обработка исключений: метод, выбросивший его, ничего не знает о том где и как оно будет обработано.
  • Увеличивается гибкость приложения, т.к. сама цепочка обработчиков может меняться независимо от клиента. Последнему необходимо знать только точку для отправки сообщения.

Можно выделить следующих участников шаблона:

  1. Интерфейс обработчика (IHandler) – определяет общий интерфейс для взаимодействия с клиентами.
  2. Обработчик (Handler) – непосредственная реализация, которая обрабатывает запрос.
  3. Управляющий объект (Manager) – не обязательный участник, который используется для построения структуры обработчиков и управления ей.

Особенности реализации и применения шаблона

При реализации Цепочки ответственностей необходимо учитывать тот факт, что сообщение может быть не обработано вовсе. Это возможно в том случае, если ни один из участников цепочки не будет знать что с ним делать. К такому же результату приводит неправильная конфигурация последовательности или ошибки при ее построении.

Стоит отметить, что обработка сообщения не означает обязательного отказа в передаче его следующему обработчику в цепочке.

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

Еще одним важным моментом является определение формата сообщения. Возможны варианты:

  • Сообщением является экземпляр определенного класса. В исходном коде приложения это будет выглядеть как обычный вызов метода с параметрами. Это удобно и достаточно безопасно, т.к. компилятор позволит подставить только заданные типы параметров. Разработчику остается проконтролировать чтобы они были в диапазоне допустимых значений. Однако, для добавления нового сообщения потребуется создание нового метода. Это возможно приведет к изменению интерфейса и модификации классов всех существующих обработчиков.
  • Сообщение передается в виде кода. Например, это как число или текстовая строка заданного формата. В этом случае появляется возможность добавлять новые типы сообщений без изменения интерфейса. Но при этом необходимо контролировать получаемые от клиентов сообщения, т.к. формат может быть нарушен как по ошибке, так и намеренно.

Реализация шаблона в общем виде

  • разрабатываем интерфейс обработчиков;
  • определяем схему построения цепочки или дерева обработчиков (от контекстных к абстрактным);
  • выбираем формат сообщений;
  • определяем точку или способ взаимодействия с клиентом;
  • клиент отправляет сообщения, которое игнорируется или обрабатывается получателями;

Пример реализации

Рассмотрим реализацию шаблона на примере системы обработки заказов. Упростим его, оставив только необходимое для демонстрации работы шаблона.

Заказы клиентов будут содержаться в экземплярах следующего класса:

public class OrderData
{
    public int Id { get; set; }
    public int ItemId { get; set; }
    public int Amount { get; set; }
    public int CustomerId { get; set; }
}

Будем считать, что номер заказа (Id) для только что созданного заказа равен нулю. Соответственно, после обработки он получит не нулевое значение. Остальные свойства отражают код товара (ItemId), количество (Amount) и код заказчика в базе данных (CustomerId).

Интерфейс обработчиков будет простой:

public interface IOrderHandler
{
    bool Process(OrderData orderData);
}

Если метод Process() вернет true, то заказ обработан и можно не вызывать другие обработчики.

Как можно заметить, IOrderHandler не содержит ссылку на следующий элемент цепочки. Поэтому потребуется управляющий объект:

public class OrderManager{
    private List<IOrderHandler> _handlers = new List<IOrderHandler>();
    private IOrderHandler _defaultHandler;

    public OrderManager(IOrderHandler defaultHandler)
    {
        if (defaultHandler == null) { throw new NullReferenceException(); }

        this._defaultHandler = defaultHandler;
    }

    public void AddHandler(IOrderHandler handler)
    {
        this._handlers.Add(handler);
    }

    public void ProcessNewOrder(OrderData orderData)
    {
        foreach (IOrderHandler handler in this._handlers) {
            if (handler.Process(orderData)) {
                return;
            }
        }

        if (!this._defaultHandler.Process(orderData)) {
            throw new InvalidOperationException();
        }
    }
}

В данной реализации можно отметить следующие моменты:

  • список обработчиков хранится в поле _handlers;
  • порядок добавления обработчиков определяет последовательность их вызовов;
  • конструктор обязывает указать обработчик по умолчанию _defaultHandler;
  • обработчик по умолчанию всегда вызывается последним и обязан обработать запрос, в противном случае возникнет исключительная ситуация.

Перейдем к созданию самих обработчиков и начнем с варианта по умолчанию. Для демонстрации просто выведем на экран уведомление об обработке и присвоим заказу отличный от нуля номер:

public class DefaultOrderHandler : IOrderHandler
{
    public bool Process(OrderData orderData)
    {
        if (orderData.Id != 0) { return false; }

        Console.WriteLine("Default order handler");
        orderData.Id = 5;

        return true;
    }
}

Разработаем еще два варианта. Один – для отгрузки оптовых партий (пусть это будет 20 и более единиц товара):

public class LargeOrderHandler : IOrderHandler
{
    public bool Process(OrderData orderData)
    {
        if ((orderData.Id != 0) || (orderData.Amount < 20)) { 
            return false; 
        }

        Console.WriteLine("Large order handler.");
        orderData.Id = 42;

        return true;
    }
}

Другой – для отдельной работы с самыми первыми клиентами. Для простоты определять их будем по коду, который должен быть меньше или равен 10:

public class FavCustomerOrderHandler : IOrderHandler
{
    public bool Process(OrderData orderData)
    {
        if ((orderData.Id != 0) || (orderData.CustomerId > 10)) { 
            return false; 
        }

        Console.WriteLine("Favorite customer order handler.");
        orderData.Id = 77;

        return true;
    }
}

Все готово для создания клиента и демонстрации. Вынесем код, создающий управляющий объект и конфигурирующий цепочку в метод CreateOrderManager(). Клиентом будет метод Execute(). Изменяя в нем данные заказа orderData можно увидеть как они попадают к разным обработчикам:

public static class Demo
{
    private static OrderManager CreateOrderManager()
    {
        var manager = new OrderManager(new DefaultOrderHandler());

        manager.AddHandler(new FavCustomerOrderHandler());
        manager.AddHandler(new LargeOrderHandler());

        return manager;
    }

    public static void Execute()
    {
        OrderManager orderManager = CreateOrderManager();

        OrderData orderData = new OrderData() {
            Id = 0,
            Amount = 100,
            CustomerId = 15,
            ItemId = 8
        };

        orderManager.ProcessNewOrder(orderData);
    }
}

Особенности реализации в .NET и C#

Одним из вариантов реализации Цепочки обязанностей в C# является использование групповых делегатов. Стоит отметить, что в этом случае теряется возможность обрабатывать возвращаемое значение методов.

Для примера используем уже созданные классы обработчиков. Вместо управляющего класса объявим делегат, который определяет сигнатуру методов-обработчиков:

delegate bool OrderHandlerDelegate(OrderData orderData);

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

public static class DelegateDemo
{
    private static OrderHandlerDelegate CreateOrderManager()
    {
        var favCustomerOrderHandler = new FavCustomerOrderHandler();
        var largeOrderHandler = new LargeOrderHandler();
        var defaultOrderHandler = new DefaultOrderHandler();

        var manager = new OrderHandlerDelegate(favCustomerOrderHandler.Process);
        manager += new OrderHandlerDelegate(largeOrderHandler.Process);
        manager += new OrderHandlerDelegate(defaultOrderHandler.Process);

        return manager;
    }

    public static void Execute()
    {
        OrderHandlerDelegate orderManager = CreateOrderManager();

        OrderData orderData = new OrderData() {
            Id = 0,
            Amount = 10,
            CustomerId = 5,
            ItemId = 8
        };

        orderManager(orderData);
    }    
}

Комментарии (11) -

А ничего что в мультикастовых делегатах нет гарантированного порядка их вызова? Ваша цепочка просто развалится

В MSDN сказано

When a multicast delegate is invoked, the delegates in the invocation list are called synchronously in the order in which they appear.

msdn.microsoft.com/.../...m.multicastdelegate.aspx


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

Алексей 24.02.2011 19:14:53

Все таки во всех книгах (и в примерах) четко сказано, что вызов в мультикастовых методах - неуправляем

Ну для меня приоритет MSDN выше книг. Кроме того, если в данном случае еще отбросить необходимость default обработчика, то в порядке нет смысла. Все равно нормально прервать цепочку нет возможности и будут вызваны все. Нужен строгий порядок - нужная другая реализация.

Алексей 24.02.2011 19:33:05

Спасибо что так оперативно реагируешь. Я как бы прогить немного могу. А вот с шаблонами только только столкнулся. Хотя немного по некоторым пробежал - в общем я часть паттернов уже сам (не понимая что это такое) реализовывал. В общем жди на других ветках вопросы))

Neftedollar 07.05.2014 0:23:00

там опечатка ?
    public void ProcessNewOrder(OrderData orderData)
    {
        foreach (IOrderHandler handler in this._handlers) {
            if (handler.Process(orderData)) { // тут опечатка? !handler.Process(orderData)
                return; //  или тут?
            }
        }

        if (!this._defaultHandler.Process(orderData)) {
            throw new InvalidOperationException();
        }
    }

Neftedollar 07.05.2014 0:32:18

@ Neftedollar:
а я понял. Моя ошибка. Смутила фраза обязан обработать запрос

Антон 02.11.2016 23:57:16

Доброго дня.

В примерах смущает отсутствие последовательности ссылающихся друг на друга один к одному обработчиков. Список обработчиков представлен колекцией (массивом, списком), но в большинстве источников Handler-ы формируют Linked list; сам Handler контролирует вызывать ли вложенный Handler, нет необходимости во внешнем коде, который выстраивает циклы и знает о результатах обработки на каждом шаге.
Как в таком случае обработать ситуацию, при которой обработчик может иметь 3 различных результата обработки (true/false/undefined) и в зависимости от типа/конфигурации/внешних параметров вызывать другой Handler/вернуть результат обратки шага как результат обработки цепочки. Разве мы не должны скрывать это все в объекте Handler? Только он должен знать как обработать степ и что сделать с результатом, внешний код должен быть независим от логики обработки.

Хотелось бы узнать ваше мнение по поводу вложенности обработчиков, нужна ли она?

Антон А почему сам обработчик должен принимать решение. Ведь его задача - обработать данные. Хотя и такая структура с принятием решения внутри обработчика существует.

Насчет большого числа вариантов результата - достаточно просто вернуть класс с 2 свойствами: (1) признак обработано или нет, (2) сам объект.

Ну и наконец, обработчик точно не должен знать что делать с результатом. Повторюсь - его задача именно обработка. Что будет дальше - задача других блоков приложения. Внешний код неависим от логики обработки, но сам решает что делать с результатом.

Почему мы делаем это действие?
if (!this._defaultHandler.Process(orderData)) {
            throw new InvalidOperationException();
        }
Зачем прерывать выполнение программы

Serega
Потому что данные не были обработаны ни одним обработчиком, включая обработчик по умолчанию.

Pingbacks and trackbacks (2)+

Добавить комментарий