领域驱动解决什么问题?
复杂系统的特征
首先我们得清楚是什么原因导致了软件复杂度升高。在《管理 3.0》一书中,Jurgen Appelo 从两个关键维度剖析了复杂性的根源:行为层面的预测力和结构层面的理解力。通常,面对高度复杂的软件系统,我们从业务和技术两个层面进行考量。结合 Jurgen Appelo 对复杂性成因的分类,我们能够构建出软件复杂性的全面框架图:
统一语言
认知成本
在一个商品项目中看到一个和库存相关的逻辑,在短短的一段代码中,就有 3 种不同的对库存的描述方式,分别是 Stock、Inventory、Sellable Amout,这意味着对同一个领域概念,我需要理解 3 遍。又比如对组织的描述 Org、Department,人员的描述:Candidate、User, 这极大地增加了我的记忆负担和认知成本。
一个团队一种语言
每个开发团队都应该有自己的命名规范,确保命名的一致性。对于核心的领域概念,应该有一个核心领域词汇表,确保这些领域词汇在代码中的表达是一致的。
然而,如果团队有一个命名规范-核心领域词汇表,加上团队成员的共同遵守和维护,其实上述问题是可以避免的。
因此需要团队在项目之初就整理出核心的领域概念,然后用中英文的形式,把这些概念放到设计文档中。要求英文的原因是,我需要保持领域概念从文档到代码的一致性。
语言在进化也需要重构
领域划分
问题域的划分策略
领域定义:领域是用来确定范围和边界的,DDD 将业务上的问题限定在一定的边界内,划分出来的一个个边界就是领域。为了降低理解成本和业务的复杂度,DDD 会将领域进一步划分成一个一个的子领域。
问题域重要度
领域模型
领域模型(domain model)可以被看作是一个系统的概念模型,用于以可视化的形式描述系统中的各个实体及其之间的关系。领域模型记录了一个系统中的关键概念和词汇表,显示出了系统中的主要实体之间的关系,并确定了它们的重要的方法和属性。领域模型提供了一种对整个系统的结构化的视图。领域模型的一个好处是描述并限制了系统边界。
来自wiki百科
我们设计了领域模型,但是在实际开发过程中好像又没用到?
系统上下文
限界上下文
限界上下文 = 限界 + 上下文 = 领域的边界 + 语义的环境。
限界上下文(微服务)拆分需要考虑哪些因素?
基于领域模型
基于领域模型进行拆分,围绕业务领域按职责单一性、功能完整性拆分。
根据主流程拆分
在商城系统里有一个重要的主流程,那就是用户商品搜索-》商品详情-》购物车-》订单模块-》支付模块。
主流程是我们这个商城系统的核心业务,使我们需要固守的一个流程。
当然在这后边我们还隐藏这很多核心流程,比如:优化结算、收货地址、物流模块等,这当中里面的任意模块
奔溃、错误都可能导致用户放弃购买。
所以我们可以根据核心主流程模块进行拆分,主要能够带来以下几个好处:
(1)服务隔离:当用户下单完成时,需要通过淘宝手机客户端推送一个消息给用户,消息推送服务的问题,不应该
到用户下单的操作。我们可以通过领域事件的走异步的策略解决这个问题。避免边缘的服务异常,影响到主流程。
(2)异常容错:主流中的模块需要合理的设计降级策略、熔断策略。防止因为部分程序问题导致服务器雪崩的情况。
(3)资源调配:针对主流程中的模块,在需要的时候我们可以分配更多的服务器资源。独立分拆主链路模块独立部署。
基于业务需求变化频率
识别领域模型中的业务需求变动频繁的功能,考虑业务变更频率与相关度,将业务需求变动较高和功能相对稳定的业务进行分离。这是因为需求的经常性变动必然会导致代码的频繁修改和版本发布,这种分离可以有效降低频繁变动的敏态业务对稳态业务的影响。
基于弹性边界
识别领域模型中性能压力较大的功能。因为性能要求高的功能可能会拖累其它功能,在资源要求上也会有区别,为了避免对整体性能和资源的影响,我们可以把在性能方面有较高要求的功能拆分出去。
基于组织架构和团队规模
除非有意识地优化组织架构,否则微服务的拆分应尽量避免带来团队和组织架构的调整,避免由于功能的重新划分,而增加大量且不必要的团队之间的沟通成本。拆分后的微服务项目团队规模保持在 10~12 人左右为宜。
基于技术异构等因素
领域模型中有些功能虽然在同一个业务域内,但在技术实现时可能会存在较大的差异,也就是说领域模型内部不同的功能存在技术异构的问题。由于业务场景或者技术条件的限制,有的可能用。NET,有的则是 Java,有的甚至大数据架构。对于这些存在技术异构的功能,可以考虑按照技术边界进行拆分。
何时合并限界上下文(微服务)?
当限界上下文之间存在强耦合,导致团队形成同生共死的关系时,说明设计上存在双向依赖或循环依赖,这需要及时优化。
解决方案:如果两个上下文紧密合作,拆分的理由不充分,可以考虑将它们合并,减少不必要的依赖和沟通成本。
如果上下文之间的相互依赖无法避免, 可以采取“合作者”方式,同时加强团队协作,确保上下文集成的稳定性,减少因依赖问题导致的交付风险。
上下文映射(微服务交互||模块的交互)
当你的系统越来越大,不同的子系统(如用户、订单、库存、绩效等)都有各自独立的模型和逻辑时,就需要把它们划分成多个 限界上下文(Bounded Context)。而这些上下文之间是需要通信、共享数据、互操作的,这时就需要上下文映射来说明它们的关系。
循环依赖
区分服务间的上下游关系
如果一个事物在另一个事物上增加了价值,或者以任何方式依赖另一个事物,那么它一定是下游,反之则是上游。
最佳实践
要解决服务的不合理依赖,必须要在微服务之间建立一些原则来约束微服务之间的通信,定期通过这些原则来审视我们的系统,找到问题并进行重构,我们可以参照以下的原则:
定义服务上下游关系,下游服务可以直接依赖上游服务,反之则不可。
上游服务的变更对下游服务产生影响需要通过领域事件(异步)的方式来实现。
服务之间要通过数据 Id(或类 Id,能够唯一代表数据且不变的属性)来进行关联,尽量不做过多的数据冗余。如果上游完成业务后,上游不在对业务数据发生改变,可以适度进行数据冗余。
一旦需要上游服务调用下游服务才能完成业务时,要考虑是否上游服务缺少业务概念(职责不清晰)。
为满足前端逻辑而导致的服务间交互逻辑要放到 BFF(Backend for frontend)中,而不是增加服务间的调用。
不允许跨上下文直接依赖其内部模型/规则,必须通过防腐层接入。
消灭微服务的坏味道 之 循环依赖_微服务_码猿外_InfoQ 写作社区
用户订单下单成功,通知派送服务,派送服务完成,更新订单状态。两个服务通过 API 进行集成,服务需要相互知道对方的部分领域知识来完成 API 的调用以实现功能,同时业务的可用性互相关联,一方服务不可用,导致整个业务的中断。同样人才盘点和问卷的关系和订单与派送服务的关系是一致的,这里面我们就可以通过引入消息中间件的方式来实现解耦。派送完成的时候发送消息到队列, 订单服务再去订阅这个消息实现订单状态的更新。
DHR 几乎所有的应用都需要依赖基础数据服务,基础服务相对于调用方来说是上游,业务方是下游。部分的 DHR 应用存在述求就是要监听基础数据的变动情况,更新业务系统关联的基础数据信息(例如:组织结构的编码,业务系统会通过组织结构的编码进行数据权限过滤,由于上游可能会调整组织结构,可能就会导致组织结构编码,如果不修正这个问题,会导致数据权限出现问题),但是我们不能要求基础服务数据变动的时候立即通过 rpc、http 直接推送给业务方,这里面就会存在双向依赖的问题,参考最佳实践我们可以通过基础数据变动的时候发布一条变动信息到消息对应的 topic 中去,业务系统有这个诉求的时候,只要与这个 topic 建立消费关系即可。
人才计划的生成是基于盘点活动中的人才盘点结果,如果是 F 改进者人员和 C 后备力量的人员,HRBP 可以基于这个结果生成对应的人才改进、提升计划。很多时候开发人员未考虑服务域的边界问题,生成人才计划的接口是由盘点活动域提供的,同时人才计划域又回查盘点活动相关的一些信息。这里面就存在一个服务的循环依赖问题。盘点活动(创建人才计划)-> 人才计划, 人才计划(盘点活动信息查询)-> 盘点活动。 遵循上面的最佳实践原则,我会推荐生成人才计划的创建接口由人才计划域提供,人才计划域调用盘点活动的人才盘点结果生成对应的人才计划。这样我的服务依赖就变成了单向依赖,耦合度可以进一步降低。
如果从“知识”和“能力”的角度去理解,财务上下文的领域模型对象并不具备计算运费的领域知识,不了解运输过程中的各种费率,如运输费、货站租赁费、货物装卸人工费、保费,也不了解运输费用的计算规则。缺乏这些知识,自然也就不具备计算运费的能力。财务上下文其实只需要获得与往来账有关的结算费用,而不是具体的运费计算过程。这个情况主要是限界上下文领域职责不清晰到导致的。出现这个情况的的主要发生在一个项目工程内,缺少上下文的边界隔离的问题,开发人员不去思考这段逻辑归属哪个上下文,做了本该属于其它上下文应该做的事情。因为都在一个项目工程内解决问题了就好无所谓这段逻辑放在哪个上下文合适。
端口适配器架构
适配器层
负责对前端展示(Web、H5、微信)的路由和适配。对于传统 B/S 系统而言,Adapter 层就相当于 MVC 中的 Controller。当然这个并不是绝对的 MQ 相关的消息、事件的消费服务也是可以放到当前模块的。按照端口适配架构的说法,消息、事件也是作为一种数据输入的方式。
应用层 &&领域层
应用服务 &&领域服务
《领域驱动设计模式、原理与实践》一书将这种封装认为是与领域的交互。该书作者给出了自己的一个判断标准:
决定一系列交互是否属于领域的一种方式是提出“这种情况总是会出现吗?”或者“这些步骤无法分开吗?”的问题。如果答案是肯定的,那么这看起来就是一个领域策略,因为那些步骤总是必须一起发生。然而,如果那些步骤可以用若干方式重新组合,那么可能它就不是一个领域概念。
如何分辨应用服务与领域服务
基础设施层
依赖反转
在面向对象编程领域中,依赖反转原则(Dependency inversion principle, DIP)是指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被反转,从而使得低层次模块依赖于高层次模块的抽象。
该原则规定:
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
-- 来自 wiki 百科
说人话:就是我们需要面向接口编程,接口规范必须由服务的调用者提供,而不是服务的提供者提供。
外部依赖反腐
@DubboServicepublic class ReviewSessionServiceImpl implements IReviewSessionService{ @Autowired private ReviewSessionMapper reviewSessionMapper; @DubboReference private IOrgService orgService; @Autowired private RedisTemplate redisTemplate; @Autowired private KafkaProducer<String, String> kafkaProducer; @Override public Long create(ReviewSessionCreate cmd){ OrgDTO orgDTO = orgService.get(cmd.getOrgId()); ReviewSessionPojo session = ReviewSessionFactory.to(cmd); Long id = reviewSessionMapper.insert(session); ReviewSessionCreatedEvent event = new ReviewSessionCreatedEvent(session.getId()); kafkaProducer.send(new ProducerRecord<>( "review_session_topic", id, JSONObject.toJSONString(event)) ).get(); return id; }}
复制代码
@Servicepublic class ReviewSessionServiceImpl implements IReviewSessionService{ @Autowired private IReviewSessionGateway reviewSessionGateway; @Autowired private IOrgGateway orgGateway; @Autowired private IMessageGateway messageGateway; @Override public Long create(ReviewSessionCreate cmd){ Org org = orgGateway.get(cmd.getOrgId()); ReviewSession session = ReviewSessionFactory.convert(cmd); Long id = reviewSessionGateway.save(session); ReviewSessionCreatedEvent event = new ReviewSessionCreatedEvent(session.getId()); messageGateway.send(event); return id; }}
复制代码
在系统的 Infrastructure 层,主要职责包括处理技术细节问题,如数据库的 CRUD 操作、搜索引擎、文件系统以及分布式服务的 RPC。此外,该层还负责领域防腐,通过 Gateway 对外部依赖进行转义处理,确保外部系统的变化不会直接影响到 Application 层和 Domain 层。
在领域驱动设计(DDD)中,强调了 Repository 操作应当面向领域对象,特别是聚合根。业务操作应该先获取完整的领域对象,执行相应的操作,再通过 Repository 将修改保存回数据库。不建议直接操作部分实体或值对象,以确保领域对象的一致性和完整性(案例:订单和订单明细)。
为了防止底层实现逻辑渗透到业务代码中,Repository 接口应存在于 Domain 层,而不应直接暴露数据对象(DO)的实现。这种做法强化了对业务代码的保护,使其专注于领域对象的操作而不需要关心数据对象的底层实现。
同时在业务系统中,数据一致性是非常重要的。尽管查询完整实体(聚合根)可能带来一定性能开销,但在业务系统中,数据一致性被视为核心目标,而性能通常不是主要问题。为了避免因追求性能而牺牲一致性导致潜在的 bug,维护数据一致性被强调为重要原则。
public interface IAccountRepository { Account find(AccountId id); Account find(AccountNumber accountNumber); Account find(UserId userId); Account save(Account account);}
复制代码
隔离业务复杂度和技术复杂度
我们先看一个简单的案例需求如下:
用户可以通过银行网页转账给另一个账号,支持跨币种转账。
同时因为监管和对账需求,需要记录本次转账活动。
public class TransactionTransferCmdExe { private final IAccountGateway accountGateway; private final IAccountMessageProducerGateway transferMessageProducerGateway; private final IExchangeRateGateway exchangeRateGateway; private final AccountTransferDomainService accountTransferDomainService; private final ITransactionGateway transferTrasactionGateway;
@Transactional(rollbackFor = Exception.class) public SingleResponse<Boolean> execute(AccountTransferCmd cmd) { Money targetMoney = new Money(cmd.getTargetAmount(), new Currency(cmd.getTargetCurrency()));
Account sourceAccount = accountGateway.find(new UserId(cmd.getSourceUserId()));
Account targetAccount = accountGateway.find(new AccountNumber(cmd.getTargetAccountNumber()));
ExchangeRate exchangeRate = exchangeRateGateway.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());
Transaction transaction = accountTransferDomainService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate); accountRepository.save(sourceAccount); accountRepository.save(targetAccount);
// 记录交易记录 Long transferTransactionId = transferTrasactionGateway.save(transaction); // 发送消息用于审计日志、短信通知 TransferTransactionSucceedEvent event = new TransferTransactionSucceedEvent( transferTransactionId, sourceAccount.getId().getValue(), targetAccount.getId().getValue(), cmd.getTargetAmount(), cmd.getTargetCurrency() ); transferMessageProducerGateway.send(event); return SingleResponse.buildSuccess(); }}
复制代码
架构设计——你的依赖反转了吗?
《代码大全》软件工程师的首要技术使命是控制复杂度。
可测试性 &&成本
数据对象视图
适配器架构的组件规范设计
常见问题
数据模型与领域模型不匹配
领域模型和数据库模型之间存在不匹配,例如值对象需要序列化存储。
解决方案:转换器模式-通过 Factory 类实现领域对象与数据库实体的双向转换。
public class OrderFactory { public static OrderPO convert(@NonNull Order order) { OrderPO orderPO = new OrderPO(); orderPO.setId(order.getId()); orderPO.setConsigneeMobile(order.getConsignee().getMobile()); orderPO.setConsigneeName(order.getConsignee().getName()); orderPO.setConsigneeShippingAddress(order.getConsignee().getShippingAddress()); orderPO.setDeductionPoints(order.getDeductionPoints()); orderPO.setSellerId(order.getSellerId()); orderPO.setTotalMoney(order.getTotalMoney()); orderPO.setOrderSubmitUserId(order.getOrderSubmitUserId()); orderPO.setStatus(order.getStatus()); orderPO.setVersion(order.getVersion()); orderPO.setCouponId(order.getCouponId()); return orderPO; }
public static OrderItemPO convert(@NonNull OrderItem item) { OrderItemPO itemPO = new OrderItemPO(); itemPO.setId(item.getId()); itemPO.setGoodsName(item.getGoodsName()); itemPO.setOrderId(item.getOrderId()); itemPO.setGoodsId(item.getGoodsId()); itemPO.setAmount(item.getAmount()); itemPO.setPrice(item.getPrice()); return itemPO; } public static Aggregate<Order> convert(@NonNull OrderPO orderPO, @NonNull List<OrderItemPO> orderItemPOS) { Order order = new Order(); List<OrderItem> orderItems = orderItemPOS.stream().map(item -> new OrderItem(item.getId(), item.getOrderId(), item.getGoodsId(), item.getGoodsName(), item.getAmount(), item.getPrice()) ).collect(Collectors.toList()); order.setVersion(orderPO.getVersion()); order.setDeleted(orderPO.getDeleted()); order.setConsignee(new Consignee(orderPO.getConsigneeName(), orderPO.getConsigneeShippingAddress(), orderPO.getConsigneeMobile())); order.setId(orderPO.getId()); order.setConsignee(new Consignee(orderPO.getConsigneeName(), orderPO.getConsigneeShippingAddress(), orderPO.getConsigneeMobile())); order.setVersion(orderPO.getVersion()); order.setStatus(orderPO.getStatus()); order.setDeductionPoints(orderPO.getDeductionPoints()); order.setCouponId(orderPO.getCouponId()); order.setActualPayMoney(orderPO.getActualPayMoney()); order.setTotalMoney(orderPO.getTotalMoney()); order.setOrderSubmitUserId(orderPO.getOrderSubmitUserId()); order.setDeleted(orderPO.getDeleted()); order.setOrderItems(orderItems); order.setSellerId(orderPO.getSellerId()); return AggregateFactory.createAggregate(order); }}
复制代码
聚合根的持久化性能
由于领域模型与数据库的数据模型可能存在差异,并且聚合通常涉及多个实体,因此直接使用 Hibernate、MyBatis 或 Spring Data 进行聚合持久化时,往往会遇到诸多挑战,不仅实现复杂,代码也显得不够优雅。
有人认为 NoSQL 是更适合聚合持久化的方案,因为在 NoSQL 数据库中,每个聚合实例可以作为一个独立的文档进行存储,这种天然的结构能很好地支持聚合的完整性。然而,NoSQL 并非万能,并不是所有系统都适合采用 NoSQL 方案。在实际应用中,是否选择 NoSQL 取决于具体的业务需求、数据访问模式以及系统的整体架构设计。
//传统模式在业务逻辑层调用数据访问层public class OrderServiceImpl implements IOrderService{ private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; @Override public Long createOrder(OrderCreateCmd cmd){ // // 执行业务处理 // orderMapper.insert(OrderFactory.toOrder(cmd)) orderItemMapper.batchInsert(OrderFactory.toOrderItem(cmd)) }}
复制代码
/**** 应用层**/public class OrderCreateCmdExe{ private final IOrderGateway orderGateway; private final OrderAssembler orderAssembler; private SingleResponse<Long> execute(OrderCreateCmd cmd){ Aggregate<Order> orderAggregate = orderAssembler.assembler(cmd); // // 执行业务处理 // Long id = orderGateway.save(orderAggregate); return SingleResponse.of(id); }}/**** 基础设施层**/public class OrderGateway extends MybatisRepositorySupport implements IOrderGateway { private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper;
@Override public Aggregate<Order> get(OrderId orderId) { OrderPO order = orderMapper.selectById(orderId.getId()); if (orderPO == null) { throw new EntityNotFoundException(String.format("Order (%s) is not found", orderId.getId())); } List<OrderItemPO> orderItem = orderItemMapper.selectList( new LambdaQueryWrapper<OrderItemPO>().eq(OrderItemPO::getOrderId, orderId.getId())); return OrderFactory.convert(order, orderItem); } @Override public Long save(Aggregate<Order> aggregate) { if (aggregate.isNew()) { return create(aggregate); } if (!aggregate.isChanged()) { return aggregate.getRoot().getId(); } return update(aggregate); } private Long create(Aggregate<Order> orderAggregate) { Order order = orderAggregate.getRoot(); super.insert(order, OrderFactory::convert); order.getOrderItems().forEach(orderItem -> { orderItem.setOrderId(order.getId()); super.insert(orderItem, OrderFactory::convert); }); return order.getId(); } private Long update(Aggregate<Order> orderAggregate) { Order order = orderAggregate.getRoot(); Order snapshot = orderAggregate.getSnapshot(); Boolean orderResult = super.executeSafeUpdate(order, snapshot, OrderFactory::convert); Boolean orderItemResult = super.executeListUpdate(order.getOrderItems(), snapshot.getOrderItems(), item -> { item.setOrderId(order.getId()); return OrderFactory.convert(item); }); if (!orderResult && !orderItemResult) { throw new OptimisticLockException(String.format("Update order (%s) error, it's not found or changed by another user", orderAggregate.getRoot().getId())); } return order.getId(); }}
复制代码
https://github.com/654894017/aggregate-persistence
复杂查询没有领域模型
我们经常会遇到一些复杂的查询统计的诉求,展示数据没有领域模型使用 DTO 直接承接数据即可
import com.pupu.onlinereview.reviewsession.gateway@Componentpublic class ReviewSessionQueryGateway implements IReviewSessionQueryGateway{ @Overroid public PageResponse<ReviewSessionDTO> query(ReviewSessionQuery query){ }}
复制代码
复杂查询性能低下
在 DDD 中,领域模型的复杂性可能导致查询性能问题。
解决方案:
领域服务命名规范
领域服务在领域建模中可能不一定存在,“服务”这个词太过宽泛。我们经常看到
ReviewSessionDomainService
OrderDomainService
我可以可以把订单、评审场次相关的所有的职责都分别分配给它们?答案是不合适的,当这些负责的职责越来越多的时候又会回到事务脚本编程的老路。
public class ReviewSessionPermissionValidateService { private final IUserRoleGateway userRoleGateway; private final IReviewSessionGateway reviewSessionGateway; /** * 校验用户是否有权限访问某场次 */ public boolean hasPermission(Long userId, Long sessionId) { ReviewSession session = reviewSessionGateway.findById(sessionId) .orElseThrow(() -> new IllegalArgumentException("评审场次不存在")); // 1. 如果用户是管理员,默认有权限 if (userRoleRepository.isAdmin(userId)) { return true; } // 2. 判断是否是该场次的创建者(管理员或组织者) if (session.isOwner(userId)) { return true; } return false; }}
复制代码
OrderPriceCalculateService 订单价格计算领域服务
class OrderPriceCalculateService{ public BigDecimal calTotalPrice(Order order){ //省略其他业务逻辑代码... return order.getOrderItems().stream().map(e -> e.getPrice().multiply(new BigDecimal(e.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add); }}
复制代码
领域服务的命名: 领域概念 + 行为(动词)+ Service
领域服务禁止数据更新
错误示例:直接在领域服务中调用 Repository 更新数据库
public class AccountTransferDomainService { private final IAccountGateway accountGateway; public Transaction transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate) { Money sourceMoney = exchangeRate.exchageTo(targetMoney); sourceAccount.deposit(sourceMoney); targetAccount.withdraw(targetMoney); //⚠️ 禁止在领域服务内部调用数据存储的行为 accountGateway.save(sourceAccount); accountGateway.save(targetAccount); return new Transaction().toTransferTransaction( sourceAccount.getId(), targetAccount.getId(), sourceMoney ); }}
复制代码
问题:
• 破坏领域层的独立性:领域服务不应该关心数据库存储,save(session) 是基础设施层的职责。
• 不符合 DDD 原则:数据更新应该通过 应用层(Application Service) 调用 Repository。
正确姿势:让应用服务负责数据存储
public class TransactionTransferCmdExe { private final IAccountGateway accountGateway; private final IAccountMessageProducerGateway transferMessageProducerGateway; private final IExchangeRateGateway exchangeRateGateway; private final AccountTransferDomainService accountTransferDomainService; private final ITransactionGateway transferTrasactionGateway; @Transactional(rollbackFor = Exception.class) public SingleResponse<Boolean> execute(AccountTransferCmd cmd) { Money targetMoney = new Money(cmd.getTargetAmount(), new Currency(cmd.getTargetCurrency())); Account sourceAccount = accountRepository.find(new UserId(cmd.getSourceUserId())); Account targetAccount = accountRepository.find(new AccountNumber(cmd.getTargetAccountNumber())); ExchangeRate exchangeRate = exchangeRateGateway.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency()); Transaction transaction = accountTransferDomainService.transfer( sourceAccount, targetAccount, targetMoney, exchangeRate ); accountGateway.save(sourceAccount); accountGateway.save(targetAccount); // 记录交易记录 Long transferTransactionId = transferTrasactionGateway.save(transaction); // 发送消息用于审计日志、短信通知 TransferTransactionSucceedEvent event = new TransferTransactionSucceedEvent( transferTransactionId, sourceAccount.getId().getValue(), targetAccount.getId().getValue(), cmd.getTargetAmount(), cmd.getTargetCurrency() ); transferMessageProducerGateway.send(event); return SingleResponse.buildSuccess(); }}
public class AccountTransferDomainService { public Transaction transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate) { Money sourceMoney = exchangeRate.exchageTo(targetMoney); sourceAccount.deposit(sourceMoney); targetAccount.withdraw(targetMoney); return new Transaction().toTransferTransaction( sourceAccount.getId(), targetAccount.getId(), sourceMoney ); }}
复制代码
改进点
1. 领域服务只处理业务逻辑,不涉及数据库操作。
2. 应用服务负责数据库事务和数据存储,符合 DDD 分层架构。
基础设施层做故障降级、数据缓存
应用层、领域层使用的 Gateway 时,不应该关系数据来源是哪里,应该关系该接口是不是能给我对应的数据即可,依赖的都是抽象接口,从在一定程度可以隔离技术复杂度和业务复杂度。
@RequiredArgsConstructorpublic class ExchangeRateGatewayProxyHandler implements InvocationHandler { private final IExchangeRateGateway exchangeRateGateway; private final IExchangeRateGateway mysqlExchangeRateGateway; @Value("${exchangerate.channel.degraded}") private Boolean isExchangeRateChannelDegraded;
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result; if (isExchangeRateChannelDegraded) { result = method.invoke(mysqlExchangeRateGateway, args); } else { result = method.invoke(exchangeRateGateway, args); } return result; }}
复制代码
public class ExchangeRateGateway implements IExchangeRateGateway { private final RedissonClient redissonClient; @Override @SentinelResource(value = "exchange_rate_resource", fallback = "exchangeRateFallback") public ExchangeRate getExchangeRate(Currency source, Currency target) { ExchangeRate exchangeRate = (ExchangeRate) redissonClient.getBucket(getCacheKey(source, target)).get(); if (exchangeRate == null) { //调用外部服务获取汇率 exchangeRate = getExchangeRate(); redissonClient.getBucket(getCacheKey(source, target)).set(exchangeRate); } return exchangeRate; } private ExchangeRate getExchangeRate(){ //调用外部服务获取汇率 return null; }
private String getCacheKey(Currency source, Currency target) { return String.format("exchange_rate_%s_%s", source.getValue(), target.getValue()); }
/** * @param source * @param target * @return */ public ExchangeRate exchangeRateFallback(Currency source, Currency target) { //读取数据库默认缓存下来的 return new ExchangeRate(BigDecimal.ONE, source, target); }}
复制代码
应用层细分
以用例场景为粒度,按照场景进行类的代码细分,一个 Cmd 对应一个 Executor(执行器)。
责任分离,让 Command 负责输入,Executor 负责执行业务逻辑,从而降低耦合、提高内聚,避免应用层变得臃肿。
良好的扩展性,新增功能时只需添加新的 Command + Executor,不会影响现有代码。同时,该模式提高了 可测试性,Executor 可独立单元测试。
统一的代码标准,让团队成员更易理解业务逻辑,提高代码一致性和可维护性,适用于复杂业务系统。
面向模型编程而不是过程
过程式编程
特点:
•业务逻辑散落在 Service 里,直接操作数据库表。
• 代码依赖 Repository 进行数据更新,没有明确的业务模型。
•容易出现重复代码,扩展性差。
public class OrderCompleteCmdExe { private final IOrderGateway orderGateway; private final IOrderMessageGateway orderMessageGateway; @Transactional public SingleResponse<Boolean> exeute(OrderCompleteCmd cmd) { Order order = orderGateway.get(cmd.getOrderId()); if (order.getStatus() != OrderStatus.PAID) { throw new IllegalStateException("订单未支付,无法完成"); } // 1. 保存订单状态 orderRepository.updatesStatus(cmd.getOrderId, OrderStatus.Completed); // 2. 发送订单完成事件 orderMessageGateway.send( OrderCompletedEvent.builder().orderId(cmd.getOrderId()).build() ); return SingleResponse.buildSuccess(); }}
复制代码
存在的问题
1. 业务逻辑(状态校验)和数据库操作混在一起,违反单一职责原则(SRP)。
2. 直接更新数据库,而不是让“订单”自己更新状态,缺乏业务概念。
3. 扩展困难,如果要新增“已完成”状态,就必须修改 updateOrderStatus 方法。
面向模型编程
特点:
• 业务逻辑封装在 领域对象(Order),让订单自己更新状态。
• OrderService 只负责 协调业务流程,而不是直接操作数据库。
• 符合 DDD(领域驱动设计),使代码结构更加清晰。
public class Order { private String id; private Long userId; private BigDecimal amount; private OrderStatus status; // 订单完成逻辑 public void completeOrder() { if (this.status != OrderStatus.PAID) { throw new IllegalStateException("订单未支付,无法完成"); } this.status = OrderStatus.COMPLETED; }}
public class OrderCompleteCmdExe { private final IOrderGateway orderGateway; private final IOrderMessageGateway orderMessageGateway; @Transactional public SingleResponse<Boolean> exeute(OrderCompleteCmd cmd) { // 1. 查询订单 Order order = orderGateway.findById(cmd.getOrderId()) .orElseThrow(() -> new IllegalArgumentException("订单不存在")); // 2. 订单自己决定是否能完成 order.completeOrder(); // 3. 保存订单状态 orderGateway.save(order); // 4. 发送订单完成事件 orderMessageGateway.send( OrderCompletedEvent.builder().orderId(cmd.getOrderId()).build() ); return SingleResponse.buildSuccess(); }}
复制代码
总结
✅ 订单自己管理状态,符合封装原则(Order.completeOrder() 负责变更状态)
✅ 业务逻辑聚合在 Order 领域对象,Application Service 只负责流程,不做业务逻辑判断
这种方式符合 DDD 的聚合根管理规则,避免直接修改 Order 状态,提升代码的可维护性,同时维护聚合根的一致性和完整性。
总结
DDD 是一种思维方式,不是概念的教条。
附录
快速入门示例
给大家分享一个端口适配器架构的实际案例,不然大家看到一堆 DDD 的名词不知道如何理解。
实际案例:支付业务
领域层(Domain layer):领域对象,例如账户,Acount1,Account2 这种,包括 id,余额,扣款,存入等。
领域服务(Domain Service):领域功能,例如转账,对应"新建一个事务,完成 account1 扣款,account2 存入"这系列操作。
应用层(Application layer):对应上层业务,比如说支付宝的"扫码支付"、"红包"、"AA"、"转账"是不同的应用,但是实现资金转移的时候底层都可以用转账 Domain Service 进行处理。
适配器层(Adapter layer):处理小程序、app、钉钉的各种不同交互。
评论