Interator, Padrão de Operação

Este texto é uma adaptação de parte do manual da estrutura Hanami para a estrutura Laravel. O que causou o interesse nesse material em particular? Ele fornece uma descrição passo a passo com uma demonstração de coisas comuns para linguagens e estruturas de programação como:


  • Usando o padrão Interatores.
  • Demonstração de TDD \ BDD.

Deve-se notar imediatamente que essas não são apenas estruturas diferentes com ideologias diferentes (em particular, com relação ao ORM), mas também linguagens de programação diferentes, cada uma com sua própria cultura específica e "práticas recomendadas" estabelecidas por razões históricas. Diferentes linguagens de programação e estruturas tendem a emprestar as soluções mais bem-sucedidas, portanto, apesar das diferenças nos detalhes, as coisas fundamentais não diferem, a menos que, é claro, tomemos PL com um paradigma inicialmente diferente. É interessante o suficiente comparar como um e o mesmo problema é resolvido em diferentes ecossistemas.


Portanto, inicialmente temos o framework Hanami (ruby) - um framework relativamente novo que gravita ideologicamente mais para o Symfony, com o ORM "nos repositórios". E a estrutura de destino Laravel \ Lumen (php) com o Active Record.


No processo de adaptação, os ângulos mais agudos foram cortados:


  • A primeira parte do guia foi omitida com a inicialização do projeto, uma descrição dos recursos da estrutura e coisas específicas semelhantes.
  • O ORM Eloquent é puxado para o mundo e também serve como repositório.
  • Etapas para gerar código e modelos para o envio de email.

Salvo e enfatizado em:


  • Interatores - uma implementação minimamente apropriada foi feita na interface.
  • Testes, desenvolvimento passo a passo através do TDD.

A primeira parte do tutorial original do Hanami, que será referenciada no texto abaixo.
Texto interativo original do tutorial
Link para o repositório com código php adaptado no final do texto.


Interactors


Novo recurso: Notificações por email


Cenário de recursos: como administrador, quando adiciono um livro, desejo receber notificações por email.


Como o aplicativo não possui autenticação, qualquer pessoa pode adicionar um novo livro. Nós especificaremos o endereço de email do administrador através de variáveis ​​de ambiente.


Este é apenas um exemplo mostrando quando usar interatores e, em particular, como usar o interator Hanami.


Este exemplo pode servir de base para outras funções, como confirmação do administrador de novos livros antes de serem publicados. Ou dando aos usuários a capacidade de especificar um endereço de e-mail para editar o livro por meio de um link especial.


Na prática, você pode usar interatores para implementar qualquer lógica comercial abstraída da camada de rede. Isso é especialmente útil quando você deseja combinar várias coisas para controlar a complexidade da base de código.


Eles são usados ​​para isolar a lógica de negócios não trivial, seguindo o Princípio de Responsabilidade Única.


Em aplicativos da web, eles geralmente são usados ​​em ações do controlador. Dessa forma, você separa tarefas, objetos de lógica comercial e interatores; eles não sabem nada sobre a camada de rede do aplicativo.


Retornos de chamada? Nós não precisamos deles!


A maneira mais fácil de implementar uma notificação por email é adicionar um retorno de chamada.


Ou seja, depois de criar uma nova entrada de livro no banco de dados, um email é enviado.


Arquitetonicamente, Hanami não fornece esse mecanismo. Isso ocorre porque consideramos os retornos de chamada do modelo um antipadrão. Eles violam o princípio da responsabilidade exclusiva. No nosso caso, eles misturam incorretamente a camada de persistência com notificações por email.


Durante o teste (e provavelmente em alguns outros casos), você deve pular o retorno de chamada. Isso é rapidamente confuso, pois vários retornos de chamada para um único evento podem ser acionados em uma ordem específica. Além disso, você pode pular alguns retornos de chamada. Os retornos de chamada tornam o código frágil e difícil de entender.


Em vez disso, recomendamos explícito, em vez de implícito.


Um interator é um objeto que representa um caso de uso específico.


Eles permitem que cada classe tenha uma única responsabilidade. A única responsabilidade do interator é combinar os objetos e as chamadas de método para obter um resultado específico.


Idéia


A idéia principal dos interatores é que você extraia as partes isoladas da funcionalidade em uma nova classe.


Você deve escrever apenas dois métodos públicos: __construct e call .
Na implementação php do interator, o método de chamada possui o modificador protegido e é chamado via __invoke .


Isso significa que esses objetos são fáceis de interpretar, pois existe apenas um método disponível para usá-los.


O comportamento de encapsular em um objeto facilita o teste. Também facilita o entendimento da sua base de código e não apenas deixa a complexidade oculta implicitamente.


Preparação


Suponha que tenhamos nosso aplicativo Estante de livros " Introdução " e desejemos adicionar o recurso "notificação por email para o livro adicionado".


Escrevendo um Interator


