我是靠谱客的博主 幸福绿草,最近开发中收集的这篇文章主要介绍《结合DDD讲清楚编写技术方案的七大维度》再讨论,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

领域、子域与限界上下文

1 核心概念

这三个词虽然不同但是实际上都是在描述范围这个概念。正如牛顿三定律有其适用范围,程序中变量有其作用域一样,DDD方法论也会将整体业务拆分成不同范围,在同一个范围内进行才可以进行分析和处理。

上文实例中领域是足球,子域包括合同、医疗、训练、比赛、采访,合同子域可以分为两个限界上下文:转会和签约,医疗子域可以分为两个限界上下文:体检和伤病。

领域可以划分子领域,子域可以再划分子子域,限界上下文本质上是一种子子域,那么在业务分解时一个业务模块到底是领域、子域还是限界上下文?

这取决于看待这个模块的角度。你认为整体可能是别人的局部,你认为的局部可能是别人的整体,叫什么名字不重要,最重要的是按照高内聚原则将业务高度相关的模块收敛。

2 限界上下文

限界上下文(Bounded contenxt)比较难理解,我们可以四个维度分析:

第一个维度是限界上下文本身含义。限界表示了规定一个边界,上下文表示在这个边界内使用相同语义对象。例如goods这个词,在商品边界内被称为商品,但是快递边界内被称为货物。

第二个维度是子域与限界上下文关系。子域可以对应一个,也可以对应多个限界上下文。如果子域划分足够小,那么就是限界上下文。如果子域可以再细分,那么可以划分多个限界上下文。

第三维度是服务如何划分。子域和限界上下文都可以作为微服务,这里微服务是指独立部署的程序进程,具体拆分到什么维度是根据业务需要、开发资源、维护成本、技术实力等因素综合考量。如果按照子域进行微服务划分可以拆分为:

  • 基础服务:player-core-service

  • 合同服务:contract-core-service

  • 医疗服务:medical-core-service

  • 训练服务:training-core-service

  • 比赛服务:game-core-service

  • 采访服务:interview-core-service

如果按照限界上下文进行微服务划分,合同和医疗服务可以再拆分:

  • 基础合同服务:contract-base-service

  • 转会合同服务:contract-transfer-service

  • 签约合同服务:contract-signing-service

  • 基础医疗服务:medical-base-service

  • 伤病医疗服务:medical-injury-service

  • 体检医疗服务:medical-exam-service

第四个维度是交互维度。在同一个限界上下文中实体对象和值对象可以自由交流,在不同限界上下文中必须通过聚合根进行交流。聚合根可以理解为一个按照业务聚合的代理对象。

例如产品经理作为需求收口人,任何需求应该先提给产品经理,通过产品经理整合后再提给程序员,而不是直接提给开发人员。

3 实体、值对象与聚合

领域模型分为三类:实体、值对象和聚合。实体是具有唯一标识的对象,唯一标识会伴随实体对象整个生命周期并且不可变更。值对象本质上是属性的集合,没有唯一标识。

聚合包括聚合根和聚合边界两个概念,聚合根可以理解为一个按照业务聚合的代理对象,一个限界上下文企图访问另一个限界上下文内部对象,必须通过聚合根进行访问。

3.1 数据维度

领域模型与数据模型一个重要的区别是值对象存储方式。领域对象在包含值对象的同时也保留了值对象的业务含义,而数据对象可以使用更加松散的结构保存值对象,简化数据库设计。

如果需要管理足球运动员基本信息和比赛数据,对应领域模型和数据模型应该如何设计?姓名、身高、体重是一名运动员本质属性,加上唯一编号可以对应实体对象。

跑动距离,传球成功率,进球数是运动员比赛表现,这些属性的集合可以对应值对象。

3.2 代码维度

3.2.1 数据对象

PO(Persistent Object)直接与数据库交互:

public class FootballPlayerPO { // 运动员ID private Long id; // 运动员姓名 private String name; // 运动员身高 private Integer height; // 运动员体重 private Integer weight; // 比赛表现(JSON) private String gamePerformance; // 创建人 private String creator; // 修改人 private String updator; // 创建时间 private Date createTime; // 修改时间 private Date updateTime; }

3.2.2 值对象

VO(Value Object)本质上是属性之集合,其不具有唯一标识:

public class GamePerformanceVO { // 跑动距离 private Double runDistance; // 传球成功率 private Double passSuccess; // 进球数 private Integer scoreNum; } public class MaintainVO { // 创建人 private String creator; // 修改人 private String updator; // 创建时间 private Date createTime; // 修改时间 private Date updateTime; }

3.2.3 实体对象

Entity具有唯一标识,这个唯一标识会伴随实体对象整个生命周期:

public class FootballPlayerEntity { // 运动员ID private Long id; // 运动员姓名 private String name; // 运动员身高 private Integer height; // 运动员体重 private Integer weight; // 比赛表现值对象 private GamePerformanceVO gamePerformanceVO; }

3.2.4 聚合对象

Agg(Aggregate)可以理解为一个按照业务聚合的代理对象,任何访问本限界上下文对象必须经过聚合。实践维度可以理解为充血模型版本BO,聚合对象中可以编写业务逻辑:

public class FootballPlayerSimpleResultAgg { // 运动员ID private Long playerId; // 运动员姓名 private String playerName; } public class FootballPlayerReadAgg implements BizValidator { // 运动员ID private Long playerId; // 页数 private Integer pageNum; // 条数 private Integer size; @Override public void validate() { AssertUtil.notNull(playerId, new BizError); AssertUtil.notBigger(size, 100, new BizError); } } public class FootballPlayerWriteAgg implements BizValidator { // 操作类型 private Integer maintainType; // 维护信息 private MaintainVO maintainInfo; // 运动员信息 private FootballPlayerEntity playInfo; @Override public void validate() { AssertUtil.notNull(maintainType, new BizError); AssertUtil.notNull(maintainInfo, new BizError); AssertUtil.notNull(playInfo, new BizError); if(maintainType == MaintainEnum.CREATE.getType()) { AssertUtil.notNull(maintainInfo.getCreator(), new BizError); AssertUtil.notNull(maintainInfo.getCreateTime(), new BizError); } if(maintainType == MaintainEnum.UPADTE.getType()) { AssertUtil.notNull(maintainInfo.getUpdator(), new BizError); AssertUtil.notNull(maintainInfo.getUpdateTime(), new BizError); } } }

3.2.5 数据传输对象

DTO(Data Transfer Object)用于接收或传输外部数据,只应该暴露必要信息:

public class FootballPlayerCreateDTO { // 运动员姓名 private String name; // 运动员身高 private Integer height; // 运动员体重 private Integer weight; // 跑动距离 private Double runDistance; // 传球成功率 private Double passSuccess; // 进球数 private Integer scoreNum; // 创建人 private String creator; // 创建时间 private Date createTime; } public class FootballPlayerUpdateDTO { // 运动员ID private Long id; // 运动员姓名 private String name; // 运动员身高 private Integer height; // 运动员体重 private Integer weight; // 跑动距离 private Double runDistance; // 传球成功率 private Double passSuccess; // 进球数 private Integer scoreNum; // 修改人 private String updator; // 修改时间 private Date updateTime; } public class FootballPlayerQueryDTO { // 运动员ID private Long playerId; // 页数 private Integer pageNum; // 条数 private Integer size; } public class FootballPlayerSimpleResultDTO { // 运动员ID private Long playerId; // 运动员姓名 private String playerName; }

4 领域事件

当某个领域发生一件事情时,如果其它领域有后续动作跟进,我们把这件事情称为领域事件,这个事件需要被感知。

球员比赛受伤,这是比赛域事件,但是医疗和训练域是需要感知的,那么比赛域发出一个事件,医疗和训练域会订阅。球员比赛取得进球,这也是比赛域事件,但是训练和合同域也会关注这个事件,所以比赛域也会发出一个比赛进球事件,训练和合同域会订阅。

通过事件交互有一个问题需要注意,通过事件订阅实现业务只能采用最终一致性,需要放弃强一致性,可能会引入新的复杂度需要权衡。

同一个进程间事件交互可以用EventBus,跨进程事件交互可以用RocketMQ等消息中间件。

5 代码结构

5.1 六层结构

DDD代码实现方案不尽相同,我认为不能为使用DDD而是使用DDD,而是应该根据实际情况选择当前最合适的方案。但是无论是什么方案都需要遵循合理分层这个原则:

(1) API

接口层:提供面向外部接口声明、DTO

(2) controller

访问层:提供HTTP访问入口

(3) service

业务层:领域层和业务层都包含业务,业务层可以组合不同领域业务,并且可以实现流控、监控、日志、权限功能,相较于领域层更丰富

(4) domain

领域层:提供Entity、VO、Agg、事件,聚合对象使用充血模型

(5) integration

整合层:访问外部限界上下文服务,解析为本限界上下文聚合对象

(6) infrastructure

基础层:提供PO、持久化能力

5.2 代码实例

如果player-core-service作为maven parent,那么其具有以下maven module和分包:

> player-core-service > player-core-api > dto > facade > player-core-controller > controller > adapter1 (DTO > Agg) > player-core-service > bizService > adapter2 (Agg > PO) > facadeService > adapter3 (Agg > DTO) > player-core-domain > vo > entity > agg > event > player-core-integration > proxy > adapter4 (DTO > Agg) > player-core-infrastructure > po > mapper

5.3 如何取舍

上述项目有六层结构,那么必然带来层次间调用对象互相转换这个问题:

adapter1接收外部请求(DTO)需要转换成(Agg) adapter2处于业务层(操作数据库)(Agg)需要转换成(PO) adapter3处于对外业务层(暴露RPC)(Agg)需要转换成(DTO) adapter4处于整合层(访问外部RPC)(DTO)需要转换成(Agg)

对象转换会带来两个问题:第一个是代码复杂度增加,第二个是有一定性能损耗。这也是分层结构必须要付出之代价。

因为每层对象看似相同(具有相同属性或者结构)但是语义和角色完全不同,每一层可以为对象新增本层之特性,相较于使用一个对象贯穿始终,可扩展性显著提升。

最后

以上就是幸福绿草为你收集整理的《结合DDD讲清楚编写技术方案的七大维度》再讨论的全部内容,希望文章能够帮你解决《结合DDD讲清楚编写技术方案的七大维度》再讨论所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部