Uma abordagem alternativa para se inscrever em eventos ou o EventObject é realmente necessário?

Sumário


O objetivo deste artigo é uma tentativa de analisar, de um ponto de vista diferente, uma descrição dos sistemas de distribuição de eventos.

No momento da redação deste artigo, a maioria dos frameworks php implementavam um sistema de eventos com base na descrição de um EventObject .

Isso se tornou o padrão no php, que foi confirmado recentemente pela adoção do padrão psr / event-dispatcher .

Mas acontece que a descrição do objeto de evento ajuda pouco no desenvolvimento do ouvinte. Para detalhes em cat.


Qual é o problema


Vejamos os papéis e objetivos daqueles que usam EventObject no desenvolvimento.


  1. Um desenvolvedor (A) que estabelece a capacidade de injetar instruções de terceiros em seu processo, gerando um evento.


    O desenvolvedor descreve o EventObject ou sua assinatura por meio de uma interface.


    Ao descrever um EventObject, o objetivo do desenvolvedor é fornecer a outros desenvolvedores uma descrição do objeto de dados e, em alguns casos de uso, descrever o mecanismo de interação com o thread principal por meio desse objeto.


  2. Desenvolvedor (B) que descreve o "ouvinte".


    O desenvolvedor assina o evento especificado. Na maioria dos casos, a descrição do ouvinte deve satisfazer o tipo de chamada .


    Ao mesmo tempo, o desenvolvedor não tem vergonha de nomear classes ou métodos do ouvinte. Mas há uma restrição mais por convenção de que o manipulador recebe um EventObject como argumento.



Quando o psr / event-dispatcher foi adotado pelo grupo de trabalho, muitas opções para o uso de sistemas de distribuição de eventos foram analisadas.


O padrão psr menciona os seguintes casos de uso:


  1. aviso unidirecional - "fiz algo se você estiver interessado"
  2. melhoria de objeto - "Aqui está uma coisa, por favor mude-a antes que eu faça algo com ela"
  3. coleção - “Dê-me todas as suas coisas para que eu possa fazer algo com esta lista”
  4. cadeia alternativa - “Aqui está a coisa, o primeiro de vocês a lidar com isso, fazer e depois parar”

Ao mesmo tempo, o grupo de trabalho levantou muitas questões para o uso semelhante de sistemas de distribuição de eventos, relacionados ao fato de que cada um dos casos de uso possui uma "incerteza" que depende da implementação do objeto Dispatcher .


Nas funções descritas acima para o desenvolvedor (B), não há uma maneira conveniente e legível de entender qual das opções de uso do sistema de eventos foi escolhida pelo desenvolvedor (A). O desenvolvedor sempre precisará examinar o código de descrição não apenas do EventObject , mas também no código em que esse evento é gerado.


Como resultado, a assinatura é uma descrição do objeto de evento, projetado para facilitar o trabalho do desenvolvedor (B) .Este trabalho não se sai bem.


Outro problema é a presença nem sempre justificada de um objeto separado, que descreve adicionalmente as entidades já descritas nos sistemas.


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

No exemplo acima, o objeto CustomerNameInterface já foi descrito no sistema.


Isso lembra a introdução excessiva de novos conceitos. Afinal, quando precisamos implementar um método, por exemplo, escrevendo no log de alterações de nome do cliente, não combinamos os argumentos do método em uma entidade separada, usamos a descrição padrão de um método do formulário:


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

Como resultado, vemos os seguintes problemas:


  1. assinatura de código do ouvinte inválida
  2. Incerteza do expedidor
  3. tipo de retorno incerteza
  4. introdução de muitas entidades adicionais, como SomeEventObject

Vamos olhar de uma perspectiva diferente


Se um dos problemas for uma descrição ruim do ouvinte, vejamos o sistema de distribuição de eventos não a partir da descrição do objeto do evento, mas a partir da descrição do ouvinte.


Desenvolvedor (A) descreve como o ouvinte deve ser descrito.


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

