1. DDD 与 Bounded Context 概述
1.1 领域驱动设计核心概念
领域驱动设计(Domain-Driven Design,DDD)是一种以业务领域为核心的软件设计方法论。它强调软件模型必须与业务专家的心智模型保持一致,从而让代码成为业务知识的精确表达。DDD 的核心不在于技术栈,而在于如何通过建模来管理业务复杂度。
DDD 三大支柱:
- 通用语言(Ubiquitous Language) — 团队(业务 + 技术)使用同一套术语沟通,术语直接映射到代码中的类、方法、变量名。
- 限界上下文(Bounded Context) — 每个模型只在明确的边界内有效,边界内术语有精确含义,边界外可存在不同解释。
- 战略设计(Strategic Design) — 识别核心域、支撑域、通用域,并定义上下文之间的集成关系。
在 Go 语言实践中,DDD 帮助团队避免“大泥球”架构——即所有业务逻辑混在一个包或模块中,导致修改一处影响全局。通过引入限界上下文,每个 Go 模块只负责一个清晰的业务子域,模块间通过接口或事件通信,实现高内聚低耦合。
1.2 Bounded Context 定义
限界上下文是 DDD 中最关键的战术概念。它定义了一个模型适用的显式边界。在边界内部,通用语言的所有术语都有唯一且明确的含义;在边界外部,同一个词可能代表完全不同的事物。
| 概念 | 说明 | Go 模块示例 |
|---|---|---|
| 限界上下文 | 模型生效的显式边界 | internal/order、internal/payment |
| 通用语言 | 边界内统一的业务术语 | 订单上下文中的“Order”包含商品、金额、状态 |
| 实体(Entity) | 有唯一标识、生命周期可追踪的对象 | type Order struct { ID string } |
| 值对象(Value Object) | 无标识、由属性值定义的对象 | type Money struct { Amount float64; Currency string } |
| 聚合(Aggregate) | 一组实体和值对象的组合,通过聚合根访问 | Order 作为聚合根管理 OrderItem |
| 领域服务(Domain Service) | 无状态、处理跨实体业务逻辑 | type OrderService struct {} |
| 领域事件(Domain Event) | 业务中发生的重要事件 | OrderCreated、PaymentCompleted |
实践提示: 在 Go 项目中,一个限界上下文通常对应
internal/ 下的一个顶级子包。例如 internal/order/ 包含该上下文的所有实体、值对象、领域服务、仓储接口和事件定义。不要将多个上下文的代码混放在同一个包中。
1.3 为什么需要边界
没有边界的软件系统会逐渐退化为“大泥球”架构。以下是缺乏边界带来的典型问题:
- 术语冲突 — 同一个词在不同业务场景下含义不同。例如“客户”在销售上下文指潜在买家,在售后上下文指已签约用户。没有边界,代码中
Customer结构体被迫包含所有场景的字段,变得臃肿且难以维护。 - 耦合爆炸 — 所有业务逻辑互相依赖,修改一个功能可能影响多个不相关的模块。Go 编译虽然快,但逻辑耦合会导致测试覆盖困难、回归风险高。
- 团队协作阻塞 — 多人同时修改同一个包,合并冲突频繁,代码审查难以聚焦。每个团队无法独立演进自己的模块。
- 技术债务累积 — 没有边界意味着没有清晰的模块职责,新需求往往被塞进现有代码中,导致代码质量持续下降。
常见误区: 不要将微服务架构中的“服务”直接等同于限界上下文。一个限界上下文可以拆分为多个微服务,也可以作为一个单体模块存在。边界是逻辑概念,部署形态是物理选择。在 Go 单体项目中,限界上下文表现为清晰的包边界;在微服务架构中,每个服务通常对应一个或多个限界上下文。
下图展示了限界上下文如何将复杂的电商系统拆分为多个独立的业务模块,每个模块拥有自己的模型和通用语言:
上图中,每个彩色矩形代表一个独立的限界上下文。上下文内部包含实体、值对象、领域服务和领域事件。上下文之间通过领域事件(虚线箭头)进行异步通信,避免了直接调用带来的耦合。例如,订单上下文发布 OrderCreated 事件后,支付上下文和库存上下文各自独立处理自己的业务逻辑。
1.4 边界在 Go 项目中的落地
在 Go 项目中,限界上下文通过包结构体现。以下是一个典型的目录组织方式:
internal/
├── order/ # 订单上下文
│ ├── entity/
│ │ ├── order.go
│ │ └── order_item.go
│ ├── valueobject/
│ │ └── shipping_address.go
│ ├── service/
│ │ └── order_service.go
│ ├── repository/
│ │ └── order_repository.go
│ └── event/
│ └── order_created.go
├── payment/ # 支付上下文
│ ├── entity/
│ │ ├── payment.go
│ │ └── transaction.go
│ ├── valueobject/
│ │ └── money.go
│ ├── service/
│ │ └── payment_service.go
│ ├── repository/
│ │ └── payment_repository.go
│ └── event/
│ └── payment_completed.go
├── inventory/ # 库存上下文
│ ├── entity/
│ │ ├── product.go
│ │ └── warehouse.go
│ ├── valueobject/
│ │ └── stock_level.go
│ ├── service/
│ │ └── inventory_service.go
│ ├── repository/
│ │ └── inventory_repository.go
│ └── event/
│ └── stock_reserved.go
└── user/ # 用户上下文
├── entity/
│ └── user.go
├── valueobject/
│ └── profile.go
├── service/
│ └── auth_service.go
├── repository/
│ └── user_repository.go
└── event/
└── user_registered.go
关键原则: 每个上下文内的包只对外暴露接口(interface)和事件结构体。实体、值对象和领域服务默认不导出(小写首字母),通过工厂函数或仓储接口创建。上下文之间禁止直接引用对方的实体类型,只能通过事件或数据传输对象(DTO)通信。
通过这种方式,每个 Go 模块成为一个独立的业务单元,可以独立测试、独立演进。当业务规则变化时,修改范围被限制在单个上下文内,不会引发连锁反应。这正是限界上下文带来的核心价值——用边界管理复杂度,让代码成为业务的可执行文档。