登录
首页 >  Golang >  Go问答

在 Golang 中,如何为接收接口的函数定义消费者接口?

来源:stackoverflow

时间:2024-02-10 22:00:25 179浏览 收藏

知识点掌握了,还需要不断练习才能熟练运用。下面golang学习网给大家带来一个Golang开发实战,手把手教大家学习《在 Golang 中,如何为接收接口的函数定义消费者接口?》,在实现功能的过程中也带大家重新温习相关知识点,温故而知新,回头看看说不定又有不一样的感悟!

问题内容

如果我正确理解 go 实践,调用者(又名消费者)应该从其依赖项(又名生产者)定义他们想要使用的接口。

但是,如果生产者有一个接受自定义类型的函数,那么最好让它接受一个接口,对吧?这样,消费者就可以传递一些符合生产者接口的值,而不知道确切的类型。因为生产者函数中的输入值使生产者成为该输入值的“消费者”。

好吧,很公平。

问题是,消费者如何定义一个接口,其中包含一个函数,其参数是生产者中定义的接口?

试图让问题更清楚

假设我有一个名为 chef 的包,它有一个结构 chef。它有一个方法 cut(fruit) error 并且 fruit 是我的 chef 包中定义的接口。

现在假设我在调用代码中,并且导入包 chef。我想给它一个水果来切,但就我而言,我实现了一个名为 apple 的特定水果。当然,我会尝试为自己构建这个界面:

type myrequirements interface {
  cut(apple) error
}

因为我有 fruit 接口的具体实现,称为 apple,所以我想表明我的接口仅适用于 apple。

但是,如果我尝试对我的界面使用 chef{},go 将抛出编译错误,因为我的界面想要 cut(apple)chef{} 想要 cut(fruit)。尽管 apple 实现了 fruit,但情况还是如此。

避免这种情况的唯一方法似乎是使 chef.fruit 成为公共接口,并在我自己的接口中使用它。

type myRequirements interface {
  Cut(chef.Fruit) error
}

但这完全破坏了我在接口下插入不同实现(而不是 chef)的能力,因为现在我与 chef 紧密耦合。

所以chef有一个内部接口fruit,但是调用者只知道apple。如何在调用者界面中指示哪些输入应进入 cut 而不引用 chef

回答评论“为什么需要 myrequirements?”

令我惊讶的是,这在 go 社区中并未得到广泛认可。

我需要 myrequirements 接口的原因是因为我是 chef 包的消费者。除了cut之外,厨师可能还有100多种方法。但我只使用cut。我想向其他开发人员表明,在我的情况下,我只使用 cut。我还希望允许测试仅模拟 cut 以使我的代码正常工作。此外,我需要能够插入 cut 的不同实现(来自不同的厨师)。这是我在文章开头提到的 golang 最佳实践。

一些引用作为证据:

golang wiki 说:“go 接口通常属于使用接口类型值的包,而不是实现这些值的包。”

dave cheney 的博客解释道:“接口声明调用者需要的行为,而不是类型将提供的行为。让调用者定义一个描述他们期望的行为的接口。该接口属于他们,即消费者,而不是你。”

jason moiron 的推文指出了一个常见的误解:“人们把它搞反了:#golang 接口是为使用它们的函数而存在的,而不是来描述实现它们的类型”

更新

到目前为止,我得到的最好建议是将接口移到第三个包中,独立于调用者和生产者。例如,制作一个kitchen包,在其中定义fruit接口,并在厨师和调用者中都使用它。有点像每个人都使用 time.time。也许这是最好的建议。尽管如此,我仍然希望从那些在实际工作中尝试解决这个问题的人那里获得权威的观点。


正确答案


我想说这取决于你能控制什么。在您的示例中,您似乎描述了两个单独的包。有多种方法可以处理此问题:

接受函数

您可以修改 apifunction 以接受处理您想要的情况的函数:

type consumerdeps interface {
    apifunction(func() string) string
}

这将使您能够向消费者注入您想要的确切功能。然而,这样做的缺点是,这很快就会变得混乱,并且可能会混淆定义函数的意图,并在实现接口时导致意想不到的后果。

接受接口{}

您可以修改 apifunction 以接受由实现该接口的人处理的 interface{} 对象:

type consumerdeps interface {
    apifunction(interface{}) string
}

type producer struct{}

type apifunctioninput interface {
    hello() string
}

func (producer) apifunction(i interface{}) string {
    return i.(apifunctioninput).hello()
}

这好一点,但现在您依赖生产者端来正确解释数据,如果它没有执行此操作所需的所有上下文,您可能会遇到意外的行为或恐慌,如果它转换为错误的类型。

接受第三方接口

你还可以创建一个第三方接口,这里称之为adapter,它将定义生产者端和消费者端都可以同意的功能:

type adapter interface {
    hello() string
}

type consumerdeps interface {
    apifunction(adapter) string
}

现在,您有了一个数据合约,消费者可以使用它发送数据,生产者可以使用它接收数据。这可能像定义一个单独的包一样简单,也可能像整个存储库一样复杂。

重新设计

最后,您可以重新设计代码库,这样生产者和消费者就不会像这样耦合在一起。尽管我不知道您的具体用例,但您遇到这个特定问题的事实意味着您的代码耦合得太紧,并且可能应该重新设计。消费者端和生产者端包之间可能存在一个元素拆分,可以将其提取到第三个包。

