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

huangapple go评论75阅读模式
英文:

In Golang, how can a consumer define an interface for a function that accepts an interface?

问题

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

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

好的,很公平。

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

尝试更清晰地提出问题

假设我有一个名为chef的包,其中有一个名为Chef的结构体。它有一个方法Cut(fruit) error,而fruit是我chef包中定义的一个接口。

现在假设我在调用代码中,导入了chef包。我想给它一个水果来切割,但在我的情况下,我实现了一个特定的水果叫做Apple。自然地,我会尝试为自己构建这个接口:

type myRequirements interface {
  Cut(Apple) error
}

因为我有一个名为Applefruit接口的具体实现,我想表明我的接口只适用于苹果。

然而,如果我尝试在我的接口中使用Chef{},Go会抛出一个编译错误,因为我的接口想要Cut(Apple),而Chef{}想要Cut(Fruit)。尽管Apple实现了fruit接口,但这样做是错误的。

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

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

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

所以Chef有一个内部接口fruit,但调用者只知道Apple。在不引用chef的情况下,调用者的接口如何指示应该将什么输入传递给Cut

回答一个评论“为什么你需要myRequirements?”

我对Go社区对这个概念没有更多的共识感到惊讶。

我需要一个myRequirements接口的原因是因为我是chef包的消费者。除了Cut之外,chef可能还有100多个方法。但我只使用Cut。我想告诉其他开发人员,在我的情况下,我只使用Cut。我还希望允许测试仅为了使我的代码工作而模拟Cut。此外,我需要能够插入不同的Cut实现(来自不同的chef)。这是Go的最佳实践,正如我在帖子开头所提到的。

以下是一些证据:

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

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

Jason Moiron的推文指出一个常见的误解:“人们搞错了:#golang接口存在于使用它们的函数,而不是描述实现它们的类型。”

更新

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

英文:

If I understand Go practices correctly, callers (aka consumers) are supposed to define interfaces of what they want to use from their dependencies (aka producers).

However, if the producer has a function that accepts a custom type, then it's better to make it accept an interface, right? This way a consumer could just pass some value that complies with producer's interface, without knowing the exact type. Because an input value into a producer's function makes the producer become the "consumer" of that input value.

Okay, fair enough.

The question is, how can consumer define an interface, which contains a function, whose parameter is an interface defined in the producer?

Trying to make the question clearer

Let's say I have a package called chef which has a struct Chef. It has a method Cut(fruit) error and fruit is an interface defined in my chef package.

Now let's say I am in the calling code, and I import package chef. I want to give it a fruit to cut, but in my case, I implemented a specific fruit called Apple. Naturally, I will try to build this interface for myself:

type myRequirements interface {
  Cut(Apple) error
}

Because I have the specific implementation of fruit interface called Apple, I want to indicate that my interface just works with apple.

However, if I try to use Chef{} against my interface, Go will throw a compile error, because my interface wants to Cut(Apple) and the Chef{} wants to Cut(Fruit). This is despite the fact that Apple implements fruit.

The only way to avoid this, it seems, is to make chef.Fruit a public interface, and use that in my own interface.

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

But this completely ruins my ability to plug a different implementation (instead of chef) under my interface, because now I'm tightly coupled to chef.

So Chef has an internal interface fruit, but caller only knows about Apple. How can I indicate in the caller's interface what input should go into Cut without referencing chef?

Answering a comment "Why do you need myRequirements?"

I was surprised that this isn't a more agreed upon concept in the Go community.

The reason I need a myRequirements interface is because I’m a consumer of chef package. Besides Cut, chef may have 100 more methods. But I only use Cut. I want to indicate to other developers, that in my situation I’m only using Cut. I also want to allow tests to only mock Cut for my code to work. Additionally, I need to be able to plug a different implementation of Cut (from a different chef). This is a golang best practice as alluded to in the beginning of my post.

Some quotes as evidence:

Golang Wiki says: "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values."

Dave Cheney's blog explains: "Interfaces declare the behaviour the caller requires not the behaviour the type will provide. Let callers define an interface that describes the behaviour they expect. The interface belongs to them, the consumer, not you."

