chain of responsibility php

Паттерн проектирования Chain of Responsibility (Цепочка обязанностей) на PHP

Перед прочтением ознакомьтесь с введением в паттерны проектирования на PHP, в котором описаны принятые соглашения и понятия. Данная статья дополняется с некоторой периодичностью, так что если вы ее читали ранее, не факт что данные не изменились.

Цепочка обязанностей (Chain of Responsibility) относится к классу поведенческих паттернов. Служит для ослабления связи между отправителем и получателем запроса. При этом сам по себе запрос может быть произвольным.

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

Структура

Структура очень простая

В исполнении на php эта схема приобретает следующий вид

Обратите внимание, если следующий обработчик не нашелся, то сообщение теряется. Если в вашем случае это критично, то стоит бросить исключение или каким-либо иным способом известить пользователя о проблеме.

Давайте перейдем от абстрактного кода, к какому-либо примеру. Разработаем логгер ошибок с несколькими уровнями критичности. Все ошибки писать в лог, уровня critical будем отправлять на e-mail, а debug выводить на экран.

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

Другие реализации

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

Можно использовать некоторый контейнер, который будет отвечать за:

При такой реализации хэндлерам нужно будет лишь реализовать интерфейс обработки сообщения.

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

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

Применяйте этот паттерн, если

Реальным примером, где этот паттерн был применен может быть практически любой event manager. Будь то стандартные события в Java Script или новая система событий в Zend Frameword 2.

Источник

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

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

Проблема

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

Описание

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

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

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

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

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

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

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

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

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

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

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

Источник

Шаблоны проектирования простым языком. Часть третья. Поведенческие шаблоны

Шаблоны проектирования — это руководства по решению повторяющихся проблем. Это не классы, пакеты или библиотеки, которые можно было бы подключить к вашему приложению и сидеть в ожидании чуда. Они скорее являются методиками, как решать определенные проблемы в определенных ситуациях.

Википедия описывает их следующим образом:

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

Будьте осторожны

Также заметьте, что примеры ниже написаны на PHP 7. Но это не должно вас останавливать, ведь принципы остаются такими же.

Типы шаблонов

Шаблоны бывают следующих трех видов:

Простыми словами: Поведенческие шаблоны связаны с распределением обязанностей между объектами. Их отличие от структурных шаблонов заключается в том, что они не просто описывают структуру, но также описывают шаблоны для передачи сообщений / связи между ними. Или, другими словами, они помогают ответить на вопрос «Как запустить поведение в программном компоненте?»

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

Цепочка обязанностей (Chain of Responsibility)

Цепочка обязанностей — поведенческий шаблон проектирования предназначенный для организации в системе уровней ответственности.

Пример из жизни: например, у вас есть три платежных метода (A, B и C), настроенных на вашем банковском счёте. На каждом лежит разное количество денег. На A есть 100 долларов, на B есть 300 долларов и на C — 1000 долларов. Предпочтение отдается в следующем порядке: A, B и C. Вы пытаетесь заказать что-то, что стоит 210 долларов. Используя цепочку обязанностей, первым на возможность оплаты будет проверен метод А, и в случае успеха пройдет оплата и цепь разорвется. Если нет, то запрос перейдет к методу B для аналогичной проверки. Здесь A, B и C — это звенья цепи, а все явление — цепочка обязанностей.

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

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

Теперь приготовим цепь, используя объявленные выше звенья (например, Bank, Paypal, Bitcoin):

Команда (Command)

Команда — поведенческий шаблон проектирования, используемый при объектно-ориентированном программировании, представляющий действие. Объект команды заключает в себе само действие и его параметры.

Пример из жизни: Типичный пример: вы заказываете еду в ресторане. Вы (т.е. Client ) просите официанта (например, Invoker ) принести еду (то есть Command ), а официант просто переправляет запрос шеф-повару (то есть Receiver ), который знает, что и как готовить. Другим примером может быть то, что вы ( Client ) включаете ( Command ) телевизор ( Receiver ) с помощью пульта дистанционного управления ( Invoker ).

