大厂文章学习《DDD在大众点评交易系统演进中的应用》
大厂文章学习《DDD在大众点评交易系统演进中的应用》
原文链接:DDD在大众点评交易系统演进中的应用 - 美团技术团队
DDD的过程总结
如下四步:识别问题域-》限界上下文-》领域建模-》模型实现。
不同限界上下文之间如何映射(转换)
原文:限界上下文封装了按照纵向切分的业务能力,那多个限界上下文如何协作来完成一个完整的业务场景呢,这就涉及到限界上下文的映射,按照通信集成模式和团队协作模式来划分,有多种映射关系,这里面我们用到最多的是通过防腐层、开放主机服务和发布语言三者联动来隔离上下游的变化、维护整个领域模型的稳定性。
Q:这里面提到「通过防腐层、开放主机服务和发布语言三者联动来隔离上下游的变化」,其中「防腐层、开放主机服务和发布语言」分别是什么意思?怎么应用?
A:
-
防腐层(Anti-Corruption Layer,ACL):将外部系统的数据格式转换为本地领域模型能理解的格式。即调用下游服务的时候,对下游服务的返回值,需要通过一个防腐层来转化成领域模型,而不是直接拿来就用。
-
开放主机服务(Open Host Service,OHS):系统对外提供的标准化服务接口。即作为被调方,不要直接返回领域模型,而应该加一个「格式化」,格式化成标准的、统一的输出格式。
-
发布语言(Published Language,PL):发布语言是系统间通信使用的公共语言或数据格式。即就是调用者和被调用者双方都能理解的业务含义和数据结构的制定。
总结:在不同限界上下文、不同系统之间,在领域模型转换到数据的时候,最好都添加一层「转换层」,进行隔离,减少领域模型更改对外部的影响和外部修改对领域模型的影响。其中防腐层(ACL)指的是调用外部服务加的转换层、开放主机服务(OHS)指的是被调用时对外数据加的转换层。
代码示例:
// 传统Service
@Service
public class UserService {// 防腐层:处理外部系统数据public User getUserFromExternalSystem(String externalUserId) {// 调用外部系统ExternalUserDTO externalUser = externalUserClient.getUser(externalUserId);// 防腐层:转换为本地领域模型return convertToLocalUser(externalUser);}// 开放主机服务:对外提供标准接口public UserResponseDTO getUserForDownstream(Long userId) {User user = userDAO.findById(userId);// 发布语言:转换为标准输出格式return convertToPublishedFormat(user);}private User convertToLocalUser(ExternalUserDTO external) {// 防腐层转换逻辑return User.builder().name(external.getUserName()) // 字段映射.email(external.getEmailAddr()) // 字段重命名.status(mapStatus(external.getState())) // 状态转换.build();}
}
领域建模
原文:在领域建模阶段,我们整体上分为领域分析建模和领域设计建模。首先,主要是对用例以及用例规约和用户故事进行详细的分析,从中通过名词法和动词法寻找领域概念来构建我们的领域分析模型。在此基础上,我们基于DDD战术设计的元模型,识别出这些概念中的实体和值对象,并且根据业务规则的不变性设计聚合。
以订单为例,这里是我们简化之后的模型,包括订单、支付单、履约单、凭证以及退款单这样几个聚合,在存在状态变化时,聚合之间通过领域事件进行协作。
点评:聚合是通过领域事件进行协作的,从图中看起来基本都是类似于事件驱动、或者说观察者设计模式的设计方式。
Q:以上面图为例,其中有订单实体和联系人值对象,其在代码里面具体是怎么表示的?是dbmodels类直接使用还是创建了一个domain object类来使用。
模型实现
模型实现主要是从领域建模如何转向具体代码实现。
模型实现的最终目的是拆分业务活动,将业务活动转化为代码实现,在前面一系列流程:理解问题域、领域建模 的目的拆分业务流程到领域模型,最终还需要落实到代码上。
具体的流程可以大概参考下面图片,拆解业务流程之后,我们按照一定的映射关系将其映射到用户接口、应用服务、领域服务、聚合和端口的实现上。
这里再回顾下DDD设计的分层架构:
Q:「我们按照一定的映射关系将其映射到用户接口、应用服务、领域服务、聚合和端口的实现上。」其中「端口」是什么意思?
A: 「端口」 ,这是一个在领域驱动设计(DDD)中,特别是与六边形架构(Hexagonal Architecture) 或清洁架构(Clean Architecture) 结合时非常关键的概念。简单来说, 「端口」就是系统与外部世界交互的契约(Contract)或接口(Interface), 可以把它想象成电脑主机上的USB端口、HDMI端口或网线接口。在代码层面,「端口」通常表现为一个接口(Interface)。
通过引入「端口」(接口),领域层不再直接依赖具体的数据库实现(如MySQLOrderRepository
)或具体的第三方服务(如AlipayPaymentService
),而是依赖于自己定义的抽象接口(如OrderRepository
接口和PaymentService
接口)。
这样一来,依赖关系就发生了“倒置”:
- 传统: 领域层 -> 依赖 -> 具体的MySQL实现
- **DDD+六边形架构:** 领域层 -> 依赖 ->
OrderRepository
接口 <- 实现 <- 具体的MySQL实现
具体代码实现上,首先服务对外提供的一般就是接口,其次repository都是接口来对外提供实现(如下面代码范例),这样可以做到依赖倒置(DIP,不依赖于具体的存储实现,而是依赖于抽象)。
// 输出端口:用户仓库接口(系统需要持久化用户的能力)
public interface UserRepository {User findById(UserId id);User findByUsername(String username);void save(User user);
}// 输出端口:消息发送接口(系统需要发送消息的能力)
public interface MessageSender {void sendWelcomeEmail(EmailAddress emailAddress);
}
然后Service是通过接口来实现功能。
总结
DDD是一种开放的思想体系,其核心在于通过领域模型的建立来引导整个设计过程。
- 领域建模是一个动态的、迭代的过程,而非一成不变的瀑布式流程。这个过程类似于一个建模涡流,从战略设计到战术设计,不断迭代。在战术设计过程中,如果发现某些方面不合理,就需要对战略设计做出调整。同样,子域的划分和限界上下文的识别也是动态的,需要根据新的发现不断优化。
- DDD不强迫采用特定的架构模式,它关注的是业务与技术复杂性是否得到了有效分离。无论是整洁架构、六边形架构还是传统的DDD分层架构,只要能够实现这一目标,它们都是可行的选择,即便是采用MVC分层架构,只要能够分离业务和技术复杂性,也同样适用。
- DDD架构需要注意DIP和常见分层架构:接口层、应用层、服务层、领域层、基础层。
最后,我们来简要强调一下工程师的思维模型,这些在领域驱动设计(DDD)的实施过程中也至关重要。一方面,工程师需要培养用户思维、业务思维和产品思维,这有助于深入理解业务和问题域。基于这样的理解,工程师可以运用结构化思维来分解问题,并通过抽象思维来提炼模型。另一方面,结合分层、分治和工程思维,工程师可以有效地将设计转化为实际的代码实现。