在Golang中使用多个接口 [英] Using multiple interfaces in Golang

查看:130
本文介绍了在Golang中使用多个接口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在学习Golang,作为使用接口的练习,我正在构建一个玩具程序.我在尝试使用应实现"类型时遇到了一些问题两个接口-解决C ++和Java问题的一种方法是使用继承(还有其他技术,但我认为这是最常见的).由于我在Golang中缺少该机制,因此我不确定如何继续进行下去.下面是代码:

I'm learning Golang and as an exercise in using interfaces I'm building a toy program. I'm having some problem trying to use a type that "should implement" two interfaces - one way to solve that in C++ and Java would be to use inheritance(there are other techniques, but I think that is the most common). As I lack that mechanism in Golang, I'm not sure how to proceed about it. Below is the code:

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

我要实现的目标:

  • 我想隐藏我的结构类型,仅导出接口,以便代码的客户端仅使用"Card" 接口
  • 我想要一个结构的字符串表示形式,因此使用"String()" 方法实现接口,以便能够调用".fmt.Println()" 关于我的struct的实例
  • I would like to hide my struct type and only export the interface so that the clients of the code use only the "Card" interface
  • I would like to have a string representation of the struct, hence the implementation of the interface with the "String()" method in order to be able to call "fmt.Println()" on instantiations of my struct

当我尝试使用新卡时,问题就来了.接口,并尝试获取字符串表示形式.我无法将接口作为"String()" 方法实现的参数传递,因为存在与核心语言级别的接口可寻址性相关的编译器错误(仍然翻阅该文档).非常简单的测试示例暴露了这个问题:

The problem comes when I'm trying to use a new card though the "Card" interface and also trying to get the string representation. I cannot pass the interface as the parameter of the implementation of the "String()" method as there is a compiler error which is related to the addressability of an interface at the core language level(still digging through that documentation). The very simple example of testing exposes the issue:

func TestString(t *testing.T) {
    card := NewCard(0)
    assert.EqualValues(t, "AceHearts ", card.String(), " newly created card's string repr should be 'AceHearts '")
}

编译器有充分的理由告诉我"card.String未定义(类型card没有字段或方法字符串)" .我可以将"String()" 方法添加到我的"Card" 接口中,但是我发现这并不是很干净:我实现了其他实体使用相同的模型,我将不得不在所有地方添加该冗余;已经有使用该方法的接口.

The compiler tells me, for good reason, that "card.String undefined (type card has no field or method string)". I could just add the "String()" method to my "Card" interface, but I do not find that to be clean: I have other entities implemented with the same model and I would have to add that redundancy everywhere; there is already an interface with that method.

对于上述问题,有什么好的解决方案?

What would be a good solution for the above issue that I'm having?

(以解决一些非常好的评论)

(to address some of the very good comments)

  • 我不希望 Card 接口有其他实现;我不确定我为什么要这么做,那就是更改界面
  • 我想拥有 Card 接口来隐藏实现细节,并让客户针对该接口而不是针对具体类型进行编程
  • 我希望始终为"card struct"结构的所有客户端访问String()接口.实例化(包括通过Card接口实例化的实例化).我对仅具有String接口的客户端不感兴趣.在其他一些语言中,这可以通过实现两个接口(多重继承)来实现.我并不是说这是好是坏,或者不是要就此展开辩论,我只是在说一个事实!
  • 我的目的是确定该语言是否具有同时满足这些要求的任何机制.如果这是不可能的,或者从设计的角度来看,应该以其他方式解决问题,那么我准备接受教育
  • 类型断言非常冗长和明确,会暴露实现细节-它们有其位置,但我认为它们不适用于我所处的情况
  • I do not expect to have another implementation of the Card interface; I'm not sure I grasp why would I want to do that, that is change the interface
  • I would like to have the Card interface to hide away implementation details and for the clients to program against the interface and not against the concrete type
  • I would like to always have access to the String() interface for all clients of the "card struct" instantiations(including the ones instantiated via the Card interface). I'm not interested in having clients only with the String interface. In some other languages this can be achieved by implementing both interfaces - multiple inheritance. I'm not saying that is good or wrong, or trying to start a debate about that, I'm just stating a fact!
  • My intent is to find out if the language has any mechanism to fulfill those requirements simultaneously. If that is not possible or maybe from the point of view of the design the problem should be tackled in a different manner, then I'm ready to be educated
  • Type assertions are very verbose and explicit and would expose implementation details - they have their places but I do not think they are appropriate in the situation I have

推荐答案

我应该先介绍一些前置点:

I should go over some prefacing points first:

  • Go中的接口与其他语言中的接口不同.您不应该假设其他语言的每个想法都应该自动转移.许多人不是.
  • Go既没有类也没有对象.
  • Go不是Java,而Go不是C ++.它的类型系统与那些语言有显着不同.

我想拥有Card接口来隐藏实现细节,并让客户针对该接口而不是针对具体类型进行编程

I would like to have the Card interface to hide away implementation details and for the clients to program against the interface and not against the concrete type