Простыми словами: Позволяет вам инкапсулировать действия в объекты. Основная идея, стоящая за шаблоном — это предоставление средств, для разделения клиента и получателя.

18–19 сентября, Онлайн, Беcплатно

Наконец, мы можем увидеть, как использовать нашего клиента:

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

Читайте также:  общая характеристика деятельности обучающегося в период прохождения практики пример

Итератор (Iterator)

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

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

Простыми словами: Представляет способ доступа к элементам объекта без показа базового представления.

Обратимся к примерам в коде. В PHP очень просто реализовать это, используя SPL (Standard PHP Library). Приводя наш пример с радиостанциями, изначально у нас есть Radiostation :

Затем у нас есть итератор:

Посредник (Mediator)

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

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

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

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

Изначально у нас есть посредник ChatRoomMediator :

Затем у нас есть наши User (коллеги):

Хранитель (Memento)

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

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

Простыми словами: Шаблон хранитель фиксирует и хранит текущее состояние объекта, чтобы оно легко восстанавливалось.

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

Затем у нас есть наш Editor (создатель), который будет использовать объект хранитель:

Наблюдатель (Observer)

Наблюдатель — поведенческий шаблон проектирования, также известен как «подчинённые» (Dependents). Создает механизм у класса, который позволяет получать экземпляру объекта этого класса оповещения от других объектов об изменении их состояния, тем самым наблюдая за ними.

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

Простыми словами: Шаблон определяет зависимость между объектами, чтобы при изменении состояния одного из них зависимые от него узнавали об этом.

Затем мы делаем публикации JobPostings на которые соискатели могут подписываться:

Посетитель (Visitor)

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

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

Простыми словами: Шаблон посетитель позволяет добавлять будущие операции для объектов без их модифицирования.

Затем у нас есть реализация для животных:

Давайте реализуем посетителя:

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

Стратегия (Strategy)

Стратегия — поведенческий шаблон проектирования, предназначенный для определения семейства алгоритмов, инкапсуляции каждого из них и обеспечения их взаимозаменяемости. Это позволяет выбирать алгоритм путём определения соответствующего класса. Шаблон Strategy позволяет менять выбранный алгоритм независимо от объектов-клиентов, которые его используют.

Пример из жизни: Возьмём пример с пузырьковой сортировкой. Мы её реализовали, но с ростом объёмов данных сортировка работа стала выполняться очень медленно. Тогда мы сделали быструю сортировку. Алгоритм работает быстрее на больших объёмах, но на маленьких он очень медленный. Тогда мы реализовали стратегию, при которой для маленьких объёмов данных используется пузырьковая сортировка, а для больших объёмов — быстрая.

Простыми словами: Шаблон стратегия позволяет переключаться между алгоритмами или стратегиями в зависимости от ситуации.

Перейдем к коду. Возьмем наш пример. Изначально у нас есть наша SortStrategy и разные её реализации:

Состояние (State)

Состояние — поведенческий шаблон проектирования. Используется в тех случаях, когда во время выполнения программы объект должен менять своё поведение в зависимости от своего состояния.

Пример из жизни: Допустим, в графическом редакторе вы выбрали кисть. Она меняет своё поведение в зависимости от настройки цвета, т. е. рисует линию выбранного цвета.

Простыми словами: Шаблон позволяет менять поведение класса при изменении состояния.

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

Изначально у нас есть интерфейс WritingState и несколько его реализаций:

Паттерн состояние применяется в проектировании распределённых сиситем, наряду с другими паттернами.

Шаблонный метод (Template Method)

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

Пример из жизни: Допустим, вы собрались строить дома. Этапы будут такими:

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

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

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

Источник

Паттерны по-моему: chain of responsibility и command

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

Chain of responsibility — Цепочка ответственности

