影子战术:将军之刃_神秘的指挥车:看战术包

影子战术:将军之刃

Command Buses have been getting a lot of community attention lately. The topic can be quite overwhelming at first when trying to understand all the concepts and terminology, but in essence – what a Command Bus does is actually incredibly simple.

指挥巴士最近已经引起了社区的广泛关注。 首先,当试图理解所有概念和术语时,这个主题可能会让人感到不知所措,但是从本质上讲,Command Bus所做的实际上非常简单。

In this article, we’ll take a closer look at variations of the Command Pattern; their components; what a Command Bus is; and an example application using the Tactician package.

在本文中,我们将仔细研究命令模式的变体。 它们的组成; 什么是命令总线; 以及使用Tactician软件包的示例应用程序。

Hero Image?

总览 (Overview)

So, what exactly is a Command Bus?

那么,命令总线到底是什么?

Command Pattern

The role of the Command Bus is to ensure the transport of a Command to its Handler. The Command Bus receives a Command, which is nothing more than a message describing intent, and passes this onto a Handler which is then responsible for performing the expected behavior. This process can therefore be looked upon as a call to the Service Layer – where the Command Bus does the plumbing of the message in-between.

命令总线的作用是确保将命令传输到其处理程序。 命令总线接收一个命令,该命令不过是描述意图的消息,并将其传递到处理程序,该处理程序随后负责执行预期的行为。 因此,可以将这个过程视为对服务层的调用,在该层中,命令总线在中间进行消息传递。

Before the introduction of the Command Bus, Service Layers were often a collection of classes without a standard way of being invoked. Command Buses solve this problem by providing a consistent interface and better defining the boundary between two layers. The standard interface also makes it possible for additional functionality to be added by wrapping with decorators, or by adding middleware.

在引入命令总线之前,服务层通常是类的集合,而没有标准的调用方式。 命令总线通过提供一致的接口并更好地定义两层之间的边界来解决此问题。 标准接口还可以通过用装饰器包装或添加中间件来添加其他功能。

Therefore, there can be more to a Command Bus than calling a Service Layer. Basic implementations may only locate a Handler based on naming conventions, but more complex configurations may pass a Command through a pipeline. Pipelines can perform additional behavior, such as: wrapping the behavior in a transaction; sending the Command over a network; or perhaps some queueing and logging.

因此,与调用服务层相比,命令总线的功能更多。 基本实现可能仅基于命名约定来定位处理程序,但是更复杂的配置可能会通过管道传递命令。 管道可以执行其他行为,例如:将行为包装在事务中; 通过网络发送命令; 或一些排队和记录。

Before we take a closer look at the advantages of using a Command Bus, let’s take a look at the individual components which make it possible.

在深入研究使用命令总线的优势之前,让我们看一下使之成为可能的各个组件。

指令 (Commands)

The Command Pattern was one of the behavioral patterns described by The Gang of Four as a way for two objects to communicate.

命令模式是“四人帮”将其作为两个对象交流的一种行为模式。

To complicate things a little, the pattern has evolved with alternative designs. Architectural Patterns such as CQS (Command-query Separation) and CQRS (Command Query Responsibility Segregation) also use Commands, however in their context – a Command is simply a message.

为了使事情复杂一点,该模式随着替代设计的发展而发展。 CQS(命令查询分离)和CQRS(命令查询责任隔离)等架构模式也使用命令,但是在它们的上下文中–命令只是一条消息。

Traditionally, a GoF Command would handle itself:

传统上,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
    }
}

Since this approach to the Command Pattern contains the behavior, there is no message that must be routed to a Handler. There is no need for a Command Bus.

由于此命令模式方法包含该行为,因此没有消息必须路由到处理程序。 不需要命令总线。

However, the Command Message Pattern suggests separating intent from interpretation, expressing actions within the Domain:

但是,命令消息模式建议将意图与解释分开,以表示域内的动作:

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);
    }
}

In this example, and throughout the rest of this article, we will use a Command as a message. It captures the intent of the user and contains the input required to carry out a task. It explicitly describes behavior the system can perform. Therefore, Commands are named imperatively, such as: CreateDeck, ShuffleDeck and DrawCard.

