使

使用干净的架构来清理代码库

2025-06-04

使用干净的架构来清理代码库

让我们来谈谈软件架构。我们大多数人都知道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

相反,我们将用使用应用程序语言而不是实现的集成点来替换它们。

这就是设计模式发挥作用的地方。通过观察你想要做什么而不是如何去做,你可以发现新出现的常见模式。

从代码来看,imageuser分别是核心/领域概念,但部分使用它们的代码是纯粹的实现。这些是我们的集成点,所以让我们深入研究一下我们究竟是如何利用“图像”和“用户”的。

在我们的例子中,很明显我们真正想做两件事

  • 存储和检索用户
  • 存储图像

其余一切都是实现细节。存储和检索显然是存储库模式。因此,让我们创建两个新的应用程序概念: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
PREV
沟通风格——团队有效工作
NEXT
别再用 ChatGPT 帮你写博客文章了!它根本没用……