Предыдущие части: Обёртки, Фасад, ПабСаб, Наблюдатель, Визитёр, Фабрика, Синглтон, Стратегия, MVC, Вступление
Чем дальше мы будем знакомиться с шаблонами проектирования, тем чаще будем замечать, что они постоянно стремятся уничтожить зависимости. Про одну из таких зависимостей я и хочу рассказать.
Какая проблема решается
Команда – концептуально довольно сложный шаблон, и потому довольно мутный. Понять, какая проблема решается, можно только тщательно изучив эту проблему. Вы можете просто о ней не знать.
Пойдём от простого. Самое малое, что может сделать Команда – это инкапсулировать некие действия.
Когда-то очень давно я писал игру под операционную систему DOS. В ней была музыка, которая проигрывалась с помощью карты Sound Blaster, а программировалась эта карта напрямую, с помощью низкоуровневых команд контроллера DMA.
Не вдаваясь в детали (которые я всё равно забыл), скажу, что для изменения, допустим, громкости звука нужно было послать определённый байт в определённый порт.
Потом эпоха DOS закончилась, и мне пришлось портировать игру на Windows. Там уже был DirectX, для работы с которым не требовалось программировать Sound Blaster напрямую.
Поэтому ту часть программы, которая управляла звуком, мне пришлось переписать заново. Само по себе это нормально, так как очевидно, что нужно что-то переписать. Но программа была слишком сильно завязана на вызовы звуковой подсистемы, чтобы их просто взять и заменить.
Как видим, тут была типичная зависимость. Управление звуком было жёстко привязано к конкретной аппаратной платформе, а программа была жёстко привязана к этому управлению.
Эту, и не только, проблему решает шаблон Команда (Command).
Как он работает
Реализация шаблона может показаться слишком громоздкой, поэтому для начала надо понять сам его принцип.
Представьте, что у вас есть старый телевизор.
У него есть различные крутилки, с помощью которых вы можете им управлять.
Очевидно, вы должны взаимодействовать с этими крутилками напрямую: трогать их руками, поворачивать в нужную сторону, ну и вообще знать, что они делают.
Потом вас появляется другой телевизор, и вдруг вы видите, что теперь там вместо крутилок ползунки и кнопки:
Управлять ими надо по-другому. То есть для каждого телевизора вам нужно переучиваться под новое управление. Понимаете, о чём речь?
Как произошла революция в управлении телевизорами? Появился универсальный пульт управления.
Он является прослойкой между вами и телевизором, предлагая вам вместо прямого управления всего лишь отдавать команды. Например, чтобы увеличить громкость, вы не крутите крутилку на телевизоре, а нажимаете на пульте кнопку "сделать громче". То есть, вы просто отдаёте команду, а выполняет её кто-то другой. И вам неважно, как он будет это делать.
Мы знаем, что одним пультом можно управлять совершенно разными устройствами, отдавая одни и те же команды.
Теперь можно осмысленно прочитать описание шаблона:
Шаблон Команда позволяет инкапсулировать запрос на выполнение определённого действия в виде отдельного объекта. Этот объект запроса на действие и называется командой. При этом объекты, инициирующие запросы на выполнение действия, отделяются от объектов, которые выполняют это действие.
В шаблоне присутствуют 4 сущности: Команда, Инициатор, Ресивер, Клиент.
Команда – объект, инкапсулирующий запрос на действие. Действие направлено на Ресивер. Инициатор – объект, умеющий выполнять команды. Клиент – тот, кто создаёт команды и привязывает их к Инициатору. То есть Клиент это просто основная программа, которая всем заведует.
В нашей схеме пульт является инициатором и выполняет заложенные в него команды, которые инкапсулируют запрос на управление ресивером – телевизором. Вы являетесь клиентом, который пользуется пультом. Вы выбираете, какие команды надо исполнить, и вызываете для этого инициатор.
Реализация
1. Команда
Центральная сущность это объект-команда.
У объекта-команды должен быть метод execute(), то есть "выполнить". А вот что выполнять и над чем?
Так появляется вторая сущность:
2. Ресивер
Это тот, над кем исполняется команда. В нашем примере ресивер это телевизор. Команда должна быть непосредственно знакома с ресивером.
Стало быть, для работы команды нужно поместить в неё ссылку на ресивер. Будем для разнообразия писать на PHP:
$receiver = new TV();
$command = new Command($receiver);
Итак, мы создали команду и передали в её конструктор ресивер, с которым она будет дальше работать. Что же происходит внутри команды?
Конечно, она сохраняет в себе ссылку на ресивер, а в своём методе execute() вызывает какой-то метод у ресивера (someMethod):
Этот какой-то метод и есть тот, с которым мы работали раньше напрямую. Теперь с ним напрямую работает команда, а не мы.
Далее, можно уже пользоваться командами, но их положено заключать в Инициатор.
3. Инициатор
Мы можем создать объект-инициатор с различными командами, передав эти команды в конструктор. Наш инициатор это пульт, поэтому мы можем создать класс Remote и поместить в него две команды: VolumeUp и VolumeDown.
Обратим внимание, что инициатору не обязательно передавать команды в конструктор. Команды можно потом установить отдельно и т.д. Как угодно, в общем.
4. Клиент
Теперь мы, являясь клиентом, можем создать себе инициатор (пульт) и воспользоваться его командами:
Команды CommandVolumeUp и CommandVolumeDown тоже нужно написать, посмотрим на одну из них:
Здесь я использовал вымышленный метод dmaSendByte(1) у ресивера, чтобы имитировать как бы посылку байта 1 в канал DMA.
Класс команды CommandVolumeDown должен выглядеть так же, только вызывать dmaSendByte(-1).
Тут становится видно, что необязательно делать две разные команды, если они концептуально одинаковы. Достаточно инкапсулировать в команду её параметры:
И теперь при создании пульта можно передать ему одну и ту же команду с разными параметрами:
А в чём профит, скажете вы?
Да, мы конечно красиво изоливали и инкапсулировали всё что можно, и теперь я могу отвязать логику программы под DOS или Windows от окружения: для того, чтобы повысить громкость, буду вызывать команду CommandVolumeUp, чтобы понизить – команду CommandVolumeDown, и этот код уже не надо будет менять практически никогда.
Это уже что-то. Это может спасти от совершенно непредвиденных проблем. Но ведь всё это решается и другими средствами – интерфейсами, фасадами, адаптерами, стратегиями.
Почему вдруг возник шаблон Команда?
Теперь, когда мы достаточно его обсудили, можно перейти к главному блюду.
Запрос
Команда это не просто покрутить громкость на телевизоре. В Команде есть очень широкое понимание запроса на действие. Запросом может быть и рисование чего-то на экране, и отправка данных через интернет.
Автономность
Команда в принципе рассчитана на то, чтобы быть автономной. Она не обязательно должна исполняться сразу. Вы создали команду сейчас, с какими-то параметрами, а выполнить её можно потом, сильно позже. Так как она полностью владеет информацией и о ресивере, и о своих параметрах, ей всё равно, когда выполняться.
В некоторых случаях, на некоторых платформах, вы можете буквально передать команду в другое приложение в виде Java-кода, или как-то ещё закодировав. И она должна там сработать.
Отслеживание
Одной из функций Инициатора является отслеживание, какие команды и когда были применены.
Отмена
Вероятно, главное отличие шаблона Команда – это возможность отмены. То есть помимо метода execute(), у команды должен быть метод undo(), который что-то там восстанавливает как было. Конечно, этот метод придётся реализовывать самостоятельно, а также он необязателен, если вам не нужна отмена.
Но тогда вам возможно не нужен и сам шаблон.
Что же касается типичного применения, это рисование в каком-нибудь Фотошопе, где каждое действие это команда, Инициатор ведёт лог выполненных команд, и благодаря этому логу команды можно отменять в обратном порядке одну за другой.