领域驱动编程,代码怎么写?
前言
Aliware
-
大家更熟悉 MVC 的编程模式,需要快速实现某个功能的时候,往往倾向于使用较为稳妥、熟悉的方式。 -
大家对领域驱动编程应该怎么编写并没有一个统一的认知( Axon Framework[1] 对领域驱动设计实现的非常好,但它太“重”了)。 -
DDD 落地本身就比较难,往往需要事件驱动和 Event Store 来完美实现,而这二者是我们不常用的。 -
领域驱动设计是面向复杂系统的,业务发展初期看上去都比较简单,一上来就搞领域驱动设计有过度设计之嫌。这也是领域驱动设计常常在系统不得不重构的是时候才被拿出来讨论的原因。
-
实施面向对象的编程模式,进而实现高内聚、低耦合。 -
在复杂业务系统的迭代过程中,保证代码结构不会无限制地变得混乱,因此保证系统可持续维护。
保险领域知识简介
Aliware
领域驱动开发的代码结构
Aliware
领域驱动的代码分层
public interface Moveable {void move();}public abstract class Animal implements Moveable {}public class Bird extends Animal {public void move(){//try to flySystem.out.println("I'am flying");}}public class Dog extends Animal {public void move(){//try to swimSystem.out.println("I'am swimming");}}
小结
领域驱动开发的代码
Aliware
用例
-
用户在前端页面选择保险产品,选择可选的保障责任,输入投/被保人信息,选择支付方式(分期/趸交等)并支付后提交投保请求; -
服务端接受投保请求 -> 核保 -> 出单 -> 下发保单权益。 这里用例 1 是用例 2 的前置用例,我们假定用例 1 已经顺利完成(用例 1 中完成了费率计算),只来实现用例 2,并且用例 2 也只是大略的实现,只要能把代码样式展示即可。
用户接口层编程实践
@AllArgsConstructor@RestController@RequestMapping("/insure")public class PolicyController {private final InsuranceInsureService insuranceInsureService;/*** 投保出单* @param request* @return 保单 ID*/@RequestMapping(value = "/issue-policy", method = RequestMethod.POST)public String issuePolicy(IssuePolicyRequest request){return insuranceInsureService.issuePolicy(request);}}
应用层编程实践
-
其中最外层接口是面向具体业务场景的,可以根据业务发展再进行分包。
-
pojo 包中定义了应用层用到的各种数据类(上面的 IssuePolicyRequest 就在这里)及其向其他层传播时需要进行类型转换的转化器。
-
tasks 包中定义了一些定时任务的入口。
注意,在领域编程实践中,会需要非常多的类型转换,我们可以借助一些框架(例如 MapStruct[2] )来减少这些类型转换给我们带来的繁琐工作。
2、 用例代码
@Service@AllArgsConstructorpublic class InsuranceInsureServiceImpl implements InsuranceInsureService {private final PolicyFactory policyFactory;private final StakeHolderConvertor stakeHolderConvertor;private final PolicyService policyService;/*** 事务控制一般在应用层* 但是需要注意底层存储对事务的支持特性* 底层是分库分表时,可能需要其他手段来保证事务,或者将非核心的操作从事务中剥离(例如数据库 ID 生成)*/@Override@Transactional(rollbackFor = Exception.class)public String issuePolicy(IssuePolicyRequest request) {Policy policy = policyFactory.createPolicy(request.getProductId(),stakeHolderConvertor.convert(request.getStakeHolders()));//出单流程控制policyService.issue(policy);PolicyIssuedMessage message = new PolicyIssuedMessage();message.setPolicyId(policy.getId());MQPublisher.publish(MQConstants.INSURANCE_TOPIC, MQConstants.POLICY_ISSUED_TAG, message);return policy.getId().toString();}}
这里代码展示的是应用层对用例 2 的处理。
-
使用领域层的工厂类构建 Policy 聚合。如果需要传递复杂对象,需要先用类型转换器将应用层的数据类转化为领域层的实体类或者值对象。
-
使用领域层服务控制出单流程
-
发送出单成功消息,其他领域监听到感兴趣的消息会进行响应。
领域层编程实践
-
anticorruption 是领域防腐层,是当前领域需要获知其他领域或者外部信息时,对其他领域二方包的封装。 防腐层从代码层面来看,可以避免调用外部客户端时,在领域内部进行复杂的参数拼装和结果的转换。
-
factory 解决了复杂聚合的初始化问题。 我们设计好领域模型供外部调用,但如果外部也必须使用如何装配这个对象,则必须知道对象的内部结构。 对调用方开发来说这是很不友好的。 其次,复杂对象或者聚合当中的领域知识(业务规则)需要得到满足,如果让外部自己装配复杂对象或聚合的话,就会将领域知识泄露到调用方代码中去。 需要注意的是,这里主要是把聚合或实体需要的数据填充进来,而不涉及对象的行为。
因此这里工厂的核心作用是从各处拉取初始化聚合或实体所需要的外部数据。
@Service@AllArgsConstructorpublic class PolicyFactory {/*** 产品领域防腐层服务*/private final ProductService productService;/*** 从各种数据来源查询直接能查到的前置数据,填充到 policy 中* @param productId* @param stakeHolders* @return*/public Policy createPolicy(Long productId, List<StakeHolder> stakeHolders) {PolicyProduct product = productService.getById(productId);//其他填充数据,这里调用了聚合自身的静态工厂方法Policy policy = Policy.create(product, stakeHolders);return policy;}}
-
model 中是领域对象的定义。其中 vo 包中定义了领域内用到的值对象。可以看到这里有 PolicyProduct 这样一个保险产品类,在投保领域,我们关注的是和保单相关的某个产品及其快照信息,因此我们在这里定义一个保单保险产品类,防腐层负责把从产品域获得的保险产品信息转换为我们关心的保单保险产品类对象。
按照领域驱动设计的最佳实践,领域对象模型中不允许出现 service、repository 这些用以获取外部信息的东西,它的核心概念是一个完备的实体初始化完成后,它能做什么,或者它经历了什么之后状态会发生怎样的变化。
下面是领域内核心的聚合 Policy 的示例代码。
@Getterpublic class Policy {private Long id;private PolicyProduct product;private List<StakeHolder> stakeHolders;private Date issueTime;/*** 工厂方法* @param product* @param stakeHolders* @return*/public static Policy create(PolicyProduct product, List<StakeHolder> stakeHolders){Policy policy = new Policy();policy.product = product;policy.stakeHolders = stakeHolders;return policy;}/*** 保单出单*/public void issue(Long id) {this.id = id;this.issueTime = new Date();}}
-
repository 是仓储包,只定义仓储接口,不关心具体实现,具体的实现交由基础设施层负责,体现了依赖倒置的思想。
-
service 是领域服务,它定义一些不属于领域对象的行为,但是又有必要的操作,比如一些流程控制。
2、用例代码
@Service@AllArgsConstructorpublic class PolicyService {private final InsureUnderwriteService insureUnderwriteService;private final PolicyRepository policyRepository;public void issue(Policy policy) {if(!insureUnderwriteService.underwrite(policy)){throw new BizException("核保失败");}policy.issue(IdGenerator.generate());//保存信息//policyRepository.save(policy);policyRepository.create(policy);}}
这里注意我们注掉了一行 policyRepository.save(policy) ;,那么为什么要区别 save 和 create 呢?
save 是领域驱动设计中最正确的做法:我的聚合或者实体有变动,仓储不用关心是新建还是更新,帮我保存起来就好了。听上去很美好,但对关系型数据库存储却是很不友好的。因此,在我们的场景里,需要违背一下书上所谓的最佳实践,我们告诉仓储是要新建还是更新,甚至如果是更新的话更新的是哪些列。
另外领域驱动的最佳实践是基于事件驱动的,AxonFramework 对其有完美的实现,应用层发出一个 IssuePolicyCommand 指令,领域层接收该指令,完成保单创建后发出 PolicyIssuedEvent ,该 event 会被监听并且持久化到 event store 中。这种方式目前看起来在我们这里落地的可能性不大,不做更多介绍。
基础设施层编程实践
@Repository@AllArgsConstructorpublic class PolicyRepositoryImpl implements PolicyRepository {private final PolicyDAO policyDAO;private final StakeHolderDAO stakeHolderDAO;private final PolicyConvertor policyConvertor;private final StakeHolderConvertor stakeHolderConvertor;@Overridepublic String save(Policy policy) {throw new UnsupportedOperationException();}@Overridepublic String create(Policy policy) {policyDAO.insert(policyConvertor.convert(policy));stakeHolderDAO.insertBatch(stakeHolderConvertor.convert(policy));//...其它数据入库return policy.getId().toString();}@Overridepublic void updatePolicyStatus(String newStatus) {}}
结语
Aliware
-
深入理解业务场景,分析用例,进行正确的领域划分。
-
确定好实现方式后,大家尽量按照既定模式/风格编程,有异议的地方可以一起讨论后统一改动。
-
不引入不必要的复杂度。
-
不断对系统设计进行优化改进,对繁琐的代码,用设计模式进行优化。
-
写注释。