Jason Moiron's tweet points out a common misunderstanding: "people have it backwards: #golang interfaces exist for the functions that use them, not to describe the types that implement them"

Update

The best advice I got so far is to move the interface into a 3rd package, independent of caller and producer. For example, make a kitchen package, define Fruit interface in it, and use it in both chefs and callers. Kind of like everyone uses time.Time. Perhaps that's the best advice. That said, I would still like to get an authoritative perspective from someone who tried to deal with this problem in their real work.

答案1

得分: 3

我认为这取决于你能控制的内容。根据你的例子,似乎你描述了两个不同的包。处理这个问题有几种方法:

接受一个函数

你可以修改ApiFunction,接受一个处理你想要的情况的函数:

type consumerDeps interface {
	ApiFunction(func() string) string
}

这样可以将你想要的功能注入到消费者中。然而,缺点是这可能会变得混乱,并且可能会在实现接口时导致意图不明确和意外后果。

接受一个interface{}

你可以修改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
}

现在,你有一个数据契约,可以由消费者发送和由生产者接收。这可以简单地定义为一个单独的包,也可以是一个完整的存储库。

重新设计

最后,你可以重新设计代码库,使生产者和消费者不像这样紧密耦合。虽然我不知道你的具体用例,但你遇到这个特定问题意味着你的代码耦合度太高,可能需要重新设计。可能有一个元素在消费者端和生产者端之间分离,可以提取到第三个包中。

英文:

I would say that it comes down to what you have control over. In your example, it appears that you've described two separate packages. There are a number of ways to handle this issue:

Accept a Function

You could modify ApiFunction to accept a function that handles the cases you want:

type consumerDeps interface {
	ApiFunction(func() string) string
}

This would allow you to inject the exact functionality you desire into the consumer. However, the downside here is that this can quickly become messy and it can obfuscate the intent of the defined function and lead to unintended consequences when the interface is implemented.

Accept an interface{}

You could modify ApiFunction to accept an interface{} object that is handled by whoever implements the 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()
}

This is a little better but now you're depending on the producer-side to interpret the data correctly, and if it doesn't have all the context necessary to do that, you might wind up with unexpected behavior or panics if it casts to the wrong type.

Accept a Third-Party Interface

You could also create a third-party interface, call it Adapter here, that will define functions both the producer-side and consumer-side can agree to:

type Adapter interface {
    hello() string
}

type consumerDeps interface {
	ApiFunction(Adapter) string
}

Now, you have a data contract that can be used to send by the consumer and to receive by the producer. This may be as simple as defining a separate package, or as complex as an entire repository.

Redesign

Finally, you could redesign your codebase so the producer and consumer are not coupled together like this. Although I don't know your specific usecase, the fact that you're having this particular problem implies that your code is coupled too tightly, and should probably be redesigned. There's probably an element split between both the consumer-side and producer-side package that could be extracted to a third package.

答案2

得分: 2

在讨论了一个并行案例后,我认为,在你的情况下,问题的本质似乎是这样的:当生产者包中的函数/方法期望一个接口作为参数时,当我们尝试在消费者包中创建一个接口来抽象生产者时,就会产生一个困境。

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

这种情况下,“在使用它们的地方定义接口”准则(如在“Effective Go / Interfaces and other types / Generality”中讨论的)与减少耦合的目标产生了紧张关系。

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

另请参见(使用不同的语言,但具有类似的思想)Vidar Hokstad的“为什么耦合总是不好/内聚性与耦合性”作为说明。


一种可能的解决方案,正如你已经注意到的,是在第三个包中定义一个共享接口。这减少了消费者和生产者包之间的耦合,因为它们都依赖于第三个包,而不是彼此。然而,这种解决方案可能会感觉过于复杂,特别是对于小接口来说。此外,它引入了一个新的依赖关系,可能增加了复杂性。

cheffruit包结构中,一种方法可以涉及在一个共享包中创建一个公共接口。让我们称之为kitchen

package kitchen

// 在共享包中定义Fruit接口。
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 {
    // 进行切割...
    return nil
}

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

package consumer

import "yourproject/kitchen"

// 根据你的包需求定义一个特定的接口。
type myRequirements interface {
  Cut(kitchen.Fruit) error
}

