确保嵌入的结构体实现接口而不引入歧义

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

Ensuring embedded structs implement interface without introducing ambiguity

问题

我正在尝试通过更好地定义接口和使用嵌入结构来重构我的代码库,以便更好地重用功能。在我的情况下,我有许多可以链接到各种对象的实体类型。我想定义捕捉要求的接口,并定义实现接口的结构,然后将其嵌入到实体中。

// 所有实体都实现这个接口
type Entity interface {
  Identifier()
  Type()
}

// 可以链接到 Foo 的实体的接口
type FooLinker interface {
  LinkFoo()
}

type FooLinkerEntity struct {
  Foo []*Foo
}

func (f *FooLinkerEntity) LinkFoo() {
  // 问题:需要在这里访问 Identifier() 和 Type()
  // 但是 FooLinkerEntity 没有实现 Entity
}

// 可以链接到 Bar 的实体的接口
type BarLinker interface {
  LinkBar()
}

type BarLinkerEntity struct {
  Bar []*Bar
}

func (b *BarLinkerEntity) LinkBar() {
  // 问题:需要在这里访问 Identifier() 和 Type()
  // 但是 BarLinkerEntity 没有实现 Entity
}

所以我最初的想法是让 FooLinkerEntityBarLinkerEntity 实现 Entity 接口。

// Entity 接口的实现
type EntityModel struct {
    Id string
    Object string
}

func (e *EntityModel) Identifier() { return e.Id }
func (e *EntityModel) Type() { return e.Type }

type FooLinkerEntity struct {
  EntityModel
  Foo []*Foo
}

type BarLinkerEntity struct {
  EntityModel
  Bar []*Bar
}

然而,这会导致任何既可以链接到 Foo 又可以链接到 Bar 的类型出现模糊错误。

// Baz.Identifier() 在 EntityModel、FooLinkerEntity 和 BarLinkerEntity 之间存在歧义。
type Baz struct {
    EntityModel
    FooLinkerEntity
    BarLinkerEntity
}

在这种情况下,正确的 Go 代码结构是什么?我是否只需在 LinkFoo()LinkBar() 中进行类型断言以访问 Identifier()Type()?是否有任何方法可以在编译时而不是运行时进行此检查?

英文:

I'm trying to clean up my code base by doing a better job defining interfaces and using embedded structs to reuse functionality. In my case I have many entity types that can be linked to various objects. I want to define interfaces that capture the requirements and structs that implement the interfaces which can then be embedded into the entities.

// All entities implement this interface
type Entity interface {
  Identifier()
  Type()
}

// Interface for entities that can link Foos
type FooLinker interface {
  LinkFoo()
}

type FooLinkerEntity struct {
  Foo []*Foo
}

func (f *FooLinkerEntity) LinkFoo() {
  // Issue: Need to access Identifier() and Type() here
  // but FooLinkerEntity doesn't implement Entity
}

// Interface for entities that can link Bars
type BarLinker interface {
  LinkBar()
}

type BarLinkerEntity struct {
  Bar []*Bar
}

func (b *BarLinkerEntity) LinkBar() {
  // Issues: Need to access Identifier() and Type() here
  // but BarLinkerEntity doesn't implement Entity
}

So my first thought was to have FooLinkerEntity and BarLinkerEntity just implement the Entity interface.

// Implementation of Entity interface
type EntityModel struct {
    Id string
    Object string
}

func (e *EntityModel) Identifier() { return e.Id }
func (e *EntityModel) Type() { return e.Type }

type FooLinkerEntity struct {
  EntityModel
  Foo []*Foo
}

type BarLinkerEntity struct {
  EntityModel
  Bar []*Bar
}

However, this ends up with an ambiguity error for any types that can link both Foos and Bars.

// Baz.Identifier() is ambiguous between EntityModel, FooLinkerEntity,
// and BarLinkerEntity.
type Baz struct {
    EntityModel
    FooLinkerEntity
    BarLinkerEntity
}

What's the correct Go way to structure this type of code? Do I just do a type assertion in LinkFoo() and LinkBar() to get to Identifier() and Type()? Is there any way to get this check at compile time instead of runtime?

答案1

得分: 4

Go不是一个完全面向对象的语言:它没有类,也没有类型继承;但是它支持一种类似的构造,称为嵌入(embedding),可以在结构体(struct)层级和接口(interface)层级上使用,而且它确实有方法(methods)。

所以你应该停止以面向对象的方式思考,开始以组合的方式思考。由于你在评论中说FooLinkerEntity永远不会单独使用,这有助于我们以一种清晰的方式实现你想要的结果。

我将使用新的名称和较少的功能来集中关注问题和解决方案,这样可以得到更短的代码,也更容易理解。

完整的代码可以在Go Playground上查看和测试。

Entity

简单的Entity及其实现如下:

type Entity interface {
    Id() int
}

type EntityImpl struct{ id int }

