基于DDD的cola架构的改进(面向微服务)
术语解释:
模块(Module):代码工程里面划分为多个小的、独立的子项目,子项目面向特定的功能场景,子项目之间可以相互关联。例如Maven工程(Project)可以被组织为多个模块(Module),此外每个模块都拥有自己的 pom.xml 文件
包(Package):包是一种组织类的方式,包的主要目的是防止命名冲突,并提供一种将相关的类、接口、枚举和注解等组织在一起的方式,从而简化代码的管理和使用。例如在Java中的Package
备注.本文也的相关代码示例都是基于Java的Maven工程
技术团队在代码工程的规范方面,尤其是分层规范,按道理应该遵循一套适用的、科学的法则。此外,在产研过程中,DDD(领域驱动设计)理论也给到了很好的指导作用,包括:需求分析设计阶段、项目代码工程分层规范。
下面将围绕代码工程分层,讨论几个落地方案。
早前的MVC模式
过去在DDD还没有这么流行的时候,在软件系统里面最常见的就是MVC模式。这里的Model是数据库模型,业务逻辑在Controller中实现(一般会由Service来辅助实现),View层主要负责视图展示,从上到下的分层设计大概是这样子:
对于业务逻辑不复杂的软件开发,MVC是简单高效的方法。但是随着业务逻辑愈来愈复杂,MVC会开始力不从心。主要体现在这几个方面:
- MVC模式仅仅反应了软件层面的架构,它不包含业务语言,无法使用该设计直接和业务对话,因此仅适用于逻辑简单的需求。
- MVC模式天然切割了数据和行为,然后用数据库实现数据,用服务实现行为,随着时间发展,中间的过渡代码也会变得严重耦合、臃肿。
- 缺乏明确的边界划分,至少在顶层设计层面没有边界划分的规范要求,更多地是靠技术负责人根据经验进行划分,大规模团队协作容易出现职责不清晰、分工不明确,最终开发质量也参差不齐。
因此,在使用MVC模式的过程中,开发人员对 “高内聚,松耦合” 的诉求会越发强烈。
阿里服务分层规范
参考链接:
你的项目应该如何正确分层?
在阿里的编码规范中,其标准示例如下图:
图中分层介绍如下:
- 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行网关安全控制、流量控制等。
- 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染、JS 渲染、JSP 渲染、移动端展示等。
- Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
- Service 层:相对具体的业务逻辑服务层。
- Manager 层:通用业务处理层,它有如下特征:一是对第三方平台封装的层,预处理返回结果及转化异常信息;二是对Service层通用能力的下沉,如缓存方案、中间件通用处理;三是与DAO层交互,对多个DAO的组合复用。
- DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 进行数据交互。
可以看到有明显的分层和复用的意义,具体实践为:
- service层的上层,作为第一层,对外提供业务交互接口,要求:轻业务逻辑、参数校验、异常兜底。
- service层作为业务组装层,包含业务编排逻辑,一般是针对特定业务定制化编写,复用性较低,一个service可能会调用多个manager的方法来实现完整的接口逻辑。
- Mannager层是基础逻辑层,遵循高内聚低耦合思想,粒度较小,提供给service层复用。
在实际编码中对应的是包(Package)的命名约定。
对于上述分层架构,需要考虑的另一个因素,是层次之间一定是相邻层互相依赖,数据的流转也只能在相邻的两层之间流转:
由此可见伴随着这种分层架构,也会有对应的领域模型,常见的有:
PO (Persistant Object) :持久化对象。可以理解为数据库中的一条数据即一个PO对象,也可以理解为POJO经过持久化后的对象。
DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
BO(Business Object):业务对象,由Service层输出的封装业务逻辑的对象。
VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
备注:关于DO ——
DO(Data Object):由阿里巴巴的开发手册定义,与数据库表结构一一对应,通过DAO层向上传输数据源对象,等同与PO。
DO(Domain Object):由领域驱动设计(DDD)定义,等同于上面的BO
但是,Do关键字还是有较大歧义,例如:do本身的英文翻译;阿里和DDD的使用差异;祖传MVC代码、Struts会用到关键字.do。因此建议慎用,也建议优先采用:DTO、PO。
以上的领域模型浅尝到此,如有疑问,可自行补充了解。在实际工作中,体现在代码工程上就是包(Package)名、后缀。至于具体用到哪几个,建议考虑配合分层架构来使用,例如:
COLA 4.0
参考链接:
COLA 4.0:应用架构的最佳实践
github.com/alibaba/COLA
COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。目前(截止到202402)COLA已经发展到COLA 4.0。
其的初衷旨在控制复杂度,救码农于水火,很大程度上是指导着程序员践行“高内聚,低耦合”。
其官方已提供了architect生成方案,并包含了配到的公共组件(cola-components目录下面),包括:
接下来回归到分成设计本身,COLA里面的很多设计思想都来自于DDD(DDD的思想是一定要去学习和贯彻的,特别是统一语言、边界上下文、防腐层的思想),其中就包括领域包的设计,其分层架构划分如下:
其代码工程结构(Module方面)划分如下:
其包结构(Package方面)命名建议如下:
最终通过其官方architect工具生成的示例工程如下:
注意工程内有starter模块,这是不错的设计,直观地描述了执行入口,也是阅读代码的入口。不过,starter模块代码不多,提供的参考能力有限,其对应的pom也能参考参考。
COLA 是上述【阿里服务分层规范】的全面补全、改进。我们总结下其优点:
- 官方提供了基础的组件,文档齐全,开箱即用
- 分层设计做到了极致,模块间依赖关系清晰,有很好的指导意义
那么其有没有些不足,或者用力过猛的地方呢? 尤其是在应对编程工作时,COLA 能否满足所有人的诉求,例如:
- 开发效率方面:对于多个模块之间的依赖,需要都定义接口+实现类 —— COLA的分层设计是否太多了,是否可以下放部分到package
- 代码可读性:由于代码工程充满接口、实现类,而阅读代码时主要关注的是实现类(的实现逻辑)
- 工程结构方面:能否从复用、父级接口定义方面,再抽象出一个module(见下面【面向微服务的最佳实践】)
下面再浅谈一下,为什么我们会觉得“用力过猛”,大概是因为:
- 国内的软件开发氛围,是比较急躁的,讲究高效、快速实现,目标是尽快抢占市场(此处应该有狗头)
- 国内的程序员,受环境氛围影响,会追求快准狠的实现,都更喜欢简单、高效、有规律可循的设计
因此,该COLA架构是否一针见血的简洁呢?显然不是了。
那么,COLA架构是面向什么场景的呢?答案就是:
- 多人协作、结对编程场景,即高级程序员负责定义上层设计(主线逻辑、接口),中初级程序员负责补齐细节逻辑(实现类,以及domain层)。听起来有点欧美风,早上写代码下午喝咖啡的味道。
- 大型的业务系统(所以内部要做好接口约定,以面向对人协作开发),例如项目早期的单体系统。但是这类系统往后有可能做垂直拆分,变成微服务,届时也许会有上述“用力过猛”的体验。
面向微服务的最佳实践(基于COLA调整)
我们通过极限思维去发散思考,也许工作中会有些稍微极端的、常见的挑战,例如:
- 单人开发、跟进 十几个系统,或者是十万行以上的代码
- 因人员变动,B员工要尽快接手A员工的 十几个系统,或者是十万行以上的代码
- 排查事故时,检查相关代码时,需要往下翻查大量的接口+实现类(此时接口的存在是多余的)
- 外部系统希望提供DAO层的pom依赖
- 外部系统希望有更纯粹的API、DTO(而不是开箱即用的client、facade)
因此,我们期望能够更合理地分层,包括:
- 减少非必要的模块,以整合过多冗余的接口-实现类(减少接口的定义),对独立开发者友好,对小众开发者友好(一般来说,垂直领域的微服务是由一两个人开发、维护)
- 更科学的依赖关系,模块间带有分层、继承的理念
具体的模块划分、之间的依赖说明如下:
对应模块说明如下:
公共组件:由架构组开发的后端工程基础组件
—— 封装parent包,以初次声明properties(包含project.build.sourceEncoding、project.reporting.outputEncoding、java.version、依赖包的version等)、声明dependencyManagement等
—— 封装common-rpc包,提供微服务注册、发现、调用以及相关治理能力
—— 封装common-dto、exception包,以声明公共模型类
—— 封装功能性组件,如定时任务、MQ等
demo-api:声明对外服务的能力,业务系统的父级
demo-facade:继承demo-api,补齐rpc(或者直接调用)的能力,向外部应用提供开箱即用的能力(调用门面)。这里命名没有用client,主要因为client概念过大、容易产生歧义。
demo-app:继承demo-api,是业务系统的主逻辑,也是发布包
demo-dao:持久层相关,一般结合mybatis实现,带generator能力
补充:如有需要,也可考虑定义 demo-constant模块,用于(收敛)声明业务变量,提供给系统自身、外部做依赖。当然,对于变量不多的情况,声明在demo-api也是可行的,这主要取决于依赖方怎样引用会更方便,他们不一定关心api、dto相关信息。
结合包名的整体设计如下:
示例工程(下载地址)如下:
腾讯FIT后台系统工程结构
工程结构如下:
Q & A
代码工程为什么拆分出dao模块?
答:确实有不少的模板工程不会独立拆分dao。那么我们思考下拆分的好处:
1.dao模块专注持久层,对本系统而言会更清爽些,还可以提供给其他系统依赖(例如数据仓库、数据湖的建设落地)
2.dao模块可以更好地做代码generate的事情,可以整合mybatis相关能力,如generator组件。此时即便工程其他模块有代码错误,只要dao模块正常则能在idea上执行代码generate。
为什么要封装公共组件,能用Spring Cloud Alibaba代替吗?
答:Spring Cloud Alibaba开箱即用,可以用于快速搭建业务系统。然而,Spring Cloud Alibaba是以依赖方式嵌入,而不是作为parent角色,因此Spring Cloud Alibaba对工程没有约束力,仅有功能上的整合。
为什么要封装公共组件的parent包?
答:parent包作为业务系统的父级,可以提前声明properties、dependencies、dependencyManagement,可以有效减少业务系统的声明内容,帮助业务系统更专注于业务开发(使业务代码占95%以上),可以帮助业务系统减少依赖的修改、简化依赖包。此外,好的架构团队能够让parent更科学、更强大。
公共组件需要建设到什么程度?
答:对于devops流程,能够帮助相关开发者提高效率,就是有意义的建设。
假如公共组件做得足够好,则能够很大程度做到开箱即用;同时相关代码也能形成内部闭环,最终业务代码逃离公司环境也是难以执行(保密要求)
参考链接
领域驱动设计-入门到实战:https://domain-driven-design.org/
殷浩详解DDD系列 第一讲 - Domain Primitive: https://developer.aliyun.com/article/713097?spm=a2c6h.12873639.article-detail.76.75b227c9SbgJx8
殷浩详解DDD系列 第二讲 - 应用架构:https://developer.aliyun.com/article/715802?spm=a2c6h.12873639.article-detail.92.395c624026LyPX
殷浩详解DDD系列 第三讲 - Repository模式:https://developer.aliyun.com/article/758292?spm=a2c6h.12873639.article-detail.87.4e2827c9egrHfh
殷浩详解DDD系列 第四讲 - 领域层设计规范: https://zhuanlan.zhihu.com/p/356518017
殷浩详解DDD系列 第五讲:聊聊如何避免写流水账代码: https://zhuanlan.zhihu.com/p/366395817