在过去几十年中,有一系列关于系统架构的想法被提出,例如
- 六边形架构
DCI
架构BCE
架构
虽然这些架构在细节上各有不同,但总体是相似的,它们都有一个共同的目标,按照不同的关注点对软件进行切割分层,并且至少有一层是只包含该软件的业务逻辑的,而用户接口,系统接口属于其他层。
这些架构通常具有以下特点。
- 独立于框架:系统架构不依赖于框架中的某个函数。不需要让系统来适应框架。
- 可被测试:系统的业务逻辑可以脱离
UI
,数据库
,Web服务
和其他外部元素,从而进行测试。 - 独立于
UI
:系统的UI
变更起来容易,不需要修改系统的其他部分。比如我们可以在不修改业务逻辑的前提下,将原来的Web
界面替换成命令行界面。 - 独立于数据库:可以轻易的更换数据库。这代表业务逻辑和数据库之间已经解耦。
- 独立于任何外部机构:系统的业务逻辑并不需要之道任何其他的外部接口。
依赖关系规则
每一层圆圈,代表一个层次,越往中心,层级越高,并且依赖关系,是由外层,依赖内层。这意味着内层的代码,不需要知道,不能知道外层的函数,变量,对象等外层的一切信息,而应当由外层依赖内层。要保证外层中发生的变化,不会影响到内层圆中的代码。
业务实体
这一层中封装的是个系统的关键业务逻辑,它既可以是一个带有方法的对象
,也可以是一组数据结构
和函数
的集合。只要它能够被系统中的其他不用应用复用就可以了。
它封装应用中最通用,最高层的业务逻辑,属于最不容易被外界影响而变动的部分。
用例
用例层包含的是特定应用场景下
的业务逻辑,它封装并实现了整个系统的所有用例。用例引导了数据在业务实体之间流入/流出,指挥着业务实体,利用其中的关键业务逻辑来实现用例的设计目标。
我们期望它既不能影响业务实体层,也不被其他外层所干扰。
然而当业务行为发生变化时,肯定会影响到用例,但是这也意味着其他层可能会发生改变,比如业务实体,或其他外层。
接口适配器
它包含,网关
,控制器
,展示器
。接口适配器通常是一组数据转换器,它们负责将业务实体
和用例
给出的数据格式
,转换为其他外层
最方便操作的格式。这一层应该包含了整个GUI
,MVC框架
。展示器
,视图
,控制器
都应该属于接口适配器层。而模型部分则应该由控制器传给用例,再由用例传回展示器和视图。
这一层也会负责将业务实体而言最为方便操作的数据格式,转换为对数据库最方便的格式。
从该层开始起(不含该层),以内的圈层,(用例层,业务实体层),它们都不应该和数据库打交道,不应该依赖数据库。
所有和数据库相关的操作,都应该被限制在这一层的代码中,并且仅限于那些需要操作数据库的代码。当然,这一层的代码也需要负责将来自外部服务的数据,转换为系统内用例,和业务实体所需要的格式。
框架与驱动程序
该层是最外层,一般由工具,数据库,Web
框架组成。这一层中,我们通常只需要编写一些与内层沟通的黏合性代码。
它们包含了所有的实现细节。我们将这些细节放在最外层,它们就很难影响到其他层了。
只有四层吗
图中的同心圆,只是为了说明架构的结构,真正的架构很可能超过这四层。但是这其中的依赖关系原则是不变的。即只能由外层依赖内层。最内层是最核心的策略,最外层是最具体的细节。
跨越边界
上图中的右下侧,示范的是架构中跨边界的情况。这是控制器,展示器与用例之间的通信过程。
控制器调用用例的输入端接口(依赖用例),用例实现该输入端。用例调用自己用例层的输出端接口(并没有依赖外层),让展示器实现该输出端。
注意控制流的方向:从控制器开始,穿过用例,最后执行展示器的代码。但是可以看到依赖方向,是相反的,即控制器依赖用例。
这里我们通常采用依赖反转原则(DIP)来解决这种相反性。例如,可以用过调整代码中的接口和继承关系,利用源码中的依赖关系,来限制控制流只能在正确的地方跨越架构边界。
假设用例代码需要调用展示器,这里一定不能直接调用,因为会违反依赖关系原则:内层圆中的代码,不能引用外层的信息。我们需要让业务逻辑代码调用一个内层接口(用例的输出端),让展示器负责实现这个接口。
我们可以采用这种方式来跨越系统中的所有的架构边界。利用多态技术,我们将源码中的依赖关系和控制流的方向进行反转。不管控制流的方向如何,我们都可以让它遵守架构的依赖关系规则。
哪些数据会跨越边界
一般来说,会跨越边界的数据在数据结构上,都是很简单的。如果可以的话,我们一般采用基本的结构体或者简单的可传输数据对象。
这里最重要的是跨越边界传输的对象,应该有一个独立,简单的数据结构。总之,不要投机取巧的,直接传递业务实体或者数据库记录对象。同时,这些传递的数据结构中,也不应该存在违反依赖规则的依赖关系。
比如数据库框架会返回一个便于查询的结果对象,我们称为行结构体
。这个结构体就不应该跨越边界向架构的内层传递。因为这等于让内层的代码引用外层的代码,违反了依赖规则。
以此,我们跨越边界传递数据时,一定要采用内层最方便使用的形式。
一个常见的应用场景
双实线,为隔离边界。控制器,展示器,都是接口适配层。
- 屏蔽前面的操作,从
Controller
开始,Controller
收到了数据。 Controller
将数据包装成Input Data
,调用InputBoundary
接口。UseCaseInteractor
实现了InputBoundary
接口,UseCaseInteractor
解析InputData
数据,调用Entities
。UseCaseInteractor
调用DataAccessInterface
接口,而DataAccess
实现了该接口,取出数据后,放入内存。UseCaseInteractor
从Entities
收集数据,组装成OutputData
数据,调用OutputBoundary
接口,该接口由Presenter(展示器)
实现。Presenter
将OutputData
打包成可展示的ViewModel
。基本上ViewModel
只会包含字符串,和一些View
会使用到的开关数据(例如按钮是否展示等开关数据)。如果OutputData
中可能包含了一些对象,Presenter
将会处理成格式化的,可对用户展示的字符拆,并放入ViewModel
中。- 最后我们需要注意一下,这张图的依赖关系规则,所有跨边界的依赖线,都是向内的,很好的遵守了架构的依赖规则。
本章小结
要遵守上面这些规则并不难,只要通过系统划分层次,并确保遵守依赖规则,就可以构建出一个天生可测试的系统。