DDD的演变过程

在这里插入图片描述

JAVA工程规范
常见问题
为何不直接使用阿里开发规范中分层架构?
在实际微服务开发过程中,此工程规范过于简单,无法覆盖很多通用场景,导致在开发过程中,不同开发人员或者外包人员比较随意的增加Package,代码评审及Review困难,如:
微服务中,一般需要提供sdk包给其他服务进行调用,如何规范。
对消息的生产及消费如何处理。
在需要es,redis场景,如何规范代码工程。
微服务间,系统间调用如何规范。
阿里规范中对组件分层,各种数据对象使用场景描述不够清晰。

工程规范

**

MVC版本

**
图片:
在这里插入图片描述

DDD版本
图片: https://odocs.myoas.com/uploader/f/TP65q4在这里插入图片描述
hOEryVkcoH.png?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzczMjk5NjgsImZpbGVHVUlEIjoiU2c0V0Z6WnFPZUlPaHZrOSIsImlhdCI6MTY3NzMyOTM2OCwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjoyMzEzMzd9.D22vyxqs97Z_52-HEHXJagGbsA6QF7schU88RaeoAOs

DDD对MVC演进及区别

一、DDD增加了依赖反转

a. Application层与Infrastructure层的依赖关系,在依赖反转下Infrastructure依赖application层,在依赖反转场景下:

  • 1.增加了application对reposiotry定义的接口
  • 2.增加Infrastructure对repository仓储的实现。
  • 3.aplication层不再依赖infrastructure层,反之ifrastructure依赖application层。
    b.依赖反转缺点:
  • 1.依赖反转由于是业务依赖基础设施,复杂的场景基础设施层不能够使用application层的DTO数据,需要增加BO业务对象,导致增加数据对象及数据对象的转换。
    c.依赖反转优点:
  • 1.各层的边界更加清晰,且在统一个微服务中能够与DDD代码兼容。
  • 2.由于业务层(application)层不依赖基础设施,会对后期对业务层的单体测试case开发会更加方便(采用内存数据库对infrastructure层进行mock)。

二、将Application层拆分为了Application层及Domain层

a.DDD工程本质是将MVC依赖反转版本中的Application层中将领域相关业务逻辑分离,增加Domain层。
b.增加Domain层缺点
1.增加了Domain层,增加了DomainEntity数据对象及需要转换。
2.Domain层关注的是单个聚合的生命周期(创建,维护,删除等),对需要大批量增删改又要求性能很高的场景支撑不友好。
3.对设计质量有依赖,如果领域层设计的不够好的情况下,修改代码的时候会需要增大的修改量。
c.增加Domain层优点:
1.强化对聚合,实体,命令,领域事件,值对象等的设计,通过统一设计方法论,提升设计质量。
2.保障设计与代码的一致性,提升工程质量。
3.将用例逻辑与单个聚合生命周期逻辑分离,使业务逻辑更加清晰。
4.通过业务与基础设施的解耦,对单体测试的友好性。

所有二级目录,需要统一管理,如果规范中未包含研发中的场景,需要完善规范。
与微服务架构的结合
在微服务架构中,倾向于将presentation层开始就从微服务中剥离到bff层,形成如下架构:
图片:

BFF可以根据子系统或者端进行拆分,例如:
1.商城系统分成面向消费者的商城子系统,及面向后端的运营子系统。(常见于企业系统)
2.针对小程序端,IOS,android端拆分不同的BFF(常见于2C的产品为了做极致及差异化的用户体验)
优点:
通过解耦bff层,将session管理在bff中完成,将底层微服务进行无状态处理。能够更便于横向扩容。
bff技术特点不一致,长期可以将bff交与前端进行处理,使用NodeJs或者faas进行处理,更利于团队协作及成长。

代码示例(不含Presentation层组件):
craftsman-ddd:
http://code.oppoer.me/crm-framework/craftsman-ddd.git
craftsman-mvc:
http://code.oppoer.me/crm-framework/craftsman-mvc.git
**

代码工程(MVC版本)

