四、DDD四层架构详解:用户接口层、应用层、领域层、基础设施层的职责与依赖规则
领域驱动设计(DDD)的四层架构是构建复杂业务系统的核心骨架。它将代码按照职责划分为四个清晰的层次:用户接口层、应用层、领域层和基础设施层。理解每一层的职责以及它们之间的依赖规则,是进行有效限界上下文划分和模块设计的前提。
核心原则:依赖倒置(Dependency Inversion Principle, DIP)。高层模块(领域层)不依赖低层模块(基础设施层),两者都依赖抽象(接口)。这确保了领域层的纯粹性与可测试性。
4.1 四层架构总览与依赖方向
下图展示了DDD四层架构的标准分层及其依赖关系。箭头方向代表“依赖”方向,即“知道”或“调用”的方向。
4.2 各层职责详解
4.2.1 用户接口层 (User Interface Layer)
这是系统的对外窗口,负责处理与外部用户或系统的交互。在Go Web应用中,它通常表现为HTTP Handler、gRPC服务端或消息监听器。
- 核心职责:请求解析、参数校验、响应渲染、会话管理。
- 典型组件:Controller、Handler、Middleware、DTO(数据传输对象)、View(模板)。
- 关键约束:不包含任何业务逻辑。它只负责将用户请求“翻译”成应用层能理解的指令,并将应用层的返回结果“翻译”成用户期望的格式。
Go 实践:在Gin或Echo框架中,Handler函数应保持精简。它们调用应用层服务,并处理HTTP状态码和JSON序列化。业务规则、数据校验(除基础格式外)不应出现在这里。
4.2.2 应用层 (Application Layer)
应用层是“薄”层,它不包含业务规则,而是负责协调领域对象来完成一个用户用例。它是用户接口层和领域层之间的桥梁。
- 核心职责:事务管理、安全授权、任务调度、事件发布、领域对象编排。
- 典型组件:Application Service、DTO、Event Publisher。
- 关键约束:应用服务本身不包含复杂的业务判断。它获取领域对象(通过Repository接口),调用领域对象的方法,然后保存结果。它决定“什么时候做”,而领域层决定“怎么做”。
// 应用层服务示例
type OrderApplicationService struct {
orderRepo domain.OrderRepository
productRepo domain.ProductRepository
eventBus domain.EventBus
}
func (s *OrderApplicationService) PlaceOrder(ctx context.Context, cmd PlaceOrderCommand) error {
// 1. 获取领域对象
product, err := s.productRepo.FindByID(ctx, cmd.ProductID)
if err != nil {
return err
}
// 2. 委托领域对象执行业务逻辑
order, err := domain.CreateOrder(cmd.UserID, product, cmd.Quantity)
if err != nil {
return err
}
// 3. 保存领域对象
if err := s.orderRepo.Save(ctx, order); err != nil {
return err
}
// 4. 发布领域事件
s.eventBus.Publish(ctx, order.Events()...)
return nil
}
4.2.3 领域层 (Domain Layer)
这是DDD的核心,承载了所有业务逻辑和业务规则。它是软件中最有价值、最稳定、最需要被保护的部分。
- 核心职责:表达业务概念、执行业务规则、维护业务状态的一致性。
- 典型组件:Entity、Value Object、Aggregate、Domain Service、Domain Event、Repository接口、Factory。
- 关键约束:领域层是独立的,不依赖任何框架、数据库、外部服务或用户接口。它只依赖纯Go标准库和自身定义的接口。这种独立性使得领域层可以被单独测试和复用。
常见误区:将数据库ORM的注解(如GORM的Tag)或序列化标签写在领域实体上。这会污染领域层,使其与基础设施耦合。领域实体应该是纯粹的Go结构体,只包含业务属性和行为。
4.2.4 基础设施层 (Infrastructure Layer)
这一层为其他层提供技术支持,包括数据持久化、消息发送、文件存储、外部API调用等。它实现了领域层定义的接口。
- 核心职责:技术实现、资源访问、第三方集成。
- 典型组件:Repository实现(如GORM、SQLx)、消息队列客户端、缓存客户端(如Redis)、HTTP客户端。
- 关键约束:基础设施层“向上”依赖领域层(实现其接口),而不是被领域层依赖。这种依赖倒置保证了领域层的纯洁性。
// 领域层定义的接口
package domain
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id OrderID) (*Order, error)
}
// 基础设施层的实现
package infrastructure
type GormOrderRepository struct {
db *gorm.DB
}
func (r *GormOrderRepository) Save(ctx context.Context, order *domain.Order) error {
// 将领域对象转换为ORM模型并保存
return r.db.WithContext(ctx).Create(order).Error
}
4.3 依赖规则与模块划分
严格的依赖规则是四层架构的基石。下表总结了各层之间的依赖关系与通信方式:
| 源层 | 目标层 | 依赖方式 | 说明 |
|---|---|---|---|
| 用户接口层 | 应用层 | 直接调用 | 注入应用层服务接口,调用其方法。 |
| 用户接口层 | 领域层 | 间接依赖 | 通常不直接依赖,但可以引用DTO或简单值对象。 |
| 应用层 | 领域层 | 直接调用 | 调用领域实体、领域服务、Repository接口。 |
| 应用层 | 基础设施层 | 依赖倒置 | 通过依赖注入获取基础设施层的实现。 |
| 领域层 | 基础设施层 | 禁止依赖 | 领域层完全不知道基础设施层的存在。 |
| 基础设施层 | 领域层 | 实现接口 | 实现领域层定义的Repository等接口。 |
4.4 在Go项目中组织四层代码
在Go项目中,我们通常按照限界上下文(Bounded Context)来组织顶层目录,在每个上下文内部再按照四层架构划分包。这是一种推荐的模块划分方式。
your-project/
├── internal/
│ ├── order/ # 订单限界上下文
│ │ ├── interface/ # 用户接口层
│ │ │ └── http/
│ │ │ └── handler.go
│ │ ├── application/ # 应用层
│ │ │ └── service.go
│ │ ├── domain/ # 领域层
│ │ │ ├── entity.go
│ │ │ ├── value_object.go
│ │ │ ├── repository.go # 接口定义
│ │ │ └── service.go
│ │ └── infrastructure/ # 基础设施层
│ │ ├── repository_impl.go
│ │ └── event_publisher.go
│ ├── product/ # 产品限界上下文
│ │ └── ...
│ └── common/ # 共享内核
│ └── ...
├── cmd/
│ └── server/
│ └── main.go # 依赖注入与启动入口
└── go.mod
依赖注入(DI):在
main.go 或一个独立的 wire.go 文件中,我们实例化所有层的具体实现,并通过构造函数将它们注入到依赖它们的对象中。这是实现依赖倒置和保持各层解耦的关键技术。
通过严格遵守四层架构的职责与依赖规则,我们能够构建出高内聚、低耦合、易于测试和维护的Go Web应用。领域层作为业务核心,其稳定性和独立性是项目长期演进的基础。