不久前,一位同事转推了一篇出色的文章“
How to Use Go Interfaces” 。 它讨论了在Go中使用接口时的一些错误,并就如何使用它们提供了一些建议。
在上述文章中,作者引用了标准库的
sort包中的接口作为抽象数据类型的示例。 但是,在我看来,这样的例子并不能很好地说明实际应用中的想法。 特别是关于实现业务领域逻辑或解决实际问题的应用程序。
另外,在Go中使用接口时,经常会有关于过度设计的争论。 碰巧的是,在阅读了此类建议之后,人们不仅停止滥用接口,而且尝试几乎完全放弃它们,从而剥夺了自己使用原则上最强大的编程概念之一的能力(以及Go in特别是)。 顺便说一句,关于Go中的典型错误,Docker的Stive Francia有一篇
不错的报告 。 在那里,特别是多次提到了接口。
总的来说,我同意本文的作者。 尽管如此,在我看来,将接口用作抽象数据类型的话题还是很肤浅,所以我想对其进行一些扩展,并与您一起思考这个话题。
参考原始
在本文的开头,作者提供了一个小的代码示例,借助该示例,他指出了使用开发人员经常创建的接口时出现的错误。 这是代码。
package animal type Animal interface { Speaks() string }
package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() }
作者称这种方法为
“ Java风格的接口用法” 。 声明接口时,我们将实现唯一满足该接口的类型和方法。 我同意作者的观点,这种方法很一般,原始文章中较为惯用的代码如下:
package animal
package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() }
通常,这里的所有内容都清晰易懂。 基本思想是:
“首先声明类型,然后再在使用时声明接口 。
” 这是正确的。 但是,现在让我们对如何将接口用作抽象数据类型提出一些想法。 作者顺便指出,在这种情况下,将接口声明为
“ upfront”没有错。 我们将使用相同的代码。
让我们玩抽象
所以我们有一个马戏团,那里有动物。 马戏团内部有一种相当抽象的方法,
称为“表演” ,它采用“
扬声器”界面并使宠物发出声音。 例如,他将根据上面的示例制作狗吠。 创建动物驯服者。 由于他不在这里呆呆,我们通常也可以让他发出声音。 我们的界面非常抽象。 :)
package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" }
到目前为止,一切都很好。 我们走得更远。 让我们教驯服者命令宠物吗? 到目前为止,我们将只有一个
语音命令。 :)
package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" }
package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d)
嗯,有趣吗? 在这种情况下,我们的同事似乎不高兴他成为宠物? :D怎么办?
说话者似乎在这里不太适合抽象。 我们将创建一个更合适的版本(或更确切地说,我们将以某种方式从
“错误的示例”中返回第一个版本),然后更改方法符号。
package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { }
您说,这不会改变任何内容,但是代码仍然会执行,因为 两种接口都实现一种方法,通常来说您是对的。
但是,此示例捕获了一个重要的想法。 当我们谈论抽象数据类型时,上下文至关重要。 至少引入了新的接口,使代码更加明显和易于阅读。
顺便说一句,迫使驯服者不执行
“ voice”命令的方法之一就是简单地添加他不应该使用的方法。 让我们添加这样的方法,它将提供有关宠物是否可训练的信息。
package circus type Animal interface { Speaker IsTrained() bool }
现在,驯服者无法代替宠物滑倒。
扩大行为
我们将迫使宠物进行更改,以执行更改,此外,让我们添加一只猫。
package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" }
package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" }
太好了,现在我们可以给动物以不同的命令,它们将执行它们。 到一个程度或另一个...:D
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d)
我们的家猫并不特别适合训练。 因此,我们将为驯服者提供帮助,并确保他不会受到驯服。
package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") }
这样更好 与最初的
动物接口(与
Speaker重复)不同,我们现在具有实现非常有意义的行为的
“动物”接口(本质上是抽象数据类型)。
让我们讨论一下界面尺寸
现在,让我们思考一下使用广泛接口之类的问题。
在这种情况下,我们使用具有大量方法的接口。 在这种情况下,建议是这样的:
“函数应接受包含其所需方法的接口 。
”通常,我同意接口应该很小,但是在这种情况下,上下文仍然很重要。 让我们回到我们的代码上,教驯兽师
“赞美”他的宠物。
为了回应称赞,宠物会发出声音。
package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() }
看起来一切都很好,我们使用了最低限度的必要接口。 没有多余的东西。 但是这里又是问题。 该死的,现在我们可以
“赞扬”另一位教练,他
将“发表意见” 。 :D抓住吗?..上下文总是很重要。
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d)
我为什么 在这种情况下,最好的解决方案仍然是使用更宽的接口(代表
“宠物”抽象数据类型)。 由于我们想学习如何称赞宠物,因此,不是任何可以发出声音的生物。
package circus
好多了。 我们可以赞美宠物,但不能赞美驯兽者。 代码再次变得更加简单和明显。
现在关于床的定律
我要谈的最后一点是建议我们应该接受抽象类型并返回特定的结构。 在原始文章中,在描述所谓的
Postel定律的部分中对此进行了提及。
作者引用了法律本身:
“对您的工作要保守,对您的接受要开放”
并根据Go语言对其进行解释
“ Go”:“接受接口,返回结构”
func funcName(a INTERFACETYPE) CONCRETETYPE
您知道,总的来说,我同意,这是一种好习惯。 但是,我想再次强调。 不要从字面上看。 细节在于魔鬼。 与往常一样,上下文很重要。
函数并不总是应返回特定类型。 即 如果需要抽象类型,请返回它。 在避免抽象的同时,无需尝试重写代码。
这是一个小例子。 大象出现在附近的
“非洲”马戏团中,您要求马戏团所有人将大象借给新节目。 对您而言,在这种情况下非常重要,只有大象可以执行与其他宠物相同的命令。 在这种情况下,大象的大小或树干的大小无关紧要。
package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} }
package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e)
如您所见,由于我们不在乎将大象与其他宠物区分开的特定参数,因此我们可以使用抽象,在这种情况下返回接口非常合适。
总结一下
当涉及到抽象时,上下文至关重要。 不要忽略抽象并害怕它们,就像您不应滥用它们一样。 您不应将建议作为规则。 有一些方法已经过时间检验;有一些方法尚待检验。 我希望我能够更深入地讨论将接口用作抽象数据类型的话题,并且摆脱标准库中的常见示例。
当然,对于某些人来说,这篇文章可能看起来太明显了,例子简直就是指尖。 对于其他人,我的想法可能会引起争议,论点令人信服。 但是,可能有人会受到启发,并开始对代码,事物的本质以及抽象进行更深入的思考。
朋友,主要是要不断发展,并从工作中获得真正的快乐。 对所有人都好!
PS。 示例代码和最终版本可以
在GitHub上找到 。