Вы что-нибудь поняли? Класс называется «обработчик», мы видим в нём одну единственную виртуальную функцию без реализации с названием «обработать», с единственным параметром с названием «запрос», поэтому похоже, что функция будет принимать и обрабатывать число, и возвращать логический true или false. Раз эта функция абстрактная, мы можем догадаться, что она создана только для того, чтобы перекрывать её в классах-потомках. Пожалуй, больше из этого кода пока не выжать.

Что мы видим здесь? Класс DivisionChecker, потомок класса Handler, видимо, какой-то реальный обработчик. Мы видим у него в полях какое-то числовое value и какой-то указатель next на другой объект Handler. Но раз класс Handler абстрактный, то next будет хранить указатель на объекты-потомки класса Handler, вполне возможно и другие объекты класса DivisionChecker.
Конструктор DivisionChecker просто инициализирует значениях своих полей, тут ничего интересного, а дальше мы видим перекрывается абстрактная функция handle.

Что же DivisionChecker делает, если его просят «обработать» какое то число, передав его в качестве параметра request? Мы видим, что он проверяет, делится ли request нацело на то число, которое он сам хранит в value, и если делится, то сразу возвращает true. А если не делится, то он вызывает функцию handle у другого обработчика next, ссылку на которого хранит. Возвращает сам то, что этот другой обработчик вернёт. Переводит стрелки, короче. Делегирует работу. Перенаправляет задание. Настоящий менеджер! А кто же крайним останется?

Читайте также:  замена суб платы что это

Вот он, наш обычный работяга, DefaultHandler, ещё один потомок класса Handler. У него нет никакой своей property, только унаследованный и перекрытый метод handle. Если у него этот метод вызывают, он сообщает, что переданное ему число является простым и возвращает false.

Теперь давайте посмотрим на основную программу. Что мы видим?

В ней сначала создаётся переменная-указатель queue, потом создаётся объект класса DefaultHandler и адрес его складывается в queue. Короче говоря, queue указывает на объект DefaultHandler, как-то так:

Потом начинает работать цикл, переменная i получает значение 2, и у объекта queue вызывается метод handle с параметром 2. Чей метод вызывается? Правильно, метод класса DefaultHandler. Что он делает? Сообщает, что число 2 простое, и возвращает false. У нас как раз в цикле стоит проверка, и если возвращено false, то создаётся новый объект, но уже класса DivisionChecker. Этому объекту в конструктор передаётся число 2 и адрес того объекта, куда сейчас указывает переменная queue, то есть адрес старого DefaultHandler. Адрес полученного объекта заносится обратно в переменную queue. В результате этих манипуляций получаем следующее:

Начинает работать следующая итерация цикла i получает значение 3, и снова у объекта queue вызывается метод handle с параметром 2. Но только теперь queue указывает на другой объект и вызывается метод класса DivisionChecker. Что он делает? Проверяет, что 3 не делится на 2, хранящееся у него в value, и раз оно не делится, то вызывает handle у того объекта next, который хранит. А чей это будет метод? Метод многострадального DefaultHandler. Что он делает снова? Сообщает, что число 3 простое, и возвращает false. Это false возвращается обратно в DivisionChecker, а из него обратно в основную программу, как раз к проверке условия. Раз возвращено false, то создаётся новый объект, снова класса DivisionChecker, ему в конструктор передаётся число 3 и адрес того объекта, куда сейчас указывает переменная queue, то есть адрес созданного ранее DivisionChecker. Адрес полученного объекта заносится обратно в переменную queue и получаем такую структуру ссылающихся друг на друга объектов:

Посмотрим ещё одну итерацию цикла, i равно 4, у объекта queue вызывается метод handle с параметром 4, queue указывает на DivisionChecker. Тот проверяет, что 4 не делится на 3, хранящееся у него в value, и раз оно не делится, то вызывает handle у того объекта next, который хранит, а это другой DivisionChecker! Он проверяет, что 4 делится на 2, хранящееся в свою очередь у него в value, и раз оно делится, то сразу возвращает true. Это true через пару возвратов прилетает обратно в основную программу, как раз к проверке условия, условие не срабатывает, поэтому никаких новых объектов не создаётся, и цикл продолжает работу своей дорогой.

