go-interface-examples icon indicating copy to clipboard operation
go-interface-examples copied to clipboard

[local-interfaces] What do you do with more complex interfaces?

Open awryme opened this issue 3 years ago • 2 comments

Go doesn't have covariance/contravariance or auto casting between identical interfaces. What it means in context of using interfaces in the package of invocation is that if you have a local interface Handler that uses another local interface, Message it cannot be implemented without importing the Message, and different callers cannot have different interfaces.

type Message interface {
    GetText() string
}
type Handler interface  {
    Handle(msg Message)
}

More to that, if the local interface if not exported (i.e. message, lowercase) it cannot be implemented at all outside of the package.

The only solution I see is to have a common "interface" package and use it in both places, which this article is against. So the question is – what would you propose in that case?

awryme avatar Jan 07 '21 03:01 awryme

Hi there, thanks for asking via issue!

I might need a little more context here. When you say "local interface" and "another local interface", are they in the same package? Where are you trying to implement them?

To some extent, you may end up returning or accepting types from another package and that's okay. In the sample code that's done here with db.User.

For the other point, I take it you mean something like this:

type message interface {
    GetText() string
}

type Handler interface  {
    // How is this called?
    Handle(msg message)
}

An exported method that uses unexported types shouldn't exist. You can't invoke it outside the package, and you certainly can't implement it either. So I would instead expect:

type message interface {
    GetText() string
}

// Maybe this is implemented by a few different types in this package
// and we're not ever expected to create our own types that implement it?
type Handler interface  {
    handle(msg message)
}

Here as well you couldn't implement or mock this in another package. I'd be a little wary of this design as a whole at this point. Part of the reason I'd like a little more context is that I'm wondering why these need to be interfaces in the first place and what their intended use is.

That aside, I'll try and answer a general question: what happens when the interfaces get to be incredibly complicated with references to multiple other packages and a fluent API which returns more interfaces that would then also need to be mocked? Because that's a problem we have right now on a project I'm working on, and following the advice of this article to the letter would absolutely create madness if you tried to generate sub-interfaces of the Kubernetes client, or the MongoDB client that is a nightmare to mock yourself due to the complicated nested types.

When you're faced with using an incredibly complex third party client with lots of generic functionality, I would suggest creating a thin wrapper around it that's specific to your needs; basically go with Adapter Pattern. We do this for the MongoDB client in our project, where the MongoDB client has a lot of very tricky generic bits. We created a thin wrapper around it and gave the functions very clear, specific behavior like "GetLatestX()" or "UpdateY(y Y)". Very similar to this.. These are kept as thin as possible, and only deal directly with data in order to avoid needing to maintain/test any logic. We have some integration tests around them that bring up an actual MongoDB container for testing and don't bother trying to mock Mongo itself. We don't try to make these functions fancy or generic. Usually they're just a few lines long at most. This has worked pretty well for us over time.

Evertras avatar Jan 11 '21 06:01 Evertras

Hey, sorry for a long reply. Thanks for your answer. What I initially meant was if there is any trick of some sorts to the "complex interfaces". i.e. both interfaces are in the same package (the package that accepts the implementation) & one of the interfaces uses the other. So in order for implementation to work it would need to import the second interface to be able to implement it. Kinda harder to describe with words for me, than with code.

Tried to put it in a gist https://gist.github.com/awryme/91b7c7f51adf2ae1a0452200a3ff9c5a

Found out there is no way to solve it, but to share the "Message" interface :( In my case I made it even simpler and turned the "Message" interface into a model, shared by both packages.

Sorry to bother you with dumb questions :) You can close the issue.

awryme avatar Jan 23 '21 21:01 awryme