**
Presentation
接入层,对传统三层中的UI层的扩展,如果PC及移动端接口分开时候,分为(web及wap),如果有无线应用,可以增加wireless包。在微服务架构中,对应于BFF层

  • 1.【强制】controller: 页面访问接口。
  • 2.【强制】converter: VO与DTO之间的转换逻辑。
  • 3.【强制】vo: Presentation层领域对象,view object,页面显示统一使用xxxVO
    a.【参考】queryVO: 不区分查询及表单的使用requestVO
    b.【参考】reponseVO: 返回给页面的VO。
    Application
    服务应用逻辑,对传统三层架构中的BLL层进行了扩展。
  • 1.【强制】api:API接口定义
    a .【强制】impl: API协议的实现,与协议相关(如Dubbo,Http等),统一调用service。
  • 2【强制】service: 相对具体的业务逻辑编排,与协议无关,事务统一放在service内。
    a.【强制】impl: 对service接口的实现。
    b.【推荐】区分commandService及querySerice
  • 3.【强制】manager: 通用的业务处理层。
  • 4.【强制】event
    a.consumer: 订阅监听消息,订阅完后可以通过调用service处理内部逻辑。
    b.DTO: 如果消费第三方的消息,但其他服务没有通过sdk将DTO引入,可以在event里面定义第三方DTO
  • 5.【强制】converter: 数据对象间转换
  • 6.【强制】DTO: application层领域对象,data transfer object。用于与其他微服务或者Presentation层传递。
    a.【推荐】commandDTO: 命令类DTO
    b.【推荐】queryDTO: 查询类DTO
    c.【推荐】response: 返回类DTO
    d.【推荐】eventDTO: 通过MQ异步发送接收的统一为messageDTO
    7.【强制】bo: business object,业务对象,在Application层内使用。
    8.【强制】AO: 业务编排层对外暴露的接口统一为AO。
    9.【强制】client: 远程接口调用。
    Domain.Shared
    是工程中很薄的项目,它只包含共享的数据类型的定义.例如,枚举,常量等.此层可以直接或间接被工程其他层次依赖。

Infrastructure
对传统DAL层进行了扩展,基础设施层是数据的出向接口,封装数据调用的技术细节。另外对邮件,短信等的技术实现也在此层。
1.【强制】dataobject: 数据对象。
a.【推荐】DO: persistDO的缩写,与数据表一一对应。
b.【推荐】queryDO:扩展数据对象,用于应用层数据查询封装,xxxQueryDO。
c.【推荐】resultDO: 扩展数据对象,用于数据库多表查询结果封装(在无法使用DO封装时使用),xxxResultDO。

  • 2.【强制】repository:仓储实现对象,对repository的实现
    a.【推荐】impl: 对application层repository的实现。
  • 3.【强制】dao:数据访问对象(dao出入参需要为dataobject)
    【推荐】command: 推荐使用spring data jdbc或者mybatis-plus进行command相关业务。
    【推荐】query: 推荐使用mybatis进行查询
  • 4.【强制】针对不同的数据库(如ES,Redis),DataObject及dao分开。(如dataobject.es.xxxDO,dao.es.xxxMapper)

代码工程(DDD版本)

Presentation
与MVC版本保持一致
Application
聚合生命周期逻辑迁移到Domain层,依赖Domain层,基础设施的实现通过依赖反转在infrastructrue层。
Domain
1.【强制】aggregate:聚合定义,包含聚合根,实体,及领域服务实现。
实体是种领域对象,它有自己的属性(状态,数据)和执行业务逻辑的方法.实体由唯一标识符(Id)表示,不同ID的两个实体被视为不同的实体
领域服务实现。
2.【强制】repository: 仓储是被领域层或应用层调用的数据库持久化接口.它隐藏了DBMS的复杂性,领域层中只定义仓储接口,而非实现.
3.【强制】service: 领域服务是一种无状态的服务,它依赖多个聚合(实体)或外部服务来实现该领域的核心业务逻辑. 领域服务直接在根目录下(如order),以DomainService结尾。如OrderDomainService.领域服务的实现在聚合的pacakge内部(领域服务实现可能调用实体的protected方法)
4.【强制】event: 领域事件是当领域某个事件发生时,通知其它领域服务的方式,为了解耦领域服务间的依赖.
5.【强制】valueobject: 值对象是另外一种类型的领域对象,使用值对象的属性来判断两个值对象是否相同,而非使用ID判断.如果两个值对象的属性值全部相同就被视为同一对象.值对象通常是不可变的,大多数情况下它比实体简单。
6.【强制】command: 命令,应用服务调用领域服务时候,传入的参数为命令。

注意:
1.所有事件如果需要跨微服务,需要定义eventDTO,从application层发送,在infrastructure层实现具体发送,与domainEvent有所区别。
**

Domain.Shared