在讨论“Problem with "define interface where it is used" principle in Golang”中的并行案例后,我相信,在您的情况下,问题的本质似乎是这样的:当生产者包中的函数/方法需要一个接口作为参数时,当我们尝试在消费者包中创建一个接口来抽象生产者时,就会产生一个难题。

这是因为我们要么必须在我们的消费者接口中引用生产者的接口(违反了在使用接口时定义接口的原则),要么我们必须在消费者包中创建一个可能不完全兼容的新接口生产者的接口(导致编译错误)。

在这种情况下,“定义使用接口的地方”准则(如“Effective Go / Interfaces and other types / Generality”中所述)与减少耦合的目标产生矛盾​​。

在这种情况下,思考一下我们为什么要尝试减少耦合可能会很有用。一般来说,我们希望减少耦合以增加灵活性:我们的消费者包对生产者包了解得越少,就越容易将生产者包换成不同的实现。
但与此同时,我们的消费者包需要足够了解生产者包才能正确使用它。

另请参见插图(用不同的语言,但具有类似的想法)“Why coupling is always bad / Cohesion vs. coupling”,来自 Vidar Hokstad

正如您所指出的,一种可能的解决方案是在第三个包中定义共享接口。这减少了消费者包和生产者包之间的耦合,因为它们都依赖于第三个包,但彼此不依赖。然而,这个解决方案可能感觉有点矫枉过正,尤其是对于小接口。另外,它引入了新的依赖项,这会增加复杂性。

鉴于 cheffruit 包结构,一种方法可能涉及在共享包中创建通用接口。我们将其命名为 kitchen

package kitchen

// define the fruit interface in a shared package.
type fruit interface {
    color() string
    taste() string
}

然后在您的 chef 包中,引用此 fruit 接口:

package chef

import "yourproject/kitchen"

type chef struct{}

func (c chef) cut(f kitchen.fruit) error {
    // do the cutting...
    return nil
}

现在,在您的消费者包中:

package consumer

import "yourproject/kitchen"

// define an interface specific to the needs of your package.
type myrequirements interface {
  cut(kitchen.fruit) error
}

该方法允许 consumer 包定义一个接口来满足其需求,而无需直接依赖 chef 包。然而,它确实依赖于对 fruit 是什么的共同理解,它是在共享 kitchen 包中定义的。

请记住,此方法引入了对 kitchen 包的依赖关系,但如果 fruit 是您的应用程序的一个基本概念,许多包都需要理解,那么将其定义在一个公共位置可能是合理的。

在实践中,该决定可能归结为实际考虑。如果生产者的接口是稳定的并且不太可能改变,并且消费者包已经以其他方式与生产者包紧密联系在一起,那么简单地在消费者包中引用生产者的接口可能是有意义的,即使这会增加耦合。

另一方面,如果生产者的接口可能会发生变化,或者如果您想保留将生产者包替换为不同实现的灵活性,那么在第三个包中定义共享接口可能是更好的选择,尽管增加了复杂性。

在最后一种情况下,您仍然拥有 chef 软件包,但您还必须处理另一个软件包 butler,它执行类似的任务,但可能不符合与 chef 相同的接口。

首先,像以前一样,您在 kitchen 包中拥有 fruit 接口:

package kitchen

// define the fruit interface in a shared package.
type fruit interface {
    color() string
    taste() string
}

chef 包中:

package chef

import "yourproject/kitchen"

type chef struct{}

func (c chef) cut(f kitchen.fruit) error {
    // do the cutting...
    return nil
}

现在,让我们想象一个新的包 butler ,它具有类似的 prepare 方法:

package butler

import "yourproject/kitchen"

type butler struct{}

func (b butler) prepare(f kitchen.fruit) error {
    // do some preparing...
    return nil
}

在您的使用者代码中,您现在可以定义两个单独的接口,一个用于每个包的要求:

package consumer

import "yourproject/kitchen"

// Define an interface for the chef package
type chefRequirements interface {
  Cut(kitchen.Fruit) error
}

// And another for the butler package
type butlerRequirements interface {
  Prepare(kitchen.Fruit) error
}

这意味着您已经为 chefbutler 定义了单独的接口,每个接口都根据消费者对每个包的用例进行了定制。
通过这样做,您可以灵活地使用 chefbutler 包(或什至同时使用两者),而无需将代码与任一包紧密耦合。您已将共享接口 (fruit) 移至第三个包 kitchen,并且您正在消费者的接口中使用它。

该示例演示了 go 接口系统的灵活性以及如何使用它来解耦包并支持概念的多种实现。但这确实增加了一些复杂性,但换来了很大的灵活性。

换句话来说,这个困境没有硬性且快速的解决方案:最佳解决方案取决于项目的具体环境和要求。两种方法都需要权衡,正确的选择取决于哪种权衡在您的情况下更容易接受。

还值得注意的是,这是一个相当小众的问题,在日常 go 编程中不太可能经常遇到。在许多情况下,您可以以不会出现此问题的方式构建代码。但当它发生时,它提醒我们,指导方针只是指南,而不是严格的规则,我们作为开发人员需要根据我们对具体问题及其存在的更广泛背景的理解做出最终判断.

好了,本文到此结束,带大家了解了《在 Golang 中,如何为接收接口的函数定义消费者接口?》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多Golang知识!

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>