转载

Go的接口

有意思的接口规则:自动实现

Go语言也支持接口,但是它的接口规则很有意思:

一个struct不需要显示声明它要实现的接口,只要实现了接口中规定的所有方法,那么就自动实现了相应的接口了。

所以正常情况下,如果有如下一个struct

type A struct {
}

func (*A) func Foo() {}
func (*A) func Bar(i int) int {
  return i
}

那么它就自动同时实现了下面三个接口

type IFoo interface {
  Foo()
}

type IBar interface {
  Bar(i int) int
}

type IFooBar interface {
  IFoo
  IBar
}

所以我们可以进行如下的赋值:

var a *A = &A{}

// 下面三个赋值都是正确的
var foo IFoo = a
var bar IBar = a
var foobar IFooBar = a

这种自动的接口实现带来了一个很好的好处,就是接口的定义者和接口的实现者之间没有了必然的依赖关系,甚至可以“先”有实现“再”有接口。这在某些时候是很有用的。

自动实现带来的便利

比如有人给一个很实用的功能提供了一个实现,比如 SomeLogger 。然后我们希望在自己的系统中使用它,但是处于代码洁癖的原因,我们不希望依赖我们的代码一个实现,而希望是依赖一个接口。这个时候在go语言中就很简单,我们只需要自己定义一个接口(比如 Logger ),里面罗列上 SomeLogger 中提供的我们需要使用的方法即可。这是我们的代码就可以依赖我们自己的 Logger 接口,来使用 SomeLogger 提供的功能。

Go的接口

图1:给第三方实现定义接口

相比之下,java是需要显式声明实现的接口的,即如果一个类实现了 A 接口,那么假设有另一个接口 B ,即使它的方法列表与 A 接口完全相同,它也跟实现类没有关系,除非实现类显式声明实现这个接口 B 。对于上面那种业务不希望依赖实现,而希望依赖与接口的情况,我们一般只能借助于适配器模式,如下图所示。

Go的接口

图2:桥接模式

两个结构对比,我们显然能看到java里面则多了很多适配器的包(这有时候也是复杂的一个原因)。自己定义适配器,有时候真的非常复杂。而Golang的这种自动实现方式简洁很多,能减少很多代码量。像这种为一套实现定义接口,而业务依赖于这一套接口,这其实是一种很实用的做法,他能有效的给组件之间解耦。

自动实现的机制还比较死板

虽然自动实现很实用,但是现在Golang的实现机制并不完善。比如对于下面这样一个例子。

// 假设有两个数据类型:接口A和它的一个实现类型B
type A interface {
}

type B struct {
  // implemented A interface
}

// 请注意到:下面的类型D和接口C之间没有实现关系
type C interface {
  Foo() A
}

type D struct {
}

func (D) Foo() B {
  ...
}

这里Golang会认为 D 类型没有实现 C 接口,因为 D 中的 Foo 方法返回的是 B 类型,而接口 C 中要求 Foo 方法返回的是 A 类型,所以他们是不一样的。这其实会给我们带来很多不便。比如有一个kv数据库的SDK有如下的几个类:

type SomeClient struct {
  GetConn() SomeConn
}

type SomeConn struct {
  Request(req SomeReq) SomeResp
}

我们还是希望我们的代码不依赖实现,而是依赖接口,这个时候我们可能会定义如下这样的接口 KvClientKvConn ,希望 SomeConn 实现 KvConn ,而 SomeClient 实现 KvClient ,这样我们的代码就可以真的依赖我们定义的接口了。但是很遗憾, SomeConn 并不实现 KvConn !原因就是它的 GetConn 方法返回的类型 KvConnSomeConn 不是同一个类型!

type KvClient interface {
  GetConn() KvConn
}

type KvConn interface {
  Request(req KvRequest) KvResponse
}

接口实现关系,Java的处理规则真的值得Golang好好学习!Java的处理规则是里氏替换规则。换句话来说就是:调用接口方法的地方,将接口的方法替换成实现类的方法,接口调用语义依旧适用,那么实现类中的方法就可以覆盖接口的方法。这样说还是有点绕口,用简单的例子来说明吧

interface A {
  Object foo();
}

class B implements A {
  public B foo() {
    return this;
  }
}

比如对于上面的例子, B 中的 foo 方法与 A 接口中定义的 foo 方法的返回值虽然不同,但是认为是可以覆盖。因为调用 A.foo() 的地方期望的是一个 Object 类型的返回值,而 B.foo 返回的 B 类型实例就是 Object 类型的实例,所以调用的逻辑是完全正确的。

A a = new B();
Object b = a.foo();

其实参数本来也可以使用这种覆盖规则的,但是由于他与 重载 冲突,所以java中没有允许入参也使用这种替换规则。如果Golang后续能使用这种方式确定是否实现了目标方法,那么前面提到的 SomeClient 不能实现 KvClient 的问题就迎刃而解了。

最后,在现在没有使用 里氏替换 的规则的情况下, SomeClientKvClient 的问题应该如何解决?答案是使用 适配器 来实现,这是一个更通用更具有一般性的解决方案。

原文  https://studygolang.com/articles/21790
正文到此结束
Loading...