2. 业务接口设计原则:ISP、DIP 与 Go 的 interface 哲学
在领域驱动设计中,接口是领域边界的第一道防线。错误的接口设计会导致领域逻辑泄漏、模块耦合加剧、测试成本飙升。本章聚焦三大核心原则——接口隔离原则(ISP)、依赖倒置原则(DIP)以及 Go 语言特有的 interface 设计哲学,并结合业务场景给出可落地的实战指南。
2.1 接口隔离原则(ISP)
定义:客户端不应被迫依赖它不使用的方法。在业务接口设计中,这意味着一个领域服务接口应该只包含该调用方真正需要的操作,而不是一个“万能”接口。
2.1.1 违反 ISP 的典型反模式
许多团队习惯将某个聚合根的所有操作塞进一个接口,例如:
// 反模式:胖接口
type OrderService interface {
CreateOrder(ctx context.Context, cmd CreateOrderCommand) (*Order, error)
CancelOrder(ctx context.Context, orderID string) error
GetOrder(ctx context.Context, orderID string) (*Order, error)
ListOrders(ctx context.Context, filter OrderFilter) ([]*Order, error)
ExportOrders(ctx context.Context, filter OrderFilter) (io.Reader, error)
CalculateOrderStats(ctx context.Context, period TimeRange) (*OrderStats, error)
}
这种设计的问题:
- 调用方被迫依赖无关方法:订单查询服务只需要
GetOrder和ListOrders,却必须感知ExportOrders和CalculateOrderStats的签名变化。 - 接口变更影响面大:新增一个统计方法会导致所有实现该接口的 mock 和 stub 都需要更新。
- 领域语义模糊:接口名称
OrderService无法表达其真实职责边界。
2.1.2 按业务角色拆分接口
正确的做法是:按调用方的业务角色拆分接口。每个接口只暴露该角色需要的方法。
| 业务角色 | 接口名称 | 方法 |
|---|---|---|
| 订单创建者(前台) | OrderCreator |
CreateOrder |
| 订单查询者(客服) | OrderQuerier |
GetOrder, ListOrders |
| 订单管理者(运营) | OrderManager |
CancelOrder, ExportOrders |
| 订单分析者(BI) | OrderAnalyzer |
CalculateOrderStats |
// 符合 ISP:按角色拆分
type OrderCreator interface {
CreateOrder(ctx context.Context, cmd CreateOrderCommand) (*Order, error)
}
type OrderQuerier interface {
GetOrder(ctx context.Context, orderID string) (*Order, error)
ListOrders(ctx context.Context, filter OrderFilter) ([]*Order, error)
}
type OrderManager interface {
CancelOrder(ctx context.Context, orderID string) error
ExportOrders(ctx context.Context, filter OrderFilter) (io.Reader, error)
}
type OrderAnalyzer interface {
CalculateOrderStats(ctx context.Context, period TimeRange) (*OrderStats, error)
}
实战建议:在领域层定义接口时,优先从“谁在用”而不是“有什么功能”出发。一个接口通常不超过 3 个方法。如果超过 5 个,几乎一定违反了 ISP。
2.2 依赖倒置原则(DIP)
定义:高层模块不应依赖低层模块,两者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。在 DDD 中,这意味着领域层(高层)不应直接依赖基础设施层(低层),而是通过接口反转依赖方向。
2.2.1 传统分层 vs DIP 分层
| 维度 | 传统分层架构 | DIP 分层架构 |
|---|---|---|
| 依赖方向 | 领域层 → 基础设施层 | 基础设施层 → 领域层(接口) |
| 领域层职责 | 调用数据库、消息队列 | 只定义接口和业务逻辑 |
| 基础设施层职责 | 被调用 | 实现领域层定义的接口 |
| 测试难度 | 高(需 mock 具体实现) | 低(可 mock 接口) |
| 更换基础设施成本 | 高(领域层代码需修改) | 低(只需新增实现) |
2.2.2 领域层定义接口,基础设施层实现
以订单仓储为例,领域层只定义接口:
// 领域层:domain/repository/order_repository.go
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, orderID string) (*Order, error)
FindByUserID(ctx context.Context, userID string, page, size int) ([]*Order, int64, error)
}
基础设施层实现该接口:
// 基础设施层:infrastructure/persistence/mysql/order_repo.go
type MySQLOrderRepository struct {
db *sql.DB
}
func (r *MySQLOrderRepository) Save(ctx context.Context, order *Order) error {
// 具体 SQL 实现
return nil
}
func (r *MySQLOrderRepository) FindByID(ctx context.Context, orderID string) (*Order, error) {
// 具体 SQL 实现
return nil
}
func (r *MySQLOrderRepository) FindByUserID(ctx context.Context, userID string, page, size int) ([]*Order, int64, error) {
// 具体 SQL 实现
return nil
}
常见误区:不要在领域层定义接口的同时,在领域层内部提供默认实现(如
NewDefaultOrderRepository)。这会导致领域层仍然隐式依赖具体实现,破坏 DIP。接口的实现必须由基础设施层提供。
2.3 Go 中 interface 的设计哲学
Go 的 interface 与其他语言(Java、C#)有本质区别:隐式实现(Duck Typing)。这一特性对 DDD 的接口设计产生了深远影响。
2.3.1 隐式实现 vs 显式实现
| 特性 | Java/C#(显式实现) | Go(隐式实现) |
|---|---|---|
| 实现声明 | class X implements Y |
无需声明,方法签名匹配即实现 |
| 接口与实现耦合 | 强耦合(编译时绑定) | 松耦合(结构体无需知道接口存在) |
| 接口定义位置 | 通常在实现方 | 通常在调用方(Consumer-Side) |
| 重构影响 | 修改接口需同步修改所有实现 | 修改接口不影响已有实现(只要方法签名不变) |
2.3.2 Consumer-Side 接口定义模式
Go 社区推荐:接口定义在调用方,而不是实现方。这意味着领域服务不应该定义一个大而全的接口让基础设施去实现,而是由领域服务定义它“需要什么”,基础设施提供满足该需求的结构体。
// 领域服务:定义它需要的接口(Consumer-Side)
type OrderNotificationService struct {
notifier OrderNotifier // 接口定义在领域服务内部
}
// 接口只包含领域服务真正需要的方法
type OrderNotifier interface {
NotifyOrderCreated(ctx context.Context, order *Order) error
}
// 基础设施层:实现该接口
type EmailNotifier struct {
// ...
}
func (e *EmailNotifier) NotifyOrderCreated(ctx context.Context, order *Order) error {
// 发送邮件
return nil
}
// 应用层:组装
func NewOrderNotificationService(notifier *EmailNotifier) *OrderNotificationService {
return &OrderNotificationService{notifier: notifier}
}
关键区别:在 Java 中,接口通常定义在基础设施层(如
UserRepository 接口定义在 repository 包),领域层引用该接口。在 Go 中,接口定义在领域层内部,基础设施层提供实现。后者更符合 DIP 的“高层模块定义抽象”原则。
2.4 综合实战:订单领域接口设计
以下 SVG 展示了 ISP + DIP + Go 哲学在订单领域中的综合应用:
2.5 接口设计检查清单
在每次定义业务接口前,对照以下清单进行自检:
- ISP 检查:该接口是否只包含调用方需要的方法?是否有方法从未被使用?
- DIP 检查:接口定义在领域层还是基础设施层?领域层是否依赖了具体实现?
- Go 哲学检查:接口是否定义在调用方(Consumer-Side)?是否利用了隐式实现的优势?
- 方法数量检查:接口方法数是否 ≤ 3?如果超过 5 个,考虑拆分。
- 命名检查:接口名称是否表达了业务角色(如
OrderCreator)而非技术角色(如OrderService)? - 测试性检查:该接口是否容易 mock?是否只需要实现 1-2 个方法即可完成测试?
终极原则:好的业务接口设计,应该让调用方感觉“这个接口就是为我量身定做的”,而不是“这个接口里我只用其中一小部分”。在 Go 中,小接口、按角色拆分、Consumer-Side 定义,是构建清晰领域边界的三大基石。