В последнее время командные автобусы привлекают большое внимание сообщества. Эта тема может быть довольно сложной, когда вы пытаетесь понять все концепции и терминологию, но, по сути, то, что делает командная шина, на самом деле невероятно просто.
В этой статье мы подробнее рассмотрим варианты шаблона команд; их компоненты; что такое командная шина; и пример приложения с использованием пакета Tactician.
обзор
Итак, что же такое командная шина?
Роль командной шины заключается в обеспечении передачи команды ее обработчику. Командная шина получает команду, которая является не чем иным, как сообщением, описывающим намерение, и передает ее обработчику, который затем отвечает за выполнение ожидаемого поведения. Таким образом, этот процесс можно рассматривать как вызов уровня обслуживания — где командная шина выполняет обработку промежуточного сообщения.
До введения Командной шины сервисные уровни часто представляли собой набор классов без стандартного способа вызова. Командные автобусы решают эту проблему путем обеспечения согласованного интерфейса и лучшего определения границы между двумя уровнями. Стандартный интерфейс также позволяет добавлять дополнительные функции, добавляя декораторы или добавляя промежуточное ПО.
Следовательно, командная шина может быть не просто вызовом сервисного уровня. Базовые реализации могут определять местонахождение обработчика только на основе соглашений об именах, но более сложные конфигурации могут передавать команду через конвейер. Конвейеры могут выполнять дополнительные действия, такие как: перенос поведения в транзакцию; отправка Команды по сети; или, возможно, некоторые очереди и журналы.
Прежде чем мы подробнее рассмотрим преимущества использования командной шины, давайте рассмотрим отдельные компоненты, которые делают это возможным.
команды
Командный паттерн был одним из поведенческих паттернов, описанных «Бандой четырех» как способ общения двух объектов.
Чтобы немного усложнить ситуацию, шаблон эволюционировал с альтернативными дизайнами. В архитектурных шаблонах, таких как CQS (разделение командного запроса) и CQRS (разделение ответственности по командным запросам), также используются команды, однако в их контексте — команда — это просто сообщение.
Традиционно, команда GoF будет обрабатывать себя:
final class CreateDeck { /** * @var string */ private $id ; /** * @param string $id */ public function __construct ( $id ) { $this - > id = $id ; } /** * @return Deck */ public function execute ( ) { // do things } }
Так как этот подход к шаблону команд содержит поведение, нет сообщения, которое должно быть перенаправлено в обработчик. Нет необходимости в командной шине.
Тем не менее, шаблон командного сообщения предлагает отделить намерение от интерпретации, выражая действия внутри домена:
final class CreateDeck { /** * @var string */ private $id ; /** * @param string $id */ public function __construct ( $id ) { $this - > id = $id ; } /** * @return DeckId */ public function getId ( ) { return DeckId : : fromString ( $this - > id ) ; } }
В этом примере и в оставшейся части этой статьи мы будем использовать команду в качестве сообщения. Он фиксирует намерения пользователя и содержит данные, необходимые для выполнения задачи. Он явно описывает поведение, которое может выполнять система. Следовательно, Команды имеют обязательное имя, например: CreateDeck
, ShuffleDeck
и DrawCard
.
Команды обычно называются DTO (объектами передачи данных), так как они используются для хранения данных, переносимых из одного места в другое. Команды поэтому неизменны. После создания данные не должны меняться. Вы заметите, что наша команда примера CreateDeck
содержит установщиков или любого другого способа изменить внутреннее состояние. Это гарантирует, что он не может измениться во время передачи в обработчик.
Обработчики команд
Обработчики интерпретируют намерение определенной Команды и выполняют ожидаемое поведение. Они имеют отношение 1: 1 к командам — это означает, что для каждой команды всегда есть только один обработчик.
final class CreateDeckHandler { /** * @var DeckRepository */ private $decks ; /** * @param DeckRepository $decks */ public function __construct ( DeckRepository $decks ) { $this - > decks = $decks ; } /** * @param CreateDeck $command */ public function handle ( CreateDeck $command ) { $id = $command - > getId ( ) ; $deck = Deck : : standard ( $id ) ; $this - > decks - > add ( $deck ) ; } }
В нашем примере выше создается новая колода. Также важно отметить, что не происходит. Это не заполняет представление; вернуть код ответа HTTP или записать на консоль. Команды могут выполняться из любого места, поэтому обработчики остаются независимыми от вызывающей среды. Это чрезвычайно полезно при построении границ между вашим приложением и внешним миром.
Командная шина
И, наконец, сама командная шина. Как кратко объяснено выше, ответственность Командной шины состоит в том, чтобы передать Команду своему Обработчику. Давайте посмотрим на пример.
Представьте, что нам нужно предоставить конечную точку RESTful API, чтобы разрешить создание новых колод:
use Illuminate \ Http \ Request ; final class DeckApiController { /** * @var CommandBus */ private $bus ; /** * @var CommandBus $bus */ public function __construct ( CommandBus $bus ) { $this - > bus = $bus ; } /** * @var Request $request */ public function create ( Request $request ) { $deckId = $request - > input ( 'id' ) ; $this - > bus - > execute ( new CreateDeck ( $deckId ) ) ; return Response : : make ( "" , 202 ) ; } }
Теперь представьте, что вам нужно создавать новые колоды с консоли. Мы можем справиться с этим требованием, снова передав эту же команду через шину:
class CreateDeckConsole extends Console { /** * @var string */ protected $signature = 'deck' ; /** * @var string */ protected $description = 'Create a new Deck' ; /** * @var CommandBus */ private $bus ; /** * @var CommandBus $bus */ public function __construct ( CommandBus $bus ) { $this - > bus = $bus ; } public function handle ( ) { $deckId = $this - > argument ( 'id' ) ; $this - > bus - > execute ( new CreateDeck ( $deckId ) ) ; $this - > comment ( "Created: " . $deckId ) ; } }
Примеры не касаются деталей реализации создания колод. Наши Командные и Консольные Команды используют Командную Шину для передачи инструкций Приложению, что позволяет им сосредоточиться на том, как следует отвечать на их запросы. Это также позволяет нам удалить то, что потенциально могло быть много дублированной логики.
Тестирование нашего контроллера и консольной команды теперь является тривиальной задачей. Все, что нам нужно сделать, это утверждать, что команда, переданная в шину, была правильно сформирована на основе запроса.
пример
Пример приложения рассматривает колоду карт для домена. Приложение имеет ряд команд: CreateDeck
, ShuffleDeck
и DrawCard
.
До этого момента CreateDeck
предоставлял контекст только при изучении концепций. Затем мы установим Tactician в качестве нашей командной шины и выполним нашу команду.
Конфигурирование тактика
После того, как вы установили Tactician , вам нужно будет настроить его где-нибудь в начальной загрузке вашего приложения.
В нашем примере мы поместили это в скрипт начальной загрузки, однако вы можете добавить это к чему-то вроде контейнера внедрения зависимости.
Прежде чем мы сможем создать наш экземпляр Tactician, нам нужно настроить наш конвейер. Tactician использует промежуточное программное обеспечение для обеспечения конвейера и действует как система плагинов пакета. Фактически, все в Tactician является промежуточным программным обеспечением — даже обработка Command .
Единственное промежуточное программное обеспечение, которое нам требуется в нашем конвейере, — это что-то для управления выполнением команд. Если это звучит сложно, то вы можете очень быстро взломать ваше собственное промежуточное ПО. Однако мы будем использовать CommandHandlerMiddleware
, предоставляемый Tactician. Создание этого объекта имеет несколько зависимостей.
public function __construct ( CommandNameExtractor $commandNameExtractor , HandlerLocator $handlerLocator , MethodNameInflector $methodNameInflector )
Как мы узнали ранее, командная шина находит обработчик для конкретной команды. Давайте подробнее рассмотрим, как каждая из этих зависимостей справляется со своей частью задачи.
CommandNameExtractor
Роль CommandNameExtractor
заключается в получении имени команды. Вы, вероятно, думаете, что PHP- функция get_class () сделает эту работу — и вы правы! Поэтому Tactician включает ClassNameExtractor, который делает именно это.
HandlerLocator
Нам также нужен экземпляр HandlerLocator
который найдет правильный обработчик на основе имени команды. Tactician предоставляет две реализации этого интерфейса: CallableLocator и InMemoryLocator .
Использование CallableLocator
полезно при разрешении обработчика из контейнера. Однако в нашем примере мы зарегистрируем наши обработчики вручную с помощью InMemoryLocator
.
Теперь у нас есть все зависимости, необходимые для получения экземпляра обработчика на основе имени команды, но этого недостаточно.
MethodNameInflector
Нам все еще нужно сказать Тактику, как вызывать Обработчик. Вот где вступает в игру MethodNameInflector
. Инфлектор возвращает имя метода, который ожидает, что команда передана ему.
Тактик снова помогает нам, предоставляя реализации для нескольких популярных соглашений. В нашем примере мы будем следовать соглашению метода handle($command)
реализованному в HandleInflector .
Простая тактическая конфигурация
Давайте посмотрим на настройку до сих пор:
use League \ Tactician \ CommandBus ; use League \ Tactician \ Handler \ Locator \ InMemoryLocator ; use League \ Tactician \ Handler \ CommandHandlerMiddleware ; use League \ Tactician \ Handler \ MethodNameInflector \ HandleInflector ; use League \ Tactician \ Handler \ CommandNameExtractor \ ClassNameExtractor ; $handlerMiddleware = new CommandHandlerMiddleware ( new ClassNameExtractor , new InMemoryLocator ( [ ] ) , new HandleInflector ) ; $bus = new CommandBus ( [ $handlerMiddleware ] ) ;
Это выглядит хорошо, но есть еще одна проблема — InMemoryLocator
требует, чтобы обработчики были зарегистрированы, чтобы их можно было найти во время выполнения. Поскольку это всего лишь код начальной загрузки для пары примеров, давайте пока сохраним ссылку на локатор, чтобы при необходимости позже можно было зарегистрировать обработчики.
$locator = new InMemoryLocator ( [ ] ) ; $handlerMiddleware = new CommandHandlerMiddleware ( new ClassNameExtractor , $locator , new HandleInflector ) ;
В правильном приложении вы, вероятно, захотите использовать локатор, который может найти обработчик команды на основе соглашения об именах.
Тактик сейчас настроен. Давайте использовать его для выполнения команды CreateDeck
.
Выполнение команды
Чтобы создать экземпляр команды, мы заполняем все необходимые требования конструктора:
<?php require 'bootstrap.php' ; $deckId = DeckId : : generate ( ) ; $newDeckCommand = new CreateDeck ( ( string ) $deckId ) ;
Последняя задача, остающаяся до того, как мы сможем отправить нашу Команду на своем пути через шину, состоит в том, чтобы мы зарегистрировали наш Обработчик в InMemoryLocator
на InMemoryLocator
мы сохранили ссылку из ранее:
$decks = new InMemoryDeckRepository ; $locator - > addHandler ( new CreateDeckHandler ( $decks ) , CreateDeck : : class ) ;
Наконец — мы готовы передать нашу Команду в Автобус:
$bus - > handle ( $newDeckCommand ) ; var_dump ( $decks - > findById ( $deckId ) ) ;
И это действительно так просто!
преимущества
Есть много преимуществ при применении шаблона команд или использовании командной шины.
-
Архитектурная граница
Одним из наиболее важных преимуществ является архитектурная граница, окружающая ваше приложение. Верхние уровни, такие как пользовательский интерфейс, могут отправлять команду на нижний уровень через границу и через согласованный интерфейс, предоставляемый командной шиной.
Верхний уровень знает контекст, в котором выдается команда, однако, как только сообщение прошло через границу — это просто сообщение, которое могло быть выдано из любого числа контекстов: HTTP-запрос; работа cron; или что-то еще полностью.
Границы способствуют разделению проблем и освобождают одну сторону от беспокойства о другой. Уровням высокого уровня больше не нужно знать, как именно выполняется задача, а нижним уровням не нужно беспокоиться о контексте, в котором они используются.
-
Framework и разделение клиентов
Приложения, окруженные границами, не зависят от их структуры. Они не имеют дело с HTTP-запросами или файлами cookie. Все свойства, необходимые для поведения, передаются как часть полезной нагрузки Command.
Отключение от вашей инфраструктуры позволяет поддерживать приложение в процессе изменения или обновления структуры и облегчает тестирование.
-
Отделяет намерение от интерпретации
Роль Командной шины заключается в переносе Команды в ее Обработчик. Это по своей сути означает, что намерение выполнить действие отделено от выполнения, иначе не было бы необходимости в Командной шине. Команды именуются с использованием языка бизнеса и явно описывают, как можно использовать приложение.
Сериализация команды становится намного проще, когда ей не нужно знать, как выполнять поведение. В распределенных системах сообщение может быть сгенерировано в одной системе, но выполнено в другой, написано на другом языке в другой операционной системе.
Когда вся единица кода создает сообщение, это очень легко проверить. Командная шина, введенная в устройство, может быть заменена, и сообщение может быть подтверждено.
Так что ты думаешь? Является ли командная шина чрезмерным проектированием или отличным инструментом для системной архитектуры?