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 等标准库小接口作为设计范本。
ISP 接口拆分对比:胖接口 vs 角色接口 胖接口(反模式) OrderService + Create(ctx, order) error + Pay(ctx, orderID) error + Ship(ctx, orderID) error + Refund(ctx, orderID) error + Export(ctx, filter) ([]byte, error) 客户端被迫依赖所有方法 修改任一方法影响所有调用方 Mock 成本高 角色接口(ISP 重构) OrderCreator OrderPayer OrderShipper OrderRefunder OrderExporter 每个客户端只依赖所需接口 接口变更影响范围最小化 Mock 轻量,测试友好

3.2 依赖倒置原则(DIP):面向接口,而非实现

DIP 包含两层含义:

  1. 高层模块不应依赖低层模块,两者都应依赖抽象(接口)。
  2. 抽象不应依赖细节,细节应依赖抽象。

在 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 实现。
DIP 依赖方向:从外层指向内层 基础设施层(外层) PostgresOrderRepository · StripePaymentGateway · RedisCache 实现领域接口,依赖领域抽象 应用层(中层) OrderService · PaymentService · NotificationService 依赖领域接口,不依赖具体实现 领域层(内层) OrderRepository (interface) · PaymentGateway (interface) · 领域实体

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 接口设计中的检查清单:
  1. 接口方法的参数是否都是基本类型或领域值对象?
  2. 接口方法的返回值是否只包含调用方真正需要的数据?
  3. 调用方是否通过接口方法直接访问了返回值的内部属性?
  4. 是否存在超过 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:这个接口的参数和返回值是否暴露了不必要的内部结构?