func (e *EntityImpl) Id() int { return e.id }

Foo和Bar

在你的示例中,FooLinkerEntityBarLinkerEntity只是装饰器(decorators),所以它们不需要嵌入(extend)Entity(在面向对象中),它们的实现也不需要嵌入EntityImpl。然而,由于我们想要使用Entity.Id()方法,我们需要一个Entity值,它可以是EntityImpl,也可以不是,但是让我们不限制它们的实现。我们可以选择嵌入它或将其作为“常规”的结构字段,这都没有关系(两种方式都可以):

type Foo interface {
    SayFoo()
}

type FooImpl struct {
    Entity
}

func (f *FooImpl) SayFoo() { fmt.Println("Foo", f.Id()) }

type Bar interface {
    SayBar()
}

type BarImpl struct {
    Entity
}

func (b *BarImpl) SayBar() { fmt.Println("Bar", b.Id()) }

使用FooBar

f := FooImpl{&EntityImpl{1}}
f.SayFoo()
b := BarImpl{&EntityImpl{2}}
b.SayBar()

输出:

Foo 1
Bar 2

FooBarEntity

现在让我们看一个“真实”的实体,它是一个Entity(实现Entity)并具有FooBar提供的功能:

type FooBarEntity interface {
    Entity
    Foo
    Bar
    SayFooBar()
}

type FooBarEntityImpl struct {
    *EntityImpl
    FooImpl
    BarImpl
}

func (x *FooBarEntityImpl) SayFooBar() {
    fmt.Println("FooBar", x.Id(), x.FooImpl.Id(), x.BarImpl.Id())
}

使用FooBarEntity

e := &EntityImpl{3}
x := FooBarEntityImpl{e, FooImpl{e}, BarImpl{e}}
x.SayFoo()
x.SayBar()
x.SayFooBar()

输出:

Foo 3
Bar 3
FooBar 3 3 3

FooBarEntity 第二轮

如果FooBarEntityImpl不需要知道(不使用)EntityFooBar实现的内部细节(在我们的例子中是EntityImplFooImplBarImpl),我们可以选择只嵌入接口而不是实现(但在这种情况下我们不能调用x.FooImpl.Id(),因为Foo没有实现Entity——这是一个实现细节,而我们最初的说法是我们不需要/使用它):

type FooBarEntityImpl struct {
    Entity
    Foo
    Bar
}

func (x *FooBarEntityImpl) SayFooBar() { fmt.Println("FooBar", x.Id()) }

它的使用方式与之前相同:

e := &EntityImpl{3}
x := FooBarEntityImpl{e, &FooImpl{e}, &BarImpl{e}}
x.SayFoo()
x.SayBar()
x.SayFooBar()

输出:

Foo 3
Bar 3
FooBar 3

Go Playground上尝试这个变体。

创建FooBarEntity

请注意,在创建FooBarEntityImpl时,一个Entity值将在多个复合字面量中使用。由于我们只创建了一个EntityEntityImpl),并且在所有地方都使用了它,因此在不同的实现类中只使用了一个id,每个结构体只传递了一个“引用”,而不是副本。这也是预期/所需的用法。

由于FooBarEntityImpl的创建是复杂且容易出错的,建议创建一个类似构造函数的函数:

func NewFooBarEntity(id int) FooBarEntity {
    e := &EntityImpl{id}
    return &FooBarEntityImpl{e, &FooImpl{e}, &BarImpl{e}}
}

请注意,工厂函数NewFooBarEntity()返回的是一个接口类型的值,而不是实现类型(这是一个良好的实践)。

另外,将实现类型设为非导出的是一个良好的实践,只导出接口,这样实现名称将是entityImplfooImplbarImplfooBarEntityImpl


一些相关问题值得查看

What is the idiomatic way in Go to create a complex hierarchy of structs?

Is it possible to call overridden method from parent struct in Golang?

Can embedded struct method have knowledge of parent/child?

Go embedded struct call child method instead parent method

英文:

Go is not (quite) an object oriented language: it does not have classes and it does not have type inheritance; but it supports a similar construct called embedding both on struct level and on interface level, and it does have methods.

So you should stop thinking in OOP and start thinking in composition. Since you said in your comments that FooLinkerEntity will never be used on its own, that helps us achieve what you want in a clean way.

I will use new names and less functionality to concentrate on the problem and solution, which results in shorter code and which is also easier to understand.

The full code can be viewed and tested on the Go Playground.

Entity

The simple Entity and its implementation will look like this:

type Entity interface {
	Id() int
}

type EntityImpl struct{ id int }

func (e *EntityImpl) Id() int { return e.id }

Foo and Bar

In your example FooLinkerEntity and BarLinkerEntity are just decorators, so they don't need to embed (extend in OOP) Entity, and their implementations don't need to embed EntityImpl. However, since we want to use the Entity.Id() method, we need an Entity value, which may or may not be EntityImpl, but let's not restrict their implementation. Also we may choose to embed it or make it a "regular" struct field, it doesn't matter (both works):