Vamos criar uma pasta para nossos interatores e uma pasta para seus testes:


 $ mkdir lib/bookshelf/interactors $ mkdir tests/bookshelf/interactors 

Nós os colocamos na lib/bookshelf porque eles não estão relacionados ao aplicativo da web. Você pode adicionar livros posteriormente através do portal de administração, API ou até mesmo um utilitário de linha de comando.


Adicione o AddBook e escreva um novo teste tests/bookshelf/interactors/AddBookTest.php :


 # tests/bookshelf/interactors/AddBookTest.php <?php use Lib\Bookshelf\Interactors\AddBook; class AddBookTest extends TestCase { private function interactor() { return $this->app->make(AddBook::class); } private function bookAttributes() { return [ "author" => "James Baldwin", 'title' => "The Fire Next Time", ]; } private function subjectCall() { return $this->interactor()($this->bookAttributes()); } public function testSucceeds() { $result = $this->subjectCall(); $this->assertTrue($result->successful()); } } 

A execução de um conjunto de testes gerará um erro Class does not exist porque não há classe AddBook . Vamos criar esta classe no arquivo lib/bookshelf/interactors/AddBook.php :


 <?php namespace Lib\Bookshelf\Interactors; use Lib\Interactor\Interactor; class AddBook { use Interactor; public function __construct() { } protected function call() { } } 

Existem apenas dois métodos que essa classe deve conter: __construct para definir dados e call para implementar o script.


Esses métodos, especialmente os call , devem chamar métodos particulares que você escreve.


Por padrão, o resultado é considerado bem-sucedido, pois não indicamos explicitamente que a operação falhou.


Vamos executar o teste:


 $ phpunit 

Todos os testes devem passar!


Agora, vamos fazer nosso AddBook realmente fazer alguma coisa!


Criação de livros


Altere tests/bookshelf/interactors/AddBookTest.php :


  public function testCreateBook() { $result = $this->subjectCall(); $this->assertEquals("The Fire Next Time", $result->book->title); $this->assertEquals("James Baldwin", $result->book->author); } 

Se você executar testes do phpunit , verá um erro:


 Exception: Undefined property Lib\Interactor\InteractorResult::$book 

Vamos preencher nosso interator e depois explicar o que fizemos:


 <?php namespace Lib\Bookshelf\Interactors; use Lib\Interactor\Interactor; use Lib\Bookshelf\Book; class AddBook { use Interactor; protected static $expose = ["book"]; private $book = null; public function __construct() { } protected function call($bookAttributes) { $this->book = new Book($bookAttributes); } } 

Duas coisas importantes a serem observadas aqui:


protected static $expose = ["book"]; string protected static $expose = ["book"]; Adiciona a propriedade book ao objeto de resultado que será retornado quando o interator for chamado.


O método de call atribui o modelo Book à propriedade book , que estará disponível como resultado.


Agora os testes devem passar.


Inicializamos o modelo Book , mas ele não é armazenado no banco de dados.


Salvando Livro


Temos um novo livro, obtido a partir do título e do autor, mas ainda não está no banco de dados.


Precisamos usar nosso BookRepository para salvá-lo.


 // tests/bookshelf/interactors/AddBookTest.php public function testPersistsBook() { $result = $this->subjectCall(); $this->assertNotNull($result->book->id); } 

Se você executar os testes, verá um novo erro com a mensagem Failed asserting that null is not null .


Isso ocorre porque o livro que criamos não possui um identificador, porque ele será recebido apenas quando for salvo.


Para que o teste seja aprovado, precisamos criar um livro salvo. Outra maneira, não menos correta, é salvar o livro que já temos.


Edite o método de call no lib/bookshelf/interactors/AddBook.php :


 protected function call($bookAttributes) { $this->book = Book::create($bookAttributes); } 

Em vez de chamar o new Book , fazemos Book::create com os atributos do livro.


O método ainda retorna o livro e também salva esse registro no banco de dados.


Se você executar os testes agora, verá que todos os testes passam.


Injeção de Dependência


Vamos refatorar para usar injeção de dependência.


Os testes ainda funcionam, mas dependem dos recursos de salvamento no banco de dados (a propriedade id é determinada após o salvamento bem-sucedido). Este é um detalhe de implementação de como a conservação funciona. Por exemplo, se você deseja criar um UUID antes de salvá-lo e indicar que a gravação foi bem-sucedida de alguma outra maneira, além de preencher a coluna de identificação, será necessário alterar este teste.


Podemos modificar nosso teste e o interator para torná-lo mais confiável: será menos propenso a falhas devido a alterações fora de seu arquivo.


Veja como podemos usar a injeção de dependência no interator:


 // lib/bookshelf/interactors/AddBook.php public function __construct(Book $repository) { $this->repository = $repository; } protected function call($bookAttributes) { $this->book = $this->repository->create($bookAttributes); } 

Essencialmente, é o mesmo, com um pouco mais de código, para criar uma propriedade de repository .


No momento, o teste verifica o comportamento do método create para garantir que seu identificador seja preenchido com $this->assertNotNull($result->book->id) .


