100个Go语言典型错误
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.6#6:在生产者端的接口

在上一节中看到了接口何时被认为是有价值的。但是 Go 开发者经常误解一个问题:接口应该放在哪里?

在深入讨论这个话题之前,让我们先确定在这一节中使用的术语是清楚的:

■ 生产者端——在与具体实现相同的包中定义的接口(参见图2.4)。

■ 消费者端——在使用它的外部包中定义的接口(参见图2.5)。

图2.4 在与具体实现相同的包中定义的接口

图2.5 在使用它的外部包中定义的接口

经常可以看到开发人员在生产者端创建接口,同时创建具体的实现。这种设计可能是具有 C#或 Java 背景的开发人员的习惯。但在Go 中,大多数情况下不应该这么做。

让我们讨论下面的例子。在这里,我们创建一个特定的包来存储和检索客户数据。同时,仍然是在同一个包中,我们决定所有的调用必须通过以下接口:

我们可能认为有一些很好的理由在生产者端创建和暴露这个接口。也许这是将客户端代码与实际实现分离的好方法。或者,也许我们可以预见它将帮助客户端创建测试副本。不管是什么原因,这都不是 Go 中的最佳实践。

如前所述,在Go 中,接口是隐式满足的,与具有显式实现的语言相比,这往往是游戏规则的改变者。在大多数情况下,遵循的方法类似于在前一节中描述的:应该发现抽象,而不是创建抽象。这意味着生产者不能强制所有客户端使用给定的抽象。相反,由客户来决定它是否需要某种形式的抽象,然后为它的需求确定最佳的抽象级别。

在前面的例子中,可能有一个客户端对解耦其代码不感兴趣。也许另一个客户端想要解耦其代码,但只对 GetAllCustomers 方法感兴趣。在这种情况下,这个客户端可以用一个方法创建一个接口,从外部包引用 Customer 结构体:

图2.6显示了包组织的调用关系。有几点需要注意:

■ 因为 customersGetter 接口只在客户端包中使用,所以它可以保持不被导出。

■ 从视觉上看,在图2.6中,它看起来像循环依赖关系。但是,从 Stere 到client之间没有依赖关系,因为接口是隐式满足的。这就是为什么这种方法在具有显式实现的语言中并不总是可行的原因。

图2.6 client包通过创建自己的接口来定义它所需要的抽象

重点是,client包现在可以为其需求定义最精确的抽象(这里只有一个方法)。它与接口隔离原则(SOLID 中的I)的概念有关,该原则指出,任何客户端都不应该被迫依赖它不使用的方法。因此,在这种情况下,最好的方法是在生产者端公开具体的实现,并让客户端决定如何使用它以及是否需要抽象。

为了完整起见,让我们讨论在标准库中使用的方法——生产者端的接口。例如,encoding 包定义了由其他子包(如 encoding/json 或 encoding/binary)实现的接口。encoding 包在这方面是错误的吗?绝对不会。在这种情况下,encoding 包中定义的抽象在整个标准库中使用,语言设计人员知道预先创建这些抽象是有价值的。回到前一节的讨论:如果你认为抽象可能在想象的未来有帮助,或者你不能证明这个抽象是有效的,就不要创建抽象。

在大多数情况下,接口应该位于消费者端。然而,在特定的上下文中(例如,当我们知道——而不是预见——抽象将对消费者有帮助时),我们可能希望在生产者端使用它。如果这样做了,应该努力使它尽可能少,增加它的可重用潜力,使它更容易组合。

让我们继续在函数签名上下文中讨论接口。