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

2023-06-08 08:36:03 浏览数 (1)

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

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

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

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

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

依赖关系规则

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

业务实体

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

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

用例

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

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

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

接口适配器

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

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

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

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

框架与驱动程序

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

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

只有四层吗

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

跨越边界

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

控制器调用用例的输入端接口(依赖用例),用例实现该输入端。用例调用自己用例层的输出端接口(并没有依赖外层),让展示器实现该输出端。

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

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

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

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

哪些数据会跨越边界

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

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

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

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

一个常见的应用场景

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

  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. 最后我们需要注意一下,这张图的依赖关系规则,所有跨边界的依赖线,都是向内的,很好的遵守了架构的依赖规则。

本章小结

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

0 人点赞