В итоге, когда цикл доработает до конца, мы увидим на экране список простых чисел, не превышающих 100, а в памяти окажется такая структура из объектов:

В чём смысл этого паттерна? Где здесь цепочка ответственных, и за что они ответственны? Так вот же, это цепочка обработчиков DivisionHandler! Почему цепочка? Потому что каждый из них знает только следующего. Почему ответственности? Потому что он передаёт следующему ответственность за решение задачи, за исполнение запроса.

Где можно встретить пример такого поведения? Да вот, у нас на лекции можно было раньше до ковидных ограничений. Приходит преподаватель в аудиторию, и ему нужен журнал группы, чтобы вписать отсутствующих. Должен преподаватель знать, у кого сегодня этот журнал? Конечно нет. А что он должен знать? Он должен знать одного старосту, и всё. Преподаватель такой: «Эй, староста! Давай гони сюда журнал!». А староста такой встаёт и говорит: «А меня сегодня нет, я на госуслугах записался и пошёл вакцинироваться! Но журнал должен быть у зам-старосты! Эй, зам-старосты, давай журнал!». А зам-старосты такая встаёт и говорит: «А меня тоже сегодня нет, я корзинку с пирожками понесла бабушке. Но журнал должен быть у зам-зам-старосты! Эй, зам-зам-старосты. ».

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

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

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

Это классический вариант паттерна Chain of Responsibility, но есть ещё один часто используемый приём, который, как мне кажется, тоже можно к этому паттерну отнести. Давайте посмотрим на другую программу.

Как будто, пока всё то же самое. Абстрактный базовый класс, объявляющий одну абстрактную функцию, один потомок, который получая число, сразу объявляет его простым и возвращает false.

А вот и отличие. Новый потомок, но не Handler, а DefaultHandler! Это значит, что он перекрывает не абстрактный метод handle базового класса Handler, а вполне себе рабочий метод handle класса DefaultHandler! Что же он делает? Он проверяет, делится ли переданное ему число на 2, и если делится, но при этом не является самим числом 2, то сразу возвращает true. А вот если это условие не выполняется, то он как и раньше вызывает другой метод, но не у какого-то другого объекта, а у самого себя же, — унаследованный метод DefaultHandler::handle. А дальше?

Новый потомок, и снова не от базового класса, а от DivisionChecker2. Проверяет переданное число на делимость на 3, и вызывает унаследованный метод DivisionChecker2::handle. Думаю, вы уже поняли, что дальше будет ещё два потомка и основная программа:

Тут, как видим, никакой цепочки из объектов не создаётся, но программа делает то же самое. И всё равно тут можно увидеть цепочку ответственности, но цепочку не из объектов, а из классов, цепочку «предок-потомок», где запрос может быть при определённых условиях обработан в классе-потомке или передан предку в иерархии. Такой приём сплошь и рядом используется при создании обработчиков событий. Там, если нужно изменить поведение какого-то обработчика, типа mouseDoubleClickEvent в Qt, мы в потомке перекрываем этот метод, проверяем в нём, должны ли мы поведение при конкретном событии менять, и если да, то выполняем нужное нам действие, а если нет, то вызываем унаследованный обработчик, передавая управление дальше по цепочке ответственных Chain of Responsibility.

Command — Команда

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

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

А вот здесь уже всё серьёзно. Абстрактные классы нам зачем даны? Чтобы описать то общее, чем будут объединены все их потомки. Значит, у нас будет много частных случаев Команды, много команд, но объединять их все будет что? То, что у всех их можно будет:

Наконец, что-то конкретное, а не абстрактное! Это конкретный потомок абстрактного класса Command, а именно класс MoveCommand. У него есть свойство для хранения указателя на какую-то точку, и два целочисленных свойства для хранения смещения. В конструктор передаются и инициализируются свойства и зануляется указатель на точку, а вот дальше посмотрим поподробнее ещё раз:

