在Go中作为抽象数据类型进行接口

不久前,一位同事转推了一篇出色的文章“ How to Use Go Interfaces” 。 它讨论了在Go中使用接口时的一些错误,并就如何使用它们提供了一些建议。

在上述文章中,作者引用了标准库的sort包中的接口作为抽象数据类型的示例。 但是,在我看来,这样的例子并不能很好地说明实际应用中的想法。 特别是关于实现业务领域逻辑或解决实际问题的应用程序。

另外,在Go中使用接口时,经常会有关于过度设计的争论。 碰巧的是,在阅读了此类建议之后,人们不仅停止滥用接口,而且尝试几乎完全放弃它们,从而剥夺了自己使用原则上最强大的编程概念之一的能力(以及Go in特别是)。 顺便说一句,关于Go中的典型错误,Docker的Stive Francia有一篇不错的报告 。 在那里,特别是多次提到了接口。

总的来说,我同意本文的作者。 尽管如此,在我看来,将接口用作抽象数据类型的话题还是很肤浅,所以我想对其进行一些扩展,并与您一起思考这个话题。

参考原始


在本文的开头,作者提供了一个小的代码示例,借助该示例,他指出了使用开发人员经常创建的接口时出现的错误。 这是代码。

package animal type Animal interface { Speaks() string } // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() } 

作者称这种方法为“ Java风格的接口用法” 。 声明接口时,我们将实现唯一满足该接口的类型和方法。 我同意作者的观点,这种方法很一般,原始文章中较为惯用的代码如下:

 package animal // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 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) // woof t.Command(circus.ActVoice, t2) // WAT? } 

嗯,有趣吗? 在这种情况下,我们的同事似乎不高兴他成为宠物? :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) // "woof" t.Command(circus.ActJump, d) // "jumps" t.Command(circus.ActSit, d) // "sit" t2 := &circus.Tamer{} c := &animal.Cat{} t2.Command(circus.ActVoice, c) // "meow" t2.Command(circus.ActJump, c) // "meow!!" t2.Command(circus.ActSit, c) // "meow!!!" } 

我们的家猫并不特别适合训练。 因此,我们将为驯服者提供帮助,并确保他不会受到驯服。

 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) // woof t.Praise(c) // meow! t.Praise(t2) // WAT? } 

我为什么 在这种情况下,最好的解决方案仍然是使用更宽的接口(代表“宠物”抽象数据类型)。 由于我们想学习如何称赞宠物,因此,不是任何可以发出声音的生物。

 package circus // Now we are using Animal interface here. func (t *Tamer) Praise(a Animal) string { return a.Speaks() } 

好多了。 我们可以赞美宠物,但不能赞美驯兽者。 代码再次变得更加简单和明显。

现在关于床的定律


我要谈的最后一点是建议我们应该接受抽象类型并返回特定的结构。 在原始文章中,在描述所谓的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) // "pawoo!" t.Command(circus.ActJump, e) // "o_O" t.Command(circus.ActSit, e) // "sit" } 

如您所见,由于我们不在乎将大象与其他宠物区分开的特定参数,因此我们可以使用抽象,在这种情况下返回接口非常合适。

总结一下


当涉及到抽象时,上下文至关重要。 不要忽略抽象并害怕它们,就像您不应滥用它们一样。 您不应将建议作为规则。 有一些方法已经过时间检验;有一些方法尚待检验。 我希望我能够更深入地讨论将接口用作抽象数据类型的话题,并且摆脱标准库中的常见示例。

当然,对于某些人来说,这篇文章可能看起来太明显了,例子简直就是指尖。 对于其他人,我的想法可能会引起争议,论点令人信服。 但是,可能有人会受到启发,并开始对代码,事物的本质以及抽象进行更深入的思考。

朋友,主要是要不断发展,并从工作中获得真正的快乐。 对所有人都好!

PS。 示例代码和最终版本可以在GitHub上找到

Source: https://habr.com/ru/post/zh-CN450386/


All Articles