Видео курс Шаблоны проектирования

Основные темы, рассматриваемые на уроке:
01:05 1 Метафора
05:53 2 Структура паттерна на языке UML
07:01 3 Структура паттерна на языке C#
09:35 4 Структура Объектов
10:41 5 Применимость паттерна
12:16 6 Достоинства и недостатки паттерна
15:47 7 Назначение паттерна
18:47 8 «Когда может понадобиться паттерн?»

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

Для просмотра полной версии видеокурса и получения доступа к дополнительным учебным материалам Вам необходимо оформить подписку
Оформить подписку Скачать материалы урока

Паттерн Chain of Responsibility

Название

Цепочка обязанностей

Также известен как

-

Классификация

По цели: поведенческий

По применимости: к объектам

Частота использования

Ниже средней    -   1 2 3 4 5

Назначение

Паттерн Chain of Responsibility - позволяет избежать привязки объекта-отправителя запроса к объекту-получателю запроса, при этом давая шанс обработать этот запрос нескольким объектам. Паттерн Chain of Responsibility – связывает в цепочку объекты-получатели запроса и передает запрос вдоль этой цепочки, пока один из объектов, составляющих эту цепочку не обработает передаваемый запрос.

Введение

В реальной жизни, сложно найти рациональный аналог модели, которую описывает паттерн Chain of Responsibility. Для лучшего понимания работы паттерна можно привести нереалистичный пример. Например, предлагается рассмотреть работу бюро переводов с английского языка на другие иностранные языки. Имеется три переводчика англо-немецкий, англо-китайский и англо-русский. Переводчики сидят за столами один за другим. Когда клиент приходит в бюро то он имеет дело только с одним переводчиком, который сидит за первым столом (англо-немецким). Клиент дает англо-немецкому переводчику документ для перевода на определенный язык. Англо-немецкий (первый в цепочке) переводчик определяет язык перевода, и если он в состоянии сделать перевод, то он переводит документ, если не в состоянии, то передает документ дальше по цепочке (сидящему у него за спиной) англо-китайскому переводчику и так далее.

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

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

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

Структура паттерна на языке UML

См. пример к главе: \013_Chain of Responsibility\001_Chain

Структура паттерна на языке C#

Структура объектов:

См. пример к главе: \013_Chain of Responsibility\001_Chain

Участники

  • Handler - Обработчик:

Предоставляет интерфейс для обработки запросов.

  • ConcreteHandler - Конкретный обработчик:

Обрабатывает запрос, который предназначен ему. Имеет ссылку на своего преемника, которому передает запрос в случае, если сам не в состоянии его обработать.

  • Client - Клиент:

Посылает запрос определенному объекту класса ConcreteHandler, который находится в цепочке объектов-обработчиков.

Отношения между участниками

Отношения между классами

  • Абстрактный класс Handler связан связью отношения самоассоциации.
  • Конкретные классы ConcreteHandler1 и ConcreteHandler2 связаны связью отношения наследования с абстрактным классом Handler.

Отношения между объектами

  • Когда клиент посылает запрос, этот запрос передается по цепочке, пока один из объектов ConcreteHandler не обработает этот запрос.

Мотивация

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

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

Проблема организации выдачи контекстно-зависимой справочной информации заключается в том, что объект, который является инициатором запроса (например, кнопка, на которую навел курсор пользователь) не владеет информацией о том, какой объект в конечном итоге предоставит справку. В связи с этим, нужно каким-то образом разделить иерархии объектов – возможных инициаторов запросов справочной информации и объектов, хранящих в себе справочные данные и предоставляющие их по требованию. Паттерн Chain of Responsibility демонстрирует, как этого можно добиться.

Идея паттерна заключается в том, чтобы дать возможность обработать запрос от объекта-инициатора нескольким объектам-получателям запроса и таким образом разорвать биективную связь от конкретного объекта-отправителя к конкретному объекту-получателю/обработчику. Таким образом, запрос будет перемещаться от объекта-отправителя по цепочке объектов-обработчиков, пока один из них не выполнит запрос на предоставление справочной информации. Нужно заметить, что у каждого объекта-обработчика имеется возможность либо обработать запрос (если есть такая возможность), либо передать его другому объекту-обработчику (если такой возможности нет), который находится далее в цепочке контекстов. В результате такой реализации, когда отправитель не знает о конкретном получателе запроса, появляется понятие анонимного получателя (implicit receiver).