Читайте также:  Upnp что такое в роутере

В метод execute у команды передаётся точка. В методе она запоминается во внутреннем указателе и у неё вызывается метод move с тем смещением _dx и _dy, которое хранилось в команде. Спрашивается, зачем кому-то вызывать метод execute, чтобы вызвался метод move у точки, если можно было просто вызвать самостоятельно метод move у той же самой точки? Не понятно. Читаем внимательно дальше.

Что произойдёт, если вызвать у такой команды метод unexecute? Как видим, команда просто вызывает тот же самый метод move у точки (какой точки? да той, которая должна храниться к этому моменту в указателе _selection), но с обратными знаками смещений. Если для одной и той же точки вызвать сначала execute, а потом unexecute, то координаты точки не изменятся. Это важно.

Ну и наконец, метод clone. Мы видим, что если его вызвать у команды MoveCommand, то он создаёт и возвращает новый, но точно такой же экземпляр класса MoveCommand. Что-то мне это напоминает, какой-то другой паттерн, но я уже забыл, какой именно. Может быть кто-то вспомнит и сможет мне напомнить в комментариях.

Ну как бы и всё. Мы надували щёки, ходили вокруг да около одного и того же метода move у точки. Но ведь чтобы создать и сдвинуть точку, нужно всего лишь вызвать у неё метод move? Как-то так:

Нет! Нельзя просто так взять и засунуть лампочку в рот сдвинуть точку! У нас есть ещё целая главная функция, давайте читать её:

Сначала мы создаём ассоциативный массив для указателей на команды, где привязанные к символам ‘a’, ‘d’, ‘w’ и ‘s’ будут лежать четыре экземпляра класса MoveCommand, отличающиеся друг от друга смещениями. Это будут четыре прообраза для четырёх возможных движений точки во все стороны света. Потом мы создаём собственно точку и рисуем её на экране. А потом мы делаем что-то совсем странное, мы создаём стек для команд. Стек, как известно, это такая несправедливая структура данных, что кто в неё пришёл последний, тот первый из неё вышел. А дальше начинается основной цикл обработки сообщений программы, и в нем:

Читаем с клавиатуры символ, нажатый пользователем и ищем по коду символа в ассоциативном массиве команду. Если такая команда нашлась, то мы делаем три вещи:

А вот если нажата клавиша ‘z’, то делается кое-что хитрое. Из стека history, если он не пустой, вытаскивается верхний объект-команда, у неё вызывается метод unexecute и эта команда удаляется.

За пределами моего обзора осталась перерисовка точки после каждой выполненной команды и выход их цикла по нажатой клавише Esc, но в этом вы, наверняка, и так разобрались сами.

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

Так что же, паттерн команда — он просто про отмену действий? Нет, это слишком близорукий взгляд на вещи. Давайте подумаем и попробуем понять идею того, что мы сделали. Ещё раз поднимем самый главный в этом паттерне вопрос. Почему нельзя просто создать объект и вызвать у него нужный метод?

Можно! Но дело всё в том, что вызов метода — это очень негибкая штука, по сравнению, например, с объектов. Метод можно только вызвать и не вызвать, и всё. А объект, как говорится, является first class citizen. Поэтому вместо такого простого кода нужно использовать вот такой, более сложный:

Так вот, суть паттерна Команда в том, что нельзя просто так брать и вызывать метод у объекта. Надо чтобы каждый вызов метода представлял собой отдельный специальный объект! Обратите внимание — не метод move превращается в объект класса CMoveCommand, а конкретный вызов этого метода превращается в отдельный объект. Собрались вызывать метод move 20 раз — значит надо создать 20 объектов класса CMoveCommand.

