解耦,未解耦的区别
This article would not be possible without the help of Rodrigo Jardim da Fonseca, Edison Junior, and Lemuel Roberto.
没有Rodrigo Jardim da Fonseca, Edison Junior和Lemuel Roberto的帮助,这篇文章是不可能的。
Disclaimer: I feel like I should address that the architecture I’m about to present already existed when I arrived at Arquivei, almost two years ago and didn’t change much since. I didn’t create it, but I did learn a lot from it. Hopefully, you will too.
免责声明:我觉得我应该说的是,大约两年前到达Arquivei时,我将要介绍的体系结构已经存在。 我没有创建它,但是我确实从中学到了很多。 希望你也会。
I don’t think I need to sell you on the idea of an architecture that enables your team to build decoupled solutions, but if I do, here are the basics:
我认为我不需要向您推销可以使您的团队构建分离的解决方案的体系结构的想法,但是如果我这样做,这里是基础知识:
- Separation of concerns: you get pieces of software that are easier to understand (and maintain), not only because of their size but also because each piece only does one job. 关注点分离:您会获得易于理解(和维护)的软件,不仅因为它们的大小,而且因为每个软件只能完成一项工作。
- Testability: automated tests (especially unit tests) are essential to ensure quality in delivery. The trend for continuous deployments with shorter intervals between them made it almost impossible for an application to be successful if it’s hard to create tests for it. 可测试性:自动化测试(尤其是单元测试)对于确保交付质量至关重要。 连续部署的趋势是它们之间的间隔较短,因此如果很难为其创建测试,则几乎不可能使应用程序成功。
With that in mind, when you look for a solution you’re likely to land on the well-known pages of Alistair Cockburn’s Hexagonal Architecture and Jeffrey Palermo’s Onion Architecture. If your journey is anything like ours, you may also find yourself reading about the five SOLID principles and Uncle Bob’s Clean Architecture, that pretty much sums it all. Those will give you a theoretical foundation to build what you need and it may look very different from what I’m about to show you, but the problem of a decoupled architecture is not a problem with a single solution. So, how do we do it at Arquivei?
考虑到这一点,当您寻找解决方案时,您可能会进入Alistair Cockburn的Hexagonal Architecture和Jeffrey Palermo的Onion Architecture的知名页面。 如果您的旅程像我们一样,您可能还会发现自己了解了SOLID的五项原则和Bob叔叔的Clean Architecture ,几乎可以全部概括。 这些将为您提供构建所需内容的理论基础,它看起来可能与我要向您展示的内容完全不同,但是架构分离的问题并不是单个解决方案的问题。 那么,我们如何在Arquivei做到这一点?
First of all, our application is divided into two layers: app and core. The app layer holds all vendor-specific code: infrastructure, adapters, and framework utilities while in the core layer you will find pure PHP code that handles our business logic. Most changes will affect the contents of app, but only changes in requirements should affect the code living in core.
首先,我们的应用程序分为两层: app和core 。 应用程序层包含所有特定于供应商的代码:基础结构,适配器和框架实用程序,而在核心层中,您将找到处理我们的业务逻辑的纯PHP代码。 大多数更改将影响应用程序的内容,但只有需求的更改会影响核心代码。
One important aspect of this division is that all contracts are specified by the core layer: gateways, requests, responses, and entities. The pieces on the app side will follow these contracts by implementing interfaces and handling the entities to produce the result expected from them.
这种划分的一个重要方面是所有合同都由核心层指定:网关,请求,响应和实体。 应用程序端的各个部分将通过实现接口并处理实体以产生预期的结果来遵循这些合同。
The main class in the core layer is called UseCase and it represents, well, a use case of the application (creating a user, for example). It is the only point of interaction with the world outside of core. Its input and output are specified by the Request and Response classes respectively. To use it, we need to pass all the dependencies required to the constructor and call its execute() with request. The method returns a response whenever one is needed.
核心层中的主类称为UseCase,它很好地表示了应用程序的用例(例如,创建用户)。 这是与核心之外的世界进行交互的唯一点。 它的输入和输出分别由Request和Response类指定。 要使用它,我们需要将所有必需的依赖项传递给构造函数,并通过请求调用其execute() 。 该方法在需要响应时返回一个响应。
<?php
namespace Arquivei\Boltons\Example\Modules\User\Creation;
use Arquivei\Boltons\Example\Modules\User\Creation\Gateways\CheckUniqueEmailGateway;
use Arquivei\Boltons\Example\Modules\User\Creation\Gateways\UserSaveGateway;
use Arquivei\Boltons\Example\Modules\User\Creation\Requests\Request;
use Arquivei\Boltons\Example\Modules\User\Creation\Responses\Response;
use Arquivei\Boltons\Example\Modules\User\Creation\Rules\CheckUniqueEmailRule;
use Arquivei\Boltons\Example\Modules\User\Creation\Rules\UserSaveRule;
use Arquivei\Boltons\Example\Modules\User\Creation\Rulesets\Ruleset;
class UseCase
{
private $checkUniqueEmailGateway;
private $userSaveGateway;
public function __construct(
CheckUniqueEmailGateway $checkUniqueEmailGateway,
UserSaveGateway $userSaveGateway
) {
$this->checkUniqueEmailGateway = $checkUniqueEmailGateway;
$this->userSaveGateway = $userSaveGateway;
}
public function execute(Request $request): Response
{
$ruleset = new Ruleset(
new CheckUniqueEmailRule(
$this->checkUniqueEmailGateway,
$request->getUser()->getEmail()
),
new UserSaveRule(
$this->userSaveGateway,
$request->getUser()
)
);
return $ruleset->apply();
}
}
Sometimes your use case is not simple. It is very common for us to retrieve data, process it in a few different steps (like generating PDF files and then a ZIP with all of them) before presenting a response. To avoid clutter in the UseCase::execute() method, we use a Ruleset.
有时,您的用例并不简单。 在呈现响应之前,我们通常会先检索数据,然后按照几个不同的步骤对其进行处理(例如生成PDF文件,然后将其全部包含为ZIP)。 为了避免UseCase :: execute()方法中的混乱,我们使用规则集。
The Ruleset class takes however many Rule objects we need (hopefully each with a single responsibility) and orchestrates how the rules are applied. A response is built with the result and returned.
规则集类负责然而,许多规则对象,我们需要(希望每一个责任)和编排规则是如何应用的。 使用结果构建响应并返回。
<?php
namespace Arquivei\Boltons\Example\Modules\User\Creation\Rulesets;
use Arquivei\Boltons\Example\Modules\User\Creation\Responses\Response;
use Arquivei\Boltons\Example\Modules\User\Creation\Rules\CheckUniqueEmailRule;
use Arquivei\Boltons\Example\Modules\User\Creation\Rules\UserSaveRule;
class Ruleset
{
private $checkUniqueEmailRule;
private $userSaveRule;
public function __construct(
CheckUniqueEmailRule $checkUniqueEmailRule,
UserSaveRule $userSaveRule
) {
$this->checkUniqueEmailRule = $checkUniqueEmailRule;
$this->userSaveRule = $userSaveRule;
}
public function apply(): Response
{
$this->checkUniqueEmailRule->apply();
$userId = $this->userSaveRule->apply();
return new Response($userId);
}
}
The constructor of each rule will take all it needs for the rule to do its job: dependencies and request data and will perform its task. If an exception is encountered, it is to be wrapped in a core-specific exception and thrown. This makes it easier for us to track which part of our application failed. Consider how easier it is to understand what is going on when you come across a UsernameNotAvailableException than a PDOException.
每个规则的构造函数都将使用规则完成其工作所需的全部:依赖关系和请求数据,并将执行其任务。 如果遇到异常,则将其包装在特定于内核的异常中并引发。 这使我们更容易跟踪应用程序的哪个部分失败。 考虑一下,遇到UsernameNotAvailableException时比PDOException来了解发生了什么事情要容易得多。
<?php
namespace Arquivei\Boltons\Example\Modules\User\Creation\Rules;
use Arquivei\Boltons\Example\Modules\User\Creation\Entities\User;
use Arquivei\Boltons\Example\Modules\User\Creation\Exceptions\UserSaveException;
use Arquivei\Boltons\Example\Modules\User\Creation\Gateways\UserSaveGateway;
class UserSaveRule
{
private $userSaveGateway;
private $user;
public function __construct(
UserSaveGateway $userSaveGateway,
User $user
) {
$this->userSaveGateway = $userSaveGateway;
$this->user = $user;
}
public function apply(): string
{
try {
return $this->userSaveGateway->save($this->user);
} catch (\Throwable $t) {
throw new UserSaveException('Error saving user', 500, $t);
}
}
}
You probably noticed that in the example above that the constructor does not take a database adapter, but a Gateway. Gateways are interfaces that dependencies must implement, that way we know they will follow the contract that the core layer specified and all the effort in switching vendors, for example, will be that of writing a new adapter and changing the calling code (usually a controller) to use it. Also, note that the core layer does not know about the data types returned by your database because it uses the entities defined within itself. App always adapts to core, never the other way around.
您可能已经注意到,在上面的示例中,构造函数没有采用数据库适配器,而是采用了网关。 网关是依赖项必须实现的接口,这样我们就知道它们将遵循核心层指定的约定,并且交换供应商的所有工作,例如,将是编写新的适配器并更改调用代码(通常是控制器) )使用它。 另外,请注意, 核心层不知道数据库返回的数据类型,因为它使用自身定义的实体。 App始终适应核心 ,而别无所求。
<?php
namespace Arquivei\Boltons\App\Example\Adapters;
class DatabaseAdapter implements CheckUniqueEmailGateway, UserSaveGateway
{
private $takenEmails = ['test@test.com', 'newtest@test.com'];
public function save(User $user): string
{
if (((int) date('s')) % 2 == 1) {
throw new \Exception('Cannot save user because the current time is odd');
}
return date('is');
}
public function isEmailTaken(string $email): bool
{
return in_array($email, $this->takenEmails);
}
}
The last piece of our puzzle is the testing and with this architecture, it is pretty simple: mock the gateways, create a request, and assert on the response. PHPUnit has utilities to ensure the flow works as expected and even the failure scenarios can easily be tested for 100% code coverage on your business logic. Keep in mind that you still need to be smart about your test cases: coverage is just one way to help you make sure you did everything you needed.
最后一个难题是测试,并使用这种架构,这非常简单:模拟网关,创建请求并在响应中进行声明。 PHPUnit具有实用程序,可确保流程按预期工作,甚至可以轻松测试失败场景,以确保业务逻辑上100%的代码覆盖率。 请记住,您仍然需要对测试用例保持精明:覆盖率只是帮助您确保已完成所需一切的一种方法。
It is also worth noting that some people in our team prefer to build tests for every class as they write tests alongside application code. Nothing wrong with that approach if it works for you.
还值得注意的是,我们团队中的某些人喜欢在为每个类编写测试以及应用程序代码的同时为其构建测试。 如果这种方法对您有用,那没什么错。
<?php
namespace Arquivei\Boltons\Example\Tests;
use Arquivei\Boltons\Example\Modules\User\Creation\Entities\User;
use Arquivei\Boltons\Example\Modules\User\Creation\Exceptions\EmailTakenException;
use Arquivei\Boltons\Example\Modules\User\Creation\Exceptions\UserSaveException;
use Arquivei\Boltons\Example\Modules\User\Creation\Gateways\CheckUniqueEmailGateway;
use Arquivei\Boltons\Example\Modules\User\Creation\Gateways\UserSaveGateway;
use Arquivei\Boltons\Example\Modules\User\Creation\Requests\Request;
use Arquivei\Boltons\Example\Modules\User\Creation\UseCase;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
public function testSuccess()
{
$checkUniqueEmailGateway = $this->createMock(CheckUniqueEmailGateway::class);
$checkUniqueEmailGateway->expects($this->once())
->method('isEmailTaken')
->willReturn(false);
$userSaveGateway = $this->createMock(UserSaveGateway::class);
$userSaveGateway->expects($this->once())
->method('save')
->with($this->callback(function ($user) {
$this->assertSame('name', $user->getName());
$this->assertSame('email', $user->getEmail());
$this->assertSame('phone', $user->getPhone());
return true;
}))
->willReturn('1');
$useCase = new UseCase($checkUniqueEmailGateway, $userSaveGateway);
$request = new Request(
new User('name', 'email', 'phone')
);
$response = $useCase->execute($request);
$this->assertSame('1', $response->getUserId());
}
public function testEmailTakenError()
{
$this->expectException(EmailTakenException::class);
$checkUniqueEmailGateway = $this->createMock(CheckUniqueEmailGateway::class);
$checkUniqueEmailGateway->expects($this->once())
->method('isEmailTaken')
->willReturn(true);
$userSaveGateway = $this->createMock(UserSaveGateway::class);
$useCase = new UseCase($checkUniqueEmailGateway, $userSaveGateway);
$request = new Request(
new User('name', 'email', 'phone')
);
$response = $useCase->execute($request);
}
public function testSaveError()
{
$this->expectException(UserSaveException::class);
$checkUniqueEmailGateway = $this->createMock(CheckUniqueEmailGateway::class);
$checkUniqueEmailGateway->expects($this->once())
->method('isEmailTaken')
->willReturn(false);
$userSaveGateway = $this->createMock(UserSaveGateway::class);
$userSaveGateway->expects($this->once())
->method('save')
->willThrowException(new \Exception('Cannot save'));
$useCase = new UseCase($checkUniqueEmailGateway, $userSaveGateway);
$request = new Request(
new User('name', 'email', 'phone')
);
$response = $useCase->execute($request);
}
}
There’s a lot of questions we ask ourselves in our weekly meetings about this architecture and you may have some right now. It can often seem like too much or like it doesn’t fit your problem and we don’t have all the answers. This is never ending work in progress that has helped us achieve a good level of decoupling and made our applications way easier to create and maintain. I hope it gives you some insights.
在每周一次的会议上,关于此体系结构我们有很多问题要问,您现在可能有一些疑问。 它通常看起来太多或不适合您的问题,并且我们没有所有答案。 这永无止境的进行中的工作已帮助我们实现了良好的去耦水平,并使我们的应用程序更易于创建和维护。 我希望它能给您一些见识。
If you want a more detailed example, you can find it here.
如果您需要更详细的示例,可以在此处找到。
Thanks for reading.
感谢您的阅读 。
解耦,未解耦的区别