有意思的接口规则:自动实现
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
提供的功能。
相比之下,java是需要显式声明实现的接口的,即如果一个类实现了A
接口,那么假设有另一个接口B
,即使它的方法列表与A
接口完全相同,它也跟实现类没有关系,除非实现类显式声明实现这个接口B
。对于上面那种业务不希望依赖实现,而希望依赖与接口的情况,我们一般只能借助于适配器模式[1],如下图所示。
两个结构对比,我们显然能看到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
}
我们还是希望我们的代码不依赖实现,而是依赖接口,这个时候我们可能会定义如下这样的接口KvClient
和KvConn
,希望SomeConn
实现KvConn
,而SomeClient
实现KvClient
,这样我们的代码就可以真的依赖我们定义的接口了。但是很遗憾,SomeConn
并不实现KvConn
!原因就是它的GetConn
方法返回的类型KvConn
与SomeConn
不是同一个类型!
type KvClient interface {
GetConn() KvConn
}
type KvConn interface {
Request(req KvRequest) KvResponse
}
接口实现关系,Java的处理规则真的值得Golang好好学习!Java的处理规则是里氏替换规则[2]。换句话来说就是:调用接口方法的地方,将接口的方法替换成实现类的方法,接口调用语义依旧适用,那么实现类中的方法就可以覆盖接口的方法。这样说还是有点绕口,用简单的例子来说明吧
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
的问题就迎刃而解了。
最后,在现在没有使用
里氏替换
的规则的情况下,SomeClient
与KvClient
的问题应该如何解决?答案是使用适配器
来实现,这是一个更通用更具有一般性的解决方案。
有疑问加站长微信联系(非本文作者)