《架构整洁之道》第 22 章 整洁架构

均为原创,读架构整洁之道的笔记。

包含了部分自己的理解,包含了原书中至少 70% 的知识点。
完整笔记,各位老哥友链加起来吧。
我的博客地址:www.yuque.com/_huangkuan


在过去几十年中,有一系列关于系统架构的想法被提出,例如

  • 六边形架构
  • DCI 架构
  • BCE 架构

虽然这些架构在细节上各有不同,但总体是相似的,它们都有一个共同的目标,按照不同的关注点对软件进行切割分层,并且至少有一层是只包含该软件的业务逻辑的,而用户接口,系统接口属于其他层

这些架构通常具有以下特点。

  • 独立于框架:系统架构不依赖于框架中的某个函数。不需要让系统来适应框架。
  • 可被测试:系统的业务逻辑可以脱离 UI数据库Web服务和其他外部元素,从而进行测试。
  • 独立于 UI:系统的 UI 变更起来容易,不需要修改系统的其他部分。比如我们可以在不修改业务逻辑的前提下,将原来的 Web 界面替换成命令行界面。
  • 独立于数据库:可以轻易的更换数据库。这代表业务逻辑和数据库之间已经解耦。
  • 独立于任何外部机构:系统的业务逻辑并不需要之道任何其他的外部接口

《架构整洁之道》第 22 章 整洁架构

依赖关系规则#

每一层圆圈,代表一个层次,越往中心,层级越高,并且依赖关系,是由外层,依赖内层。这意味着内层的代码,不需要知道,不能知道外层的函数,变量,对象等外层的一切信息,而应当由外层依赖内层。要保证外层中发生的变化,不会影响到内层圆中的代码。

业务实体#

这一层中封装的是个系统的关键业务逻辑,它既可以是一个带有方法的对象,也可以是一组数据结构函数的集合。只要它能够被系统中的其他不用应用复用就可以了。

它封装应用中最通用,最高层的业务逻辑,属于最不容易被外界影响而变动的部分。

用例#

用例层包含的是特定应用场景下的业务逻辑,它封装并实现了整个系统的所有用例。用例引导了数据在业务实体之间流入 / 流出,指挥着业务实体,利用其中的关键业务逻辑来实现用例的设计目标。

我们期望它既不能影响业务实体层,也不被其他外层所干扰。

然而当业务行为发生变化时,肯定会影响到用例,但是这也意味着其他层可能会发生改变,比如业务实体,或其他外层。

接口适配器#

它包含,网关控制器展示器。接口适配器通常是一组数据转换器,它们负责将业务实体用例给出的数据格式,转换为其他外层最方便操作的格式。这一层应该包含了整个 GUIMVC框架展示器视图控制器都应该属于接口适配器层。而模型部分则应该由控制器传给用例,再由用例传回展示器和视图。

这一层也会负责将业务实体而言最为方便操作的数据格式,转换为对数据库最方便的格式。

从该层开始起(不含该层),以内的圈层,(用例层,业务实体层),它们都不应该和数据库打交道,不应该依赖数据库。

所有和数据库相关的操作,都应该被限制在这一层的代码中,并且仅限于那些需要操作数据库的代码。当然,这一层的代码也需要负责将来自外部服务的数据,转换为系统内用例,和业务实体所需要的格式。

框架与驱动程序#

该层是最外层,一般由工具,数据库,Web 框架组成。这一层中,我们通常只需要编写一些与内层沟通的黏合性代码。

它们包含了所有的实现细节。我们将这些细节放在最外层,它们就很难影响到其他层了。

只有四层吗#

图中的同心圆,只是为了说明架构的结构,真正的架构很可能超过这四层。但是这其中的依赖关系原则是不变的。即只能由外层依赖内层。最内层是最核心的策略,最外层是最具体的细节。

跨越边界#

上图中的右下侧,示范的是架构中跨边界的情况。这是控制器,展示器与用例之间的通信过程。

《架构整洁之道》第 22 章 整洁架构
控制器调用用例的输入端接口(依赖用例),用例实现该输入端。用例调用自己用例层的输出端接口(并没有依赖外层),让展示器实现该输出端。

注意控制流的方向:从控制器开始,穿过用例,最后执行展示器的代码。但是可以看到依赖方向,是相反的,即控制器依赖用例。

这里我们通常采用依赖反转原则(DIP)来解决这种相反性。例如,可以用过调整代码中的接口和继承关系,利用源码中的依赖关系,来限制控制流只能在正确的地方跨越架构边界。

假设用例代码需要调用展示器,这里一定不能直接调用,因为会违反依赖关系原则:内层圆中的代码,不能引用外层的信息。我们需要让业务逻辑代码调用一个内层接口(用例的输出端),让展示器负责实现这个接口。

我们可以采用这种方式来跨越系统中的所有的架构边界。利用多态技术,我们将源码中的依赖关系和控制流的方向进行反转。不管控制流的方向如何,我们都可以让它遵守架构的依赖关系规则。

哪些数据会跨越边界#

一般来说,会跨越边界的数据在数据结构上,都是很简单的。如果可以的话,我们一般采用基本的结构体或者简单的可传输数据对象。

这里最重要的是跨越边界传输的对象,应该有一个独立,简单的数据结构。总之,不要投机取巧的,直接传递业务实体或者数据库记录对象。同时,这些传递的数据结构中,也不应该存在违反依赖规则的依赖关系。

比如数据库框架会返回一个便于查询的结果对象,我们称为行结构体。这个结构体就不应该跨越边界向架构的内层传递。因为这等于让内层的代码引用外层的代码,违反了依赖规则。

以此,我们跨越边界传递数据时,一定要采用内层最方便使用的形式。

一个常见的应用场景#

《架构整洁之道》第 22 章 整洁架构

双实线,为隔离边界。控制器,展示器,都是接口适配层。

  1. 屏蔽前面的操作,从 Controller 开始,Controller 收到了数据。
  2. Controller 将数据包装成 Input Data,调用 InputBoundary 接口。
  3. UseCaseInteractor 实现了 InputBoundary 接口,UseCaseInteractor 解析 InputData 数据,调用 EntitiesUseCaseInteractor 调用 DataAccessInterface 接口,而 DataAccess 实现了该接口,取出数据后,放入内存。
  4. UseCaseInteractorEntities 收集数据,组装成 OutputData 数据,调用 OutputBoundary 接口,该接口由 Presenter(展示器)实现。
  5. PresenterOutputData 打包成可展示的 ViewModel。基本上 ViewModel 只会包含字符串,和一些 View 会使用到的开关数据(例如按钮是否展示等开关数据)。如果 OutputData 中可能包含了一些对象,Presenter 将会处理成格式化的,可对用户展示的字符拆,并放入 ViewModel 中。
  6. 最后我们需要注意一下,这张图的依赖关系规则,所有跨边界的依赖线,都是向内的,很好的遵守了架构的依赖规则。

本章小结#

要遵守上面这些规则并不难,只要通过系统划分层次,并确保遵守依赖规则,就可以构建出一个天生可测试的系统。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。