**
与MVC版本保持一致
Infrastructure
1.【强制】repository.impl:在domain及application层定义接口,在Infrastructure实现。

其他规范(MVC&DDD)
通用规范
以下的功能包可能在各个层,各层按需进行建设,保障组件间最小依赖,但需要统一命名规范。统一放在common目录下。
1.【强制】config:微服务内配置,包含javaConfig,aspect,interceptor等。
2.【强制】interceptor: 拦截器
3.【强制】filter: 过滤器
4.【强制】aspect: 自定义切面
5.【强制】util:通用工具类,如StringUtils。(推荐每个产品将工具类统一使用)

【推荐】对象命名默认使用业务+package,如OrderService,OrderManager。部分缩写的在文档注明后按照缩写,如xxPersistDO缩写为xxDO。
【强制】各个组件间采用严格分层,各层间的依赖关系需要和规范一致。
【推荐】数据对象转换,使用MapStruct。
【推荐】数据增删改的场景使用Spring data jdbc或者mybatis plus,推荐spring data jdbc,复杂查询使用mybatis。

关于数据验证(validator)

1.validator应该写在各自的业务实现同一级,因为validator是属于业务逻辑剥离出来的。有些验证在业务逻辑中一起也是可以的。如:
a.presentation写web.controller.validator
b.application写在service.impl.validator
c.domain写在aggregate.validator。
2.DDD中,Domain中对command的验证需要在domain层进行处理。application层只进行跨聚合的一些验证。针对query的验证在application.service中进行。
3.所有异常推荐通过错误码统一抛出异常后统一拦截处理,不推荐通过返回值进行包装。
4.推荐使用crm-component-exception中的通用异常类设置异常码及异常信息,在BFF层做国际化处理。特殊情况才进行异常类自定义。
特殊情况: 对于批量的验证,属于扩展的前端逻辑,在BFF层直接处理。

关于定时任务(scheduler)

  • 1.推荐统一定时任务引擎(如云平台CloudJob或者魔盒的任务调度),通过调用event.consumer或者api实现。
  • 2.如果特殊场景需要在自己微服务处理,在application层实现,在application.scheduler里,调用event.consumer或者API。

关于事件发布及消费

  • 1.Domain Event
    DomainEvent只允许在微服务内部使用
    a.事件发布:Domain Event在application中的service里进行发布(通过Spring event)
    b.事件消费:在application.service.listener中进行消费。
    推荐在微服务内,尽量使用显式的事务中对Domain逻辑进行编排,少使用DomainEvent保障工程可读性。

  • 2.Application Event
    a.事件发布:当事件需要其他服务进行消费时,需要包装为Application Event DTO,在Application Service API中定义。在service中调用application repository,在infrastructrue的repository.impl中进行实现。(推荐使用子包repository.producer)
    b.事件消费:在application层中的event.consumer中监听消息进行消费,在consumer中调用service进行内部逻辑处理

领域对象传输规则

终端显示
图片:

服务间调用

服务间调用有两种方式。
1.需要防腐处理,推荐跨系统调用进行防腐处理,通过Infrastucture组件调用,转换为内部需要的数据对象。
在这里插入图片描述

2.不需要防腐处理,直接在Application组件调用。推荐系统内部不进行防腐处理。在application层的client子包进行定义接口。
在这里插入图片描述

分包管理

通过分离service及service-api,保障调用方通过sdk包更方便的使用服务方。

  • 1.application层的api及dto是其他服务需要调用的。
  • 2.如果涉及到一些constant方便其他服务调用通过Domain.Shared进行共享。

组件化开发

参照C4Model,第三层级为组件级,我们的微服务应该是通过组件式进行开发。Presentation,Application,Infrastructure,Domain.Shared本身也是个组件。另外通过集成通用技术类,框架类,产品本身的组件,让开发更关注于业务代码的实现。
在这里插入图片描述

如何写好DDD代码

DDD需要结合CQRS实践,Domain层核心负责Command逻辑并通过Application层对其他服务开放接口,Application层核心负责Query逻辑。
**

Domain层的职责