这是您其他问题的根源.

This is the root of your other problems.

正如评论中提到的,我在其他多个软件包中都看到了这一点,并将其视为一种特别令人讨厌的反模式.首先,我将解释为什么这种模式是反"的原因.本质上.

As mentioned in the comments, I see this in multiple other packages and regard it as a particularly pesky anti-pattern. First, I will explain the reasons why this pattern is "anti" in nature.

  • 首先,也是最相关的是,您的例子证明了这一点.您采用了这种模式,这导致了不良后果.正如mkopriva指出的那样,它造成了一个您必须解决的矛盾.
  • 接口的这种用法与它们的预期用途背道而驰,并且这样做并没有带来任何好处.

接口是Go的多态性机制.参数中接口的使用使您的代码更具通用性.想想无处不在的 io.Reader io.Writer .它们是接口的绝佳示例.这就是为什么您可以将两个看似无关的库一起修补,并使它们正常工作的原因.例如,您可以登录到stderr,或登录到磁盘文件,或登录到http响应.这些方法的工作方式完全相同,因为 log.New 使用 io.Writer 参数以及磁盘文件,stderr和http响应writer全部实现 io.Writer .要简单地使用接口来隐藏实施细节",则仅使用接口即可.(我稍后会解释为什么这一点失败了),并没有为您的代码增加任何灵活性.如果有的话,通过利用接口来执行原本不打算完成的任务,这是对接口的滥用.

