外部包如何隐式实现接口?

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

How can an external package implement an interface implicitly?

问题

我正在编写一段依赖于某个实现的代码。我希望将实现与我的代码解耦,并使实现尽可能独立。我考虑使用接口而不是具体类型来实现这种方法,代码如下:

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())
}

实现可能如下所示:

package external

// CustomType隐式实现了MyType接口
type CustomType struct {
    title string
    price int
}
func (t CustomType) Title() string { return t.title }
func (t CustomType) Price() int { return t.price }


// CustomTypeGetter隐式实现了TypeGetter接口。或者说是这样吗?
type CustomTypeGetter struct {
}
func (g CustomTypeGetter) GetType() CustomType {
    return CustomType{"Hello", 42}
}

然后,代码可能会这样做:

package main

import "mypackage"
import "external"

func main() {
    tg := external.CustomTypeGetter{}
    mypackage.MyHandler(tg)            // <--- 编译器不喜欢这个
}

我希望示例本身能说明问题:在"mypackage"和"external"包之间没有耦合,可以替换、用于测试的模拟等。

问题是:编译器抱怨调用MyHandler的对象实现了func GetType() CustomType,而不是func GetType() MyType

我找到的唯一解决办法是将接口声明(MyTypeTypeGetter)移动到第三个包中,然后"mypackage"和"external"包都可以使用它。

但我想避免这样做。

Go语言的隐式接口实现的概念是否与第三个"公共"包的想法相矛盾?

有没有一种方法可以实现这样的功能,而不需要将这两个包"绑定"在一起?

英文:

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(&quot;Title: %s, Price: %d&quot;, 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{&quot;Hello&quot;, 42}
}

Then, the code would do something like this:

package main

import &quot;mypackage&quot;
import &quot;external&quot;

func main() {
    tg := external.CustomTypeGetter{}
    mypackage.MyHandler(tg)            // &lt;--- 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?

答案1

得分: 1

Go的隐式接口实现的概念是否与第三个公共包的想法相矛盾?

我认为是的。Go的作者引入了隐式接口实现,以消除包之间的不必要依赖关系。这对于像io.Reader这样的简单接口很有效,但并不适用于所有情况。

语言的创造者之一,Rob Pike,表示非声明性接口满足并不是Go接口背后思想的核心部分。这是一个不错的特性,但并不是语言的所有元素都是实用的或者每次都能使用的。

对于复杂的接口,你需要导入定义接口的包。例如,如果你想要实现一个与标准库中的sql包一起工作的SQL驱动程序,你必须导入sql/driver包。

我建议在项目开始时不要引入接口。通常,这会导致你需要解决一些人为的问题,比如每次更新对领域模型的理解时都需要重写接口。从第一次尝试中得出一个好的抽象很困难,在我看来,很多情况下是不必要的。

我需要查询外部数据源以获取产品。我不关心外部数据源如何存储数据(数据库、文件、网络)。我只需要一个“产品”类型。所以要么我定义一个Product类型,强制外部实现导入和使用它,要么按照Go的方式-定义一个Product接口并让实现隐式实现这个接口。显然这种方式行不通。

我在这里看到两个 loosly 相关的目标:

  1. 定义一个接口来交换产品源的实现。
  2. 实现产品源的包不应该导入定义接口的包。

根据我的经验,我建议只在至少有一个可工作的产品源服务实现时才执行第1点。

第2点并不总是可能实现的,这是可以接受的;请参考上面的标准Go库的示例。

附言:
请考虑不创建Product接口。虽然最终提出ProductSource接口是有意义的,但Product很可能只是一组数据;struct是表示这种信息的完美方式。请参考这个非常相关的代码示例这篇文章以获得灵感。

英文:

> 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.

答案2

得分: 0

你的方法存在一个问题,你希望有人实现一个引用你的类型(MyType)的接口。显然,如果没有实现引用你的类型,这是不可能的。这是导致上述代码无法工作的唯一原因。

如果你去掉MyType

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

和实现部分:

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

那么这段代码将会工作:

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

是的,这需要重复,但只是因为你不希望一个未知/未来的实现引用到_你的_类型(不依赖于它)。

在这种情况下,你可以将MyHandler()更改为接受MyType类型的值(去掉"factory"):

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

任何实现了MyType的值都可以传递进来。在external包中添加一个"factory":

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

然后像这样使用它:

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

如果你真的需要工厂模式,那么是的,创建一个第三个包来保存MyType是正确的方法。然后你的应用程序和实现都可以引用这个第三个包。

英文:

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{&quot;Hello&quot;, 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(&quot;Title: %s, Price: %d&quot;, 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(&quot;title&quot;, 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.

huangapple
  • 本文由 发表于 2022年1月3日 03:20:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/70559038.html
匿名

发表评论

匿名网友

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

确定