交互器,操作模式

本文是对Laravel框架Hanami框架手册的一部分的改编。 是什么引起了对这种特殊材料的兴趣? 它提供了分步说明,并演示了编程语言和框架的常见用法,例如:


  • 使用交互器模式。
  • 演示TDD \ BDD。

应当立即指出,这些不仅是具有不同意识形态的不同框架(特别是关于ORM),而且是不同的编程语言,每种语言都有其特定的文化并出于历史原因建立了“最佳实践”。 不同的编程语言和框架往往会相互借鉴最成功的解决方案,因此,尽管细节有所不同,但基本内容不会有所不同,除非我们当然将PL最初采用不同的范例。 比较有趣的是,比较如何在不同的生态系统中解决同一问题。


因此,最初我们有Hanami(ruby)框架-这是一个相当新的框架,在思想上更倾向于Symfony,并且ORM位于“存储库”中。 而目标Laravel \ Lumen(php)框架具有Active Record。


在适应过程中,最锐角被切掉:


  • 该项目的初始化,对框架功能的描述以及类似的特定内容都省略了指南的第一部分。
  • ORM Eloquent被推向全球,并充当存储库。
  • 生成代码和发送电子邮件模板的步骤。

保存并强调:


  • 交互器-在接口上进行了最低限度的适当实现。
  • 通过TDD进行测试,逐步开发。

Hanami原始教程的第一部分,将在下面的文本中引用。
原始交互式教程文本
在文本末尾使用修改后的php代码链接到存储库。


互动者


新功能:电子邮件通知


功能场景:作为管理员,当我添加一本书时,我想接收电子邮件通知。


由于该应用程序没有身份验证,因此任何人都可以添加新书。 我们将通过环境变量指定管理员的电子邮件地址。


这只是显示何时使用交互器,尤其是如何使用Hanami交互器的示例。


此示例可以用作其他功能的基础,例如管理员在发行新书之前对其进行确认。 或者使用户能够通过特殊链接指定电子邮件地址来编辑图书。


实际上,您可以使用交互器来实现从网络层抽象的任何业务逻辑。 当您想要组合几件事以控制代码库的复杂性时,这特别有用。


它们用于遵循单一职责原则隔离非平凡的业务逻辑。


在Web应用程序中,通常是通过控制器操作来使用它们。 这样,您就可以分离任务,业务逻辑对象和交互器;它们对应用程序的网络层一无所知。


回调? 我们不需要它们!


实现电子邮件通知的最简单方法是添加回调。


也就是说,在数据库中创建新书条目之后,将发送电子邮件。


在结构上,Hanami不提供这种机制。 这是因为我们认为模型回调是一种反模式。 他们违反了唯一责任原则。 在我们的案例中,他们错误地将持久层与电子邮件通知混合在一起。


在测试期间(很可能在其他情况下),您将要跳过回调。 这很快就会造成混乱,因为可以按特定顺序触发单个事件的多个回调。 另外,您可以跳过一些回调。 回调使代码易碎且难以理解。


相反,我们建议使用显式而不是隐式。


交互器是代表特定用例的对象。


他们允许每个班级都有一个责任。 交互者的唯一责任是将对象和方法调用结合起来以达到特定的结果。


主意


交互器的主要思想是将功能的隔离部分提取到新类中。


您应该只编写两个公共方法: __constructcall
在交互器的php实现中,call方法具有protected修饰符,并通过__invoke进行__invoke


这意味着此类对象易于解释,因为只有一种可用的方法可以使用它们。


将行为封装在一个对象中使测试变得更加容易。 它还使您更容易理解代码库,而不仅仅是隐式地保留隐藏的复杂性。


准备工作


假设我们有“入门 ”书架应用程序,并且我们想添加“已添加书的电子邮件通知”功能。


编写一个交互器


让我们为交互器创建一个文件夹,为他们的测试创建一个文件夹:


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

我们将它们放在lib/bookshelf因为它们与Web应用程序无关。 您可以稍后通过管理门户,API甚至命令行实用程序添加书籍。


添加AddBook交互器并编写一个新的测试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()); } } 

由于没有AddBook类,因此运行测试套件将引发Class does not exist is Class does not exist错误。 让我们在文件lib/bookshelf/interactors/AddBook.php


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

