求求你们,不要再往接口里面乱加方法了
2020-03-09
真是受不了项目里面,一些接口被搞得越来越庞大,所以来吐糟了。rob pike 教导我们说,接口应该尽量地小:
The bigger the interface, the weaker the abstraction – Rob Pike
只有小的接口,才是可组合的。否则去写 java 好了,为啥要来写 Go 呢?
最糟糕的一类是这样子写的:
type I interface {
...
XX()
}
type T1 struct {}
func (t T1) XX() {
}
type T2 struct {}
func (t T2) XX() {
panic("unsupported")
}
如果定义了一个接口中,某些实现需要做,而另一些实现又不支持的,说明此方法根本不通用!即使不是通用的方法,为什么要往接口里面塞呢?Don't panic,反面教材。
其次糟糕的一类:
func f(t I) {
switch t.(type) {
case T1:
case T2:
}
}
都定义了接口了,却需要用 type switch 去看底层是哪种实现,这说明这个接口抽象的不好呀!依赖于接口而不是实现。
还一类常见的糟糕的地方,冗余的方法全部往接口里面塞,这里一个反面教材:
type Table interface {
...
AllocHandle(ctx sessionctx.Context) (int64, error)
AllocHandleIDs(ctx sessionctx.Context, n uint64) (int64, int64, error)
Allocator(ctx sessionctx.Context, allocatorType autoid.AllocatorType) autoid.Allocator
AllAllocators(ctx sessionctx.Context) autoid.Allocators
}
Table 接口里面为什么有这么多 Alloc
相关的方法?而且,其中一些还是可以用另一些推导出来的。难道直接定义返回一个 allocator,不香么?难道把各个 table 各个实现里面都加几个这样的函数很有意思么?
对冗余的处理,我们可以简单的只保留一个。剩下的去包装一下就好。比如,下面都是等价的写法。
func AllocHandle(ctx sessionctx.Context, t Table) (int64, error) {
allocator := t.Allocator(ctx)
allocator.Alloc(id)
}
AllocHandle(ctx, t)
跟这个:
func (t *tableImpl) AllocHandle(ctx sessionctx.Context) (int64, error) {
...
}
t.AllocHandle(ctx)
区别是前一种不会把冗余的方法丢到接口定义里面,多个不同的 Table 实现,不会出现代码冗余:
func (t1 T) AllocHandle() { ...}
func (t2 T) AllocHandle() { ...}
func (t3 T) AllocHandle() { ...}
有些接口有一个默认实现,算是基类吧,然后派生类都是继承这个基类的,通常使用的结构体嵌入的形式。
type base struct {}
type derive struct {
base
...
}
随着代码的腐烂,基类里面的方法越来越多了。然后这个接口也就越来越大,越来越恶心了。比如说这里有一个坏的例子。
type Executor interface {
Open(context.Context) error
Next(ctx context.Context, req *chunk.RecordBatch) error
Close() error
Schema() *expression.Schema
// 这里就把基类的一些细节在往接口里面加,接口就被搞大了
retTypes() []*types.FieldType
newFirstChunk() *chunk.Chunk
}
这就涉及到对共同部分的处理。既然基类是所有继承类都是带的,我们可以在接口里面定义一个方法把基类返回出去。这样往基类加东西就不会影响接口的方法:
type Executor interface {
base() *baseExecutor
Open(context.Context) error
Next(ctx context.Context, req *chunk.Chunk) error
Close() error
Schema() *expression.Schema
}
好的例子。
再说一个场景,即想调用基类的公共函数,又想派生类中重载,要怎么搞?
type base struct{}
func (b base) Method() {}
type derived struct {
base
}
var d derived
d.Method() // 这个会调用到 base 的 Method()
如果用 derived
重写掉 Method()
,每个派生类都要把这一段代码重写一遍:
type derivd1 struct{}
func (d derived1) Method() {
d.base.Method()
...
}
type derived2 struct{}
func (d derived2) Method() {
d.base.Method()
...
}
答案是,用函数包装:
func Method(b derived) {
f.Base()
...
}
多写写函数,把接口对象当参数传进去,效果一样的,而不必在接口添加方法,好好体会一下。
随便你们怎么写代码,总之,求你们别再往接口里面乱加方法了!