这种方法允许consumer包定义一个与其需求匹配的接口,而不直接依赖于chef包。然而,它确实依赖于对Fruit是什么的共同理解,这在共享的kitchen包中定义。

请记住,这种方法引入了对kitchen包的依赖,但如果Fruit是你的应用程序中许多包都需要理解的基本概念,那么在一个共同的地方定义它可能是合理的。


实际上,决策可能取决于实际考虑因素。如果生产者的接口是稳定的且不太可能改变,并且如果消费者包已经与生产者包有其他紧密联系,那么在消费者包中直接引用生产者的接口可能是有意义的,尽管这增加了耦合。

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

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

首先,你在kitchen包中有Fruit接口,就像之前一样:

package kitchen

// 在共享包中定义Fruit接口。
type Fruit interface {
    Color() string
    Taste() string
}

然后,在chef包中:

package chef

import "yourproject/kitchen"

type Chef struct{}

func (c Chef) Cut(f kitchen.Fruit) error {
    // 进行切割...
    return nil
}

现在,假设有一个新的butler包,它有一个类似的Prepare方法:

package butler

import "yourproject/kitchen"

type Butler struct{}

func (b Butler) Prepare(f kitchen.Fruit) error {
    // 进行一些准备...
    return nil
}

在你的消费者代码中,你现在可以定义两个单独的接口,每个包的需求一个:

package consumer

import "yourproject/kitchen"

// 为chef包定义一个接口
type chefRequirements interface {
  Cut(kitchen.Fruit) error
}

// 为butler包定义另一个接口
type butlerRequirements interface {
  Prepare(kitchen.Fruit) error
}

这意味着你为chefbutler定义了单独的接口,每个接口都根据消费者对每个包的使用情况进行了定制。
通过这样做,你保留了使用chefbutler包(甚至两者)的灵活性,而不会将代码紧密耦合到任何一个包中。你已经将共享接口(Fruit)移到了第三个包kitchen中,并在消费者的接口中使用它。

这个例子展示了Go接口系统的灵活性,以及如何使用它来解耦包并支持概念的多个实现。但这确实增加了一些复杂性,以换取更大的灵活性。


换句话说,对于这个困境没有硬性的解决方案:最佳解决方案取决于你的项目的具体上下文和要求。这两种方法都有权衡,正确的选择取决于在你的情况下哪些权衡更可接受。

值得注意的是,在日常的Go编程中,你不太可能经常遇到这个相对较小的问题。在许多情况下,你可以以一种使这个问题不会出现的方式组织你的代码。但当它确实出现时,这是一个提醒,指导方针只是指导,而不是刚性规则,我们作为开发人员需要根据对特定问题的理解和它所存在的更广泛的上下文做出最终判断。

英文:

After discussing a parallel case in "Problem with "define interface where it is used" principle in Golang", I believe that, in your case, the essence of the problem seems to be this: when a function/method in a producer package expects an interface as an argument, that creates a conundrum when we try to create an interface in the consumer package to abstract away the producer.

That is because we either have to reference the producer's interface in our consumer interface (violating the principle of defining interfaces where they are used), or we have to create a new interface in the consumer package that might not be fully compatible with the producer's interface (resulting in compile errors).

That is a situation where the "define interfaces where they are used" guideline (as discussed in "Effective Go / Interfaces and other types / Generality") comes into tension with the goal of reducing coupling.

In such cases, it might be useful to think about why we are trying to reduce coupling. In general, we want to reduce coupling to increase flexibility: the less our consumer package knows about the producer package, the easier it is to swap out the producer package for a different implementation.
But at the same time, our consumer package needs to know enough about the producer package to use it correctly.

See also, as illustration (in different languages, but with a similar idea) "Why coupling is always bad / Cohesion vs. coupling" from Vidar Hokstad.


One possible solution, as you have noted, is to define a shared interface in a third package. That reduces the coupling between the consumer and producer packages, since they both depend on the third package, but not on each other. However, this solution can feel like overkill, especially for small interfaces. Plus, it introduces a new dependency, which can increase complexity.

Given the chef and fruit package structure, one approach could involve creating a common interface in a shared package. Let's name it kitchen.

