Interacteur, modèle d'opération

Ce texte est une adaptation d'une partie du manuel du framework Hanami pour le framework Laravel. Qu'est-ce qui a suscité l'intérêt pour ce matériau particulier? Il fournit une description étape par étape avec une démonstration de choses courantes pour les langages de programmation et les frameworks tels que:


  • Utilisation du modèle Interactors.
  • Démonstration de TDD \ BDD.

Il convient de noter tout de suite qu'il ne s'agit pas seulement de cadres différents avec des idéologies différentes (en particulier, en ce qui concerne l'ORM), mais également de langages de programmation différents, chacun ayant sa propre culture spécifique et des "meilleures pratiques" établies pour des raisons historiques. Différents langages et frameworks de programmation ont tendance à emprunter les solutions les plus efficaces les uns aux autres, par conséquent, malgré les différences de détails, les choses fondamentales ne diffèrent pas, à moins bien sûr que nous prenions PL avec un paradigme initialement différent. Il est assez intéressant de comparer comment un même problème est résolu dans différents écosystèmes.


Donc, au départ, nous avons le framework Hanami (ruby) - un framework assez nouveau qui gravite idéologiquement davantage vers Symfony, avec ORM "sur les référentiels". Et le framework Laravel \ Lumen (php) cible avec Active Record.


En cours d'adaptation, les angles les plus aigus ont été coupés:


  • La première partie du guide a été omise avec l'initialisation du projet, une description des caractéristiques du cadre et des choses spécifiques similaires.
  • ORM Eloquent est tiré sur le globe et sert également de référentiel.
  • Étapes pour générer du code et des modèles pour l'envoi d'e-mails.

Enregistré et souligné sur:


  • Interacteurs - une implémentation minimale appropriée a été effectuée sur l'interface.
  • Tests, développement pas à pas via TDD.

La première partie du tutoriel Hanami original auquel le texte sera lié
Texte original du didacticiel interactif
Lien vers le référentiel avec du code php adapté à la fin du texte.


Interacteurs


Nouvelle fonctionnalité: Notifications par e-mail


Scénario de fonctionnalité: En tant qu'administrateur, lorsque j'ajoute un livre, je souhaite recevoir des notifications par e-mail.


Étant donné que l'application n'a pas d'authentification, tout le monde peut ajouter un nouveau livre. Nous spécifierons l'adresse e-mail de l'administrateur via des variables d'environnement.


Ceci est juste un exemple montrant quand utiliser des interacteurs, et en particulier comment utiliser l'interacteur Hanami.


Cet exemple peut servir de base à d'autres fonctions, telles que la confirmation par l'administrateur de nouveaux livres avant leur publication. Ou en donnant aux utilisateurs la possibilité de spécifier une adresse e-mail pour modifier le livre via un lien spécial.


En pratique, vous pouvez utiliser des interacteurs pour implémenter toute logique métier abstraite de la couche réseau. Ceci est particulièrement utile lorsque vous souhaitez combiner plusieurs éléments afin de contrôler la complexité de la base de code.


Ils sont utilisés pour isoler une logique métier non triviale selon le principe de responsabilité unique.


Dans les applications Web, ils sont généralement utilisés à partir des actions du contrôleur. De cette façon, vous séparez les tâches, les objets de logique métier et les interacteurs; ils ne savent rien de la couche réseau de l'application.


Rappels? Nous n'en avons pas besoin!


La façon la plus simple de mettre en œuvre une notification par e-mail consiste à ajouter un rappel.


Autrement dit, après avoir créé une nouvelle entrée de livre dans la base de données, un e-mail est envoyé.


Sur le plan architectural, Hanami ne propose pas un tel mécanisme. En effet, nous considérons les rappels de modèle comme un anti-modèle. Ils violent le principe de la responsabilité exclusive. Dans notre cas, ils mélangent incorrectement la couche de persistance avec les notifications par e-mail.


Pendant les tests (et très probablement dans certains autres cas), vous souhaiterez ignorer le rappel. Cela est rapidement déroutant, car plusieurs rappels pour un même événement peuvent être déclenchés dans un ordre spécifique. De plus, vous pouvez ignorer certains rappels. Les rappels rendent le code fragile et difficile à comprendre.


Au lieu de cela, nous recommandons explicite, au lieu d'implicite.


Un interacteur est un objet qui représente un cas d'utilisation spécifique.


Ils permettent à chaque classe d'avoir une seule responsabilité. La seule responsabilité de l'interacteur est de combiner les objets et les appels de méthode pour obtenir un résultat spécifique.


Idée


L'idée principale des interacteurs est d'extraire les parties isolées de la fonctionnalité dans une nouvelle classe.


Vous ne devez écrire que deux méthodes publiques: __construct et call .
Dans l'implémentation php de l'interacteur, la méthode call a le modificateur protected et est appelée via __invoke .


Cela signifie que ces objets sont faciles à interpréter, car il n'y a qu'une seule méthode disponible pour les utiliser.


Le comportement d'encapsulation dans un objet facilite le test. Il facilite également la compréhension de votre base de code, et ne laisse pas simplement la complexité cachée implicitement.


La préparation


Supposons que nous ayons notre application Bookshelf «Getting Started » et que nous voulons ajouter la fonction «notification par e-mail pour le livre ajouté».


Écrire un interacteur


Créons un dossier pour nos interacteurs et un dossier pour leurs tests:


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

Nous les mettons dans lib/bookshelf car ils ne sont pas liés à l'application web. Vous pouvez ajouter des livres ultérieurement via le portail d'administration, l'API ou même un utilitaire de ligne de commande.


Ajoutez l'interacteur AddBook et écrivez un nouveau test 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()); } } 

L'exécution d'une suite de tests AddBook erreur de Class does not exist car il n'y a pas de classe AddBook . Créons cette classe dans le fichier lib/bookshelf/interactors/AddBook.php :


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

Il n'y a que deux méthodes que cette classe doit contenir: __construct pour définir les données et call pour implémenter le script.


Ces méthodes, en particulier call , doivent appeler des méthodes privées que vous écrivez.


Par défaut, le résultat est considéré comme réussi, car nous n'avons pas indiqué explicitement que l'opération a échoué.


Lançons le test:


 $ phpunit 

Tous les tests doivent réussir!


Maintenant, faisons que notre interacteur AddBook fasse vraiment quelque chose!


Création de livre


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

Si vous exécutez des tests phpunit , vous verrez une erreur:


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

Remplissons notre interacteur, puis expliquons ce que nous avons fait:


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

Deux choses importantes à noter ici:


Chaîne protected static $expose = ["book"]; Ajoute la propriété book à l'objet de résultat qui sera renvoyé lors de l'appel de l'interacteur.


La méthode d' call affecte le modèle Book à la propriété book , qui sera disponible en conséquence.


Maintenant, les tests devraient réussir.


Nous avons initialisé le modèle Book , mais il n'est pas stocké dans la base de données.


Livre de sauvegarde


Nous avons un nouveau livre, obtenu à partir du titre et de l'auteur, mais il n'est pas encore dans la base de données.


Nous devons utiliser notre BookRepository pour le sauvegarder.


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

Si vous exécutez les tests, vous verrez une nouvelle erreur avec le message Failed asserting that null is not null .


C'est parce que le livre que nous avons créé n'a pas d'identifiant, car il ne le recevra que lorsqu'il sera enregistré.


Pour que le test réussisse, nous devons créer un livre enregistré. Une autre façon, non moins correcte, est de sauvegarder le livre que nous avons déjà.


Modifiez la méthode d' call dans le lib/bookshelf/interactors/AddBook.php :


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

Au lieu d'appeler new Book , nous faisons Book::create avec les attributs du livre.


La méthode renvoie toujours le livre et enregistre également cet enregistrement dans la base de données.


Si vous exécutez les tests maintenant, vous verrez que tous les tests réussissent.


Injection de dépendance


Refactorisons l'utilisation de l'injection de dépendance.


Les tests fonctionnent toujours, mais ils dépendent des caractéristiques de l'enregistrement dans la base de données (la propriété id est déterminée après un enregistrement réussi). Il s'agit d'un détail de mise en œuvre du fonctionnement de la conservation. Par exemple, si vous souhaitez créer un UUID avant de l'enregistrer et indiquer que l'enregistrement a réussi d'une autre manière que de remplir la colonne id, vous devrez modifier ce test.


Nous pouvons modifier notre test et l'interacteur pour le rendre plus fiable: il sera moins sujet aux ruptures en raison de modifications extérieures à son fichier.


Voici comment nous pouvons utiliser l'injection de dépendance dans l'interacteur:


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

Essentiellement, c'est la même chose, avec un peu plus de code, pour créer une propriété de repository .


À l'heure actuelle, le test vérifie le comportement de la méthode create pour s'assurer que son identifiant est rempli avec $this->assertNotNull($result->book->id) .


Il s'agit d'un détail d'implémentation.


Au lieu de cela, nous pouvons modifier le test pour nous assurer simplement que la méthode create été appelée sur le référentiel et pour avoir confiance que le référentiel enregistrera l'objet (puisque c'est sa responsabilité).


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

