亚马逊|领域驱动编程,代码怎么写?( 二 )


)来减少这些类型转换给我们带来的繁琐工作 。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 聚合 。 如果需要传递复杂对象 , 需要先用类型转换器将应用层的数据类转化为领域层的实体类或者值对象 。使用领域层服务控制出单流程 发送出单成功消息 , 其他领域监听到感兴趣的消息会进行响应 。4 领域层编程实践 1、分包结构 这里领域层一共有5个一级分包 。anticorruption 是领域防腐层 , 是当前领域需要获知其他领域或者外部信息时 , 对其他领域二方包的封装 。 防腐层从代码层面来看 , 可以避免调用外部客户端时 , 在领域内部进行复杂的参数拼装和结果的转换 。factory 解决了复杂聚合的初始化问题 。 我们设计好领域模型供外部调用 , 但如果外部也必须使用如何装配这个对象 , 则必须知道对象的内部结构 。 对调用方开发来说这是很不友好的 。 其次 , 复杂对象或者聚合当中的领域知识(业务规则)需要得到满足 , 如果让外部自己装配复杂对象或聚合的话 , 就会将领域知识泄露到调用方代码中去 。 需要注意的是 , 这里主要是把聚合或实体需要的数据填充进来 , 而不涉及对象的行为 。因此这里工厂的核心作用是从各处拉取初始化聚合或实体所需要的外部数据 。@Service@AllArgsConstructorpublic class PolicyFactory { /** * 产品领域防腐层服务 */ private final ProductService productService; /** * 从各种数据来源查询直接能查到的前置数据 , 填充到 policy 中 * @param productId * @param stakeHolders * @return */ public Policy createPolicy(Long productId ListStakeHolderstakeHolders) { 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 ListStakeHolderstakeHolders; private Date issueTime; /** * 工厂方法 * @param product * @param stakeHolders * @return */ public static Policy create(PolicyProduct product ListStakeHolderstakeHolders){ 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 中 。 这种方式目前看起来在我们这里落地的可能性不大 , 不做更多介绍 。5 基础设施层编程实践 1、分包结构 这里只展示了 repository 的实现 , 但实际上这里还有 RPC 调用的二方包实现类注入等很多内容 。 上文说到领域层不关心仓储的实现 , 交由基础设施层负责 。 基础设施层可以根据需要使用关系型数据库、缓存或者NoSQL , 领域层是无感知的 。 这里我们以关系型数据库为例来 , dao 和 dataobject 等都可以使用例如 mybatis generator 等工具生成 , 领域对象 和 dataobject 之间的转换由 convertor 负责 。2、用例代码 @Repository@AllArgsConstructorpublic class PolicyRepositoryImpl implements PolicyRepository { private final PolicyDAO policyDAO; private final StakeHolderDAO stakeHolderDAO; private final PolicyConvertor policyConvertor; private final StakeHolderConvertor stakeHolderConvertor; @Override public String save(Policy policy) { throw new UnsupportedOperationException();@Override public String create(Policy policy) { policyDAO.insert(policyConvertor.convert(policy)); stakeHolderDAO.insertBatch(stakeHolderConvertor.convert(policy)); //...其它数据入库 return policy.getId().toString();@Override public void updatePolicyStatus(String newStatus) {这部分代码比较简单 , 无需赘言 。五 结语 关于领域驱动 , 笔者仍处于初学者阶段 , 再好的设计 , 随着业务的发展 , 代码也难免变得混乱 , 这个过程中 , 每个参与者都有责任 。 最后 , 总结一下我们维持代码初心的一些原则 , 和大家分享 。深入理解业务场景 , 分析用例 , 进行正确的领域划分 。确定好实现方式后 , 大家尽量按照既定模式/风格编程 , 有异议的地方可以一起讨论后统一改动 。不引入不必要的复杂度 。不断对系统设计进行优化改进 , 对繁琐的代码 , 用设计模式进行优化 。写注释 。[1