package kitchen

// Define the Fruit interface in a shared package.
type Fruit interface {
    Color() string
    Taste() string
}

Then in your chef package, you reference this Fruit interface:

package chef

import "yourproject/kitchen"

type Chef struct{}

func (c Chef) Cut(f kitchen.Fruit) error {
    // Do the cutting...
    return nil
}

Now, in your consumer package:

package consumer

import "yourproject/kitchen"

// Define an interface specific to the needs of your package.
type myRequirements interface {
  Cut(kitchen.Fruit) error
}

That approach allows the consumer package to define an interface to match its needs without directly depending on the chef package. However, it does rely on a common understanding of what a Fruit is, which is defined in the shared kitchen package.

Keep in mind, this approach introduces a dependency on the kitchen package, but if Fruit is a fundamental concept to your application that many packages will need to understand, it might be reasonable to define it in a common place.


In practice, the decision might come down to practical considerations. If the producer's interface is stable and unlikely to change, and if the consumer package is already closely tied to the producer package in other ways, then it might make sense to simply reference the producer's interface in the consumer package, even though this increases coupling.

On the other hand, if the producer's interface is likely to change, or if you want to preserve the flexibility to swap out the producer package for a different implementation, then defining a shared interface in a third package might be the better choice, despite the added complexity.

In that last case, you would still have the chef package, but you would also have to deal with another package, butler, which does similar tasks but might not comply with the same interface as chef.

First, you have the Fruit interface in the kitchen package like before:

package kitchen

// Define the Fruit interface in a shared package.
type Fruit interface {
    Color() string
    Taste() string
}

And in the chef package:

package chef

import "yourproject/kitchen"

type Chef struct{}

func (c Chef) Cut(f kitchen.Fruit) error {
    // Do the cutting...
    return nil
}

Now, let's imagine a new package butler which has a similar Prepare method:

package butler

import "yourproject/kitchen"

type Butler struct{}

func (b Butler) Prepare(f kitchen.Fruit) error {
    // Do some preparing...
    return nil
}

In your consumer code, you can now define two separate interfaces, one for each package's requirements:

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
}

That means you have defined separate interfaces for the chef and butler, each tailored to the consumer's use case for each package.
By doing this, you retain the flexibility to use either the chef or butler packages (or even both) without tightly coupling your code to either one. You have moved the shared interface (Fruit) to a third package, kitchen, and you are using that in your consumer's interfaces.

That example demonstrates the flexibility of Go's interface system and how it can be used to decouple packages and support multiple implementations of a concept. But that does add some complexity, in exchange for a good deal of flexibility in return.


In other words, there is no hard and fast resolution to this dilemma: the best solution depends on the specific context and requirements of your project. Both approaches have trade-offs, and the right choice depends on which trade-offs are more acceptable in your situation.

It is also worth noting that this is a fairly niche problem that you are unlikely to encounter often in day-to-day Go programming. In many cases, you can structure your code in such a way that this issue does not arise. But when it does, it is a reminder that guidelines are just that—guides, not rigid rules, and it is up to us as developers to make the final judgment based on our understanding of the specific problem and the broader context in which it exists.

答案3

得分: 1

我不太确定为什么你要引入myRequirements接口。如果Chef需要一个Fruit来进行切割,并且你想定义一个名为Apple的特定水果,你只需要定义一个实现了Fruit接口的Apple结构体即可。

type Chef struct {
}

type fruit interface {
	Cut() error
}

func (c Chef) Cut(fruit fruit) error {
	return fruit.Cut()
}

然后,根据你的需求,你只需要定义一个实现了Fruit接口的Apple结构体:

package kitchen

import chef "goplayground/interfaces/fruits/chef"

type Apple struct {
}

func (a Apple) Cut() error {
	// 进行切割操作
	return nil
}

type myRequirements interface {
	Cut(Apple) error
}

type myChef struct {
	chef chef.Chef
}

func (m myChef) Cut(apple Apple) error {
	// 由于Apple实现了chef的fruit接口,所以可以这样做
	return m.chef.Cut(apple)
}

