在过去几十年中,有一系列关于系统架构的想法被提出,例如
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
中。要遵守上面这些规则并不难,只要通过系统划分层次,并确保遵守依赖规则,就可以构建出一个天生可测试的系统。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。