在此示例中,以及在本文的其余部分中,我们将使用Command作为消息。 它捕获了用户的意图,并包含执行任务所需的输入。 它明确描述了系统可以执行的行为。 因此,命令必须强制命名,例如: CreateDeckShuffleDeckDrawCard

Commands are commonly referred to as DTO’s (Data Transfer Objects) as they are used to contain data being transported from one location to another. Commands are therefore immutable. After creation the data is not expected to change. You will notice that our CreateDeck example Command contains no setters, or any other way to alter the internal state. This ensures that it can not change during transit to the handler.

命令通常被称为DTO(数据传输对象),因为它们用于包含从一个位置传输到另一位置的数据。 因此,命令是不可变的。 创建后,数据不会更改。 您会注意到,我们的CreateDeck示例命令不包含setter或任何其他更改内部状态的方法。 这确保了它在传输到处理程序期间不会更改。

命令处理程序 (Command Handlers)

Handlers interpret the intent of a specific Command and perform the expected behavior. They have a 1:1 relationship with Commands – meaning that for each Command, there is only ever one Handler.

处理程序解释特定命令的意图并执行预期的行为。 它们与命令具有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);
    }
}

In our example above, a new Deck is being created. What’s also important to note is what isn’t happening. It does not populate a view; return a HTTP response code or write to the console. Commands can be executed from anywhere, therefore Handlers remain agnostic to the calling environment. This is extremely powerful when architecting boundaries between your application and the outside world.

在上面的示例中,正在创建一个新的Deck。 还需要注意的是未发生的事情。 它不会填充视图; 返回HTTP响应代码或写入控制台。 命令可以在任何地方执行,因此处理程序与调用环境无关。 在设计应用程序与外界之间的边界时,此功能非常强大。

命令总线 (The Command Bus)

And finally, the Command Bus itself. As briefly explained above, the responsibility of a Command Bus is to pass a Command onto its Handler. Let’s look at an example.

最后是Command Bus本身。 如以上简要说明的,命令总线的职责是将命令传递到其处理程序。 让我们来看一个例子。

Imagine we needed to expose a RESTful API endpoint to allow the creation of new Decks:

假设我们需要公开一个RESTful API端点,以允许创建新的Decks:

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);
    }
}

Now imagine the need to also create new Decks from the console. We can handle this requirement by passing the same Command again through the Bus:

现在想象需要从控制台创建新的Decks。 我们可以通过再次通过总线传递相同的命令来满足此要求:

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);
    }
}

The examples are not concerned with the implementation details of creating Decks. Our Controller and Console Commands use a Command Bus to pass instructions to the Application allowing them to focus specifically on how their type of request should be answered. It also allows us to remove what could have potentially been a lot of duplicated logic.

这些示例与创建Decks的实现细节无关。 我们的控制器和控制台命令使用命令总线将指令传递给应用程序,从而使他们可以专注于应如何回答其请求类型。 它还使我们可以消除可能存在大量重复逻辑的内容。

Testing our Controller and Console Command is now a trivial task. All we need to do is assert that the Command passed to the Bus was wellformed based on the request.

现在,测试我们的Controller and Console Command是一项艰巨的任务。 我们需要做的就是断言,根据请求,传递给总线的命令的格式正确。

(Example)

The example application considers a Deck of Cards for a domain. The application has a series of commands: CreateDeck, ShuffleDeck and DrawCard.

示例应用程序考虑了域的卡片组。 该应用程序具有一系列命令: CreateDeckShuffleDeckDrawCard

Up until this point CreateDeck has only been providing context when exploring the concepts. Next, we will set up Tactician as our Command Bus and execute our Command.

到目前为止, CreateDeck仅在探索概念时提供上下文。 接下来,我们将Tactician设置为Command Bus并执行Command。

配置战术师 (Configuring Tactician)

Once you have installed Tactician, you’ll need to configure it somewhere within the bootstrapping of your application.

安装Tactician之后 ,您需要在应用程序的引导程序中的某个位置进行配置。

In our example, we have placed this within a bootstrap script, however you may wish to add this to something like a Dependency Injection Container.

在我们的示例中,我们将其放置在引导脚本中,但是您可能希望将其添加到诸如依赖注入容器之类的东西中。

Before we can create our instance of Tactician, we need to set up our pipeline. Tactician uses middleware to provide a pipeline and acts as the package’s plugin system. In fact, everything in Tactician is middleware – even the handling of a Command.