Este é um detalhe de implementação.


Em vez disso, podemos modificar o teste para garantir apenas que o método create foi chamado no repositório e confiar que o repositório salvará o objeto (já que essa é sua responsabilidade).


Vamos mudar o teste testPersistsBook :


 // tests/bookshelf/interactors/AddBookTest.php public function testPersistsBook() { $repository = Mockery::mock(Book::class); $this->app->instance(Book::class, $repository); $attributes = [ "author" => "James Baldwin", 'title' => "The Fire Next Time", ]; $repository->expects()->create($attributes); $this->subjectCall($attributes); } 

Agora, nosso teste não viola os limites de sua zona.


Tudo o que fizemos foi adicionar a dependência do interator no repositório.


Notificação por email


Vamos adicionar uma notificação por email!


Você também pode fazer qualquer coisa aqui, por exemplo, enviar um SMS, enviar uma mensagem de bate-papo ou ativar um gancho da Web.


Deixaremos o corpo da mensagem vazio, mas no campo de assunto indicamos "Livro adicionado!".


Crie um teste de notificação tests/bookshelf/mail/BookAddedNotificationTest.php :


 <?php use Lib\Bookshelf\Mail\BookAddedNotification; use Illuminate\Support\Facades\Mail; class BookAddedNotificationTest extends TestCase { public function setUp() { parent::setUp(); Mail::fake(); $this->mail = new BookAddedNotification(); } public function testCorrectAttributes() { $this->mail->build(); $this->assertEquals('no-reply@example.com', $this->mail->from[0]['address']); $this->assertEquals('admin@example.com', $this->mail->to[0]['address']); $this->assertEquals('Book added!', $this->mail->subject); } } 

Adicione a classe de notificação lib/Bookshelf/Mail/BookAddedNotification.php :


 <?php namespace Lib\Bookshelf\Mail; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class BookAddedNotification extends Mailable { use SerializesModels; public function build() { $this->from('no-reply@example.com') ->to('admin@example.com') ->subject('Book added!'); return $this->view('emails.book_added_notification'); } } 

Agora todos os nossos testes passam!


Mas a notificação ainda não foi enviada. Precisamos ligar para o envio do nosso AddBook .


AddBook teste AddBook para garantir que a mala direta será chamada:


 public function testSendMail() { Mail::fake(); $this->subjectCall(); Mail::assertSent(BookAddedNotification::class, 1); } 

Se executarmos os testes, obteremos o erro: The expected [Lib\Bookshelf\Mail\BookAddedNotification] mailable was sent 0 times instead of 1 times. .


Agora integramos o envio da notificação ao interator.


 public function __construct(Book $repository, BookAddedNotification $mail) { $this->repository = $repository; $this->mail = $mail; } protected function call($bookAttributes) { $this->book = $this->repository->create($bookAttributes); Mail::send($this->mail); } 

Como resultado, o interator enviará uma notificação sobre a adição do livro ao email.


Integração do controlador


Finalmente, precisamos chamar o interator da ação.


Edite o arquivo de ação app/Http/Controllers/BooksCreateController.php :


 <?php namespace App\Http\Controllers; use Lib\Bookshelf\Interactors\AddBook; use Illuminate\Http\Request; use Illuminate\Http\Response; class BooksCreateController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct(AddBook $addBook) { $this->addBook = $addBook; } public function call(Request $request) { $input = $request->all(); ($this->addBook)($input); return (new Response(null, 201)); } } 

Nossos testes são aprovados, mas há um pequeno problema.


Testamos o código de criação de livros duas vezes.


Geralmente, essa é uma prática ruim, e podemos corrigi-la ilustrando outra vantagem dos interatores.


BookRepository a menção no BookRepository nos testes e usaremos a simulação para o nosso AddBook :


 <?php use Lib\Bookshelf\Interactors\AddBook; class BooksCreateControllerTest extends TestCase { public function testCallsInteractor() { $attributes = ['title' => '1984', 'author' => 'George Orwell']; $addBook = Mockery::mock(AddBook::class); $this->app->instance(AddBook::class, $addBook); $addBook->expects()->__invoke($attributes); $response = $this->call('POST', '/books', $attributes); } } 

Agora nossos testes são aprovados e são muito mais confiáveis!


A ação recebe entrada (dos parâmetros da solicitação http) e chama o interator para fazer seu trabalho. A única responsabilidade da ação é trabalhar com a rede. E o interator trabalha com nossa lógica real de negócios.


Isso simplifica bastante as ações e seus testes.


As ações são praticamente isentas da lógica de negócios.


Quando modificamos o interator, não precisamos mais alterar a ação ou seu teste.


Observe que em um aplicativo real, você provavelmente deseja fazer mais do que a lógica acima, por exemplo, para garantir que o resultado seja bem-sucedido. E se ocorrer uma falha, você desejará retornar erros do interator.


Repositório com código

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


All Articles