type Foo interface {
	SayFoo()
}

type FooImpl struct {
	Entity
}

func (f *FooImpl) SayFoo() { fmt.Println("Foo", f.Id()) }

type Bar interface {
	SayBar()
}

type BarImpl struct {
	Entity
}

func (b *BarImpl) SayBar() { fmt.Println("Bar", b.Id()) }

Using Foo and Bar:

f := FooImpl{&EntityImpl{1}}
f.SayFoo()
b := BarImpl{&EntityImpl{2}}
b.SayBar()

Output:

Foo 1
Bar 2

FooBarEntity

Now let's see a "real" entity which is an Entity (implements Entity) and has both the features provided by Foo and Bar:

type FooBarEntity interface {
	Entity
	Foo
	Bar
	SayFooBar()
}

type FooBarEntityImpl struct {
	*EntityImpl
	FooImpl
	BarImpl
}

func (x *FooBarEntityImpl) SayFooBar() {
	fmt.Println("FooBar", x.Id(), x.FooImpl.Id(), x.BarImpl.Id())
}

Using FooBarEntity:

e := &EntityImpl{3}
x := FooBarEntityImpl{e, FooImpl{e}, BarImpl{e}}
x.SayFoo()
x.SayBar()
x.SayFooBar()

Output:

Foo 3
Bar 3
FooBar 3 3 3

FooBarEntity round #2

If the FooBarEntityImpl does not need to know (does not use) the internals of the Entity, Foo and Bar implementations (EntityImpl, FooImpl and BarImpl in our cases), we may choose to embed only the interfaces and not the implementations (but in this case we can't call x.FooImpl.Id() because Foo does not implement Entity - that is an implementation detail which was our initial statement that we don't need / use it):

type FooBarEntityImpl struct {
	Entity
	Foo
	Bar
}

func (x *FooBarEntityImpl) SayFooBar() { fmt.Println("FooBar", x.Id()) }

Its usage is the same:

e := &EntityImpl{3}
x := FooBarEntityImpl{e, &FooImpl{e}, &BarImpl{e}}
x.SayFoo()
x.SayBar()
x.SayFooBar()

Its output:

Foo 3
Bar 3
FooBar 3

Try this variant on the Go Playground.

FooBarEntity creation

Note that when creating FooBarEntityImpl, a value of Entity is to be used in multiple composite literals. Since we created only one Entity (EntityImpl) and we used this in all places, there is only one id used in different implementation classes, only a "reference" is passed to each structs, not a duplicate / copy. This is also the intended / required usage.

Since FooBarEntityImpl creation is non-trivial and error-prone, it is recommended to create a constructor-like function:

func NewFooBarEntity(id int) FooBarEntity {
	e := &EntityImpl{id}
	return &FooBarEntityImpl{e, &FooImpl{e}, &BarImpl{e}}
}

Note that the factory function NewFooBarEntity() returns a value of interface type and not the implementation type (good practice to be followed).

It is also a good practice to make the implementation types un-exported, and only export the interfaces, so implementation names would be entityImpl, fooImpl, barImpl, fooBarEntityImpl.


Some related questions worth checking out

https://stackoverflow.com/questions/29144622/what-is-the-idiomatic-way-in-go-to-create-a-complex-hierarchy-of-structs

https://stackoverflow.com/questions/21251242/is-it-possible-to-call-overridden-method-from-parent-struct-in-golang

https://stackoverflow.com/questions/30622605/can-embedded-struct-method-have-knowledge-of-parent-child

https://stackoverflow.com/questions/29390736/go-embedded-struct-call-child-method-instead-parent-method

答案2

得分: 0

在我看来,一个结构体中有三个ID,并且方法依赖于它们,甚至在语义上都是不正确的。为了避免歧义,我认为你应该写一些更多的代码。例如像这样:

type Baz struct {
    EntityModel
    Foo []*Foo
    Bar []*Bar
}
func (b Baz) LinkFoo() {
    (&FooLinkerEntity{b.EntityModel, b.Foo}).LinkFoo()
}
func (b Baz) LinkBar() {
    (&BarLinkerEntity{b.EntityModel, b.Bar}).LinkBar()
}
英文:

Seems to me having three ID in one structure with methods relying on them is even semantically incorrect. To not be ambiguous you should write some more code to my mind. For example something like this

type Baz struct {
    EntityModel
    Foo []*Foo
    Bar []*Bar
}
func (b Baz) LinkFoo() {
    (&FooLinkerEntity{b.EntityModel, b.Foo}).LinkFoo()
}
func (b Baz) LinkBar() {
    (&BarLinkerEntity{b.EntityModel, b.Bar}).LinkBar()
}

huangapple
  • 本文由 发表于 2016年4月19日 14:30:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/36710259.html
匿名

发表评论

匿名网友

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

确定