文章目录

  • 为什么要做架构推演这个事情?
  • 传统mvc架构问题
    • 业务与技术解耦设计方案
    • RPC调用防腐设计方案
    • 业务堆积设计方案
  • 分层优化后职责讨论
    • domain的持久化
    • domain高内聚低耦合保证
    • domain与infrastructure交互思考
    • domain不是银弹
    • 事件处理机制
    • 事务处理及控制
    • 抛弃传统mvc架构?
    • 回头看MVC分层
    • 流程编排怎么落地?
      • 流程维度
      • 编排维度
  • 架构演进后职责总结
  • 架构演后分包
    • model实体类该怎么放,怎么规范?
      • model分类
      • 编码规范
      • 指令类型区分和层与层
      • 是否暴露
      • 作用范围
      • 通用性
    • model之间的转化
    • aop怎么放?
    • 异常怎么处理?
  • 架构演进前后对比

为什么要做架构推演这个事情?

工作中遇到过的研发问题

问题描述解决方案
效率低1、搭建新项目,从老项目拷贝
2、对接效率:历史原因基础服务无对接文档,接口文档等
3、基础组件:通用问题未沉淀通用方案
1、定制脚手架
2、留存研发各流程阶段文档,并制定文档模板
3、沉淀通用解决方案形成pom依赖
交接/学习成本老项目无文档,接手成本高1、留存研发各流程阶段文档
2、并审核保证文档质量
问题成本公司无脚手架工程,新项目从老项目拷贝,pom依赖集成问题,
其他人也可能遇到,相同问题反复处理
1、制定脚手架
2、规范脚手架依赖
沟通成本各个业务系统会依赖调用基础服务,历史原因基础服务没有相关文档,
各个业务系统开发人员分别去咨询基础服务负责人,
或者各业务系统开发人员咨询最先对接那个人
1、留存研发各流程阶段文档
2、梳理基础服务二方接口形成文档
维护困难1、项目结构及分包不统一(无架构规范)
2、编码风格不统一(无明确规范)
3、编码可读性、扩展性不够优雅(cr缺失)
4、对底层中间件的使用方式不统一(无基础组件沉淀)
5、业务系统中日志处理不统一(通用功能未沉淀)
6、日子格式不统一
7、调用链路不规范,分层分包未约束(老项目),方法之间调用各自model转化
1、制定分层分包规范
2、制定编码规范(使用阿里开发规范)
3、定期拉会分享学习阿里规范
4、审核方案设计
5、代码CR
问题预警无监控无追踪,目前若为某个业务预警只能在代码中硬编码1、监控体系
2、通过监控数据预警
问题处理1、elk缺失、无链路追踪,定位问题速度和精准度落后
2、无监控,一出现性能问题,可能无从下手
1、搭建日志平台
2、搭建监控平台
3、搭建链路追踪

上述问题总的来说解决方案分三个维度

  1. 规范研发流程,保证各阶段质量与交付
  2. 搭建基础平台体系
  3. 架构(mvc)质量升级与定制

如何去推动这三个维度事项落地?

对于第1、2点任任何一家达到一定规模的公司,领导层应该都会有意识的落地。
而对于第3点,可不就是这么容易的事情了,困难在哪儿?

  1. 公司已有的框架,大家已经习惯,人都会有一个舒适区,接触新的东西不习惯(思想层面难以接受)
  2. 领导层可能已经不在编码,结果导向,不关心架构质量问题在未来引发的问题

基于这些困难,所以需要一篇具有说服力的方案,有理有据才能打动人心。小编在完成这篇文章后呢,又编写了编写《从MVC架构推演DDD架构》感悟及方法论。

注意:名词概念参考文档,在推演过程中出现了一些概念,可在参考概念中学习

传统mvc架构问题


虽然说mvc三层架构简单,易上手,但是在企业级复杂业务下存在问题:

