Home > Software design >  In Golang, how can a consumer define an interface for a function that accepts an interface?
In Golang, how can a consumer define an interface for a function that accepts an interface?

Time:10-06

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.

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.

How do I allow Chef to have an internal interface fruit, but still allow the caller's interface to input its own value into the Cut function, which isn't literally fruit, but something compatible with fruit?

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"

CodePudding user response:

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.

package chef

type Chef struct {
}

type Fruit interface {
    Cut() error
}

func (c Chef) Cut(fruit Fruit) {
    err := 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
}

func cook() {
    remy := chef.Chef{}
    apple := Apple{}
    remy.Cut(apple)
}

CodePudding user response:

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.

  • Related