Interfaces are Go's mechanism of polymorphism. The usage of interfaces in parameters makes your code more versatile. Think of the ubiquitous io.Reader and io.Writer. They are fantastic examples of interfaces. They are the reason why you can patch two seemingly unrelated libraries together, and have them just work. For example, you can log to stderr, or log to a disk file, or log to an http response. Each of these work exactly the same way, because log.New takes an io.Writer parameter, and a disk file, stderr, and http response writer all implement io.Writer. To use interfaces simply to "hide implementation details" (I explain later why this point fails), does not add any flexibility to your code. If anything, it is an abuse of interfaces by leveraging them for a task they weren't meant to fulfill.

  1. 通过确保隐藏所有细节,隐藏我的实现可提供更好的封装和安全性."

  • 您没有实现更大的封装或安全性.通过使struct字段不导出(小写),您已经防止了软件包的任何客户端弄乱您的struct内部.程序包的客户端只能访问您导出的字段或方法.导出结构并隐藏每个字段都没有错.
    1. 结构值既脏又原始,我对传递它们感到不舒服."

    • 然后不传递结构,而将指针传递给struct.那就是你已经在这里做的.传递结构没有内在的错误.如果您的类型的行为类似于可变对象,则指向struct的指针可能是合适的.如果您的类型的行为更像一个不变的数据点,那么struct可能是合适的.
      1. "如果我的程序包导出了 package.Struct ,这令人困惑,但是客户端必须始终使用 * package.Struct 吗?如果他们犯错了怎么办?复制我的struct值是不安全的;事情会破裂的!"
      1. "Isn't it confusing if my package exports package.Struct, but clients have to always use *package.Struct? What if they make a mistake? It's not safe to copy my struct value; things will break!"

      • 为防止出现问题,您实际要做的就是确保您的软件包仅返回 * package.Struct 值.那就是你已经在这里做的.在大多数情况下,人们将使用短赋值:= ,因此他们不必担心正确的类型.如果他们确实手动设置了类型,并意外地选择了 package.Struct ,则当他们尝试为其分配 * package.Struct 时,它们将出现编译错误.
        • All you realistically have to do to prevent problems is make sure that your package only returns *package.Struct values. That's what you're already doing here. A vast majority of the time, people will be using the short assignment :=, so they don't have to worry about getting the type correct. If they do set the type manually, and the choose package.Struct by accident, then they will get a compilation error when trying to assign a *package.Struct to it.
          1. 这有助于将客户代码与程序包代码解耦"

          • 也许.但是,除非您有现实的期望,即您有多个这种类型的现有实现,否则这是过早优化的一种形式(是的,它的确会产生后果).即使您的接口有多种实现,这仍然不是您应该真正地返回该接口值的好理由.在大多数情况下,仅返回具体类型仍然更合适.要了解我的意思,请查看以下页面中的 image 包标准库.
            • Maybe. But unless you have a realistic expectation that you have multiple existent implementations of this type, then this is a form of premature optimization (and yes it does have consequences). Even if you do have multiple implementations of your interface, that's still not a good reason why you should actually return values of that interface. A majority of the time it is still more appropriate to just return the concrete type. To see what I mean, take a look at the image package from the standard library.
            • 制作一个过早的接口 AND 返回可能帮助客户的主要现实情况是:

              The main realistic case where making a premature interface AND returning it might help clients, is this:

              • 您的软件包引入了接口的第二种实现
                • AND客户端已静态且明确地(不是:= )在函数或类型中使用了此数据类型
                • AND客户端也希望将这些类型或功能重用于新的实现.
                • Your package introduces a second implementation of the interface
                  • AND clients have statically and explicitly (not :=) used this data type in their functions or types
                  • AND clients want to reuse those types or functions for the new implementation also.

                  请注意,即使您没有返回过早的接口,这也不会是API的重大改变,因为您只是添加新类型和构造函数.

                  Note that this wouldn't be a breaking API change even if you weren't returning the premature interface, as you're only adding a new type and constructor.

                  如果您决定只声明这个过早的接口,并且仍然返回具体类型(如在 image 包中所做的那样),那么所有客户端可能都需要做要解决此问题,请花费几分钟,使用其IDE的重构工具将 * package.Struct 替换为 package.Interface .

                  If you decided to only declare this premature interface, and still return concrete types (as done in the image package), then all the client would likely need to do to remedy this is spend a couple minutes using their IDE's refactor tool to replace *package.Struct with package.Interface.

                  Go受到了一个有用的工具Godoc的祝福.Godoc会自动从源代码中生成软件包的文档.当您在包中导出类型时,Godoc会向您显示一些有用的东西:

                  Go has been blessed with a useful tool called Godoc. Godoc automatically generates documentation for a package from source. When you export a type in your package, Godoc shows you some useful things:

                  • 该类型,该类型的所有导出方法以及返回该类型的所有函数都在doc索引中组织在一起.
                  • 该类型及其每种方法在页面上都有一个专用部分,用于显示签名以及说明其用法的注释.

                  一旦您将结构气泡包装到接口中,您的Godoc表示就会受到伤害.您的类型的方法不再显示在程序包索引中,因此,程序包索引不再是程序包的准确概述,因为它缺少许多关键信息.而且,每种方法在页面上都不再具有自己的专用空间,这使得其文档更难以查找和阅读.最后,这还意味着您不再能够单击doc页面上的方法名称来查看源代码.并非偶然的是,在许多采用这种模式的软件包中,即使对软件包的其余部分进行了充分的记录,这些去强调的方法也常常没有文档注释.

                  Once you bubble-wrap your struct into an interface, your Godoc representation is hurt. The methods of your type are no longer shown in the package index, so the package index is no longer an accurate overview of the package as it is missing a lot of key information. Also, each of the methods no longer has its own dedicated space on the page, making it's documentation harder to both find and read. Finally it also means that you no longer have the ability to click the method name on the doc page to view the source code. It's also no coincidence that in many packages that employ this pattern, these de-emphasized methods are most often left without a doc comment, even when the rest of the package is well documented.

                  https://pkg.go.dev/github.com/zserge/lorca

                  https://pkg.go.dev/github.com/googollee/go-socket.io

                  在这两种情况下,我们都会看到一个误导性的程序包概述,以及大多数未记录的接口方法.

                  In both cases we see a misleading package overview, along with a majority of interface methods being undocumented.

                  (请注意,我对这些开发人员没有任何反对;显然,每个软件包都存在缺陷,这些示例都是精心挑选的.我并不是说他们没有理由使用这种模式,只是他们的软件包文档是合理的.受其阻碍)

                  (Please note I have nothing against any of these developers; obviously every package has it's faults and these examples are cherry picked. I'm also not saying that they had no justification to use this pattern, just that their package doc is hindered by it)

                  如果您对如何使用接口"感到好奇,我建议您仔细阅读标准库的文档,并注意接口的声明位置,参数和返回位置.

                  If you are curious about how interfaces are "intended to be used", I would suggest looking through the docs for the standard library and taking note of where interfaces are declared, taken as parameters, and returned.

                  https://golang.org/pkg/net/http/

                  https://golang.org/pkg/io/

                  https://golang.org/pkg/crypto/

                  https://golang.org/pkg/image/

                  这是我所知道的唯一一个与接口隐藏"相当的标准库示例.图案.在这种情况下,reflect是一个非常复杂的程序包,并且内部有 reflect.Type 的几种实现.另请注意,在这种情况下,即使有必要,也没人会对它满意,因为对客户而言,唯一真正的效果就是文档更加混乱.

                  Here is the only standard library example I know of that is comparable to the "interface hiding" pattern. In this case, reflect is a very complex package and there are several implementations of reflect.Type internally. Also note that in this case, even though it is necessary, no one should be happy about it because the only real effect for clients is messier documentation.

                  https://golang.org/pkg/reflect/#Type

                  此模式将损害您的文档,但在此过程中什么也不做,除非您可以在非常特殊的情况下使客户端使用您将来可能会或可能不会引入的这种并行实现的速度更快一些.

                  This pattern will hurt your documentation, while accomplishing nothing in the process, except you might make it slightly quicker in very specific cases for clients to use parallel implementations of this type that you may or may not introduce in the future.

                  这些界面设计原则是为了客户的利益,对吗?把自己放在客户的鞋子上,问:我真正收获了什么?

                  These interface design principles are meant for the benefit of the client, right? Put yourself in the shoes of the client and ask: what have I really gained?

                  这篇关于在Golang中使用多个接口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