Home > Blockchain >  How can an external package implement an interface implicitly?
How can an external package implement an interface implicitly?

Time:01-04

I'm writing a piece of code that relies on some implementation.
I want to decouple the implementation from my code, and make the implementation as independent as possible.
I thought of achieving this approach by using interfaces instead of concrete types, like so:

package mypackage

type MyType interface {
    Title() string
    Price() int
}

type TypeGetter interface {
    GetType() MyType
}

func MyHandler(tg TypeGetter) {
    t := tg.GetType()
    fmt.Printf("Title: %s, Price: %d", t.Title(), t.Price())
}

And an implementation might be something like this:

package external

// CustomType implicitly implements the MyType interface
type CustomType struct {
    title string
    price int
}
func (t CustomType) Title() string { return t.title }
func (t CustomType) Price() int { return t.price }


// CustomTypeGetter implicitly implements the TypeGetter interface. Or is it???
type CustomTypeGetter struct {
}
func (g CustomTypeGetter) GetType() CustomType {
    return CustomType{"Hello", 42}
}

Then, the code would do something like this:

package main

import "mypackage"
import "external"

func main() {
    tg := external.CustomTypeGetter{}
    mypackage.MyHandler(tg)            // <--- the compiler does not like this
}

I hope the example speaks for itself: I have no coupling between "mypackage" and the "external" package, which may be replaced, substituted my mocks for testing, etc.

The problem: the compiler complains that the call to MyHandler has an object that implements:
func GetType() CustomType, instead of:
func GetType() MyType

The only solution I found is to move the interface declarations (MyType and TypeGetter) to a third package, and then both "mypackage" and "external" packages can use it.
But I want to avoid that.

Isn't Go's concept of implicit implementation of interfaces contradict the idea of a third common package?

Is there a way to implement such thing, without binding the two packages together?

CodePudding user response:

Isn't Go's concept of implicit implementation of interfaces contradict the idea of a third common package?

I think it does. Go authors introduced an implicit interface implementation to eliminate unnecessary dependencies between packages. That works well for simple interfaces like io.Reader, but you cannot apply it everywhere.

One of the language creators, Rob Pike, says that the non-declarative satisfaction of interfaces is not the essential part of the idea behind interfaces in Go. It's a nice feature, but not all elements of the language are practical or possible to use every time.

For complex interfaces, you need to import a package where the interface is defined. For example, if you want to implement an SQL driver that works with the sql package from the standard library, you must import the sql/driver package.

I would recommend not introducing interfaces at the beginning of your project. Usually, it leads to situations where you need to solve artificial problems like rewriting the interface each time you updates your understanding of the domain model. It is hard to come up with a good abstraction from the first attempt, and, in many cases, it is unnecessary, in my opinion.

I need to query external source for products. I don't care how the external sources store the data (db, file, network). I just need a "product" type. So it's either I define a Product type, forcing the external implementations to import and use it, or the Go way - define a Product interface and let the implementations implicitly implement this interface. Which apparently doesn't work

I see two loosely related goals here:

  1. Define an interface to swap implementations of the product source.
  2. A package that implements the product source should not import the package that defines the interface.

From my experience, I would recommend doing point 1 only when you have at least one working implementation of the product source service.

Point 2 is not always possible to achieve, and it is fine; please see the example from the standard Go library above.

P.S. Please, consider not creating Product interface. While it does makes sense to come up with the PorductSource interface eventually, Product is most probably just a set of data; struct is a perfect way to represent such information. Please, see this very relevant code smaple and this article for inspiration.

CodePudding user response:

The problem with your approach is that you want someone to implement an interface that refers to your type (MyType). This obviously cannot be done without the implementation referring to your type. This is the only thing that prevents the above code from working.

If you get rid of the MyType:

type TypeGetter interface {
    GetType() interface {
        Title() string
        Price() int
    }
}

And the implementation:

func (g CustomTypeGetter) GetType() interface {
    Title() string
    Price() int
} {
    return CustomType{"Hello", 42}
}

Then this code will work:

func main() {
    tg := external.CustomTypeGetter{}
    mypackage.MyHandler(tg)
}

Yes, this requires repetition, but only because you don't want an unknown / future implementation to refer to your type (to not depend on it).

In this case you may change MyHandler() to take a value of type MyType (get rid of the "factory"):

func MyHandler(t MyType) {
    fmt.Printf("Title: %s, Price: %d", t.Title(), t.Price())
}

And any value that implements MyType may be passed. Add a "factory" to the external package:

func NewCustomType(title string, price int) CustomType {
    return CustomType{
        title: title,
        price: price,
    }
}

And use it like this:

func main() {
    t := external.NewCustomType("title", 1)
    mypackage.MyHandler(t)
}

If you truly require the factory pattern, then yes, creating a 3rd package that will hold MyType is the way to go. Then both your app and the implementations may refer to this 3rd package.

  •  Tags:  
  • Related