Maintenant, notre test ne viole pas les limites de sa zone.


Tout ce que nous avons fait a été d'ajouter la dépendance de l'interacteur sur le référentiel.


Notification par e-mail


Ajoutons une notification par e-mail!


Vous pouvez également faire n'importe quoi ici, par exemple, envoyer un SMS, envoyer un message de discussion ou activer un raccordement Web.


Nous laisserons le corps du message vide, mais dans le champ sujet, nous indiquerons «Livre ajouté!».


Créez un test de notification 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); } } 

Ajoutez la classe de notification 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'); } } 

Maintenant, tous nos tests réussissent!


Mais la notification n'est pas encore envoyée. Nous devons appeler l'envoi depuis notre interacteur AddBook .


AddBook test AddBook pour nous assurer que le mailer sera appelé:


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

Si nous exécutons les tests, nous obtenons l'erreur: The expected [Lib\Bookshelf\Mail\BookAddedNotification] mailable was sent 0 times instead of 1 times. .


Maintenant, nous intégrons l'envoi de notification dans l'interacteur.


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

En conséquence, l'interacteur enverra une notification concernant l'ajout du livre à l'e-mail.


Intégration du contrôleur


Enfin, nous devons appeler l'interaction de l'action.


Modifiez le fichier d'action 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)); } } 

Nos tests réussissent, mais il y a un petit problème.


Nous testons le code de création de livre deux fois.


Il s'agit généralement d'une mauvaise pratique, et nous pouvons y remédier en illustrant un autre avantage des interacteurs.


Nous allons supprimer la mention sur BookRepository dans les tests et utiliser la maquette pour notre interacteur 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); } } 

Maintenant, nos tests réussissent et ils sont beaucoup plus fiables!


L'action prend une entrée (à partir des paramètres de la requête http) et appelle l'interacteur pour faire son travail. La seule responsabilité de l'action est de travailler avec le réseau. Et l'interaction fonctionne avec notre vraie logique métier.


Cela simplifie considérablement les actions et leurs tests.


Les actions sont pratiquement exemptées de la logique métier.


Lorsque nous modifions l'interacteur, nous n'avons plus besoin de changer l'action ou son test.


Notez que dans une application réelle, vous souhaiterez probablement faire plus que la logique ci-dessus, par exemple pour vous assurer que le résultat est réussi. Et si une panne se produit, vous souhaiterez renvoyer des erreurs de l'interacteur.


Référentiel avec code

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


All Articles