10

求求你们,不要再往接口里面乱加方法了

 3 years ago
source link: https://www.zenlife.tk/stop-add-method-to-interface.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

求求你们,不要再往接口里面乱加方法了

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()
    ...
}

多写写函数,把接口对象当参数传进去,效果一样的,而不必在接口添加方法,好好体会一下。

随便你们怎么写代码,总之,求你们别再往接口里面乱加方法了!


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK