【问题标题】:Struggling to See the Purpose of an Interface Type努力了解接口类型的用途
【发布时间】:2013-09-17 15:32:39
【问题描述】:

我最近喜欢上了 Go 编程语言,到目前为止我觉得它很棒,但我真的很难理解接口。我已经阅读了很多关于它们的内容,但它们对我来说仍然很抽象。

我写了一小段代码,使用下面的接口:

package main

import (
  "fmt"
  "math"
)

type Circer interface {
    Circ() float64
}

type Square struct {
    side float64
}

type Circle struct {
    diam, rad float64
}

func (s *Square) Circ() float64 {
    return s.side * 4
}

func (c *Circle) Circ() float64 {
    return c.diam * math.Pi
}

func (c *Circle) Area() float64 {
    if c.rad == 0 {
        var rad = c.diam / 2
        return (rad*rad) * math.Pi
    } else {
        return (c.rad*c.rad) * math.Pi
    }
}

func main() {

    var s = new(Square)
    var c = new(Circle)

    s.side = 2
    c.diam = 10

    var i Circer = s

    fmt.Println("Square Circ: ", i.Circ())

    i = c

    fmt.Println("Circle Circ: ", i.Circ())
}

我看不出Circer 界面的用途。这些方法已经编写好了,我可以通过直接在结构上调用它们来节省两行代码,而不是使用 Circer 作为包装器。

我有什么遗漏吗?我是否错误地使用了界面?任何帮助或示例表示赞赏。

【问题讨论】:

标签: interface go


【解决方案1】:

接口的重点是你可以制作像下面的ShowMeTheCircumference这样的通用函数。

package main

import (
    "fmt"
    "math"
)

type Circer interface {
    Circ() float64
}

type Square struct {
    side float64
}

type Circle struct {
    diam, rad float64
}

func (s *Square) Circ() float64 {
    return s.side * 4
}

func (c *Circle) Circ() float64 {
    return c.diam * math.Pi
}

func ShowMeTheCircumference(name string, shape Circer) {
    fmt.Printf("Circumference of %s is %f\n", name, shape.Circ())
}

func main() {
    square := &Square{side: 2}
    circle := &Circle{diam: 10}
    ShowMeTheCircumference("square", square)
    ShowMeTheCircumference("circle", circle)

}

Playground link

【讨论】:

【解决方案2】:

您缺少的是您无法静态知道您手头有什么样的东西的场景。让我们具体一点。

io.Reader 为例。有很多东西实现了接口的read 方法。假设您编写了一个使用io.Reader 的程序。例如,程序可能会打印 io.Reader 中内容的 MD5 和。

package mypackage

import (
    "fmt"
    "crypto/md5"
    "io"
    "strings"
)

func PrintHashsum(thing io.Reader) {
    hash := md5.New()
    io.Copy(hash, thing)
    fmt.Println("The hash sum is:", hash.Sum(nil))
}

并说你在别处的另一个文件中使用了这个mypackage

func main() {
    mypackage.PrintHashsum(strings.NewReader("Hello world"))
}

现在假设您使用 io.Reader 的实现,它可以动态解压缩 zip 文件,例如 archive/zip 包中的那个。

import "archive/zip"
// ... 
func main() {
    // ...
    anotherReader = zip.NewReader(...)
    // ...
}

由于接口的工作方式,您可以将这样一个 zip 来源的阅读器输入到 MD5-sum 计算 mypackage.PrintHashsum 函数中,而无需对其现有代码进行任何其他操作或重新编译 mypackage

func main() {
    // ...
    anotherReader = zip.NewReader(...)
    mypackage.PrintHashsum(anotherReader)
}

接口与让程序对动态扩展开放有关。在您的示例中,您可能会争辩说编译器应该确切地知道应该调用什么方法。但是在你的编译器支持单独编译(如 Go)以提高速度的情况下,编译器不可能知道:在编译 mypackage 时,编译器无法看到 io.Reader 的所有可能实现:它不是读心者或时间旅行者!

【讨论】:

    【解决方案3】:

    “我们要求严格界定怀疑和不确定的领域!” - 道格拉斯·亚当斯,银河系漫游指南

    为了理解 Go 中的接口,我们必须首先了解我们为什么要对接口进行编程。

    将内容与方法分开

    我们使用接口来隐藏抽象背后的实现细节。我们喜欢隐藏这些细节,因为细节(即如何)比抽象更有可能发生变化,并且因为它允许我们扩展和更改我们的应用程序,而不会在整个程序中产生变化。当消费者依赖于接口而不是具体类型时,他们将他们的程序从接口背后的实现细节中解耦,这保护消费者免受变化,并使其更容易测试、扩展、并维护他们的应用程序。

    Golang 接口

    Go 有一个非常强大的接口实现。与大多数语言一样,它提供了一种通过抽象来指定对象行为的方法,因此任何使用抽象的地方都可以使用该抽象的任何实现,但在 Go 中则不需要明确声明你的具体实现了一个给定的接口,因为 Go 会自动处理这个。

    删除显式声明要求会产生有趣的后果,例如:您可以让程序在执行过程中显示接口,以帮助您识别适当的抽象,而无需在发现所有实现时对其进行注释。这也意味着为测试创建的接口不需要污染您的实现代码。此外,接口和实现者之间没有明确的关系,因此实现者在那个方向上没有依赖/耦合。

    Circer 接口示例

    在您提供的示例中,避免使用接口而不是绑定到实现(具体化)的复杂性和“认知负荷”当然更简单、更容易。在大多数琐碎的例子中,使用接口似乎是教条而不是工程。

    总结

    接口是我们解耦应用程序以使其更容易随着时间的推移而增长的强大方式。如果您预计会发生变化/变化(并且需要保护您的应用程序免受这种变化/变化的影响),那么创建并依赖于接口是朝着正确方向迈出的一步。

    更多...

    看到这个“好”Go Object Oriented Design post

    看看SOLIDdesign principles,因为它是考虑抽象的含义以及管理依赖项和更改的绝佳起点。

    【讨论】:

    • 这就是 Java 接口的使用方式,但我认为 Go 接口并没有习惯性地用于允许未来的实现变化。相反,它们仅在需要时才引入。看看标准库;它包含了 Java 风格将使用接口的具体类型。例如:golang.org/src/pkg/net/http/response.go?s=489:2363#L17
    • Go 绝对允许消费者依赖抽象而不是具体化,Go 标准库经常这样做。您指出了 Response 结构,它封装了状态而不是行为,我正在讨论抽象与具体行为分离的力量。看看在 http 响应库中使用的 golang.org/src/pkg/io/io.go。我完全不同意不使用接口来防止变化是惯用的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多