问题描述解决方案
业务与技术耦合技术组件升级困难业务与技术解耦,业务内敛,技术组件下沉
业务堆积在service逐渐形成大泥球业务迭代会越来越困难,只能不断堆代码,后期甚至不敢在改动老代码业务分包
rpc调用分散各个业务service直接应用rpc,当rpc改动时,影响本多个service屏蔽外部变化,防腐处理

参考学习:系统复杂性的来源与应对

业务与技术解耦设计方案

思考技术组件的共性

mysql、redis、mq,他们的共性是数据存储与传递,那为什么dao操作mysql,而redis和mq的操作确要放在service?是否可以抽离出一个基础设施层来完成这个事情?

以商品发布举例,操作步骤如下:

  1. 新增一个商品到mysql
  2. 新增商品缓存到redis
  3. 新增商品发布消息到MQ
  4. 另外一个服务订阅MQ将商品同步到ES


提问:这么分带来的好处是什么?

  1. service层与技术解耦,未来组件升级不用动service层
  2. 替换组件更加容易,如mysql替换为MongoDB,mq从卡夫卡替换rocketMQ
  3. 如果技术组件不下沉,后期技术组件升级替换是基本不可能的

思考这儿mysql和redis等中间件数据一致性怎么保证?数据异构方案

mysql主从,其他所有组件都可以看着mysql slave,通过canal同步数据,高可用保证可在canal和其他中间件之间加MQ

RPC调用防腐设计方案

各个业务service直接应用rpc,当rpc改动时,影响本多个service

RPC调用目前还在service中,我们不妨将RPC抽象,是不是就是获取数据传输数据存储,这跟infrastructure是不是功能类似了?所有rpc调用应该下层
并且应该封装rpc屏蔽外部变化,故而infrastructure中应该存在一个防腐分包(acl)

Q:防腐为什么是在基础设施层呢,针对不同的rpc调用是否应该在应用层做好处理,后续走领域层统一处理
A:1、防腐用于屏蔽外部的变化,防止外部变化在本系统扩散;2、在DDD中防腐层作用, 一个上下文通过一些适配和转换与另一个上下文交互;3、RPC调用分读写情况,这儿讨论读情况,RPC获取数据用于业务处理,在DDD中业务内敛到damian领域层中,那RPC为什么要在基础设施层而不在应用层呢?就要说领域层聚合根的使用了,应用层用于流程编排,业务处理交给聚合根,在应用层怎么创建聚合根?情况1:通过入参通过new创建聚合根;情况2:通过仓储获取聚合根,可能需要的一些数据来源RPC,将外部上下文转化为内部上下文,而仓储实现在基础设施层,故而acl防腐层在基础设施层;

业务堆积设计方案

上述将技术与业务进行了分离,怎么解决业务堆积在service逐渐形成大泥球,业务在service该怎么落地呢?

大泥球:业务堆积、service交织、可读性没得到控制、技术耦合分散在各个service
怎么解决这个大泥球?举两个例子如下

购物车:

查询用户购物车数据购物车渲染:数量、选中状态、添加删除、过滤、计价更新购物车

上诉操作传统做法全耦合在service,面向数据操作,既有流程编排(用户故事),又有业务实现细节(购物车渲染),当业务细节封装不好,甚至面向过程编码,导致可读性很差,一眼难以辨别业务

  1. 业务与技术解耦:1和3实现细节应该放在基础设施层
  2. 业务内敛:2相关业务操作内敛到一个用户购物车对象中,通过该对象聚合业务

下单:

验单:商品验证、用户验证、价格验证等(订单服务)锁定库存(商品服务)锁优惠劵(优惠价服务)生成订单(订单服务)删除购物车购买商品(购物车服务)

上诉操作传统做法全耦合在service,service之间相互调用混乱,最后形成大泥球
业务内敛后,service调用的就是聚合对象进行操作,这儿有订单聚合、商品聚合、购物车聚合等,
service编排聚合对象,完成用户故事提供具有业务意义的功能
什么叫用户故事?用户故事或用例流,一件用户通过系统完成他一个有价值的目标(买一罐饮料)的事就是“用户故事”或“用例流”

内敛的聚合放在哪儿?

