订阅事件的一种替代方法,还是EventObject真的必要吗?

总结


本文的目的是尝试从不同的角度看待事件分发系统的描述。

在撰写本文时,大多数领先的php框架都基于对EventObject的描述来实现事件系统。

这已成为php中的标准,最近已通过采用psr / event-dispatcher标准来确认。

但是事实证明,事件对象的描述对开发侦听器几乎没有帮助。 有关猫的详细信息。


有什么问题


让我们看看在开发中使用EventObject的角色和目标。


  1. 开发人员(A)可以通过生成事件来将第三方指令插入其流程中。


    开发人员通过接口描述EventObject或其签名。


    在描述EventObject时,开发人员的目标是为其他开发人员提供数据对象的描述,并在某些使用情况下,描述通过该对象与主线程进行交互的机制。


  2. 描述“侦听器”的开发者(B)。


    开发者订阅指定的事件。 在大多数情况下,侦听器描述应满足可调用类型。


    同时,开发人员不会为侦听器的类或方法命名。 但是根据惯例,还有一个限制,那就是处理程序接收EventObject作为参数。



当工作组采用psr / event-dispatcher时 ,分析了使用事件分发系统的许多选项。


psr标准提到以下用例:


  1. 单向通知-“如果您有兴趣,我会做一些事情”
  2. 对象改进-“这是一件事情,请在我对其进行操作之前进行更改”
  3. 收藏-“将您所有的东西给我,以便我可以使用此列表来做点事”
  4. 替代链-“这是事情,你们首先要应对,然后做,然后停下来”

同时,工作组对事件分配系统的类似用法提出了许多问题,这与每个用例都有一个“不确定性”这一事实有关,该不确定性取决于Dispatcher对象的实现。


在上述针对开发人员(B)的角色中,没有方便且易读的方式来了解开发人员(A)选择了使用事件系统的哪个选项。 开发人员将不仅要查看EventObject的描述代码,还要查看生成该事件的代码。


结果,签名是事件对象的描述,旨在促进开发人员(B)的工作,但这项工作做得不好。


另一个问题是单独对象的存在并不总是合理的,它另外描述了系统中已经描述的实体。


namespace MyApp\Customer\Events; use MyApp\Customer\CustomerNameInterface; use MyFramevork\Event\SomeEventInterface; class CustomerNameChangedEvent implements SomeEventInterface { /** *     * @return int */ public function getCustomerId(): int; /** *    */ public function getCustomerName(): CustomerNameInterface; } 

在上面的示例中,已经在系统中描述了CustomerNameInterface对象。


这让人想起过多引入新概念。 毕竟,当我们需要实现一个方法时,例如,写入客户的名称更改日志时,我们不会将方法参数合并到一个单独的实体中,而是使用以下形式的方法的标准描述:


  function writeToLogCustomerNameChange( int $customerId, CustomerNameInterface $customerName ) { // ... } 

结果,我们看到以下问题:


  1. 不良的侦听器代码签名
  2. 调度员不确定性
  3. 返回类型不确定性
  4. 引入许多其他实体,例如SomeEventObject

让我们从不同的角度来看它


如果问题之一是对侦听器的描述不正确,那么让我们不是从事件对象的描述而是从侦听器的描述来看事件分发系统。


开发人员(A)描述了应如何描述侦听器。


  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ); } 

优秀的开发人员(A)能够将侦听器的描述和传输的数据传达给第三方开发人员。


在编写侦听器时,开发人员(B)在环境实现中键入CustomerNameChangedListener ,并且IDE可以在其类中添加对该侦听器方法的描述。 代码完成很棒。


让我们看一下新的侦听器方法签名。 即使快速浏览一下也足以了解所使用的事件分发系统的版本是:“单向通知”。


输入数据不会通过引用进行传输,这意味着无法以任何方式进行修改,以使更改落入主流。 缺少返回值;主线程无反馈。


那其他用例呢? 让我们玩一下事件侦听器接口的描述。




  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): CustomerNameInterface; } 

