3. 业务接口定义原则:ISP、DIP、最小知识原则在 Go 接口设计中的应用
在 Go 业务接口设计中,接口隔离原则(ISP)、依赖倒置原则(DIP)和最小知识原则(LoD)是构建高内聚、低耦合领域边界的三大基石。它们共同决定了接口的粒度、方向与可见性,直接影响业务代码的可维护性与可测试性。
核心认知:Go 的隐式接口(duck typing)天然支持 ISP 和 DIP,但需要开发者主动设计小接口、面向抽象编程,并严格控制依赖范围。
3.1 接口隔离原则(ISP):小接口,大聚合
ISP 要求:客户端不应被迫依赖它不使用的方法。在 Go 中,这意味着将胖接口拆分为多个角色特定的细粒度接口。
| 反模式(胖接口) | ISP 重构(小接口) | 业务收益 |
|---|---|---|
OrderService 包含 Create、Pay、Ship、Refund、Export |
OrderCreator、OrderPayer、OrderShipper、OrderRefunder、OrderExporter |
每个客户端只依赖所需方法,修改一个接口不影响其他客户端 |
UserRepository 包含 Save、FindByID、FindAll、Delete、Count、Search |
UserWriter、UserReader、UserSearcher |
查询层不依赖写操作,测试时可轻松 mock 小接口 |
// 反模式:胖接口
type OrderService interface {
Create(ctx context.Context, order *Order) error
Pay(ctx context.Context, orderID string) error
Ship(ctx context.Context, orderID string) error
Refund(ctx context.Context, orderID string) error
Export(ctx context.Context, filter OrderFilter) ([]byte, error)
}
// ISP 重构:角色接口
type OrderCreator interface {
Create(ctx context.Context, order *Order) error
}
type OrderPayer interface {
Pay(ctx context.Context, orderID string) error
}
type OrderShipper interface {
Ship(ctx context.Context, orderID string) error
}
type OrderRefunder interface {
Refund(ctx context.Context, orderID string) error
}
type OrderExporter interface {
Export(ctx context.Context, filter OrderFilter) ([]byte, error)
}
Go 实践要点:
- 接口定义在消费者侧(调用方),而非生产者侧(实现方)。
- 接口通常包含 1~3 个方法,超过 3 个方法应审视是否可拆分。
- 使用
io.Reader、io.Writer等标准库小接口作为设计范本。
3.2 依赖倒置原则(DIP):面向接口,而非实现
DIP 包含两层含义:
- 高层模块不应依赖低层模块,两者都应依赖抽象(接口)。
- 抽象不应依赖细节,细节应依赖抽象。
在 Go 业务架构中,DIP 的典型体现是:领域层定义接口,基础设施层实现接口,依赖方向从外层指向内层。
// 领域层(内层):定义接口,不依赖任何外部实现
package domain
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, orderID string) (*Order, error)
}
type PaymentGateway interface {
Charge(ctx context.Context, amount Money, source PaymentSource) (TransactionID, error)
}
// 应用层(中层):依赖领域接口,不依赖具体实现
package application
type OrderService struct {
repo domain.OrderRepository
gateway domain.PaymentGateway
}
func (s *OrderService) PlaceOrder(ctx context.Context, cmd PlaceOrderCommand) error {
order := domain.NewOrder(cmd.Items)
if err := s.repo.Save(ctx, order); err != nil {
return err
}
txID, err := s.gateway.Charge(ctx, order.Total(), cmd.Source)
if err != nil {
return err
}
order.ConfirmPayment(txID)
return s.repo.Save(ctx, order)
}
// 基础设施层(外层):实现领域接口
package infrastructure
type PostgresOrderRepository struct { /* ... */ }
func (r *PostgresOrderRepository) Save(ctx context.Context, order *domain.Order) error { /* ... */ }
func (r *PostgresOrderRepository) FindByID(ctx context.Context, orderID string) (*domain.Order, error) { /* ... */ }
type StripePaymentGateway struct { /* ... */ }
func (g *StripePaymentGateway) Charge(ctx context.Context, amount domain.Money, source domain.PaymentSource) (domain.TransactionID, error) { /* ... */ }
常见陷阱:
- 在领域层定义接口时,方法签名中不应出现基础设施层的类型(如
*sql.DB、*http.Client)。 - 接口定义在领域层,但接口的实例化应在应用启动时通过依赖注入完成,避免领域层直接
new实现。
3.3 最小知识原则(LoD):只与直接朋友通信
最小知识原则(Law of Demeter)要求:一个对象应尽可能少地了解其他对象。在 Go 接口设计中,体现为:
- 方法参数:只传递所需的最小数据,而非整个对象。
- 返回值:只返回调用方真正需要的数据,避免返回大对象。
- 链式调用:避免过深的
a.GetB().GetC().DoSomething()。
| 违反 LoD 的接口 | 符合 LoD 的接口 | 说明 |
|---|---|---|
func (s *Service) Process(order *Order) error内部调用 order.GetCustomer().GetAddress().GetCity() |
func (s *Service) Process(city string, amount Money) error |
只传递所需参数,避免遍历对象图 |
func (r *Repo) FindByID(id string) *User返回整个 User 对象,即使只需 Name |
func (r *Repo) FindNameByID(id string) (string, error) |
返回最小数据,减少耦合 |
order.GetItems()[0].GetProduct().GetCategory() |
order.FirstItemCategory() 封装在 Order 内部 |
避免外部遍历内部结构 |
// 违反 LoD:接口暴露内部结构
type Order struct {
Customer *Customer
}
type Customer struct {
Address *Address
}
type Address struct {
City string
}
func (s *ShippingService) CalculateCost(order *Order) float64 {
city := order.Customer.Address.City // 遍历三层对象图
// ...
}
// 符合 LoD:接口只暴露所需数据
func (s *ShippingService) CalculateCost(city string, weight float64) float64 {
// 直接使用参数,不关心来源
}
// 或在 Order 上提供封装方法
func (o *Order) ShippingCity() string {
return o.Customer.Address.City
}
LoD 在 Go 接口设计中的检查清单:
- 接口方法的参数是否都是基本类型或领域值对象?
- 接口方法的返回值是否只包含调用方真正需要的数据?
- 调用方是否通过接口方法直接访问了返回值的内部属性?
- 是否存在超过 2 层的链式调用?
3.4 三原则协同:构建清晰的领域边界
ISP、DIP 和 LoD 并非孤立原则,它们在 Go 业务接口设计中形成互补关系:
| 原则 | 关注点 | 在 Go 中的体现 | 协同效果 |
|---|---|---|---|
| ISP | 接口粒度 | 小接口、角色接口 | 减少接口变更影响范围 |
| DIP | 依赖方向 | 领域层定义接口,基础设施层实现 | 确保领域层不依赖外部细节 |
| LoD | 信息暴露 | 最小参数、最小返回值、封装内部结构 | 降低接口消费者对内部结构的认知负担 |
// 三原则协同的完整示例
package domain
// ISP:小接口
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, orderID string) (*Order, error)
}
type InventoryClient interface {
Reserve(ctx context.Context, sku string, quantity int) error
}
// DIP:领域层定义接口,不依赖实现
type OrderService struct {
repo OrderRepository
inv InventoryClient
}
// LoD:方法参数最小化,只传递所需数据
func (s *OrderService) PlaceOrder(ctx context.Context, userID string, items []OrderItem) error {
order := NewOrder(userID, items)
for _, item := range items {
// LoD:不遍历 order 内部,直接使用 items 参数
if err := s.inv.Reserve(ctx, item.SKU, item.Quantity); err != nil {
return err
}
}
return s.repo.Save(ctx, order)
}
设计决策参考:当你在 Go 中定义一个新的业务接口时,依次问自己三个问题:
1. ISP:这个接口是否只包含调用方真正需要的方法?能否拆成更小的角色接口?
2. DIP:这个接口定义在哪一层?是否让内层依赖了外层?
3. LoD:这个接口的参数和返回值是否暴露了不必要的内部结构?
1. ISP:这个接口是否只包含调用方真正需要的方法?能否拆成更小的角色接口?
2. DIP:这个接口定义在哪一层?是否让内层依赖了外层?
3. LoD:这个接口的参数和返回值是否暴露了不必要的内部结构?