简介
因为身处在应对ToB需求的SAAS行业,复杂的需求在代码上造成的杂乱始终是我们的一大困扰,所以我们在一些项目中尝采用整洁架构的分层模式对部门代码做了一些改善和实践。
在这篇文章中我来分享一下我在分层架构上的思考,一些实践方法。
为什么要分层?
我们都知道ToB行业的一大特点就是需求非常复杂,我们面对的客户都是大型企业,企业的流程和需求都各不相同。
可以想象一下,当你在家里用两个路由器去组建一个网络的时候,还是比较简单的事情。但当你需要帮助一个机房几百个互换机组建网络的时候,这个事情就会变得复杂起来。更大规模的组网需求导致了这个这个组网工作的复杂度陡增。
再举一个例子,把你丢在一个只有四栋楼的陌生小区里不给你任何的工具,让你走出这个小区,我相信你能很快走出这个小区;但是把你丢在一个陌生都会里不给你任何的工具,让你去一个指定的地点,你是很容易迷路的。更大规模的路线需要你去相识,假如没有舆图,你很难凭借本身的判定来相识这个都会的路线。
由此可见复杂性和规模有很大的关系,再回到我们体系面对的问题上,随着体系承载的需求规模的增加,需求之间交织的影响会越来越多,对体系的全面理解会越来越困难。
在《A Philosophy of Software Design》这本书中总结了三种复杂症状:
- 变更放大(Change amplification): 看似简单的变更需要在许多不同地方进行代码修改。
- 认知负荷(Cognitive load): 开辟人员需要许多知识才气完成一项任务。
- 未知的未知(Unknown unknowns): 开辟人员在到场开辟的工作中,不知道本身不知道什么。
分层架构很大程度上就是在试图办理这几种复杂症状,通太过层架构,我们将体系不停拆解,分离形成自治的稳定空间,去降低体系的认知负担和理解成本,简化和明确体系变更造成的影响范围。
分层架构的探索过程
巨人的肩膀
说到分层架构,除了经典的三层架构之外,最出名的就是Uncle Bob的《架构整洁之道》中提到的整洁架构了,他的关键布局表示如下:
除此之外,在DDD的六边形架构中,也有差不多的一个分层架构:
六边形架构给出了更加具体的条理,他将代码的条理分为了四层架构的:
几个条理之间的依赖关系如下:
实在以上提到的两种模式核心的想法都是一样的,以本身要承载的目标业务对象为最底层,高层代码答应依赖底层,但是底层代码不要依赖高层,这也是对依赖倒置原则很好的实践。
项目分包布局思考过程
我们通过学习整洁架构和六边形架构的内容,将他们的思想映射到体系中,我们形成了这样的分包演进路线,按照四层架构的分层,我们可以先将体系分成以下几个package:- .
- ├── view // 视图
- ├── usecase // 用例
- ├── domain // 领域层
- └── infrastructure // 基础设施
复制代码 在现实项目中会碰到几个问题:
- 体系会存在一些配置信息,存放项目所需要的配置类
- 像目前前后端分离的情况下,前端的页面作为用户展示层并不会放入后端的项目, 所以view层需要被移除掉。
- 用例usecase每每与具体的业务有关,我们将其与domain的对象聚合在一个package下,所以将usecase层移除
这样思考之后,我们形成了这样的一个目录布局:- .
- ├── config // 配置类
- ├── domain // 领域层
- └── infrastructure // 基础设施
复制代码 以上的每个package(用例,领域层,基础设施)都分开分析一下,先看基础设施,实际上,基础设施是分为两个维度的的:
- 对外暴露的服务接口,可以命名为gateway,表示对外的接入层
- 对外部中心件和数据库的依赖框架,也是我们最风俗认为的基础设施
将这个package布局继承改进之后,形成这样的布局:- .
- ├── config // 配置类
- ├── domain // 领域层
- ├── gateway // 视图
- └── infrastructure // 基础设施
复制代码 在项目中还会存在非常基础和通用的代码,这个部门的基础设施实际上是横跨整个项目,有点像是当前这个项目中java.lang包,我们将其命名为tool。- .
- ├── config // 配置类
- ├── domain // 领域层
- ├── gateway // 视图
- ├── infrastructure // 基础设施
- └── tool // 基础工具包
复制代码 截止到以上的部门,就是第一级比较宏观的分层了。但是一套业务体系之中,其核心还是业务,怎样在domain这个包下面要划分package,则真的要完全基于业务来进行划分了,从技术层面对这些条理(用例,基础设施等等)界说,还能有一些通用的说法,但是业务千变万化,是很难有通用的划分条理的,所以对业务进行划分也是最难的。
我们的业务中会分为几种不同的业务:
- 核心业务: 我们体系核心卖点,具有清晰的业务目标的业务
- 支持业务: 支持着核心业务多个模块完成运转的业务,好比发送短信消息,发送开辟者回调,文件存储等等。
所以domain下面我们可以分为这样两个包:- .
- ├── corebiz // 核心业务
- └── support // 支撑业务
复制代码 对于任何一个核心业务或支持业务的单一的小范围业务而言,他们最好是都能独立自治,形成一个迷你体系,根据《架构整洁之道》中的模型,每个业务package可以分为这样几个条理:- .
- ├── application // 应用服务
- │ └── param // 与用例相关的入参和出参
- ├── acl // 防腐层
- ├── model // 领域对象
- ├── repo // 仓储层
- └── service // 领域服务
复制代码 以下是对上面分包每一种package布局的解释:
- application包中, 存放业务的主流程
- 前面提到的usecase,表达业务的动作
- 能讲明白业务的流程,表达业务的寄义,不要出现复杂的数据组装逻辑和和复杂的判定逻辑
- 具体的业务判定逻辑在domainService或者model中完成
- 数据组装逻辑应该尽量在基础设施层完成
- acl是对外部体系的调用,使用Java interface表示
- 当业务使用到外部体系的时候,使用ACL屏蔽外部对接的实现,让业务只关心做什么,而不是怎么做。
- 命名应该采用具有业务寄义的命名,不要出现:RedisClient、CacheClient,MySQLClient等等,应该出现: IMessageClient, IUserClient等等
- model是对业务对象的出现
- 每个业务应该尽量新建本身的业务model,复用对象应该谨慎,避免出现过大的类,大类容易出现信息过载,当本身使用get方法找属性要停顿思考一下的时候就意味着类太大了。
- model的类可以有一些本身的行为方法(method)
- 业务对象实在是有分类的:
- entity:具有完备生命周期的对象,就是完备的具有业务寄义的对象,好比:Receiver、Label、Document
- value object:值对象,本身只是为了承载一些数据,脱离entity没故意义,例如Label的Position对象
- aggregate:表示聚合,当多个entity需要协作会内聚到一个聚合中,聚合也是核心的业务操尴尬刁难象,好比:AutoSignContract
- repo表达的是model的数据源,使用Java interface表示
- 应该仅考虑给聚合model提供repo
- 数据的组装在实现中完成
- domain service:
- domain service相当于业务对象(model)的延伸
- domain service应该只处理惩罚与本身对应model相干的业务,好比:ReceiverDomainService只处理惩罚Receiver相干方法,不要出现以处理惩罚Document对象为主的方法。
- 每个方法应该只完成一个动作(粒度要小),靠application service编排domain service的方法
颠末以上的思考和界说之后,我们形成了这样的一个分层架构:- .
- ├── config // 配置类
- ├── domain // 领域层
- │ ├── corebiz // 核心业务
- │ │ ├── business1
- │ │ │ ├── application // 应用服务,usecase
- │ │ │ │ └── param // 与用例相关的入参和出参
- │ │ │ ├── acl // 防腐层
- │ │ │ ├── model // 领域对象
- │ │ │ ├── repo // 仓储层
- │ │ │ └── service // 领域服务
- │ │ └── business2
- │ └── support // 支撑业务
- │ │ ├── business3
- │ │ │ ├── application // 应用服务,usecase
- │ │ │ │ └── param // 与用例相关的入参和出参
- │ │ │ ├── acl // 防腐层
- │ │ │ ├── model // 领域对象
- │ │ │ ├── repo // 仓储层
- │ │ │ └── service // 领域服务
- │ │ └── business4
- ├── gateway // 视图
- ├── infrastructure // 基础设施
- └── tool // 基础工具包
复制代码 有了业务的分层之后,两个基础设施层(gateway和infrastructure)要开始适配我们的业务分层。
先看gateway,它要作为对外暴露的协议接入层,所以不同的协议上会有一些会合处理惩罚,所以我们给出了这样的分层:- .
- └── gateway // 协议接入层
- ├── dubbo // Dubbo协议层
- ├── http // HTTP协议接入层
- ├── mq // MQ协议接入层
- └── schedule // 定时任务接入层
复制代码 别的一遍infrastructure会包含许多中心件的本身的代码,另有一部门是中心件的代码和业务代码交互的部门,所以应该形成这样的布局去承载这两种功能:- .
- └── infrastructure // 基础设施层
- ├── impl // 业务代码和基础设施的适配层
- │ ├── corebiz
- │ │ ├── business1 // 与domain层的package分类要适配
- │ │ │ ├── acl // 防腐层实现
- │ │ │ └── repo // 仓储层实现
- │ │ └── business2
- │ └── support
- │ ├── business3 // 与domain层的package分类要适配
- │ │ ├── acl // 防腐层实现
- │ │ └── repo // 仓储层实现
- │ └── business4
- ├── mysql // mysql的客户端实现,存放比如数据库的映射代码,XXXDAO等,方便impl统一调用
- ├── redis // redis的客户端实现,存放一些和redis交互代码,方便impl统一调用
- ├── kafka // kafka的客户端实现,存放一些和kafka交互代码,方便impl统一调用
- └── thirdparty // 与第三方系统交互的适配代码,方便impl统一调用
复制代码 还剩下一个部门没有进行分包,那就是tool,之前提到说这个包下主要是当做这个项目的java.lang包用的,也就是一些可以在项目中比较通用的代码,这个包下的分类就比较看本身项目的需求了, 对于我们而言,比较通用的代码有这些:
- .
- └── tool // 工具代码
- ├── livingdocument // 我们的业务文档注解聚集地
- ├── symbol // 一些标记代码
- └── utils // 一些常用的Utils,比如StringUtils,PDFUtils等等
复制代码 但是一定要注意tool这个包下对代码的规模控制,否则也会造成过多的信息,导致出现大量无用的Utils。
至此,我们形成了这样的一个分层架构:- .
- ├── config // 配置类
- ├── domain // 领域层
- │ ├── corebiz // 核心业务
- │ │ ├── business1
- │ │ │ ├── application // 应用服务,usecase
- │ │ │ │ └── param // 与用例相关的入参和出参
- │ │ │ ├── acl // 防腐层
- │ │ │ ├── model // 领域对象
- │ │ │ ├── repo // 仓储层
- │ │ │ └── service // 领域服务
- │ │ └── business2
- │ └── support // 支撑业务
- │ │ ├── business3
- │ │ │ ├── application // 应用服务,usecase
- │ │ │ │ └── param // 与用例相关的入参和出参
- │ │ │ ├── acl // 防腐层
- │ │ │ ├── model // 领域对象
- │ │ │ ├── repo // 仓储层
- │ │ │ └── service // 领域服务
- │ │ └── business4
- ├── gateway // 协议接入层
- │ ├── dubbo // Dubbo协议层
- │ ├── http // HTTP协议接入层
- │ ├── mq // MQ协议接入层
- │ └── schedule // 定时任务接入层
- ├── infrastructure // 基础设施
- │ ├── impl // 业务代码和基础设施的适配层
- │ │ ├── corebiz
- │ │ │ ├── business1 // 与domain层的package分类要适配
- │ │ │ │ ├── acl // 防腐层实现
- │ │ │ │ └── repo // 仓储层实现
- │ │ │ └── business2
- │ │ └── support
- │ │ ├── business3 // 与domain层的package分类要适配
- │ │ │ ├── acl // 防腐层实现
- │ │ │ └── repo // 仓储层实现
- │ │ └── business4
- │ ├── mysql // mysql的客户端实现,存放比如数据库的映射代码,XXXDAO等,方便impl统一调用
- │ ├── redis // redis的客户端实现,存放一些和redis交互代码,方便impl统一调用
- │ ├── kafka // kafka的客户端实现,存放一些和kafka交互代码,方便impl统一调用
- │ └── thirdparty // 与第三方系统交互的适配代码,方便impl统一调用
- └── tool // 基础工具包
- ├── livingdocument // 我们的业务文档注解聚集地
- ├── symbol // 一些标记代码
- └── utils // 一些常用的Utils,比如StringUtils,PDFUtils等等
复制代码 这里通过一个图例更加直观地阐明这个条理布局:
分层架构的实践
怎样在项目中加入分层架构
在探索得到这样的一个分层架构之后,首要面对的问题就是怎样将这样的架构应用到体系中,也不会对体系造成很大的影响。在灵敏开辟中,很重要的一个实践就是要做精益交付,就是一次性不要实验做大型交付,好比将体系推到重来,或者一次交付一个需要几周才气完成的工作,要想办法尽快让其有反馈。
在不影响原来的代码的情况下,我们决定在项目中添加了新的一个package,使得形成这样的一个布局:- .
- ├── extant-package // 现有的代码package
- └── new-package // 分层架构的package
复制代码 这样我们在找定一个业务中之后,可以在不影响原来项目的情况下立马开始实践,即使出问题了,在发布分支上删除掉整个package也没有问题。(可以Google查询Martin Fowler的“绞杀者模式”)
新旧需求怎样使用分层架构
新需求每每包含着一些完备的业务逻辑,所以可以比较方便在分层架构下构建代码,在这种分层架构下新编写业务逻辑,然后暴露协议给外部或者老代码使用。
对于一些老的需求,我们也是采用了演进式的方式来进行适配,我们会把老需求修改的部门通太过层架构进行编写,然后暴露一个interface给老代码去使用。
但不论是新需求还是老的需求改造,其中非常重要的两件事情:
- 每一个business package下要能够独立自治,形成高度内聚的逻辑,这样就能一定程度上减缓Unkonw unkonws的困扰。
- 每一个business package下的规模要足够的小,只有足够的小,才气让背面来维护的其他人认知负担小,让这个模块可以做到快速交付。
对于代码层面具体的编写是一个比较大的内容,我们会单独去写一篇文章来进行分享。
分层架构是怎样帮助单位测试的
单位测试不停是我们希望去强调的一个质量保证手段,但是很长一段时间单位测试的执行结果是不抱负的,但分层架构一定程度上帮助到了单位测试的推进。
原来单位测试对于大部门开辟同事来说是最痛苦的就是运行它,因为一段布满了依赖和坏味道的代码实在是不好运行的,也非常慢,这也阻碍了我们进行测试。单位测试本意是为了测试我们那一小块业务逻辑,我们并不应该将无关的代码启动起来,通太过层架构我们可以将基础设施和业务代码分开。
前面我们提到了一个business的布局如下:- .
- ├── application // 应用服务
- │ └── param // 与用例相关的入参和出参
- ├── acl // 防腐层,只有interface
- ├── model // 领域对象
- ├── repo // 仓储层,只有interface
- └── service // 领域服务
复制代码 我们只去测试business代码,从application为入口,进行测试。由于ACL和Repo都是interface,我们很容易就能mock这些interface,给这些interface一些我们预期的输入和返回值,然后验证我们在application中的编排和业务逻辑是否是正确的,这里去构建单位测试的时候,是不需要借助任何运行时候的框架的(好比spring,Dubbo等等),仅仅是我们在验证业务逻辑,快速获得我们编写的业务逻辑是否符合我们的预期。
这里尤其要注意使用spring的项目,做IOC注入的时候应该使用构造器注入或者是setter注入,这样才气方便单位测试。
怎样管理日益新增的需求
随着需求日益增加,package会陷入别的一种杂乱。给大家感受一下:- .
- ├── corebiz
- │ ├── business1
- │ ├── business2
- │ ├── business3
- │ ├── business4
- │ ├── business5
- │ ├── business6
- │ ├── business7
- │ ├── business8
- ...
- │ └── business100
- └── support
- ├── business1
- ├── business2
- ├── business3
- ├── business4
- ├── business5
- ├── business6
- ├── business7
- ├── business8
- ...
- └── business50
复制代码 由于过多的package,还是会陷入别的一种由于规模造成的认知负担。假如我们可以有一个需求目录,想象一下假如用脑图的情势让你组织本身负责的产品如今的需求目录你会怎么做?颠末精心地整理,是否可以整理成这样:- .
- ├── corebiz
- │ ├── business1
- │ │ ├── business1.1
- │ │ ├── business1.2
- │ │ └── business1.3
- │ ├── business2
- │ │ ├── business2.1
- │ │ ├── business2.2
- │ │ └── business2.3
- │ ├── business3
- │ │ ├── business3.1
- │ │ │ ├── business3.1.1
- │ │ │ ├── business3.1.2
- │ │ └── business3.2
- │ └── business4
- └── support
- ├── business1
- │ ├── business1.1
- │ ├── business1.2
- │ └── business1.3
- ├── business2
- │ ├── business2.1
- │ ├── business2.2
- │ └── business2.3
- └── business3
复制代码 可以让这些package本身就组织地与文档一样,在需求演进的过程中设定一个规则,若模块超过x个,就进行拆分。最终的目的一定是:
- 让这个布局本身成为文档,让package的名称具有业务寄义
- 让这个布局认知负担低,方便其他人查找。
由此还可以可见,当这个package下的目录达到一定的规模之后,我们就应该思考,这个体系是否应该进行拆分了。
总结
以上就是我们对于分层架构的探索实践的分享,《架构整洁之道》中有这么一句话来形容软件架构的目标:
软件架构的最终目标是:用最小的人力成本来满意构建和维护体系的需求。
通太过层架构的实践,产出的代码一定程度上降低了大家理解体系的认知负担,改善了修改体系的成本,算是达到了我们使用分层架构的目的。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |