1 六个问题
1.1 为什么使用DDD
分而治之是指直接面对大业务我们无从下手,需要按照一定方法进行分解,分解为高内聚的小领域,使得业务有边界清晰,而这些小领域是我们有能力处理的,这就是领域驱动设计的核心。
在微服务流行的互联网行业,当业务逐渐复杂时,技术人员需要解决如何划分微服务边界的问题,DDD这种清晰化业务边界的特性正好可以用来解决这个问题。
1.2 方法与目标
1.3 整体与局部
领域可以划分多个子领域,子域可以再划分多个子子域,限界上下文本质上也是一种子子域,那么在业务分解时一个业务模块到底是领域、子域还是子子域?
1.4 粒度粗与细
业务划分粒度的粗细并没有统一的标准,还是要根据业务需要、开发资源、技术实力等因素综合考量。例如微服务拆分过细反而会增加开发、部署和维护的复杂度,但是拆分过粗可能会导致大量业务高度耦合,开发部署起来是挺快的,但是缺失可维护性和可扩展性,这需要根据实际情况做出权衡。
1.5 领域与数据
领域对象在包含值对象的同时也保留了值对象的业务含义,而数据对象可以使用更加松散的结构保存值对象,简化数据库设计。
值对象在数据对象中可以用松散的数据结构进行存储,而值对象在领域对象中需要保留其业务含义:
1.6 抽象与灵活
我们再回到数据模型的讨论,可以发现脚本化是一种拓展灵活性的方式,脚本化不仅指使用groovy、QLExpress脚本增强系统灵活性,还包括松散可扩展的数据结构。数据模型抽象出了姓名、身高、体重这些基本属性,对于频繁变化的比赛表现属性,这些属性值可能经常变化,甚至属性本身也是经常变化,例如可能会加上射门次数,突破次数等,所以采用松散的JSON数据结构进行存储。
2 基本概念
2.1 领域、子域与限界上下文
限界上下文(Bounded contenxt)比较难理解可以从四个维度分析:
第一个维度是限界上下文本身含义。限界表示了规定一个边界,上下文表示在这个边界内使用相同语义对象。例如goods这个词,在商品边界内被称为商品,但是快递边界内被称为货物。
第二个维度是子域与限界上下文关系。子域可以对应一个,也可以对应多个限界上下文。如果子域划分足够小,那么就是限界上下文。如果子域可以再细分,那么可以划分多个限界上下文。
第三维度是服务如何划分。子域和限界上下文都可以作为微服务,这里微服务是指独立部署的程序进程,具体拆分到什么维度是根据业务需要、开发资源、维护成本、技术实力等因素综合考量。
第四个维度是交互维度。在同一个限界上下文中实体对象和值对象可以自由交流,在不同限界上下文中必须通过聚合根进行交流。聚合根可以理解为一个按照业务聚合的代理对象。
2.2 实体、值对象与聚合
聚合包括聚合根和聚合边界两个概念,聚合根可以理解为一个按照业务聚合的代理对象,一个限界上下文企图访问另一个限界上下文内部对象,必须通过聚合根进行访问。例如产品经理作为需求收口人,任何需求应该先提给产品经理,通过产品经理整合后再提给程序员,而不是直接提给开发人员。
2.3 领域事件
通过事件交互有一个问题需要注意,通过事件订阅实现业务只能采用最终一致性,需要放弃强一致性,可能会引入新的复杂度需要权衡。同一个进程间事件交互可以用EventBus,跨进程事件交互可以用RocketMQ等消息中间件。
3 分析七大步骤
3.1 七大步骤
四色分领域
流程三剑客
纵横做设计
接口看对接
3.2 四色分领域
3.2.1 流程梳理
首先梳理业务流程,这里有两个问题需要考虑,第一个问题是从什么视角去梳理?因为不同的人看到的流程是不一样的。答案是取决于系统需要解决什么问题,因为我们要管理运动员从转会到上场比赛整条链路信息,所以从运动员视角出发是一个合适的选择。
假设足球业务专家梳理出了业务流程,运动员提出转会,协商一致后到新俱乐部体检,体检通过就进行签约。进入新俱乐部后进行训练,训练指标达标后上场比赛,赛后参加新闻发布会。当然实际流程会复杂很多,本文还是着重讲解方法论。
3.2.2 四色建模
(1 时标对象
时标对象具有两个特点:第一是事实不可变性,记录了过去某个时间点或时间段内发生的事实。第二是责任可追溯性,记录了管理者关注的信息。现在我们分析本系统时标对象有哪些,需要留下哪些核心业务单据。
(2 参与方、地、物
我们分析本例可以知道参与方包含总经理、队医、教练、球迷、记者,地包含训练地址、比赛地址、采访地址,物包含签名球衣和签名足球:
(3 角色对象
(4 描述对象
3.2.3 划分领域
3.2.4 领域事件
例如球员比赛受伤了,这是比赛子域事件,但是医疗和训练子域是需要感知的,那么比赛子域就发出一个事件,医疗和训练子域会订阅。球员比赛取得进球,这也是比赛子域事件,但是训练和合同子域也会关注这个事件,所以比赛子域也会发出一个比赛进球事件,训练和合同子域会订阅。
3.3 用例看功能
下图表示在比赛领域,运动员视角(什么人)使用系统进行进球统计,助攻统计,犯规统计,跑动距离统计,比赛评分统计,传球成功率统计,受伤统计(干什么事),同理我们也可以选择四色建模中其它参与者视角绘制用例图。
extend关键字表示扩展关系。例如点球统计是进球统计的扩展,因为不一定可以获得点球,所以点球统计即使不存在,也不会影响进球统计功能。黄牌统计、红牌统计是犯规统计的扩展,因为普通犯规不会获得红黄牌,所以红黄牌统计不存在,也不会影响犯规统计功能。
3.4 流程三剑客
用例图是从外部视角描述系统,但是分析系统总是要深入系统内部的,其中流程视图就是描述系统内如何流转的视图。活动图、序列图、状态机图是流程视图中最重要的三种视图,我们称为流程三剑客。三者侧重点有所不同:活动图侧重于逻辑分支,顺序图侧重于交互,状态机图侧重于状态流转。
3.4.1 活动图
我们经常说一图胜千言,其中一个重要原因是文字是线性的,所以表达逻辑分支能力不如流程视图,而在流程视图中表达逻辑分支能力最强的是活动图。
3.4.2 顺序图
如果某个逻辑分支特别重要,可以选择再画一个顺序图。例如支付流程中有支付成功正常流程,也有支付失败异常流程,这两个流程都非常重要,所以可以用两张顺序图体现。回到本文实例,我们可以通过顺序图体现球员从提出转会到比赛全流程。
3.4.3 状态机图
状态机图侧重于状态流转,说明了哪些状态之间可以相互流转,再结合状态机代码模式,可以解决上述状态异常情况。回到本文实例,我们可以通过状态机图表示球员从提出转会到签约整个状态流程。
3.5 领域与数据
实体是具有唯一标识的对象,唯一标识会伴随实体对象整个生命周期并且不可变更。值对象本质上是属性的集合,没有唯一标识。
现在我们需要管理足球运动员基本信息和比赛数据,对应领域模型和数据模型应该如何设计?姓名、身高、体重是一名运动员本质属性,加上唯一编号可以对应实体对象。跑动距离,传球成功率,进球数是运动员比赛表现,这些属性的集合可以对应值对象。
// 数据对象
public class FootballPlayerDO {
private Long id;
private String name;
private Integer height;
private Integer weight;
private String gamePerformance;
}
// 领域对象
public class FootballPlayerDMO {
private Long id;
private String name;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformanceVO;
}
public class GamePerformanceVO {
private Double runDistance;
private Double passSuccess;
private Integer scoreNum;
}
如果需要根据JSON结构中KEY进行检索,例如查询进球数大于5的球员,这也不是没有办法。我们可以将MySQL表中数据平铺到ES中,一条数据根据JSON KEY平铺变成多条数据,这样就可以进行检索了。
3.6 纵横做设计
我们首先分析一个下单场景做铺垫。当前有ABC三种订单类型,A订单价格9折,物流最大重量不能超过8公斤,不支持退款。B订单价格8折,物流最大重量不能超过5公斤,支持退款。C订单价格7折,物流最大重量不能超过1公斤,支持退款。按照需求字面含义平铺直叙地写代码也并不难:
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Override
public void createOrder(OrderBO orderBO {
if (null == orderBO {
throw new RuntimeException("参数异常";
}
if (OrderTypeEnum.isNotValid(orderBO.getType( {
throw new RuntimeException("参数异常";
}
// A类型订单
if (OrderTypeEnum.A_TYPE.getCode(.equals(orderBO.getType( {
orderBO.setPrice(orderBO.getPrice( * 0.9;
if (orderBO.getWeight( > 9 {
throw new RuntimeException("超过物流最大重量";
}
orderBO.setRefundSupport(Boolean.FALSE;
}
// B类型订单
else if (OrderTypeEnum.B_TYPE.getCode(.equals(orderBO.getType( {
orderBO.setPrice(orderBO.getPrice( * 0.8;
if (orderBO.getWeight( > 8 {
throw new RuntimeException("超过物流最大重量";
}
orderBO.setRefundSupport(Boolean.TRUE;
}
// C类型订单
else if (OrderTypeEnum.C_TYPE.getCode(.equals(orderBO.getType( {
orderBO.setPrice(orderBO.getPrice( * 0.7;
if (orderBO.getWeight( > 7 {
throw new RuntimeException("超过物流最大重量";
}
orderBO.setRefundSupport(Boolean.TRUE;
}
// 保存数据
OrderDO orderDO = new OrderDO(;
BeanUtils.copyProperties(orderBO, orderDO;
orderMapper.insert(orderDO;
}
}
上述代码从功能上完全可以实现业务需求,但是程序员不仅要满足功能,还需要思考代码的可维护性。如果新增一种订单类型,或者新增一个订单属性处理逻辑,那么我们就要在上述逻辑中新增代码,如果处理不慎就会影响原有逻辑。
需求变化通过扩展,而不是通过修改已有代码实现,这样就保证代码稳定性。扩展也不是随意扩展,因为事先定义了算法,扩展也是根据算法扩展,用抽象构建框架,用实现扩展细节。标准意义的二十三种设计模式说到底最终都是在遵循开闭原则。
3.6.1 纵向做隔离
// 优惠策略
public interface DiscountStrategy {
public void discount(OrderBO orderBO;
}
// A类型优惠策略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO {
orderBO.setPrice(orderBO.getPrice( * 0.9;
}
}
// B类型优惠策略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO {
orderBO.setPrice(orderBO.getPrice( * 0.8;
}
}
// C类型优惠策略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO {
orderBO.setPrice(orderBO.getPrice( * 0.7;
}
}
// 优惠策略工厂
@Component
public class DiscountStrategyFactory implements InitializingBean {
private Map<String, DiscountStrategy> strategyMap = new HashMap<>(;
@Resource
private TypeADiscountStrategy typeADiscountStrategy;
@Resource
private TypeBDiscountStrategy typeBDiscountStrategy;
@Resource
private TypeCDiscountStrategy typeCDiscountStrategy;
public DiscountStrategy getStrategy(String type {
return strategyMap.get(type;
}
@Override
public void afterPropertiesSet( throws Exception {
strategyMap.put(OrderTypeEnum.A_TYPE.getCode(, typeADiscountStrategy;
strategyMap.put(OrderTypeEnum.B_TYPE.getCode(, typeBDiscountStrategy;
strategyMap.put(OrderTypeEnum.C_TYPE.getCode(, typeCDiscountStrategy;
}
}
// 优惠策略执行
@Component
public class DiscountStrategyExecutor {
private DiscountStrategyFactory discountStrategyFactory;
public void discount(OrderBO orderBO {
DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType(;
if (null == discountStrategy {
throw new RuntimeException("无优惠策略";
}
discountStrategy.discount(orderBO;
}
}
3.6.2 横向做编排
横向维度表示场景,一种订单类型在广义上可以认为是一种业务场景,在场景中将独立的策略进行串联,模板方法设计模式适用于这种场景。
// 创建订单服务
public interface CreateOrderService {
public void createOrder(OrderBO orderBO;
}
// 抽象创建订单流程
public abstract class AbstractCreateOrderFlow {
@Resource
private OrderMapper orderMapper;
public void createOrder(OrderBO orderBO {
// 参数校验
if (null == orderBO {
throw new RuntimeException("参数异常";
}
if (OrderTypeEnum.isNotValid(orderBO.getType( {
throw new RuntimeException("参数异常";
}
// 计算优惠
discount(orderBO;
// 计算重量
weighing(orderBO;
// 退款支持
supportRefund(orderBO;
// 保存数据
OrderDO orderDO = new OrderDO(;
BeanUtils.copyProperties(orderBO, orderDO;
orderMapper.insert(orderDO;
}
public abstract void discount(OrderBO orderBO;
public abstract void weighing(OrderBO orderBO;
public abstract void supportRefund(OrderBO orderBO;
}
// 实现创建订单流程
@Service
public class CreateOrderFlow extends AbstractCreateOrderFlow {
@Resource
private DiscountStrategyExecutor discountStrategyExecutor;
@Resource
private ExpressStrategyExecutor expressStrategyExecutor;
@Resource
private RefundStrategyExecutor refundStrategyExecutor;
@Override
public void discount(OrderBO orderBO {
discountStrategyExecutor.discount(orderBO;
}
@Override
public void weighing(OrderBO orderBO {
expressStrategyExecutor.weighing(orderBO;
}
@Override
public void supportRefund(OrderBO orderBO {
refundStrategyExecutor.supportRefund(orderBO;
}
}
3.6.3 综合应用
上述实例业务和代码并不复杂,其实复杂业务场景也不过是简单场景的叠加、组合和交织,无外乎也是通过纵向做隔离、横向做编排寻求答案。
横向维度将能力从能力池选出来,按照业务需求串联在一起,形成不同业务流程。因为能力可以任意组合,所以体现了很强的灵活性。除此之外,不同能力既可以串行执行,如果不同能力之间没有依赖关系,也可以如同流程Y一样并行执行,提升执行效率。
3.7 分层看架构
3.7.1 维度一
第一种层次关系是指本项目在整个公司位于哪一层。持久层、缓存层、中间件、业务中台、服务层、网关层、客户端和代理层是常见的分层架构。
3.7.2 维度二
第二针对面向B端、面向C端、面向运营三种端,各自拆分出一个应用,在此应用中进行转换、适配和裁剪,并且处理各自业务。
第四如果后续要做秒杀系统,那么也可以理解其为一个前台应用(seckill-front)聚合各种中台接口。
3.7.3 维度三
分层缺点是层之间通信时,需要通过适配器,翻译成本层或者下层可以理解的信息,通信成本有所增加。我认为工程分层需要从六个维度思考:
(1 单一
(2 降噪
信息在每一层进行传输,满足最小知识原则,只向下层传输必要信息
(3 适配
(4 纵向
纵向做隔离,同一个领域内业务要在本领域内聚
(5 横向
(6 数据
数据对象尽量纯净,尽量使用基本类型
- 工具层:util
- 整合层:integration
- 基础层:infrastructure
- 领域层:domain
- 应用层:application
- 门面层:facade
- 客户端:client
- 控制层:controller
- 启动层:boot
3.8 接口看对接
自动生成优点是代码即文档,还具有调试功能,在公司内部进行联调时非常方便。但是如果接口是提供给外部第三方使用,那么还是需要手工编写接口文档。对于一个接口的描述无外乎接口名称、接口说明、接口协议,输入参数、输出参数信息。
4 代码详解
user-demo-service
-user-demo-service-application
-user-demo-service-boot
-user-demo-service-client
-user-demo-service-controller
-user-demo-service-domain
-user-demo-service-facade
-user-demo-service-infrastructure
-user-demo-service-integration
-user-demo-service-util
4.1 util
工具层承载工具代码
只依赖一些通用工具包
user-demo-service-util
-/src/main/java
-date
-DateUtil.java
-json
-JsonUtil.java
-validate
-BizValidator.java
4.2 infrastructure
基础层承载数据访问和entity
4.2.1 项目结构
user-demo-service-infrastructure
-/src/main/java
-base
-service
-redis
-RedisService.java
-mq
-ProducerService.java
-player
-entity
-PlayerEntity.java
-mapper
-PlayerEntityMapper.java
-game
-entity
-GameEntity.java
-mapper
-GameEntityMapper.java
-/src/main/resources
-mybatis
-sqlmappers
-gameEntityMapper.xml
-playerEntityMapper.xml
4.2.2 本项目依赖
- util
4.2.3 核心代码
创建运动员数据表:
CREATE TABLE `player` (
`id` bigint(20 NOT NULL AUTO_INCREMENT COMMENT '主键',
`player_id` varchar(256 NOT NULL COMMENT '运动员编号',
`player_name` varchar(256 NOT NULL COMMENT '运动员名称',
`height` int(11 NOT NULL COMMENT '身高',
`weight` int(11 NOT NULL COMMENT '体重',
`game_performance` text COMMENT '最近一场比赛表现',
`creator` varchar(256 NOT NULL COMMENT '创建人',
`updator` varchar(256 NOT NULL COMMENT '修改人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
运动员实体对象,gamePerformance字段作为string保存在数据库,体现了数据层尽量纯净,不要整合过多业务,解析任务应该放在业务层:
public class PlayerEntity {
private Long id;
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private String creator;
private String updator;
private Date createTime;
private Date updateTime;
private String gamePerformance;
}
运动员Mapper对象:
@Repository
public interface PlayerEntityMapper {
int insert(PlayerEntity record;
int updateById(PlayerEntity record;
PlayerEntity selectById(@Param("playerId" String playerId;
}
4.3 integration
本层调用外部服务,转换外部DTO成为本项目可以理解对象。
4.3.1 项目结构
user-demo-service-integration
-/src/main/java
-user
-adapter
-UserClientAdapter.java
-proxy
-UserClientProxy.java
-vo // 本项目对象
-UserSimpleAddressVO.java
-UserSimpleContactVO.java
-UserSimpleBaseInfoVO.java
4.3.2 本项目依赖
- util
4.3.3 核心代码
(1 外部服务
// 外部对象
public class UserInfoClientDTO implements Serializable {
private String id;
private String name;
private Date createTime;
private Date updateTime;
private String mobile;
private String cityCode;
private String addressDetail;
}
// 外部服务
public class UserClientService {
// RPC
public UserInfoClientDTO getUserInfo(String userId {
UserInfoClientDTO userInfo = new UserInfoClientDTO(;
userInfo.setId(userId;
userInfo.setName(userId;
userInfo.setCreateTime(DateUtil.now(;
userInfo.setUpdateTime(DateUtil.now(;
userInfo.setMobile("test-mobile";
userInfo.setCityCode("test-city-code";
userInfo.setAddressDetail("test-address-detail";
return userInfo;
}
}
(2 本项目对象
// 基本对象
public class UserBaseInfoVO {
private UserContactVO contactInfo;
private UserAddressVO addressInfo;
}
// 地址值对象
public class UserAddressVO {
private String cityCode;
private String addressDetail;
}
// 联系方式值对象
public class UserContactVO {
private String mobile;
}
(3 适配器
public class UserClientAdapter {
public UserBaseInfoVO convert(UserInfoClientDTO userInfo {
// 基础信息
UserBaseInfoVO userBaseInfo = new UserBaseInfoVO(;
// 联系方式
UserContactVO contactVO = new UserContactVO(;
contactVO.setMobile(userInfo.getMobile(;
userBaseInfo.setContactInfo(contactVO;
// 地址信息
UserAddressVO addressVO = new UserAddressVO(;
addressVO.setCityCode(userInfo.getCityCode(;
addressVO.setAddressDetail(userInfo.getAddressDetail(;
userBaseInfo.setAddressInfo(addressVO;
return userBaseInfo;
}
}
(4 调用外部服务
public class UserClientProxy {
@Resource
private UserClientService userClientService;
@Resource
private UserClientAdapter userIntegrationAdapter;
// 查询用户
public UserBaseInfoVO getUserInfo(String userId {
UserInfoClientDTO user = userClientService.getUserInfo(userId;
UserBaseInfoVO result = userIntegrationAdapter.convert(user;
return result;
}
}
4.4 domain
4.4.1 概念说明
通过三组对比理解领域层:
- 领域对象 VS 数据对象
- 领域对象 VS 业务对象
- 领域层 VS 应用层
(1 领域对象 VS 数据对象
public class PlayerEntity {
private Long id;
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private String creator;
private String updator;
private Date createTime;
private Date updateTime;
private String gamePerformance;
}
领域对象需要体现业务含义:
public class PlayerQueryResultDomain {
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
}
public class GamePerformanceVO {
// 跑动距离
private Double runDistance;
// 传球成功率
private Double passSuccess;
// 进球数
private Integer scoreNum;
}
(2 领域对象 VS 业务对象
业务对象同样会体现业务,领域对象和业务对象有什么不同?最大不同是领域对象采用充血模型聚合业务。
public class PlayerCreateBO {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
private MaintainCreateVO maintainInfo;
}
运动员新增领域对象:
public class PlayerCreateDomain implements BizValidator {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
private MaintainCreateVO maintainInfo;
@Override
public void validate( {
if (StringUtils.isEmpty(playerName {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == height {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (height > 300 {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == weight {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null != gamePerformance {
gamePerformance.validate(;
}
if (null == maintainInfo {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
maintainInfo.validate(;
}
}
(3 领域层 VS 应用层
第一个区别:领域层关注纵向,应用层关注横向。领域层纵向做隔离,本领域业务行为要在本领域内处理完。应用层横向做编排,聚合和编排领域服务。
4.4.2 项目结构
user-demo-service-domain
-/src/main/java
-base
-domain
-BaseDomain.java
-event
-BaseEvent.java
-vo
-BaseVO.java
-MaintainCreateVO.java
-MaintainUpdateVO.java
-player
-adapter
-PlayerDomainAdapter.java
-domain
-PlayerCreateDomain.java // 领域对象
-PlayerUpdateDomain.java
-PlayerQueryResultDomain.java
-event // 领域事件
-PlayerUpdateEvent.java
-PlayerMessageSender.java
-service // 领域服务
-PlayerDomainService.java
-vo // 值对象
-GamePerformanceVO.java
-game
-adapter
-GameDomainAdapter.java
-domain
-GameCreateDomain.java
-GameUpdateDomain.java
-GameQueryResultDomain.java
-service
-GameDomainService.java
4.4.3 本项目依赖
- util
- client
领域对象进行业务校验,所以需要依赖client模块:
- BizException
- ErrorCodeBizEnum
4.4.4 核心代码
// 修改领域对象
public class PlayerUpdateDomain extends BaseDomain implements BizValidator {
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private String updator;
private Date updatetime;
private GamePerformanceVO gamePerformance;
private MaintainUpdateVO maintainInfo;
@Override
public void validate( {
if (StringUtils.isEmpty(playerId {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (StringUtils.isEmpty(playerName {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == height {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (height > 300 {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == weight {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null != gamePerformance {
gamePerformance.validate(;
}
if (null == maintainInfo {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
maintainInfo.validate(;
}
}
// 比赛表现值对象
public class GamePerformanceVO implements BizValidator {
// 跑动距离
private Double runDistance;
// 传球成功率
private Double passSuccess;
// 进球数
private Integer scoreNum;
@Override
public void validate( {
if (null == runDistance {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == passSuccess {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (Double.compare(passSuccess, 100 > 0 {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == runDistance {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == scoreNum {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
}
}
// 修改人值对象
public class MaintainUpdateVO implements BizValidator {
// 修改人
private String updator;
// 修改时间
private Date updateTime;
@Override
public void validate( {
if (null == updator {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
if (null == updateTime {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
}
}
}
// 领域服务
public class PlayerDomainService {
@Resource
private UserClientProxy userClientProxy;
@Resource
private PlayerRepository playerEntityMapper;
@Resource
private PlayerDomainAdapter playerDomainAdapter;
@Resource
private PlayerMessageSender playerMessageSender;
public boolean updatePlayer(PlayerUpdateDomain player {
AssertUtil.notNull(player, new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT;
player.validate(;
// 更新运动员信息
PlayerEntity entity = playerDomainAdapter.convertUpdate(player;
playerEntityMapper.updateById(entity;
// 发送更新消息
playerMessageSender.sendPlayerUpdatemessage(player;
// 查询用户信息
UserSimpleBaseInfoVO userInfo = userClientProxy.getUserInfo(player.getMaintainInfo(.getUpdator(;
log.info("updatePlayer maintainInfo={}", JacksonUtil.bean2Json(userInfo;
return true;
}
}
4.5 application
本层关注横向维度聚合领域服务,引出一种新对象称为聚合对象。因为本层需要聚合多个维度,所以需要通过聚合对象聚合多领域属性,例如提交订单需要聚合商品、物流、优惠券多个领域。
// 订单提交聚合对象
public class OrderSubmitAgg {
// userId
private String userId;
// skuId
private String skuId;
// 购买量
private Integer quantity;
// 地址信息
private String addressId;
// 可用优惠券
private String couponId;
}
// 订单应用服务
public class OrderApplicationService {
@Resource
private OrderDomainService orderDomainService;
@Resource
private CouponDomainService couponDomainService;
@Resource
private ProductDomainService productDomainService;
// 提交订单
public String submitOrder(OrderSubmitAgg orderSumbitAgg {
// 订单编号
String orderId = generateOrderId(;
// 商品校验
productDomainService.queryBySkuId(orderSumbitAgg.getSkuId(;
// 扣减库存
productDomainService.subStock(orderSumbitAgg.getStockId(, orderSumbitAgg.getQuantity(;
// 优惠券校验
couponDomainService.validate(userId, couponId;
// ......
// 创建订单
OrderCreateDomain domain = OrderApplicationAdapter.convert(orderSubmitAgg;
orderDomainService.createOrder(domain;
return orderId;
}
}
4.5.1 项目结构
user-demo-service-application
-/src/main/java
-player
-adapter
-PlayerApplicationAdapter.java
-agg
-PlayerCreateAgg.java
-PlayerUpdateAgg.java
-service
-PlayerApplicationService.java
-game
-listener
-PlayerUpdateListener.java // 监听运动员更新事件
4.5.2 本项目依赖
- util
- domain
- integration
- infrastructure
4.5.3 核心代码
本项目领域事件交互使用EventBus框架:
// 运动员应用服务
public class PlayerApplicationService {
@Resource
private LogDomainService logDomainService;
@Resource
private PlayerDomainService playerDomainService;
@Resource
private PlayerApplicationAdapter playerApplicationAdapter;
public boolean updatePlayer(PlayerUpdateAgg agg {
// 运动员领域
boolean result = playerDomainService.updatePlayer(agg.getPlayer(;
// 日志领域
LogReportDomain logDomain = playerApplicationAdapter.convert(agg.getPlayer(.getPlayerName(;
logDomainService.log(logDomain;
return result;
}
}
// 比赛领域监听运动员变更事件
public class PlayerUpdateListener {
@Resource
private GameDomainService gameDomainService;
@PostConstruct
public void init( {
EventBusManager.register(this;
}
@Subscribe
public void listen(PlayerUpdateEvent event {
// 更新比赛计划
gameDomainService.updateGameSchedule(;
}
}
4.6 facade + client
设计模式中有一种Facade模式,称为门面模式或者外观模式。这种模式提供一个简洁对外语义,屏蔽内部系统复杂性。
- 简洁性:对外服务语义明确简洁
- 安全性:敏感字段不能对外透出
4.6.1 项目结构
(1 client
user-demo-service-client
-/src/main/java
-base
-dto
-BaseDTO.java
-error
-BizException.java
-BizErrorCode.java
-event
-BaseEventDTO.java
-result
-ResultDTO.java
-player
-dto
-PlayerCreateDTO.java
-PlayerQueryResultDTO.java
-PlayerUpdateDTO.java
-enums
-PlayerMessageTypeEnum.java
-service
-PlayerClientService.java
(2 facade
user-demo-service-facade
-/src/main/java
-player
-adapter
-PlayerFacadeAdapter.java
-impl
-PlayerClientServiceImpl.java
-game
-adapter
-GameFacadeAdapter.java
-impl
-GameClientServiceImpl.java
4.6.2 本项目依赖
client不依赖本项目其它模块,这一点非常重要:因为client会被外部引用,必须保证本层简洁和安全。
- domain
- client
- application
4.6.3 核心代码
(1 DTO
以查询运动员信息为例,查询结果DTO只封装强业务字段,运动员ID、创建时间、修改时间等业务不强字段无须透出:
public class PlayerQueryResultDTO implements Serializable {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceDTO gamePerformanceDTO;
}
(2 客户端服务
public interface PlayerClientService {
public ResultDTO<PlayerQueryResultDTO> queryById(String playerId;
}
(3 适配器
public class PlayerFacadeAdapter {
// domain -> dto
public PlayerQueryResultDTO convertQuery(PlayerQueryResultDomain domain {
if (null == domain {
return null;
}
PlayerQueryResultDTO result = new PlayerQueryResultDTO(;
result.setPlayerId(domain.getPlayerId(;
result.setPlayerName(domain.getPlayerName(;
result.setHeight(domain.getHeight(;
result.setWeight(domain.getWeight(;
if (null != domain.getGamePerformance( {
GamePerformanceDTO performance = convertGamePerformance(domain.getGamePerformance(;
result.setGamePerformanceDTO(performance;
}
return result;
}
}
(4 服务实现
本层可以引用applicationService,也可以引用domainService,因为对于类似查询等简单业务场景,没有多领域聚合,可以直接使用领域服务。
public class PlayerClientServiceImpl implements PlayerClientService {
@Resource
private PlayerDomainService playerDomainService;
@Resource
private PlayerFacadeAdapter playerFacadeAdapter;
@Override
public ResultDTO<PlayerQueryResultDTO> queryById(String playerId {
PlayerQueryResultDomain resultDomain = playerDomainService.queryPlayerById(playerId;
if (null == resultDomain {
return ResultCommonDTO.success(;
}
PlayerQueryResultDTO result = playerFacadeAdapter.convertQuery(resultDomain;
return ResultCommonDTO.success(result;
}
}
4.7 controller
facade服务实现可以作为RPC提供服务,controller则作为本项目HTTP接口提供服务,供前端调用。
4.7.1 项目结构
user-demo-service-controller
-/src/main/java
-controller
-player
-PlayerController.java
-game
-GameController.java
4.7.2 本项目依赖
- facade
4.7.3 核心代码
@RestController
@RequestMapping("/player"
public class PlayerController {
@Resource
private PlayerClientService playerClientService;
@PostMapping("/add"
public ResultDTO<Boolean> add(@RequestHeader("test-login-info" String loginUserId, @RequestBody PlayerCreateDTO dto {
dto.setCreator(loginUserId;
ResultCommonDTO<Boolean> resultDTO = playerClientService.addPlayer(dto;
return resultDTO;
}
@PostMapping("/update"
public ResultDTO<Boolean> update(@RequestHeader("test-login-info" String loginUserId, @RequestBody PlayerUpdateDTO dto {
dto.setUpdator(loginUserId;
ResultCommonDTO<Boolean> resultDTO = playerClientService.updatePlayer(dto;
return resultDTO;
}
@GetMapping("/{playerId}/query"
public ResultDTO<PlayerQueryResultDTO> queryById(@RequestHeader("test-login-info" String loginUserId, @PathVariable("playerId" String playerId {
ResultCommonDTO<PlayerQueryResultDTO> resultDTO = playerClientService.queryById(playerId;
return resultDTO;
}
}
4.8 boot
boot作为启动层承载启动入口
4.8.1 项目结构
user-demo-service-boot
-/src/main/java
-com.user.demo.service
-MainApplication.java
4.8.2 依赖本项目
- 所有模块
4.8.3 核心代码
@MapperScan("com.user.demo.service.infrastructure.*.mapper"
@SpringBootApplication
public class MainApplication {
public static void main(final String[] args {
SpringApplication.run(MainApplication.class, args;
}
}
5 文章总结
本文第一提出并回答了六个问题,第二介绍了DDD相关基本概念,第三介绍了DDD分析七大步骤,第四介绍了代码分层结构,希望本文对大家有所帮助。
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习