此类仅应包含两个方法:用于设置数据的__construct和用于实现脚本的call


这些方法(尤其是call )应调用您编写的私有方法。


默认情况下,结果被认为是成功的,因为我们没有明确指出操作失败。


让我们运行测试:


 $ phpunit 

所有测试必须通过!


现在,让我们的AddBook交互器真正发挥作用!


书籍创作


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

如果运行phpunit测试,将会看到错误:


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

让我们填写我们的交互器,然后解释我们做了什么:


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

这里要注意的两个重要事项:


protected static $expose = ["book"];字符串protected static $expose = ["book"];book属性添加到调用交互器时将返回的结果对象。


call方法将Book模型分配给book属性,结果将可用。


现在测试应该通过了。


我们初始化了Book模型,但是它没有存储在数据库中。


保存书


我们有一本新书,是从书名和作者那里获得的,但尚未在数据库中。


我们需要使用BookRepository保存它。


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

如果运行测试,您将看到一个新错误,并显示消息Failed asserting that null is not null


这是因为我们创建的书没有标识符,因为它只有在保存时才会收到。


为了使测试通过,我们需要创建一个保存的书。 另一种同样正确的方法是保存我们已经拥有的书。


lib/bookshelf/interactors/AddBook.php编辑call方法:


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

我们不调用new Book而是使用new Book的属性来Book::create


该方法仍返回书籍,并将此记录保存在数据库中。


如果现在运行测试,您将看到所有测试均通过。


依赖注入


让我们重构为使用依赖注入。


测试仍然可以进行,但是它们取决于保存到数据库的功能(id属性是在成功保存之后确定的)。 这是保护工作原理的实施细节。 例如,如果要在保存前创建一个UUID并通过保存ID列以外的其他方式指示保存成功,则必须更改此测试。


我们可以修改测试和交互器以使其更可靠:由于其文件外部的更改,它不太容易损坏。


这是我们可以在交互器中使用依赖注入的方法:


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

从本质上讲,创建repository属性的过程是相同的,只是添加了一些代码。


现在,测试将检查create方法的行为,以确保其标识符填充有$this->assertNotNull($result->book->id)


这是一个实现细节。


相反,我们可以修改测试以确保在存储库上调用了create方法,并相信存储库将保存对象(因为这是它的责任)。


让我们更改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); } 

现在我们的测试没有违反其区域的边界。


我们所做的只是在存储库上添加了交互器依赖项。


电邮通知


让我们添加电子邮件通知!


您还可以在此处执行任何操作,例如,发送短信,发送聊天消息或激活Web挂钩。


我们将邮件正文保留为空,但是在主题字段中,我们将指示“已添加图书!”。


创建一个通知测试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); } } 

添加通知类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'); } } 

现在我们所有的测试都通过了!


但是通知尚未发送。 我们需要从AddBook交互器调用发送。


AddBook测试,以确保将调用该邮件:


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

如果运行测试, The expected [Lib\Bookshelf\Mail\BookAddedNotification] mailable was sent 0 times instead of 1 times.出现错误: The expected [Lib\Bookshelf\Mail\BookAddedNotification] mailable was sent 0 times instead of 1 times.


现在,我们将通知发送集成到交互器中。


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

结果,交互器将发送有关将书添加到电子邮件的通知。


控制器整合


最后,我们需要从动作中调用交互器。


编辑动作文件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)); } } 

我们的测试通过了,但是有一个小问题。


我们对书籍创建代码进行了两次测试。


这通常是一个不好的做法,我们可以通过说明交互器的另一个优点来修复它。


我们将在测试中删除BookRepository上的提及, BookRepository模拟用于我们的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); } } 

现在我们的测试通过了,它们更加可靠!


该动作(从http请求的参数中)获取输入,并调用交互程序以完成其工作。 该操作的唯一责任是使用网络。 交互器与我们真正的业务逻辑一起工作。


这极大地简化了动作及其测试。


实际上,操作不受业务逻辑的约束。


当我们修改交互器时,我们不再需要更改操作或其测试。


请注意,在实际的应用程序中,您可能想要做的不仅仅是上述逻辑,例如,确保结果成功。 如果发生故障,您将需要从交互器返回错误。


带有代码的存储库

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


All Articles