cel*_*vek 2 design-patterns interface go
我正在学习 Golang,作为使用接口的练习,我正在构建一个玩具程序。我在尝试使用“应该实现”两个接口的类型时遇到了一些问题 - 在 C++ 和 Java 中解决这个问题的一种方法是使用继承(还有其他技术,但我认为这是最常见的)。由于我在 Golang 中缺乏这种机制,我不确定如何进行。下面是代码:
var (
faces = []string{"Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"}
suits = []string{"Hearts", "Diamonds", "Spades", "Clubs"}
)
type Card interface {
GetFace() string
GetSuit() string
}
type card struct {
cardNum int
face string
suit string
}
func NewCard(num int) Card {
newCard := card{
cardNum: num,
face: faces[num%len(faces)],
suit: suits[num/len(faces)],
}
return &newCard
}
func (c *card) GetFace() string {
return c.face
}
func (c *card) GetSuit() string {
return c.suit
}
func (c *card) String() string {
return fmt.Sprintf("%s%s ", c.GetFace(), c.GetSuit())
}
Run Code Online (Sandbox Code Playgroud)
我正在努力实现的目标:
当我尝试通过“卡片”界面使用新卡片并尝试获取字符串表示时,问题就出现了。我无法将接口作为“String()”方法实现的参数传递,因为存在与核心语言级别的接口可寻址性相关的编译器错误(仍在深入研究该文档)。一个非常简单的测试示例暴露了这个问题:
func TestString(t *testing.T) {
card := NewCard(0)
assert.EqualValues(t, "AceHearts ", card.String(), " newly created card's string repr should be 'AceHearts '")
}
Run Code Online (Sandbox Code Playgroud)
编译器告诉我,有充分的理由,“card.String undefined (type card has no field or method string)”。我可以将“String()”方法添加到我的“Card”接口中,但我认为这并不干净:我已经用相同的模型实现了其他实体,我必须在任何地方添加冗余;已经有一个带有该方法的接口。
对于我遇到的上述问题,什么是好的解决方案?
编辑:(解决一些非常好的评论)
我应该先回顾一些序言:
我想让 Card 接口隐藏实现细节,让客户端根据接口而不是具体类型进行编程
这是你其他问题的根源。
正如评论中提到的,我在多个其他包中看到了这一点,并将其视为一种特别讨厌的反模式。首先,我将解释这种模式本质上是“反”的原因。
接口是 Go 的多态机制。参数中接口的使用使您的代码更加通用。想想无处不在的io.Reader和io.Writer。它们是接口的绝佳示例。它们是您可以将两个看似无关的库拼凑在一起并让它们正常工作的原因。例如,您可以登录到 stderr,或登录到磁盘文件,或登录到 http 响应。它们中的每一个都以完全相同的方式工作,因为log.New接受一个io.Writer参数,并且一个磁盘文件、stderr 和 http 响应编写器都实现了io.Writer. 简单地使用接口来“隐藏实现细节”(我稍后会解释为什么这点失败),不会给您的代码增加任何灵活性。如果有的话,那就是滥用接口,将它们用于它们不打算完成的任务。
package.Struct,但客户必须总是使用*package.Struct?如果他们犯了错误怎么办?复制我的结构值是不安全的;事情会崩溃!”*package.Struct值。这就是你已经在这里做的事情。大多数情况下,人们会使用 short assignment :=,因此他们不必担心类型是否正确。如果他们确实手动设置了类型,并且package.Struct不小心选择了类型,那么在尝试为其分配 a 时会出现编译错误*package.Struct。image标准库中的包。制作一个过早的界面并返回它可能对客户有帮助的主要现实情况是:
:=在其函数或类型中静态且显式地(未)使用此数据类型请注意,即使您没有返回过早的接口,这也不会是破坏性的 API 更改,因为您只是添加了新类型和构造函数。
如果您决定只声明这个过早的接口,并且仍然返回具体类型(如image包中所做的那样),那么所有客户端可能需要做的就是使用他们的 IDE 的重构工具花几分钟来替换*package.Struct为package.Interface.
Go 拥有一个名为 Godoc 的有用工具。Godoc 从源代码自动生成包的文档。当你在你的包中导出一个类型时,Godoc 会向你展示一些有用的东西:
一旦你将你的结构泡沫包装到一个接口中,你的 Godoc 表示就会受到伤害。您的类型的方法不再显示在包索引中,因此包索引不再是包的准确概述,因为它缺少很多关键信息。此外,每个方法在页面上不再有自己的专用空间,这使得查找和阅读文档变得更加困难。最后,这也意味着您不再能够单击文档页面上的方法名称来查看源代码。在许多采用这种模式的包中,这些不强调的方法通常没有文档注释,这也并非巧合,即使包的其余部分都有很好的文档记录。
https://pkg.go.dev/github.com/zserge/lorca
https://pkg.go.dev/github.com/googollee/go-socket.io
在这两种情况下,我们都看到了误导性的包概述,以及大多数未记录的接口方法。
(请注意,我不反对这些开发人员中的任何一个;显然每个包都有它的缺点,这些示例都是精心挑选的。我也不是说他们没有理由使用这种模式,只是他们的包文档受到了阻碍)
如果您对接口如何“打算使用”感到好奇,我建议您查看标准库的文档,并注意接口的声明、作为参数和返回的位置。
https://golang.org/pkg/net/http/
https://golang.org/pkg/crypto/
这是我所知道的唯一一个与“接口隐藏”模式相当的标准库示例。在这种情况下,reflect 是一个非常复杂的包,reflect.Type内部有多种实现。还要注意,在这种情况下,即使是必要的,也没有人应该对此感到高兴,因为对客户来说唯一真正的影响是更混乱的文档。
https://golang.org/pkg/reflect/#Type
这种模式会损害您的文档,同时在此过程中不会完成任何工作,除非您可能会在非常特定的情况下稍微加快客户端使用您将来可能会或可能不会引入的这种类型的并行实现的速度。
这些界面设计原则是为了客户的利益,对吧?站在客户的角度思考:我真正获得了什么?