重读 Eric Evans 的 DDD 蓝皮书
引言
软件系统的复杂性往往被误解为技术问题。在多年的开发实践中,我们习惯了用各种设计模式、架构框架、技术栈来应对复杂性,却忽略了一个根本事实:软件的核心复杂性在于领域本身,而非技术实现。Eric Evans 在《领域驱动设计》一书中提出了这一深刻见解,并给出了一套系统化的方法论——领域驱动设计。
DDD 不是一套必须严格遵守的规范,也不是一个可以简单套用的框架,它是一种思维方式,一种通过深入理解业务领域来构建高质量软件的方法论。重读这本经典著作,让我们重新审视这些核心理念。
通用语言
通用语言是 DDD 的基石。开发团队与领域专家共享同一套语言,这套语言不仅用于沟通,也体现在代码中。代码即文档,文档即代码。
在传统开发模式中,业务专家使用业务术语,技术人员使用技术术语,两者之间存在天然的语言鸿沟。业务需求被翻译成技术文档,技术文档又被翻译成代码,每个翻译环节都会产生信息丢失和误解。通用语言的提出,就是为了消除这种鸿沟。
通用语言应该由开发和业务专家共同构建,在持续的交流中不断完善。它体现在所有方面:需求讨论、文档、代码、测试。当领域专家能够阅读代码并理解其含义时,当开发人员能够直接使用领域术语编写代码时,通用语言才算真正建立起来。
战略设计
战略设计关注的是系统的整体结构和各个部分之间的关系,它帮助我们决定如何划分系统边界、如何组织团队。
限界上下文
同一个概念在不同上下文中有不同含义。限界上下文明确定义了特定领域模型适用的边界,在这个边界内,所有术语都有明确且唯一的含义。
以电商系统为例,“产品"这个概念在销售上下文中可能指销售的商品,包含价格和促销信息;在库存上下文中可能指库存管理的物理商品,包含数量和位置信息;在财务上下文中可能指成本核算的对象,包含成本和利润信息。它们都是"产品”,但含义完全不同。
限界上下文的重要性在于,它帮助我们避免将不同上下文的概念混在一起,从而避免模型的混乱。每个限界上下文都有自己的领域模型,这些模型之间通过明确的接口进行交互。
上下文映射
上下文映射描述了不同限界上下文之间的关系。Eric Evans 定义了多种关系模式,每种模式适用于不同的场景。
共享内核是指两个或多个上下文共享一部分模型和代码,适用于上下文之间高度耦合且需要紧密协作的情况。但共享内核会增加维护成本,需要谨慎使用。
客户-供应商关系中,上游上下文为下游上下文提供服务,双方建立明确的契约。上游必须考虑下游的需求,下游则依赖上游的稳定性。
防腐层用于隔离外部系统或遗留系统的不一致性。通过在边界处建立翻译层,将外部模型翻译成内部模型,保护内部领域模型不受外部污染。
开放主机服务通过定义正式的协议和接口,让其他上下文能够方便地集成。发布语言则是开放主机服务的具体实现形式,通常是文档化的 API 或消息格式。
各行其道适用于两个上下文之间几乎没有交集的情况,各自独立演化,互不干扰。
顺从者是指下游上下文完全采用上游上下文的模型,放弃自己的模型。这适用于上游模型已经非常完善且完全符合下游需求的情况。
战术设计
战术设计关注如何在限界上下文内构建有效的领域模型,它提供了一组模式来帮助我们建模和实现。
实体
实体是具有唯一标识和生命周期的对象。两个实体即使所有属性都相同,只要标识不同,它们就是不同的实体。
实体标识的选择至关重要。标识应该是稳定的、唯一的,并且在实体的整个生命周期中保持不变。常见的标识类型包括自然标识和代理标识。自然标识直接来源于业务,如身份证号、订单号;代理标识则是系统生成的,如数据库自增 ID、UUID。
实体的生命周期管理是另一个重要议题。实体的创建、状态变更、删除都需要在领域模型的控制下进行,确保业务规则得到遵守。
值对象
值对象是无标识、不可变、通过属性相等性判断的对象。值对象关注的是"是什么"而不是"是谁"。
值对象的不可变性意味着一旦创建就不能修改,如果需要改变,就创建新的值对象。这使得值对象天然线程安全,可以放心地在不同上下文中共享和使用。
值对象通过属性值来判断相等性,而不是通过标识。两个值对象如果所有属性都相同,它们就是相等的。这使得值对象非常适合用于描述领域的概念,如金额、时间范围、地址等。
值对象可以包含其他值对象,形成复杂的值对象结构。这种组合能力使得我们能够用值对象精确地表达领域的概念。
聚合与聚合根
聚合是一组相关对象的集合,它们作为一个整体被访问和修改。聚合根是聚合的入口点,外部只能通过聚合根来访问聚合内部的对象。
聚合定义了一致性边界。聚合内部的对象之间保持一致性,聚合之间的对象不需要保持强一致性。聚合同时也是事务边界,所有对聚合的修改都应该在单个事务中完成。
聚合的设计需要权衡一致性和性能。过大的聚合会降低并发性能,过小的聚合会增加复杂性。一个实用的原则是,聚合应该尽量小,但必须保证业务不变量。
聚合根负责维护聚合内部的一致性。所有对聚合的修改都必须通过聚合根进行,聚合根确保修改不会破坏业务规则。
领域服务
领域服务包含不属于任何实体或值对象的领域逻辑。当某个操作涉及多个实体或值对象,或者这个操作本身就是一个重要的领域概念时,就应该考虑使用领域服务。
领域服务是无状态的,它接收领域对象作为参数,执行业务逻辑,返回结果。领域服务不应该包含基础设施相关的逻辑,如数据库访问、网络调用等。
领域服务的命名应该反映领域的概念,而不是技术实现。一个好的领域服务名称应该让业务专家也能理解。
领域事件
领域事件记录了领域中发生的有意义的事情。它表示过去发生的事情,通常用于触发其他业务流程或更新其他聚合。
领域事件应该是不可变的,它记录事件发生时的状态。事件的命名应该使用过去式,如 OrderCreated、PaymentCompleted。
领域事件是解耦的重要手段。通过领域事件,不同的聚合可以在不直接依赖的情况下进行协作。一个聚合发生变更后发布领域事件,其他聚合订阅这些事件并做出响应。
仓储
仓储是聚合的持久化抽象。它封装了数据访问的细节,让领域层不依赖具体的数据存储技术。
仓储应该只对聚合根提供访问,而不是对聚合中的每个对象都提供访问。这确保了聚合的一致性边界得到遵守。
仓储接口定义在领域层,实现在基础设施层。这种分离使得领域模型保持纯净,不依赖于具体的技术实现。
工厂
工厂负责创建复杂对象。当对象的创建逻辑复杂,或者需要确保创建过程中的业务规则得到遵守时,就应该使用工厂。
工厂可以是一个简单的方法,也可以是一个专门的类。重要的是,它将对象的创建逻辑集中管理,避免创建逻辑散落在各处。
分层架构
DDD 推荐使用分层架构来组织代码,每一层都有明确的职责。
用户接口层负责处理用户请求,将用户输入转换为应用层可以理解的格式,并将应用层的返回结果转换为用户友好的格式。这一层可以是 Web 界面、命令行界面或其他形式的用户界面。
应用层负责协调领域对象完成业务用例。它不包含业务逻辑,只是将各个领域对象组织起来完成用户请求。应用层是领域模型的门面,对外暴露简洁的接口。
领域层包含核心的业务逻辑,这是整个系统的核心。领域层不依赖任何其他层,它是最独立、最纯净的层。
基础设施层提供技术支持,如数据持久化、消息发送、外部服务调用等。基础设施层为其他层提供服务,但其他层不依赖基础设施层的具体实现。
这种分层架构使得各层职责明确,依赖关系清晰,有利于系统的维护和演化。
DDD 与微服务的关系
微服务架构的兴起让 DDD 重新受到关注。限界上下文天然地成为微服务边界的指导原则。
每个限界上下文可以独立部署为一个微服务,微服务之间的通信对应上下文映射的关系模式。共享内核可能意味着服务之间共享数据库或代码,防腐层对应服务之间的适配器,开放主机服务对应服务之间的 API 契约。
DDD 强调的边界划分、独立性、团队自治,与微服务的理念高度契合。但需要注意的是,DDD 是指导微服务划分的方法,而不是强制要求每个限界上下文都必须是一个微服务。服务拆分需要综合考虑技术、组织、运维等多种因素。
DDD 的常见误区
在实践中,DDD 经常被误解和误用,最常见的误区包括过度设计、忽视通用语言、把 DDD 当作技术框架而非思维方式。
过度设计是指在简单的业务中强制使用 DDD 的所有模式。DDD 不是银弹,它适合处理复杂的业务领域。对于简单业务,使用 DDD 可能会增加不必要的复杂性。
忽视通用语言则体现在开发人员仍然使用技术术语进行思考和沟通,代码中充斥着技术细节而非领域概念。这种情况下,DDD 只是换了一种写代码的方式,而没有真正理解业务。
把 DDD 当作技术框架是另一个常见误区。DDD 不是 Spring、不是 Hibernate、不是任何技术框架,它是一种思维方式。学习 DDD 不是学习如何使用某个框架,而是学习如何通过深入理解业务来构建更好的软件。
实践建议
DDD 适合在以下场景中使用:业务领域复杂、业务规则复杂、业务逻辑经常变化、团队规模较大。在这些场景中,DDD 能够帮助团队更好地理解业务、构建更易维护的系统。
在团队中推广 DDD 需要循序渐进。首先要建立通用语言,让团队养成使用业务术语思考和沟通的习惯。然后可以从小范围开始实践,选择一个合适的限界上下文,应用 DDD 的战术模式。随着经验的积累,再逐步推广到整个系统。
DDD 的学习曲线较陡峭,需要团队投入足够的时间和精力。但一旦掌握,它将帮助团队构建出真正符合业务需求的软件系统。
结语
重读 Eric Evans 的 DDD 蓝皮书,最大的收获不是学习了一套新的技术,而是获得了一种新的思维方式。软件的核心在于领域,技术只是实现手段。通过深入理解业务领域,使用通用语言进行思考和沟通,我们能够构建出真正有价值的软件系统。
DDD 不是终点,而是起点。它引导我们走上持续学习和改进的道路,在这个道路上,我们不断加深对业务的理解,不断优化我们的设计和实现。这就是领域驱动设计的真正价值所在。