**
Domain层的职责是保障单个聚合的生命周期完整性,聚合生命周期指的是对聚合的命令,如订单的提交,审核,关闭。
Domain层除了和单个聚合生命周期逻辑相关的查询(如加载单个聚合数据,辅助聚合验证逻辑的一些查询),其他如列表查询,报表查询等都不能作为domain层的逻辑。

  • 1.【强制】Application层只能通过Command命令更新Domain层的聚合及实体。如果参数小于三个,可以不封装为Command对象,大于等于三个的时候必须封装为Command对象。
  • 2.【强制】Application层不能直接调用实体的set方法,会破坏聚合生命周期完整性。
  • 3.【推荐】优先在聚合中使用充血模式完成command逻辑,如果逻辑中需要使用Repository调用其他聚合或者其他服务的数据,需要增加DomainService。
  • 4.【强制】Command参数的验证也属于领域逻辑,如订单【提交】命令,订单总商品数量最大为
  • 【20】此逻辑也需要在Domain层完善。
  • 5.【强制】聚合生命周期逻辑(Command)必须封装在Domain层,当需要查询本微服务数据或者其他微服务数据的场景有以下两种做法:
    a.在DomainService中查询其他数据,进行逻辑处理。查询的数据需要封装为ValueObject或者是Java基本类型。推荐同一个微服务或者对其他服务主数据的依赖的时候采用

b.在ApplicationService查询其他数据,通过组装Command参数传入Domain层进行逻辑处理。推荐微服务之间业务横向依赖时采用。
在DomainService或者ApplicationService使用都有其有缺点,具体使用需要根据场景判断,无乱哪种方式逻辑处理还是在Domain层。

  • 6.【强制】 一个聚合只能有一个repository,聚合内多个实体的关联性存储在的逻辑在infrastructure层通过DAO进行实现。
  • 7.【推荐】聚合尽可能小. 大多数聚合只有原始属性, 不会有子集合,如不要把订单和订单历史设计为一个聚合,把这些视为设计决策:
  • 加载和保存聚合的 性能内存 成本 (请记住,聚合通常是做为一个单独的单元被加载和保存的). 较大的聚合会消耗更多的CPU和内存.
  • 一致性 & 有效性 边界.

Application层的职责

Application层的职责是保障微服务微服务业务用例完整性。包含了对多个聚合的编排Command,及Query(如列表,报表,详细页面查询)的场景。
【推荐】为每个 聚合根 创建一个应用服务.
【强制】 微服务内及跨微服务事务在Application层进行处理。
关于批量处理
【强制】一些基于性能批量的逻辑属于异构逻辑(如需要对Sql进行拼接),可以在Domain层进行异构处理,Domain层有批量更新的command接口,在Infrstructure进行实现。
【强制】批量的Validate逻辑,属于前端逻辑,可以直接在BFF层或者UI层查询底层数据后处理,而非在底层服务处理。因为此逻辑处理完后不会直接进行Command处理。
【强制】批量插入数据的时候,需要在BFF层将实体对应ID先初始化到参数中。如批量导入串码(IMEI),重复的更新,不重复的插入。由于页面的参数中没有ID,需要先将系统中有的IMEI查询出对应ID组合到对应入参中。

DDD FAQ

  1. applicationRepository和domainRepository的区别是什么?
    applicationRepository是非直接领域对象的CRUD,例如分页查询,domainRepository是领域对象的CRUD,跟具体使用jdbc还是mybatis没有本质的关系。

最核心的逻辑还是Domain层的边界是负责聚合的生命周期,不会负责类似分页查询的逻辑。DomainRepository和ApplicationRepository是根据各层业务逻辑所需定义的Repository。
具体使用mybatis还是jdbc的确没有关系,但推荐Domain层使用Spring data JDBC进行实现,Application层因为都是多表查询,推荐使用mybaits。

  1. 理论上的DDD会返回一个完整的聚合根给application,现实的情况是,很多操作只需要个别字段,完整的加载很可能有性能问题,如何权衡处理?如果允许返回个别字段,是否应该定义在applicationRepository中?

和问题1的大逻辑类似,基于性能优化的逻辑可以在Domain层进行异构处理,最重要的还是保障Domain层职责的边界。在
只要是属于Domain层职责边界,对Domain层之外不暴露内部实现细节进行异构是合理的。

  1. 如果调用第三方系统,是统一放在application service,拿到数据封装command到domain/domain service处理,还是domain service里也可以调第三方系统。这牵扯到client接口定义在哪的问题,而且有时候是根据前文的if-else才能决定是否调第三方,这块想明确一下client的定义是否像domainRepository一样定义在domain,如果定义在application,domainService没法调用

参考服务间调用规范。和第三方调用使用的也是repository,client是对第三方系统调用的具体实现。

参考
阿里开发规范
COLA规范
ABP规范
使用 DDD 和 CQRS 模式降低微服务中的业务复杂性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值