在创建Tactician实例之前,我们需要建立管道。 Tactician使用中间件提供管道,并充当软件包的插件系统。 实际上,Tactician中的所有内容都是中间件,甚至是Command处理

Middleware Pipeline

The only middleware we require within our pipeline is something to handle the execution of Commands. If this sounds complex, then you can very quickly hack together your own middleware. We will, however, use the CommandHandlerMiddleware that Tactician provides. The creation of this object has several dependencies.

我们在管道中所需的唯一中间件是处理命令执行的东西。 如果这听起来很复杂,那么您可以非常快速地将自己的中间件黑客在一起。 但是,我们将使用Tactician提供的CommandHandlerMiddleware 。 该对象的创建具有多个依赖性。

public function __construct(
    CommandNameExtractor $commandNameExtractor,
    HandlerLocator $handlerLocator,
    MethodNameInflector $methodNameInflector
)

As we learned earlier, a Command Bus locates the Handler for a particular Command. Let’s take a closer look at how each of these dependencies handle their part of the task.

如前所述,命令总线为特定命令定位处理程序。 让我们仔细看看这些依赖项如何处理任务的一部分。

CommandNameExtractor (CommandNameExtractor)

The role of CommandNameExtractor is to get the name of the Command. You’re probably thinking PHP’s get_class() function would do the job – and you’re correct! Therefore Tactician includes ClassNameExtractor which does exactly that.

CommandNameExtractor的作用是获取命令的名称。 您可能会认为PHP的get_class()函数可以完成这项工作–您是对的! 因此,Tactician包括ClassNameExtractor ,它正是这样做的。

HandlerLocator (HandlerLocator)

We also need an instance of HandlerLocator which will find the correct Handler based on the name of the Command. Tactician provides two implementations of this interface: CallableLocator and InMemoryLocator.

我们还需要一个HandlerLocator实例,该实例将根据Command的名称查找正确的Handler。 Tactician提供了此接口的两种实现: CallableLocatorInMemoryLocator

Using CallableLocator is useful when resolving a Handler from a container. However, in our example, we’ll register our Handlers manually with an InMemoryLocator.

从容器解析处理程序时,使用CallableLocator很有用。 但是,在我们的示例中,我们将使用InMemoryLocator手动注册处理程序。

We now have all the dependencies required to get an instance of a Handler based on the name of the Command – but that isn’t enough.

现在,我们具有根据Command的名称获取Handler实例所需的所有依赖关系–但这还不够。

MethodNameInflector (MethodNameInflector)

We still need to tell Tactician how to invoke the Handler. That’s where the MethodNameInflector comes into play. The inflector returns the name of the method that expects the Command passed to it.

我们仍然需要告诉战术家如何调用处理程序。 那就是MethodNameInflector发挥作用的地方。 变形器返回期望将命令传递给它的方法的名称。

Tactician helps us out again by providing implementations for several popular conventions. In our example, we will follow the handle($command) method convention implemented within HandleInflector.

Tactician通过提供几种流行约定的实现来再次帮助我们。 在我们的示例中,我们将遵循在HandleInflector中实现的handle($command)方法约定。

简单的战术师配置 (Simple Tactician Configuration)

Let’s take a look at the setup so far:

让我们来看看到目前为止的设置:

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
]);

That looks good, but there is still one problem – the InMemoryLocator needs the Handlers to be registered so they can be found at runtime. Since this is only bootstrap code for a couple of examples, let’s store a reference to the locator for now so Handlers can be registered when needed later.

看起来不错,但是仍然存在一个问题– InMemoryLocator需要注册处理程序,以便可以在运行时找到它们。 由于这只是几个示例的引导程序代码,因此让我们现在存储对定位器的引用,以便以后可以在需要时注册处理程序。

$locator = new InMemoryLocator([]);

$handlerMiddleware = new CommandHandlerMiddleware(
    new ClassNameExtractor,
    $locator,
    new HandleInflector
);

In a proper application, you’ll probably want to use a locator that can find a Command’s Handler based on a naming convention.

在适当的应用程序中,您可能需要使用一个定位器,该定位器可以基于命名约定找到命令的处理程序。

Tactician is now configured. Let’s use it to execute the CreateDeck Command.

现在已配置战术师。 让我们用它来执行CreateDeck命令。

