Название шаблона
Цепочка ответственностей (Chain of Responsibility).
Тип
Поведенческий шаблон проектирования (Behavioral).
Описание
Шаблон Цепочка ответственностей предназначен для предотвращения связывания инициатора сообщения с конкретным экземпляром его обработчика. При этом само сообщение передается по цепочке, в которой каждое звено выбирает между обработкой и пересылкой данных дальше.
Цепочка ответственностей применяется в случаях, если:
- существует более одного обработчика исходного запроса;
- (или) обработчики сообщения определяются во время выполнения приложения;
- (или) необходимо отправить сообщение без явного указания получателя.
Как можно понять из описания, использование данного шаблона приводит к организации обработчиков событий в виде древовидной структуры или последовательной цепи. При этом само сообщение передается от более низких (детализированных) слоев к более высоким (более абстрактным).
Рассмотрим в качестве примера событие "нажатие клавиши", когда в приложении открыто диалоговое окно. Сообщение будет передано самому контекстному обработчику, принадлежащего активному элементу управления. Далее, в случае необходимости, оно будет переслано к более абстрактным обработчикам, принадлежащим (в порядке очередности) диалогу, его родительскому окну, а затем приложению. И если ни один из них не отреагирует на это сообщение, то оно будет утеряно.
Еще одним примером может служить обработка исключений. Сообщение проходит по цепи обработчиков, представляющих в древовидную структуру, начиная с локальных и заканчивая уровнем приложения. Последний, в отличии от предыдущего примера, как правило реагирует на любое полученное сообщение и прерывает работу самого приложения.
В результате применения шаблона:
- Уменьшается связанность, т.к. отправителю и получателю нет необходимости знать обо всех обработчиках в цепочке, а так же какие именно их них отреагируют на запрос. Более того, сами обработчики могут не знать друг о друге и даже о следующем звене. Наглядным примером может служить обработка исключений: метод, выбросивший его, ничего не знает о том где и как оно будет обработано.
- Увеличивается гибкость приложения, т.к. сама цепочка обработчиков может меняться независимо от клиента. Последнему необходимо знать только точку для отправки сообщения.
Можно выделить следующих участников шаблона:
- Интерфейс обработчика (IHandler) – определяет общий интерфейс для взаимодействия с клиентами.
- Обработчик (Handler) – непосредственная реализация, которая обрабатывает запрос.
- Управляющий объект (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);
}
}