在软件开发中,我们常常面临这样的困境:系统随着需求迭代逐渐变得臃肿、难以维护,最终沦为”屎山代码”。业务逻辑散落各处、模块边界模糊,修改一处牵一发而动全身——这就是软件退化(Software Erosion)。
如何在业务快速变化中保持架构的灵活性,防止系统走向混乱?领域驱动设计(Domain-Driven Design, DDD)正是为此而生。
DDD 是一种演进式架构思想,通过通用语言、界限上下文、聚合等核心概念,让代码结构与业务领域保持一致。它不仅是微服务架构设计的指导思想,更是构建可演进、可维护系统的有效方法论。
什么是微服务
传统软件项目大多采用单体架构,所有功能模块集成在一个应用程序中。单体结构有结构简单、部署简单等优点,但有如下缺点:
- 代码间耦合严重,模块间相互依赖,牵一发而动全身。
- 项目只能采用单一语言和技术栈
- 扩展性差,只能对整体进行服务器扩容,无法对单一模块进行服务器扩容
- 任何改动都需要重新部署整个应用
微服务架构把项目拆分多个应用程序,每个服务独立开发、部署和运行。
微服务架构有如下优点
- 高内聚低耦合,业务逻辑清晰,对其他微服务依赖低
- 不同微服务可用不同语言和技术栈
- 一个微服务运行不会影响其他微服务
- 一个服务出错不会导致整个系统崩溃
- 可针对高负载服务单独扩容
缺点
- 运维工作带来更大挑战,需要管理多个服务的部署、监控和日志
- 需要处理服务间通信、容错、负载均衡等问题
- 跨服务的事务处理和数据一致性难以保证
…
微服务架构误区
设计不好的微服务架构中,微服务之间调用关系非常复杂,一个请求经过七八层微服务调用,这样的设计不仅导致系统间耦合严重,还使服务器端处理效率地下。
微服务架构应该是进化而来的。在进行系统架构设计时,我们应该认真思考“这个项目真的需要微服务架构吗”。
DDD 与微服务
DDD 为微服务拆分提供了理论指导:
- 限界上下文用于划分服务边界
- 聚合用于设计服务粒度
- 领域事件用于服务间解耦通信
通过 DDD 的指导,微服务拆分变得有章可循,避免服务划分过细或过粗的问题。
领域与领域模型
领域(Domain) 是指一个组织所从事的业务范围和内容。例如电商领域包括商品管理、订单处理、支付结算、物流配送等业务。
领域模型(Domain Model) 是领域确定后,从领域内对象建模抽象出模型的概念。建模是DDD中非常核心的事情,一旦定义出领域模型,我们就可以用领域模型驱动项目开发。
DDD 将系统划分为四层:
- 用户接口层(User Interface)(表现层):
- 负责处理所有外部流量的入口,包括用户界面(如Web前端)、API接口(如REST、gRPC)以及消息队列等。
- 主要职责是接收用户请求和展示数据,并将数据转换为应用层能处理的格式。
- 应用层(Application Layer):协调领域对象完成用户任务,不包含领域逻辑
- 领域层(Domain Layer):包含核心业务逻辑和数据模型,是系统的核心
- 基础设施层(Infrastructure Layer):为其他层提供技术支持,如数据持久化、消息队列、外部服务调用等
领域层是 DDD 的核心,所有业务规则都应该在领域层实现,而不是散落在应用层或基础设施层。
通用语言与界限上下文
通用语言是指团队中所有人(包括开发人员、产品经理、业务专家)都使用相同的术语来描述业务概念。通用语言应该体现在代码、文档、对话中。
例如,在电商系统中:
- 不要用
User来指代买家,应该明确使用Customer(客户)或Buyer(买家) - 不要用
Order的状态字段status=5表示已完成,应该使用OrderStatus.Completed
通用语言的好处:
- 减少沟通成本和理解偏差
- 让代码更具可读性和可维护性
- 业务变化时,代码能够快速响应
界限上下文是对业务领域的明确边界划分。在不同的界限上下文中,同一个术语可能有不同的含义。
例如,”商品”在不同上下文中的含义:
- 商品目录上下文:商品包含名称、描述、分类、图片等信息
- 库存上下文:商品只关注 SKU、库存数量、仓库位置
- 订单上下文:商品包含价格、购买数量、优惠信息
界限上下文帮助我们:
- 明确服务边界,为微服务拆分提供依据
- 避免统一大模型导致的复杂性
- 让不同团队可以独立开发各自的上下文
实体类与值对象
在领域模型中,实体(Entity)和值对象(Value Object)是构成领域模型的基本元素,二者的核心区别在于是否具有唯一标识。
实体是具有唯一标识的领域对象,其生命周期中标识保持不变,即使属性发生变化,也依然是同一个实体,业务上有独立存在意义。
例如:Order(订单)、Customer(客户)
1 | public class Order |
值对象是通过属性值来定义的对象,没有唯一标识,属性完全相同则视为同一个对象,一般生命周期短。
例如:Address(地址)
1 | public class Address |
实体和值对象的区分有助于合理建模、简化业务逻辑。
聚合与聚合根
当领域模型中的实体和值对象数量增多时,如何保证它们之间的一致性?DDD 通过聚合(Aggregate) 和聚合根(Aggregate Root) 解决这一问题。
聚合是一组紧密关联的实体和值对象的集合,它们作为一个整体负责维护业务规则(不变量),确保数据一致性。
特征:
- 一组相关对象(实体和值对象)的集合,作为数据的一致性边界
- 聚合内部对象只通过聚合根与外界交互
- 聚合保证内部业务规则和事务一致性
该聚合的不变量是 “订单总金额 = 所有订单项金额之和”,无论添加、删除订单项,这一规则必须始终成立。
聚合根是聚合的 “入口点”,是聚合中唯一允许外部直接访问的实体,负责协调聚合内的对象,维护聚合的完整性。
特征:
- 是聚合中唯一具有全局唯一标识的实体
- 外部对象只能通过聚合根的 ID 引用聚合,不能直接访问聚合内的其他实体或值对象
- 聚合根负责聚合内对象的生命周期管理(创建、修改、删除)
示例:
订单聚合(Order为聚合根) ≈ Order(订单)+ OrderItems(订单行项)+ ShippingAddress(收货地址)。
聚合设计原则:
- 单个聚合尽量小,只解决核心业务一致性
- 跨聚合一致性用最终一致性、领域事件等解决
领域服务与应用服务
在 DDD 的分层架构中,领域层和应用层分别通过领域服务和应用服务处理业务逻辑,但二者的职责有明确区分。
领域服务是领域层的组件,用于封装不属于单个实体或值对象的领域逻辑。当一个业务操作涉及多个实体 / 值对象协作时,适合用领域服务。
特征:
- 包含纯领域逻辑,与技术实现无关
- 通常以动词命名(如CalculateOrderDiscount、SettleAccount)
- 输入和输出多为领域对象(实体、值对象、聚合根)
应用服务是应用层的组件,负责协调领域对象完成用户任务,不包含领域逻辑,主要承担流程编排的角色。
特征:
- 面向用户用例(如 “创建订单”“取消支付”),以用例命名(如CreateOrderService)
- 调用领域层(实体、聚合根、领域服务)完成业务,自身不处理具体业务规则
- 处理跨切面关注点(如事务管理、权限校验、日志记录)
- 负责数据转换(将用户接口层的 DTO 转换为领域对象,再将领域对象转换为 DTO 返回)
二者的区别在于领域服务处理 “如何做”(业务规则),属于领域核心,应用服务处理 “做什么”(流程步骤),属于协调层。
领域事件与集成事件
在复杂领域中,对象间的协作往往需要 “事件驱动” 来解耦。DDD 通过领域事件和集成事件实现不同范围的事件通信。
领域事件用于表达领域模型中某个有意义的事件刚刚发生,解耦了聚合间、模块间的强依赖关系。
特征:
- 属于领域层,由聚合根在状态发生关键变化时发布(如订单创建、支付完成)
- 命名格式通常为 “[实体][动作] Event”(如OrderCreatedEvent、PaymentCompletedEvent)
- 包含事件发生的时间、相关实体 ID 等关键信息
- 上下文内的其他组件(如领域服务、聚合根)可订阅事件并作出响应
集成事件是跨限界上下文(微服务) 的事件,用于实现不同上下文间的异步通信,解决服务间的耦合问题。
特征:
- 属于基础设施层或应用层,由应用服务将领域事件转换而来
- 包含跨服务通信所需的最小信息(避免传递领域对象细节)
- 通过消息队列(如 RabbitMQ、Kafka)等中间件传递,保证可靠性
- 用于触发其他上下文的业务操作(如订单支付后通知物流服务发货)
领域事件是集成事件的源头,集成事件是领域事件的 “跨域版本”。通过这种分层设计,既保证了限界上下文内的高内聚,又实现了跨上下文的低耦合协作。
推荐书籍
- 《ASP.NET Core 技术内幕与项目实战》-杨中科
- 《领域驱动设计:软件核心复杂性应对之道》-Eric Evans,「领域驱动设计之父」
- 《实现领域驱动设计》-Vaughn Vernon