执行命令 (Executing the Command)

To create an instance of a Command, we fill out all the required constructor requirements:

要创建Command的实例,我们填写所有必需的构造函数要求:

<?php require 'bootstrap.php';

$deckId = DeckId::generate();

$newDeckCommand = new CreateDeck((string) $deckId);

The final task remaining before we can send our Command on it’s way through the bus is for us to register our Handler with the InMemoryLocator we stored a reference to from before:

在通过总线发送命令之前,剩下的最后一个任务是让我们向InMemoryLocator注册我们的Handler,我们从之前存储了对它的引用:

$decks = new InMemoryDeckRepository;

$locator->addHandler(
    new CreateDeckHandler($decks),
    CreateDeck::class
);

Finally – we are ready to pass our Command into the Bus:

最后–我们准备将我们的命令传递到总线上:

$bus->handle($newDeckCommand);

var_dump(
    $decks->findById($deckId)
);

And it really is that simple!

真的就是这么简单!

优点 (Advantages)

There are many advantages when applying the Command Pattern or utilizing a Command Bus.

应用命令模式或利用命令总线有许多优点。

  1. Architectural Boundary

    建筑边界

    One of the most important benefits is the architectural boundary that surrounds your Application. Upper layers, such as the User Interface, can send a Command to a lower layer, across the boundary and through the consistent interface provided by the Command Bus.

    最重要的好处之一是围绕应用程序的体系结构边界。 上层(例如用户界面)可以跨边界并通过命令总线提供的一致接口将命令发送到下层。

    The upper layer knows the context in which the Command is being issued, however, once the message has passed through the boundary – it’s just a message which could have been issued from any number of contexts: an HTTP request; a cron job; or something else entirely.

    上层知道发出命令的上下文,但是,一旦消息通过边界,它只是可以从任意多个上下文发出的消息:HTTP请求; 定时工作; 或完全其他的东西。

    Boundaries aid separation of concerns and free one side from worrying about the other. High-level layers no longer have the burden of knowing exactly how a task is completed – and lower layers do not need to worry about the context in which they are being used.

    边界有助于分离关注点,使一方不必担心另一方。 高层不再需要确切地知道任务的完成方式,而较低层也不必担心使用它们的上下文。

  2. Framework and Client Decoupling

    框架和客户端解耦

    Applications surrounded with boundaries are agnostic to their framework. They do not deal with HTTP requests or cookies. All properties needed to perform behavior get passed along as part of the Command payload.

    边界包围的应用程序与其框架无关。 它们不处理HTTP请求或cookie。 执行行为所需的所有属性都作为Command有效负载的一部分传递。

    Decoupling from your framework allows you to maintain your Application as a framework changes or updates – and makes it easier to test.

    与框架的解耦使您可以在框架发生更改或更新时维护您的应用程序–并使测试更加容易。

  3. Separates Intent from Interpretation

    将意图与解释分开

    The role of the Command Bus is to transport a Command to its Handler. This inherently means that the intention of conducting an action is separate from the execution, otherwise there would be no need for a Command Bus. Commands are named using the language of the business and explicitly describe how an application can be used.

    命令总线的作用是将命令传输到其处理程序。 本质上,这意味着执行动作的意图与执行是分开的,否则将不需要命令总线。 命令使用业务语言命名,并明确描述如何使用应用程序。

    Serializing a Command becomes much easier when it does not need to know how to perform the behavior. In Distributed Systems, a message could be generated on one system – but performed on another, written in a different language on a different operating system.

    当不需要知道如何执行行为时,序列化命令变得容易得多。 在分布式系统中,消息可以在一个系统上生成,但可以在另一个系统上执行,并以不同的语言在不同的操作系统上编写。

    When all a unit of code does is produce a message, it is extremely easy to test. The Command Bus injected into the unit can be replaced and the message can be asserted.

    当所有代码单元都产生一条消息时,测试非常容易。 可以替换注入到设备中的命令总线,并且可以声明该消息。

So, what do you think? Is a Command Bus over-engineering, or a great tool for system architecture?

所以你怎么看? Command Bus是工程过度的设计,还是系统体系结构的绝佳工具?

翻译自: https://www.sitepoint.com/command-buses-demystified-a-look-at-the-tactician-package/

影子战术:将军之刃

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值