Um excelente desenvolvedor (A) conseguiu transmitir uma descrição do ouvinte e dos dados transmitidos aos desenvolvedores de terceiros.


Ao escrever um ouvinte, o desenvolvedor (B) digita CustomerNameChangedListener no ambiente de implementos e o IDE pode adicionar uma descrição do método do ouvinte à sua classe. A conclusão do código é ótima.


Vamos dar uma olhada na nova assinatura do método listener. Mesmo uma rápida olhada é suficiente para entender que a versão do sistema de distribuição de eventos usada é: "notificação unidirecional".


Os dados de entrada não são transmitidos por referência, o que significa que não há como modificá-los de forma que as alterações caiam no fluxo principal. Valor de retorno ausente; nenhum feedback do thread principal.


E quanto a outros casos de uso? Vamos brincar com a descrição da interface do ouvinte de eventos.




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

Havia um requisito para o valor de retorno, o que significa que o ouvinte pode (mas não é obrigado a) retornar um valor diferente se corresponder à interface especificada. Caso de uso: "aprimoramento de objeto".




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

Havia um requisito para o valor de retorno de um determinado tipo pelo qual se pode entender que esse é um elemento da coleção. Caso de uso: "coleção".




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

Caso de uso: "cadeia alternativa, votação".




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

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

Sem discussão, interromper a propagação de eventos é bom ou ruim.


Esta opção é lida sem ambiguidade, o encadeamento principal fornece uma oportunidade para o ouvinte parar o evento.


Portanto, se passarmos à descrição do ouvinte, obteremos assinaturas legíveis muito melhores dos métodos do ouvinte ao desenvolvê-los.


Além disso, temos a oportunidade de o thread principal apontar explicitamente para:


  1. aceitabilidade das alterações nos dados recebidos
  2. tipos de retorno que não sejam de entrada
  3. transferência explícita de um objeto com o qual interromper a propagação de um evento

Como implementar uma assinatura de evento


As opções podem ser diferentes. O senso geral de todas as opções se resume ao fato de que, de alguma forma, precisamos informar o objeto ListenerProvider (o objeto que oferece a oportunidade de se inscrever no evento), a qual evento pertence a interface específica.


Você pode considerar a conversão do objeto passado em um tipo que pode ser chamado como exemplo. Deve-se entender que pode haver muitas opções para obter meta-informações adicionais:


  1. pode ser passado explicitamente, como no exemplo
  2. pode ser armazenado em anotações de interfaces do ouvinte
  3. você pode usar o nome da interface do ouvinte como o nome dos eventos



Exemplo de implementação de assinatura


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

Adicionamos um método de configuração ao objeto de inscrição, que para cada interface do ouvinte descreve seus metadados, como o método chamado e o nome do evento.


De acordo com esses dados, no momento da assinatura, transformamos o manipulador $ passado em um objeto que pode ser chamado, indicando o método chamado.


Se você perceber, o código implica que um objeto manipulador $ pode implementar muitas interfaces do ouvinte de eventos e será inscrito em cada uma delas. Este é um análogo do SubscriberInterface para assinatura em massa de um objeto em vários eventos. Como você pode ver, na implementação acima, um mecanismo separado não é necessário, pois addSubscriber(SubscriberInterface $subscriber) acabou por funcionar imediatamente.


Despachante


Infelizmente, a abordagem descrita é contrária à interface aceita como o padrão psr / event-dispatcher


Como não precisamos passar nenhum objeto para o Dispatcher. Sim, você pode passar um objeto como açúcar:


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

E use-o ao gerar um evento na interface psr, mas é simplesmente feio.


De uma maneira boa, a interface do Dispatcher ficaria melhor assim:


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

Por que dois métodos? É difícil combinar todos os casos de uso em uma única implementação. É melhor adicionar seu próprio método para cada caso de uso, haverá uma interpretação inequívoca de como o Dispatcher processará os valores retornados pelos ouvintes.


Isso é tudo. Seria interessante discutir com a comunidade se a abordagem descrita tem direito à vida.

Source: https://habr.com/ru/post/pt463313/


All Articles