接口越大,抽象程度越弱 - Rob Pike,Go 语言之父

1. Go 推荐定义“小接口”

接口背后的概念是通过将对象的行为抽象为契约来允许重用。契约有繁有简,Go 选择了去繁就简,这主要体现在以下两点上:

  • 契约的自动遵守:Go 语言中接口与其实现者之间的关系是隐式的,无需像其他语言(比如:Java)那样要求实现者显式放置”implements”声明;实现者仅需实现接口方法集中的全部方法,便算是自动遵守了契约,实现了该接口;
  • 小契约:契约繁了便束缚了手脚,缺少了灵活性,抑制了表现力。Go 选择了使用 ”小契约“,表现在代码上便是尽量定义小接口。

下面是 Go 标准库中的一些常用接口的定义:

  1. // $GOROOT/src/builtin/builtin.go
  2. type error interface {
  3. Error() string
  4. }
  5. // $GOROOT/src/io/io.go
  6. type Reader interface {
  7. Read(p []byte) (n int, err error)
  8. }
  9. // $GOROOT/src/net/http/server.go
  10. type Handler interface {
  11. ServeHTTP(ResponseWriter, *Request)
  12. }
  13. type ResponseWriter interface {
  14. Header() Header
  15. Write([]byte) (int, error)
  16. WriteHeader(int)
  17. }

我们看到上述这些接口的方法数量在 1~3 个之间,这种”小接口“的 Go 最佳实践已被 Go 程序员和各个社区项目广泛采用。下面是对 Go 标准库(go 1.13 版本)、Docker 项目(docker 19.03 版本)以及Kubernetes 项目(k8s 1.17 版本)中定义的接口的方法数的统计1数据折线图(X 轴为方法数量,Y 轴是接口数量):

image.png

从图中我们可以看到无论是标准库,还是社区项目,都遵循了”尽量定义小接口“的建议,接口方法数量在 1~3 范围内的接口占了绝大多数。下面是每个项目的接口方法数量占比的柱状图,对比起来更直观一些:

image.png

2. 小接口的优势

a) 接口越小,抽象程度越高,被接纳程度越好

计算机程序本身就是对真实世界的抽象与再建构。抽象是对同类事物去除其现象的、次要的方面,抽取其相同的、主要的方面的方法。不同的抽象程度,会导致抽象出的概念对应的事物的集合不同。抽象程度越高,对应的集合空间越大;抽象程度越低(即越具像化,更接近事物真实面貌),对应的集合空间越小。下面的示意图就是对不同抽象程度的形象诠释:

image.png

我们将上面的抽象转换为 Go 代码:

  1. // 会飞的
  2. type Flyable interface {
  3. Fly()
  4. }
  5. // 会游泳的
  6. type Swimable interface {
  7. Swim()
  8. }
  9. // 会飞会游泳的
  10. type FlySwimable interface {
  11. Flyable
  12. Swimable
  13. }

我们用上述定义的接口替换上图中的抽象得到下面示意图:

image.png

b) 易于实现和测试

这是一个显而易见的优点。小接口拥有较少的方法,一般情况仅一个方法。要想满足这一接口,我们仅需实现一个方法或少数几个方法即可,这显然要比实现拥有较多方法的接口要容易的多。尤其是在单元测试环节,构建类型去实现仅有少量方法的接口要比实现拥有较多方法的接口(快速实现拥有较多方法的接口以满足测试的技巧见条目 25)付出的努力要少许多。

c) 契约职责单一,易于复用组合

Go 的设计原则推崇通过组合的方式构建程序。Go 开发人员一般会首先尝试通过嵌入其他已有接口类型的方式来构建新接口类型,就像通过嵌入 io.Reader 和 io.Writer 构建 io.ReadWriter 那样。

3. 定义小接口,你可以遵循的几点

  • 先抽象出接口

在定义小接口之前,我们需要首先针对问题领域进行深入理解,聚焦抽象并发现接口。

image.png

初期先不要介意接口的大小,因为对问题域的理解是循序渐进的,在第一版代码中直接定义出小接口可能并不现实。

  • 将大接口拆分为小接口

有了接口后,我们就会看到接口被用在代码各个地方。一段时间后,我们就来分析哪些场合使用了接口的哪些方法,是否可以将这些场合使用的接口的方法提取出来放入一个新的小接口中,就像下面图示中的那样:

image.png

  • 接口的单一契约职责

上面已经被拆分成的小接口是否需要进一步拆分直至每个接口只有一个方法呢?这个依然没有标准答案,不过大家依然可以考量一下现有小接口是否需要满足单一契约职责,就像 io.Reader 那样。如果需要,则可进一步拆分,提升抽象程度。