业务内敛的目标:是高内聚低耦合
聚合存放位置:

  1. 在service中分个包承载这些聚合
  2. 另起一个层,service层依赖它

承载聚合的位置应该叫什么?领域(domain)即问题空间,是一种边界,可以理解为业务边界

分层优化后职责讨论

domain的持久化

注意:domain指业务边界,聚合内敛业务,聚合中包含有数据库实体

  1. infrastructure本质是对原始数据的存储与查询,而domain是对元素数据的封装,提供业务意义操作动作,业务执行完domain状态变化后,应该要持久化这种状态
  2. 现在这种结构只能通过service从domain拿到需要持久化的数据操作infrastructure进行持久化


当前这种架构domain如何持久化?

  1. 当一个domain中有多个数据库实体需要持久化时(如商品,有sku、规格等),需在service分别拿到相应分别数据持久化,事务就被拆开需在service保证,退化
  2. 现在我们的service职责清晰化为流程编排,明显上述操作在职责之外,所以domain与infrastructure之间有交互

是否能直接通过infrastructure持久化domain(聚合)?infrastructure依赖domain即可

该架构问题

开发可直接在domain使用基础技术服务,开发者是不可控的,需难免出现滥用的情况,造成domain的污染

domain高内聚低耦合保证

要保证domain不被被污染,domain就不能依赖其他任何外部,但是domain持久化又需与infrastructure产生关系,故只能infrastructure去依赖domain,这样domain就操作不了外部,规约了开发滥用

那现在domain怎么与infrastructure交互?

domain与infrastructure交互思考

domain持久化应该要用定义持久化的操作接口,实现在infrastructure中,这些接口叫仓储(repository)

好处:domain中分包repository定义持久化操作接口,屏蔽存储细节,实现在infrastructure,当存储组件切换时,无需动domain层,修改infrastructure的仓储实现就行

domain不是银弹

如一个聚合执行完业务,要紧接着执行一个任务,这个任务可能与我们的业务无关(任务的触发不应该在聚合中),如数据上报(用于监控,也当前业务无关)怎么处理?放在service层进行编排不就好了吗?

