Go语言中常见100问题-#6 生产者端接口
生产者端接口
在深入探讨问题之前,先对提及的术语做一个定义说明,确保我们对其有清晰的理解。
- 生产者端:接口定义与具体实现在同一个包中,称这种为生产者端接口。像下图所示,接口的定义和具体实现都在foo包中,调用客户端代码在bar包中。
- 消费者端:接口定义与具体实现不在相同的包中,而是定义在调用的客户端代码所在的包中,称这种为消费者端接口。如下图所示,接口定义在使用方包bar中。
在生产者端定义接口,与具体实现放在一起,这是具有C#或Java背景语言的人惯用写法。然而,在Go语言中,在大多数情况下,我们不应该采用这种写法。下面通过一个示例进行说明。
示例中,我们创建一个特定的包来存储和查询客户数据。同时在该包中定义一个接口,所有对客户数据的操作都通过接口来实现。对应到前面,这种实现就是生产者端接口。
package store
type CustomerStorage interface {
StoreCustomer(customer Customer) error
GetCustomer(id string) (Customer, error)
UpdateCustomer(customer Customer) error
GetAllCustomers() ([]Customer, error)
GetCustomersWithoutContract() ([]Customer, error)
GetCustomersWithNegativeBalance() ([]Customer, error)
}
对于上面在生产者端创建接口,并对外暴露这个接口。可能认为有很好的理由这样做,我们可能会说,1.这样可以将客户端代码与实际实现分离,2. 在测试的时候,调用方很方便Mock一个假的接口实现进行测试。但是,这不是Go中的最佳实践。
对于上述示例,也许客户端A不会对解耦它的代码感兴趣,也许客户端B想要解耦它的代码,但只对 GetAllCustomers 方法感兴趣。在这种情况下,可以使用单个方法创建一个接口,引用外部包中的 Customer 结构体。
package client
type customersGetter interface {
GetAllCustomers() ([]store.Customer, error)
}
从包组织引用关系来看,客户端和存储两个包关系如下图。需要注意几点:
-
由于 customersGetter 接口仅在客户端包中使用,因此该接口可以保持不导出。
-
咋一看,下图看起来像存在循环依赖。但是,由于隐式满足接口,存储与客户端之间没有循环依赖关系。这也是为什么这种方法在具有显示实现的语言中并不总是可行的原因。
采用在客户端中定义接口的关键点是客户端可以根据需要定义最准确的抽象(像customersGetter只有一个方法),契合接口隔离原则(SOLID中的I),该原则指出不应强迫任何客户端依赖它不使用的方法。因此,在这种情况下,最好的方法是在生产者端公开具体实现(方法可导出),让客户端决定如何使用它以及是否需要抽象。
因此,在大多数情况下,Go语言中的接口应该位于消费者端。但是,在特定情况下,例如,当我们知道(不是预想)抽象对消费者有帮助时,我们可能希望将其放在生产者端。如果要这样做,应该努力让接口尽可能地最小化(接口中的方法仅可能少),像encoding/json中定义的Marshaler接口只包含1个方法,这样增加它的可重用潜力并使其更容易组合。
转载自:https://juejin.cn/post/7338994082859171866