Что это даёт и почему это важно. Представьте, что вы — директор маленького стартапа разработчиков влоггеров (хотел написать «разработчиков-программистов», но, как известно, сегодня эта профессия уже выходит из моды и все дети хотят стать влоггерами). Утром вы приходите на работу и лично подходите к каждому своему сотруднику, и говорите: «Арсений, ты сегодня внимаешь сюжет про то-то. Ты, Света, сегодня снимаешь сюжет про сё-то. А ты, Денис, сегодня должен два сюжета снять про то, как они будут снимать сюжеты». Они сразу говорят: «Будет сделано, Олег Сергеевич» и начинают… эээ… работать (это у влоггеров всё равно так называется?).

На что это похоже в мире объектов? На прямой вызов метода. Есть тот, кто вызывает, и есть тот, кто сразу исполняет. Директивно, сразу, безо всяких посредников.

Потом вы получили MBA и начинаете внедрять новые технологии непрямого управления. Вместо того, чтобы утром подходить к каждому сотруднику, вы нанимаете ассистента и кожаный диван, и удобно на нём развалившись утром пишете бумажки с поручениями. На одной бумажке — задача для Арсения, на другой — для Дениса, на третьей — для Светланы. Вы остаётесь на диване, а ассистент разносит бумажки по исполнителям, которые точно так же начинают работать.

В чём разница? Между вами и исполнителями появляется бумажка-задание, разрывается эта жёсткая связь между тем, кто даёт задание и тем, кто его исполняет. Исполнитель может припоздать на работу — он увидит бумажку с заданием чуть позже и начнёт работать. Директор может уехать на Таити в отпуск, наготовив достаточно бумажек с заданиями — ассистент вполне справится с раздачей этих бумажек каждое утро, ничего не понимая в теме. Директор может не думать, кому поручить конкретное задание — новомодная agile-команда сама разберётся, кто его будет выполнять. Исполнитель может перенаправить бумажку кому-то другому, если он приболел. Директор не сможет сказать, что он поручал совсем другое, а исполнитель не сможет сказать, что ему ничего не поручали — вот она, бумажка с конкретным заданием. Ассистент может по пути создать копию этой бумажки и отправить в налоговую сложить в сейф для истории. Наконец, в дальнем углу сейфа может лежать три конверта с тремя бумажками, как в известном анекдоте, помните — ведь он на самом деле про паттерн Команда!

Итак, переводим всю эту аналогию обратно на язык классов, объектов и методов. Каждый конкретный объект-команда — это замена одного вызова метода. Да, неудобно: лишний уровень indirection. Но зато с объектом, в отличие от метода, можно делать всё, что угодно: отдать куда-то, получить откуда-то, сохранить, загрузить, скопировать, и так далее. Это позволяет:

На сдачу, как побочный эффект, мы получаем лёгкость реализации undo, отмены действий. Если всё, что происходит, происходит в результате выполнения последовательности атомарных команд, каждая из которых умеет отменять, откатывать своё действие, то сохранив копии этих действий и их порядок выполнения, мы всегда можем «проиграть» эту цепочку в обратном порядке и откатиться назад.

Вот диаграмма классов для паттерна Команда из книжки. С Command и ConcreteCommand, я думаю, и так всё ясно, но будет поучительно, если вы попробуйте найти, где у нас в программе Invoker, Client и Receiver. Да, у нас отдельных классов таких нет, но мы же должны за деревьями лес видеть? Найдите в нашей программе те части, которые отвечают за Создание команд (Client), за хранение и вызов команд (Invoker) и за получение и исполнение команд (Receiver), убедитесь, что они достаточно друг от друга независимы.

Пожалуй, на сегодня всё. Сегодня у меня для вас три домашних задания. Во-первых, кто первый нашёл страшную специально оставленную косямбу, может броситься писать об этом комментарий предъявить мне сертификат на +1 балл на ближайшей пересдаче по ООП. Во-вторых, нужно найти несоответствие диаграммы классов нашей первой программы для Chain of Responsibility и такой же диаграмме в книжке GoF. В-третьих, надо в программу для паттерна Command вставить функциональность redo. Если вдруг кому понадобится, то программы лежат здесь.

Источник

Образовательный портал