Предположим, пользователь наводит курсор на кнопку printButton, которая находится в диалоговом окне printDialog

В данном случае ни кнопка printButton, ни форма printDialog, ни даже объекты-обработчики типов ButtonHandler или DialogHandler не обрабатывают запрос на предоставление справки, и он достигает объекта-обработчика aplicationHandler типа ApplicationHandler, который может его обработать или игнорировать. Таким образом, объект-отправитель не знает о том какой именно объект-получатель обработает его запрос.

Чтобы гарантировать анонимность получателя, все объекты в цепочке (ButtonHandler, DialogHandler, ApplicationHandler) имеют единый интерфейс IHelpHandler для обработки запросов и для доступа к следующему в цепочке объектов-обработчиков. Для обработки запросов классы ButtonHandler, DialogHandler и ApplicationHandler используют метод HandleHelp, где по умолчанию запрос передается следующему в цепочке объекту-обработчику, так называемому преемнику.

 

См. пример к главе: \013_Chain of Responsibility\002_ContextsHelp

Применимость паттерна

Паттерн Chain of Responsibility рекомендуется использовать, когда:

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

Результаты

Паттерн Chain of Responsibility обладает следующими преимуществами:

  • Позволяет ослабить связанность между объектами.

Паттерн Chain of Responsibility избавляет клиента от необходимости знать, какой объект обработает его запрос. Клиенту и обработчику запроса, ничего не известно друг о друге. «Объект-звено» цепочки ничего не знает о структуре цепочки, в которую он включен.

  • Добавляет гибкости при распределении обязанностей между объектами.

Паттерн Chain of Responsibility позволяет добавлять новые и изменять существующие объекты-обработчики входящие в состав цепочки.

Паттерн Chain of Responsibility обладает следующим недостатком:

  • Обработка запроса не гарантирована.

В связи с тем, что у запроса может не оказаться в цепочке объекта, который в состоянии обработать данный запрос, запрос может достичь конца цепочки и пропасть. 

Реализация

Особенности, которые следует учесть при реализации паттерна Chain of Responsibility:

  • Реализация цепочки преемников.

Существует несколько вариантов реализации:

  • Использовать существующие связи между объектами (например, между кнопкой и формой на которой она размещена, существует связь через свойство Parent). Использование уже существующих связей, которые, уже поддерживают необходимую цепочку, позволяет уйти от использования дублирующих связей. Но если существующая цепочка не отображает необходимой цепочки обязанностей, то построения необходимой цепочки связей избежать не удастся.
  • Определить новые связи (через свойство Successor реализованное в абстрактном классе Handler, и, возможно, переопределенное в конкретных классах-наследниках ConcreteHandler1 и ConcreteHandler2).
  • Соединение преемников.

Если создание цепочки связей является необходимостью, то реализация метода HandleRequest в классе Handler по умолчанию будет логичной. Этот метод будет перенаправлять запрос преемнику, если тот существует, используя ссылку на преемника, которая доступна через свойство Successor. Такой подход позволит наследникам абстрактного класса Handler не переопределять метод HandleRequest если не планируется его специфической обработки на уровне такого наследника. В таком случае, запрос по умолчанию передастся следующему объекту-обработчику в цепочке.

abstract class HelpHandler
{
    public HelpHandler Successor { get; set; }
    public virtual void HandleHelp(HelpHandler h)
    {
        if(Successor!=null)
        {
            Successor.HandleHelp(h);
        }
    }        
}
  • Представление запросов.

В самом простом варианте, например, в случае абстрактного класса HelpHandler из раздела мотивации, запрос жестко кодируется в виде вызова метода HandleHelp. С одной стороны такой подход придает определенное удобство и обеспечивает безопасность, но с другой стороны, при этом переадресовывать можно только фиксированное количество запросов, которые определяются интерфейсом заданным абстрактным классом Handler. Существует альтернативный подход – использовать метод-обработчик, который в качестве параметра принимает определенный код запроса,  и в зависимости от значения этого кода, обрабатывает запрос определенным образом. Так можно организовать обработку заранее известного количества различных запросов.

 