func cook() {
	remy := myChef{}
	apple := Apple{}
	_ = remy.Cut(apple)
}
英文:

I'm not quite sure why you would introduce the myRequirements interface. If Chef requires a Fruit to Cut and you want to define a specific fruit called Apple - all you need to do is to define an Apple struct which implements the Fruit inteface.

type Chef struct {
}

type fruit interface {
	Cut() error
}

func (c Chef) Cut(fruit fruit) error {
	return fruit.Cut()
}

All you need to do is then to define Apple which implements the Fruit interface based on your requirements:

package kitchen

import chef "goplayground/interfaces/fruits/chef"

type Apple struct {
}

func (a Apple) Cut() error {
	// lets cut
	return nil
}

type myRequirements interface {
	Cut(Apple) error
}

type myChef struct {
	chef chef.Chef
}

func (m myChef) Cut(apple Apple) error {
	// since Apple implements the chef`s fruit interface this is possible
	return m.chef.Cut(apple)
}

func cook() {
	remy := myChef{}
	apple := Apple{}
	_ = remy.Cut(apple)
}


答案4

得分: 1

正确使用鸭子类型(duck-typing)存在一些微妙之处,而Go类型系统在处理接口时就是采用了鸭子类型。通常最好在使用接口时定义接口,但是io.Reader接口是在标准库中定义的,所以这个建议的适用性是有限的。

在你的情况下,chef包有两个接口,ChefFruit。这两个接口是紧密耦合的,因为Chef有一个使用Fruit的方法。在当前的Go类型系统中,如果不将Fruit从该包中导出,你无法使用Chef。所以:

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

是你可以使用chef.Chef和你的包中的Apple实现的唯一方式。

但是你想要的是:

type myRequirements interface {
  Cut(Apple) error
}

并且你希望能够传达这是Chef的一个子集,也就是说,Cut的语义与Chef的语义相同。然而,这两者的语义是不同的,否则就是不安全的。

假设你将Apple实现为:

type Apple struct {}

func (a Apple) SomeFunc()
func (a Apple) FruitFunc()

chef.Fruit是:

type Fruit interface {
   FruitFunc()
}

显然,Apple实现了chef.Fruit,所以你可以将Apple传递给任何需要chef.Fruit的地方。但是你不能将chef.Fruit传递给myRequirements.Cut(Apple)函数,因为在myRequirements.Cut中,你还暗示可能使用Apple.SomeFunc,而这在chef.Fruit中是没有定义的。

所以,如果你真的想定义一个像myRequirements这样的接口,那么你必须使用chef.Fruit来定义它。如果你使用Apple来定义它,那么myRequirements.Cut方法就不同于chef.Cut方法。

英文:

There are nuances to the correct use of duck-typing, which is what Go type system is when it comes to interfaces. It is usually a good practice to define the interfaces where you use them, but io.Reader interface is defined in the standard library. So there are limits to the applicability of that advice.

In your case, the package chef has two interfaces, Chef and Fruit. These two interfaces are closely coupled, because Chef has a method that uses Fruit. With the current Go type system, you cannot use Chef without exporting Fruit from that package. So:

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

is the only way you can use the chef.Chef with an Apple implementation from your package.

But what you want to do is:

type myRequirements interface {
  Cut(Apple) error
}

and that you want to be able to convey that this is a subset of Chef, that is, the semantics of Cut is the same as the semantics of Chef. Well, the semantics are different. It is unsafe otherwise.

Say, you implemented Apple as:

type Apple struct {}

func (a Apple) SomeFunc()
func (a Apple) FruitFunc()

whereas chef.Fruit is:

type Fruit interface {
   FruitFunc()
}

Clearly, Apple implements chef.Fruit, so you can pass Apple to whereever a chef.Fruit is required. But you cannot pass chef.Fruit to myRequirements.Cut(Apple) func. Because in myRequirements.Cut you also implied that you may use Apple.SomeFunc, which is not defined in chef.Fruit.

So, if you really would like to define an interface like myRequirements, then you have to define it using chef.Fruit. If you define is using Apple, the myRequirements.Cut method is different from chef.Cut.

huangapple
  • 本文由 发表于 2022年10月5日 06:33:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/73954228.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定