使用干净的架构来清理代码库
让我们来谈谈软件架构。我们大多数人都知道MVC,它几乎是所有 Web 框架的基础。然而,随着产品的发展,MVC 的问题会开始显现。即使是一个相对简单的产品,最终也可能拥有臃肿混乱的代码库。MVC 是我们最初的起点,但当你需要超越它时,你该怎么做呢?
在进一步讨论之前,让我们先来分析一下为什么解释答案如此困难。
以下是开发人员常见的对话
devA:“我们的代码库真的很乱,怎么清理?”
devB:“我们需要重构,将代码移到对象中,分离关注点。”
devA:“好的,太好了。怎么做呢?”
devB:“我们将使用设计模式并遵循 SOLID 原则。”
我们通常就到此为止了,似乎觉得这个答案足够让人开始动手了。这就像你问一个木匠如何做桌子,他只是指着他的工具说“用那些”。这个答案从技术上来说是正确的,但它并没有说明全部情况,而且对于学习编写软件(或制作表格)的人来说肯定没什么用。*工具很重要,你需要知道它们是什么,但它们只是整个过程的一小部分。
学习模式是不够的
这才是最让人抓狂的部分,光学习模式是不够的。事实上,你刚学完这些模式,情况可能反而更糟,因为你突然间拥有了强大的工具,却不知道该怎么用。
学习这些模式会引出一系列全新的问题,您必须先回答这些问题,然后才能有效地使用它们。
- 我应该在哪里使用设计模式?
- 我如何决定使用哪一个?
- 我应该在哪里画出抽象的线条?
- 什么时候应该使用接口?
如果没有一些指导原则来解答这些问题,开发人员最终会在对象之间随意设置边界,并尽可能地使用模式。这会导致代码不一致,而且比没有这些“设计”时更糟糕。
难怪人们抱怨“设计”和“模式”,我们没有很好地解释如何有效地使用它们。
从哪里开始
对我来说,当我了解到清洁架构时,一切才恍然大悟**。
清晰架构是拆分代码的指南,它指明了某些概念的归属。清晰架构有很多种,但它们都共享相同的核心概念。
- 分层分离代码
- 将抽象放在内层
- 将实现细节放在外层
- 依赖关系只能指向内部,而不能指向外部。
这是一篇关于这个概念的精彩文章,它比我以往所能表达的都要好得多。
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
很酷吧?如果你以前没看过,那真是令人大开眼界。
它提供了如何区分你的关注点以及在哪里划定界限的指导。一旦划定了界限,模式就变得显而易见。它是区分好坏的好工具。
让我们看一个例子来了解我的意思。
一个例子
这里我们有一个设置用户个人资料图片的简单用例。该类由控制器和控制台命令内部使用,因此代码非常精简。
<?php
namespace App\Usecases;
use Ramsey\Uuid\Uuid;
use SplFileInfo;
use Aws\S3\S3Client;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;
use Domain\User;
class SetProfileImage
{
private $filesystem;
public function __construct()
{
$client = S3Client::factory([
'credentials' => [
'key' => getenv('AWS_ACCESS_KEY'),
'secret' => getenv('AWS_SECRET'),
],
'region' => getenv('AWS_REGION'),
'version' => 'latest',
]);
$adapter = new AwsS3Adapter($client, 'bucket-o-images');
$this->filesystem = new Filesystem($adapter);
}
public function handle(Uuid $user_id, SplFileInfo $image): Uuid
{
$image_id = Uuid::uuid4();
$filepath = "profle_image/$image_id.".$file->getExtension()
$image_contents = $image->fread($image->getSize());
$this->filesystem->write($filepath, $image_contents);
$user = User::find($user_id->toString());
$user->setProfileImage($image_id);
User::where('id', $user_id->toString())->update($user->toArray());
return $image_id;
}
}
上面这段话有什么问题?嗯,虽然很简洁,但实际上却混杂了五个概念,让人摸不着头脑。
- 我们的应用程序
- 配置
- AWS S3
- 飞行系统
- Eloquent ORM
你必须理解每一个概念,才能理解这个类内部到底想做什么。为了理解这么简单的一个类,你需要记住的概念数量之多令人吃惊。
更糟糕的是,它们并不一致。它们各自用自己的语言描述解决方案,使用了一些难以融合的细节和概念。例如,像 这样的 ORM 语言find
在Flysystem 语言中where
并不update
存在。如果再把应用程序概念也混在一起,你就会明白为什么事情会变得令人困惑。
这就像读一本书,书里的内容会不时在法语、英语和俄语之间切换,有时甚至一个词一个词地切换。当然,最终你还是能读懂,但过程中会犯很多错误,最后你会一头雾水,一团糟。所以,我们试着把问题解决掉吧。
划分图层
首先,我们需要知道从应用层删除什么,因此让我们使用语言作为指导。
由于我们希望语言以应用为中心,因此我们需要删除以下概念所特有的任何词语。
- 配置
- 飞行系统
- AWS S3
- Eloquent ORM
相反,我们将用使用应用程序语言而不是实现的集成点来替换它们。
这就是设计模式发挥作用的地方。通过观察你想要做什么而不是如何去做,你可以发现新出现的常见模式。
从代码来看,image
和user
分别是核心/领域概念,但部分使用它们的代码是纯粹的实现。这些是我们的集成点,所以让我们深入研究一下我们究竟是如何利用“图像”和“用户”的。
在我们的例子中,很明显我们真正想做两件事
- 存储和检索用户
- 存储图像
其余一切都是实现细节。存储和检索显然是存储库模式。因此,让我们创建两个新的应用程序概念:UserRepository 和 ImageRepository,并将它们实现为接口。
应用程序级别
<?php
namespace App\Usecases;
use Ramsey\Uuid\Uuid;
use SplFileInfo;
class SetProfileImage
{
private $image_repo;
private $user_repo;
public function __construct(ImageRepo $image_repo, UserRepository $user_repo)
{
$this->image_repo = $image_repo;
$this->user_repo = $user_repo;
}
public function handle(Uuid $user_id, SplFileInfo $image): Uuid
{
$image_id = Uuid::uuid4();
$this->image_repo->store($image_id, $image);
$user = $this->user_repo->get($user_id);
$user->setProfileImage($image_id);
$user_repository->store($user);
return $image_id;
}
}
namespace App\Services;
use Ramsey\Uuid\Uuid;
use SplFileInfo;
interface ImageRepository
{
public function store(Uuid $image_id, SplFileInfo $image);
}
namespace App\Services;
use Uuid;
use Domain\User;
interface UserRepostiory
{
public function get(Uuid $user_id): User
public function store($user);
}
这就是应用程序级别的重构。我们将语言和概念精炼到应用程序所需的最低限度,以表达我们的意图。我们还创建了两个隐藏在旧实现中的新概念,并将它们提炼成一个简单的模式——存储库模式。
顺便说一句,我们可以毫无问题地将设计模式语言添加到应用程序层,因为它是开发人员之间的共享语言,它有助于清晰度而不是模糊清晰度。
现在让我们看一下实现情况。
基础设施
<?php
namespace Infrastructure\App\Services;
use App\Services\ImageRepository;
use Ramsey\Uuid\Uuid;
use Aws\S3\S3Client;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;
class S3ImageRepository implements ImageRepository
{
private $filesystem;
public function __construct()
{
$client = S3Client::factory([
'credentials' => [
'key' => getenv('AWS_ACCESS_KEY'),
'secret' => getenv('AWS_SECRET'),
],
'region' => getenv('AWS_REGION'),
'version' => 'latest',
]);
$adapter = new AwsS3Adapter($client, 'bucket-o-images');
$this->filesystem = new Filesystem($adapter);
}
public function store(Uuid $image_id, SplFileInfo $image)
{
$filepath = "profle_image/$image_id.".$file->getExtension()
$image_contents = $image->fread($image->getSize());
$this->filesystem->write($filepath, $image_contents);
}
}
namespace Infrastructure\App\Services;
use App\Services\UserRepostiory;
use Ramsey\Uuid\Uuid;
use Domain\User;
class EloquentUserRepostiory implements UserRepostiory
{
public function get(Uuid $user_id):User
{
return User::find($user_id->toString());
}
public function store($user)
{
User::where('id', $user_id->toString())->update($user->toArray());
}
}
就是这样,我们将用例的代码分为两层。
为什么这样更好?
更易于阅读
从语言角度来看,这更加清晰。它专注于用语言描述我们的应用程序想要做什么,而不是它打算如何做。这种有凝聚力的语言降低了理解的门槛。简而言之,为了理解它,需要解析的概念更少了。
测试
测试起来容易得多。原版代码包含太多内容,测试起来很困难。我们的验收测试必须连接到 S3 和数据库才能证明代码有效。这会将测试与实现耦合在一起,使代码变得脆弱,修改成本高昂。
在新版本中,每一层都可以单独进行测试,用例可以进行单元测试,实现可以进行集成测试。这使得我们的验收测试更容易验证,我们只需要检查输出,而无需关注副作用。
我们正在有效地利用模式
在上面,我们以一种对应用程序有意义的方式使用了存储库模式,而不是使用我们当时喜欢的任何模式。这使得事情保持专注,并避免我们陷入设计模式的疯狂。在长期使用设计模式失败之后,有效地使用它们感觉真的很好。
松散耦合
上面实际上是依赖倒置原则的一个SOLID *** 示例。我们反转了代码的依赖关系,也就是说,应用程序不再依赖于数据库,而是数据库依赖于应用程序。简而言之,这意味着数据库的更改将不再影响业务逻辑,因此更改数据库的风险更低。
改变实施
另一个好处是,切换实现极其简单。想象一下,我们需要切换存储机制,比如从 MySQL 切换到 MongoDB。我们无需彻底删除用例,只需使用 MongoDB 库创建一个新的接口实现即可。搞定!无需在应用程序层面进行任何更改。
这个例子凸显了编程语言在编写分层代码时如何起到引导作用。新引入的 MongoDB 编程语言并没有影响到应用层的编程语言,它保持了原样,无需任何更改。这无疑表明我们已经正确地实现了层级划分。
为什么情况更糟?
说实话,事情并非总是一帆风顺,所以让我们先来谈谈这个const ELEPHANT = true;
问题class Room
:
首先,代码库更大了,文件更多了,需要阅读的代码也更多了。编写代码的时间也更长了。你必须花时间将代码提取到各个层级,花时间思考代码的设计,所以这其中有一定的牺牲。如果你只想扔掉再也不用看的一次性软件,这可能会成为阻碍****。
不过,我认为以上这些理由不足以阻止它。软件会不断变化,尤其是产品的核心部分,所以现在多花些时间是值得的,因为以后可以节省很多时间。
而且,你做得越多,就越擅长。最终,你会一次又一次地注意到相同的模式,并且能够在一开始就创建抽象,直观地将你想要做的事情和你使用的工具区分开来。
我们可以更进一步吗
从上面可以看出,我们几乎在所有地方都使用了 Uuid 库。我们可以将其从代码中抽象出来,使用接口来实现,这样我们就有了一个内部使用 Uuid 库的 ID 接口。
我们没这么做的原因很简单,不值得付出努力。清晰度方面的回报太小了。我们几乎在代码库的每一层都使用 Uuid,而且我们发现解释 Uuid 库额外语言的开销非常小。所以我们干脆把它变成了核心层的一部分,而不是把它看作一个实现细节。这意味着我们可以在任何其他层直接使用它。
何时做
以上提出了一个重要的观点:这种关注点分离方法是一种在需要时使用的技术,有时甚至不需要。上面我们判断了哪些部分应该提取到它自己的层中,哪些部分应该成为核心的一部分。还有其他一些指导原则,但归根结底,关键在于清晰度。
如果您发现某一层中的内容变得混乱,请考虑将代码/概念移动到另一个层,或者在需要时创建该层。
编写干净的代码
编写干净的代码很难,随意使用模式只会让事情变得更糟。这就是为什么我建议应用“干净架构”的思维模式,这是一种清理代码的简单技巧。它对我有帮助,希望它也能帮助到你。
*我认为这是因为我们把所有的注意力都放在了工具上,而不是流程上。大家都忙着争论哪种凿子最好,以至于他们开始认为软件设计的全部就是工具。
**也称为六边形架构或洋葱架构。Vaughan Vernon 在他的书《实现领域驱动设计》中介绍了这个概念,从此我就对它爱不释手了。
***最糟糕的双关语
****我唯一没再看过的软件是那种没能创造价值的。只要有人用,它就会变。
*****快速建议,“核心”应该只包含独立的代码,它不应该包含任何连接到外部服务或有副作用的代码,如数据库代码。
文章来源:https://dev.to/barryosull/cleaning-up-your-codebase-with-a-clean-architecture