这样做是能达到效果,但service层中多个service都用到该domain都编排下?如果哪天需求变动,除非了数据上报,还行需要对该动作判定危险级别,这样在service层改动就大了
所有在domain中应该有个地方承载上诉关系,在domain分包domainservice(领域服务


这样的话service层编排职责清晰为:编排domain中的聚合和领域服务

事件处理机制

事件的发布订阅职责应该在那层?

事件的发布

事件发布跟在某个业务动作后,跟domain执行业务状态变化要持久化一样,应该落在infrastructure层,包括事件的追踪溯源,在这儿可以设计事件记录表,通过aop记录事件日志
infrastructure封装技术细节,这儿封装的是发布细节,如kafka、rocketmq、spring提供的事件机制

事件的订阅

事件发布是通知其他人要做什么事情,订阅者订阅事件后要触发相应业务,与业务相关的层在domain和service

事件的发布订阅是影响业务的,事件这儿大致分两类,分为微服务内微服务之间的事件
微服务之间事件发布订阅

后续相应业务处理可能涉及本业务编排,甚至还会依赖其他微服务进行编排,而其他服务的编排在service层,所有应该放在service层

微服务内事件发布订阅

一个业务动作后触发另外一个业务,貌似service层和domain层都能放
放在service:需要区别服务间和服务的事件和订阅了
放在domain:明确知道是一个damian动作触发了另外damian动作
所以放在domain更好
进程内通信机制,这儿可以采用spring提供的事件机制


发布事件通知领域触发业务,这样的事件叫领域事件

事务处理及控制

事务分:本地事务和分布式事务

本地事务:无外乎操作了多张表,需要保证一致性,讨论几种情况保证事务

  1. 单个领域设计多张表操作(如生成订单):通过repository实现控制本地事务
  2. 单个领域涉及服务内事件:spring事件机制默认用当前线程,在repository实现也能控制本地事务;若使用多线程异步处理,需要改写事务管理
  3. 单个领域涉及服务间事件(如发布商品):通过mq保证最终一致性
  4. 多个领域编排保证事务:(应该放在领域服务domainservice)
    1. 若放在在service保证事务?可以,但是事务范围扩大了,大事务存在问题
    2. 如5个领域对象编排,操作了10张表,只有其中两个领域对象需要保证数据一致性,如何保证这两个领域事务而不包含另外三个领域?
    3. 若放在service层:service层对多个领域编排就行抽离一个方法,编排需要保证事务的两个领域
    4. 若放在domain:只需在领域服务编排这两个领域控制事务,service层编排领域服务和聚合就行
  5. 多个领域对象编排涉及服务间事件:事务保证与4一样的

分布式事务:本地事务如何和外部服务保证事务一致性,如何保证?

  1. 多个领域对象编排涉及服务间事件:
    1. 通过MQ保证最终一致性
    2. 通过rocketMQ保证强一致性
  2. 多个领域对象编排和rpc调用(写操作)组合,如何保证分布式事务?大致几种方案如下
    1. 在不借助额外技术手段的情况(下述还是存在问题)
      1. rpc调用与领域对象编排无顺序依赖,rpc调用提前,rpc调用失败本地事务不执行,rpc调用成功只需保证本地事务只需成功(重试机制)
      2. rpc调用在最后,本地事务失败回滚,rpc调用不执行;本地事务成功,保证rpc调用成功(重试机制)
      3. rpc调用在多个领域对象编排中间,保证rpc调用和其之后的本地事务成功(重试机制)
    2. 将rpc调用,改写为事件形式,保证最终一致性或强一致
    3. 本地事务表
    4. 引入seta技术组件保证事务
  3. 多个领域对象编排涉及服务间事件和rpc调用:方案与2类似

总结:一个domain对应一个事务,在repository保证,多个domain事务在domainservice中保证,分布式事务在service服务编排层保证

抛弃传统mvc架构?

分层后的职责含义变化,是否就抛弃了传统mvc架构?

对一些简单操作,没有什么业务,就是简单的将数据持久化,在传统mvc架构中,service直接调用dao就行
上述分层怎么做?强行走一波domain?走domain链路拉长,开发效率降低,所以传统mvc架构中service职责还是有所保留(如商品下架,简单的根据id根据上下架状态,调用链路controller->service->infrastructure)

回头看MVC分层


回头看controller

controller接受用户请求(用户指令),我们抽象下controller是否就是一个指令接收器,接受http调用是指令、rpc调用是指令,事件是指令等指令。controller适配不同的指令,转化为内部语言,执行内部业务。controller改叫适配层更贴切

回头看service

  1. 现在service已经很薄了,业务与技术解耦,业务解耦,业务细节到在domain和domainservice中,service层编排对外提供应用级别功能(如下单)
  2. 也能通过操作infrastructure将一些基础功能暴露
  3. 总的来说service层已经没有业务逻辑了,通过编排通过应用功能,改名应用层(application)更为贴切

定时任务应该在那层?定时任务采用分布式定时任务,一般做什么?

1、定时执行某个业务 2、定时同步数据 3、定时执行脚本(如lua脚本)

定时任务与业务关联情况:

  1. 定时任务与业务关联, 可放在service层
  2. 与业务无关的定时任务可放在infrastructure,如通过定时同步数据

定时任务抽象下,可认为是一个定时指令,指令就可以进行适配放在适配层,既可以操作service层也可以操作infrastructure层,统一管理,解决零散不好管理问题
适配层与其他层的关系:

rpc实现:dubbo实现使用dubbo的@Service注解,调用应用服务
定时任务:可调用应用层和基础设施层
controller:只能调用应用层
事件订阅器:可调用应用层和基础实施层

流程编排怎么落地?

流程维度


编排管理:提供流程编排可视化界面后,运营人员可根据需求改动流程
流程引擎:编排核心

编排维度


参考学习:架构 – 理解构架的分层

架构演进后职责总结

项目结构├─ddd-boot-parent(父POM)│├─ddd-boot-api(暴露接口)│├─ddd-boot-adapter(适配层)│├─ddd-boot-application (应用层)│├─ddd-boot-domain(领域层)│├─ddd-boot-infrastructure(基础设施层)
  1. 适配层:负责接收指令和响应,可以是http指令、rpc指令、事件指令、自动化测试指令、批处理脚本指令等。
  2. 应用层:很薄的一层,理论不应该存在业务规则或逻辑,主要面向用例和流程相关操作,编排领域层中聚合、领域服务以及外部服务,协同完成业务操作。
  3. 领域层:内敛核心业务逻辑,体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。
  4. 基础设施层:贯穿所有层,为其他各层提供技术和基础服务支持,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。

架构演后分包

├─ddd-boot-parent(父POM,管理依赖版本)│├─ddd-boot-api(暴露二方接口)│├─├─api(定义暴露接口)│├─├─model(定义暴露实体)│├─├─├─cqrs(命名Qry结尾[查询实体,读操作],命名以Cmd结尾[命令实体,写操作])│├─├─├─dto(主要用于返回响应)│├─ddd-boot-infrastructure(基础设施层,不同的技术沉淀为组件,在该层引用)│├─├─aspect(切面)│├─├─├─annotation(切面注解)│├─├─repository(仓储实现)│├─├─publisher(事件发布器)│├─├─acl(防腐,封装远程rpc调用,转化为内部语言)│├─├─config(配置,这儿可以考虑放入技术名分包中)│├─├─entity(持久化实体,用于支持基础组件,如mybatisplus、jpa、springdate-es等)│├─├─convertor(转换器,转化上层与该层实体)│├─├─utils(工具包)│├─├─mybatisplus│├─├─├─handler(mybatis类型转化器)│├─├─├─mapper│├─├─├─xml│├─├─easyexcel│├─├─├─converter(easyexcel类型转化器)│├─├─redis│├─├─├─lock(基于redis分布式锁实现)│├─├─oos(对象存储)│├─├─es(搜索引擎)│├─├─mq(消息队列)│├─├─shardingsphere(分库分表)│├─├─elasticjob(分布式定时任务)│├─├─canal(数据同步)│├─├─....根据技术分包│├─ddd-boot-domain(领域层,按业务分包)│├─├─├─domainservice(领域服务无状态,可编排业务)│├─├─├─subscribe(事件订阅器)│├─├─xxx(业务名)│├─├─├─aggregates(聚合,有状态)│├─├─├─repository(仓储接口)│├─ddd-boot-application (应用层)│├─├─service(应用服务,编排流程)│├─├─model(内部实体)│├─├─├─cqrs(命名Qry结尾[查询实体,读操作],命名以Cmd结尾[命令实体,写操作])│├─├─├─dto(主要用于方法返回响应)│├─ddd-boot-adapter(适配层,注意rpc和controller不是共存的)│├─├─rpc(rpc接口实现,app移动端项目toc)│├─├─controller(web控制器,web后台项目tob)│├─├─subscribe(事件订阅器)│├─├─job(任务处理器)

讨论上述分包结构在实战可能会遇到的情况:

  1. model实体类该怎么放,怎么规范?
  2. aop怎么放?
  3. 异常怎么处理?

model实体类该怎么放,怎么规范?

model分类

首先明确model分类

分类维度描述
编码规范可分DO、DTO等
指令类型读、写、事件
是否暴露内部model和外部model
作用范围全局model和局部model
层与层(方法)层与层之间方法调用的入参与返回
通用性如分页实体,响应体Response等

编码规范

model概念理解
POJO(Plain ordinary java object) 简单java对象:POJO专指只有setter/getter/toString的简单类,包括DO/DTO/BO/VO等一个POJO持久化以后就是PO;直接用它传递、传递过程中就是DTO;直接用来对应表示层就是VO
PO和DO(Persistant Object) 持久对象:用于表示数据库中的一条记录映射成的 java 对象。PO 仅仅用于表示数据,没有任何数据操作。通常遵守 Java Bean 的规范,拥有 getter/setter 方法
(Data Object):阿里规范,与数据库表结构一一对应,通过DAO层向上传输数据源对象
都是数据库持久对象,不同的规范中叫法不一样
BO和DO(Business Object) 业务对象:封装对象、复杂对象,里面可能包含多个类 主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象
(Domain Object):DDD中领域对象等同BO用于表示一个业务对象
BO就是PO的组合,具有丰富行为的对象,如PO1是交易记录,PO2是登录记录,PO3是商品浏览记录,PO4是添加购物车记录,PO5是搜索记录,BO聚合他们表示个人网站行为对象
VO(Value Object) 表现对象:显示层对象,通常是Web向模板渲染引擎层传输的对象。
DTO(Data Transfer Object) 数据传输对象:泛指用于展示层与服务层之间的数据传输对象**BO和DTO的区别:**主要是就是字段的删减
BO业务计算需要辅助数据,对外暴露一般不是全量数据,因此DTO需要在BO的基础上,只要自己需要的数据
AO(Application Object)应用对象: 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。

实际应用:

  1. po不能缺少,命名为po或entity
  2. 业务不是很复杂的系统DTO是可以和BO合并成一个,当业务扩展的时候注意拆分就行
  3. 有复杂业务时,划分好领域采用DO
  4. VO一般不用,直接用DTO
  5. 概念是给人用的,多人协作的时候一定要保证大家的概念一致

指令类型区分和层与层

指令类型命名后缀描述
Qryquery缩写表示读操作实体
Cmdcommand缩写表示写操作实体
Event事件

传统用法:

  1. RPC调用一般使用DTO
  2. 在web项目中,请求参数可能通过Param或Query区分

传统读写操作区分:

  1. web中可根据请求类型注解区分,也就是restful接口风格。若是命名区分不了在,在远程调用(feign)时调用方还是不能见名知意
  2. RPC调用只能通过方法命名区分

统一规范:model命名规范=业务+后缀,这样就知道model用于那个业务的读写,还是事件监听

一个本质:数据传输起始都可以认为是DTO

  1. 入参采用Qry和Cmd方式,RPC可采用QryDTO或CmdDTO
  2. 返回采用DTO方式

是否暴露

这儿的暴露之是否暴露给外部服务,需暴露的model应该放在api工程中

作用范围

  1. 全局model:如Result响应体、全局code码等应该贯穿整个工程。沉淀为组件后,在组infrastructure引入,或在父工程引入全局使用
  2. 局部model:如领域层中聚合也是model实体,提供给领域服务、应用层、仓储使用

通用性

  1. 包括Response、PageQuery等,返回的DTO数据会通过Response包装,分页请求数据通过PageQuery包装,当出现异常时会通过Response包装异常相关信息返回
  2. 所以通用model应该是贯穿整个项目,应该放在infrastructure,

沉淀为组件后,在组infrastructure引入,或在父工程引入全局使用

model之间的转化

  1. api模块model流转:适配层->应用层,所以应用层应该引用api
  2. 内部model流程:Qry/Cmd/DTO,适配层->应用层,model应该在应用层定义

aop怎么放?

日常开发中aop使用场景

  1. 分布式锁
  2. 幂等处理
  3. 重试机制
  4. 系统日志记录

上诉场景不涉及业务,都是技术侧的非功能性需求,并且这些功能很通用,应该放在infrastructure,最好是沉淀为组件,在组infrastructure中引入。

注意:aop一般会提供一个注解服务于我们的系统,可以在各层使用

异常怎么处理?

异常分为异常定义、异常code和异常处理,应该贯穿整个项目。沉淀为组件后,在组infrastructure引入,或在父工程引入全局使用。异常处理器应用:

  1. web异常可以基于springmvc框架的@ControllerAdvice拦截controller处理
  2. 其他异常可以基于aop进行统一处理,实现在基础设施,应用在各层

架构演进前后对比