Единственное о чем нужно позаботиться в данном случает, это организации системы кодирования-декодирования, которая поддерживалась бы и объектами-отправителями и объектами-обработчиками запросов. Естественно, что при этом могут возникнуть проблемы с безопасностью, т.к. такой подход менее безопасен, чем прямые вызовы методов. Решить эту проблему может использование отдельных объектов-запросов, в которых бы инкапсулировались параметры запроса. Такой подход придаст дополнительную гибкость в реализации, поскольку через наследование от базового класса Request можно порождать новые типы запросов, которые могут иметь дополнительные параметры. В таком случае, объект обработчик должен уметь идентифицировать наследников класса Request и особым способом обрабатывать каждую разновидность. Этого можно добиться, задав в классе Request интерфейс метода доступа, который бы возвращал идентификатор класса. Но такой механизм диспетчеризации нужен, если язык программирования не поддерживает механизмы определения типов, для того чтобы объект обработчик мог определить тип запроса самостоятельно. Платформа .Net позволяет это сделать используя как метод GetType механизма рефлексии, так и ключевые слова is и as

 

Следует обратить внимание, что использование GetType совместно с оператором typeof не идентично использованию is или as. Нужно учитывать то, что оператор typeof возвращает имя типа, которое указано при компиляции и не учитывает дерево наследования типов, а GetType возвращает имя типа во время инстанцирования экземпляра, учитывая дерево наследования типов, аналогично работают и операторы is и as т.е. учитывая upcasting.

 

Вот как использована такая возможность в классе DialogHandler из раздела Мотивация:

class DialogHandler : IHelpHandler
{
    ToolTip tt = new ToolTip();
    public void HandleHelp(Control ctrl)
    {
        if (ctrl is Dialog && ctrl.Name == "saveDialog")
        {
            tt.Show("This is Save Dialog", ctrl);
        }
        else if (Successor != null)
            Successor.HandleHelp(ctrl);
    }

    public IHelpHandler Successor { get; set; }
}

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

Пример кода

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

public partial class Application : Form
{
    Dialog dialog;
    IHelpHandler buttonHandler;
    IHelpHandler dialogHandler;
    IHelpHandler aplicationHandler;
    public Application()
    {
        InitializeComponent();
    }
        
    private void Application_MouseHover(object sender, EventArgs e)
    {
        this.aplicationHandler = new ApplicationHandler();
        this.aplicationHandler.HandleHelp(this);
    }

    private void Dialog_MouseHover(object sender, EventArgs e)
    {
        this.dialogHandler = new DialogHandler();
        this.aplicationHandler = new ApplicationHandler();
        this.dialogHandler.Successor = this.aplicationHandler;
        this.dialogHandler.HandleHelp(sender as Dialog);
    }

    private void Button_MouseHover(object sender, EventArgs e)
    {
        this.buttonHandler = new ButtonHandler();
        this.dialogHandler = new DialogHandler();
        this.aplicationHandler = new ApplicationHandler();
        this.buttonHandler.Successor = this.dialogHandler;
        this.dialogHandler.Successor = this.aplicationHandler;
        this.buttonHandler.HandleHelp(sender as Button);
    }  
}

Интерфейс  IHelpHandler задает контракт для классов-обработчиков ApplicationHandler, DialogHandler, ButtonHandler.

interface IHelpHandler
{
    void HandleHelp(Control ctrl);
    IHelpHandler Successor { get; set; }\
}

class ApplicationHandler:IHelpHandler
{
    ToolTip tt = new ToolTip();
    public void HandleHelp(Control ctrl)
    {
            tt.Show("This is Application", ctrl);
    }
    public IHelpHandler Successor { get; set; }
}

class DialogHandler : IHelpHandler
{
    ToolTip tt = new ToolTip();
    public void HandleHelp(Control ctrl)
    {
        if (ctrl is Dialog && ctrl.Name == "saveDialog")
        {
            tt.Show("This is Save Dialog", ctrl);
        }
        else if (Successor != null)
            Successor.HandleHelp(ctrl);
    }

    public IHelpHandler Successor { get; set; }
}

class ButtonHandler : IHelpHandler
{
    ToolTip tt = new ToolTip();
    public void HandleHelp(Control ctrl)
    {
        if (ctrl is Button && ctrl.Name == "saveButton")
        {
            tt.Show("This is Save Button", ctrl);
        }
        else if (Successor != null)
            Successor.HandleHelp(ctrl);
    }

    public IHelpHandler Successor { get; set; }
}

См. пример к главе: \013_Chain of Responsibility\002_ContextsHelp

© 2017 ITVDN, все права защищены