需要返回值,这意味着侦听器可以(但不要求)返回与指定接口匹配的其他值。 用例:“对象改进”。




  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { /** * @return ItemInterface[] */ public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): array; } 

需要某种类型的返回值,从中可以理解这是集合的元素。 用例:“集合”。




  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName ): VoteInterface; } 

用例:“替代链,投票”。




  namespace MyFramework\Events; interface EventControllInterface { public function stopPropagation(); } 

  namespace MyApp\Customer\Events; interface CustomerNameChangedListener { public function onCustomerNameChange( int $customerId, CustomerNameInterface $customerName, EventControllInterface &$eventControll ); } 

未经讨论,停止事件的传播是好是坏。


明确读取此选项,主线程为侦听器提供了停止事件的机会。


因此,如果我们继续对侦听器进行描述,则在开发侦听器方法时,它们会获得更好的可读性签名。


此外,我们还有机会在主线程中明确指向:


  1. 更改输入数据的可接受性
  2. 返回类型,而不是传入类型
  3. 显式传输对象以停止事件的传播

如何实现事件订阅


选项可能有所不同。 所有选项的一般含义都归结为以下事实:我们需要以某种方式通知特定接口所属的事件ListenerProvider对象(该对象提供订阅事件的机会)。


您可以考虑将传递的对象转换为可调用类型的示例。 应该理解的是,可能有很多选择来获取附加的元信息:


  1. 可以如示例中那样显式传递
  2. 可以存储在侦听器接口的注释中
  3. 您可以使用侦听器接口的名称作为事件的名称



订阅实现示例


  namespace MyFramework\Events; class ListenerProvider { private $handlerAssociation = []; public function addHandlerAssociation( string $handlerInterfaceName, string $handlerMethodName, string $eventName ) { $this->handlerAssociation[$handlerInterfaceName] = [ 'methodName' => $handlerMethodName, 'eventName' => $eventName ]; } public function addHandler(object $handler) { $hasAssociation = false; foreach( $this->handlerAssociation as $handlerInterfaceName => $handlerMetaData ) { if ( $handler interfaceof $handlerInterfaceName ) { $methodName = $handlerMetaData['methodName']; $eventName = $handlerMetaData['eventName']; $this->addListener($eventName, [$handler, $methodName]); $hasAssociation = true; } } if ( !$hasAssociation ) { throw new \Exception('Unknown handler object'); } } } 

我们向订阅对象添加一个配置方法,该方法针对每个侦听器接口描述其元数据,例如被调用的方法和事件的名称。


根据此数据,在订阅时,我们使用指定的方法将传递的$处理程序转换为可调用对象。


如果您注意到的话,代码意味着一个$处理程序对象可以实现许多事件侦听器接口,并且将订阅每个接口。 这是SubscriberInterface的类似物,用于将一个对象批量订阅到多个事件。 如您所见,在上面的实现中,不需要单独的机制作为addSubscriber(SubscriberInterface $subscriber)它可以直接使用。


调度员


las,所描述的方法与被接受为psr / event-dispatcher标准的接口背道而驰


由于我们不需要将任何对象传递给Dispatcher。 是的,您可以传递像糖这样的对象:


  class Event { public function __construct(string $eventName, ...$arguments) { // ... } public function getEventName(): string { // ... } public function getArguments(): array { // ... } } 

并在psr接口上生成事件时使用它,但这很丑陋。


以一种很好的方式,Dispatcher接口看起来会更好,如下所示:


  interface EventDispatcherInterface { public function dispatch(string $eventName, ...$arguments); public function dispatchStopabled(string $eventName, ...$arguments); } 

为什么要用两种方法? 很难将所有用例组合到一个实现中。 最好为每个用例添加自己的方法,这将清楚地说明Dispatcher将如何处理从侦听器返回的值。


仅此而已。 与社区讨论所描述的方法是否具有生命权将是很有趣的。

Source: https://habr.com/ru/post/zh-CN463313/


All Articles