【问题标题】:Ensuring embedded structs implement interface without introducing ambiguity确保嵌入式结构实现接口而不引入歧义
【发布时间】:2016-04-19 06:30:43
【问题描述】:

我正在尝试通过更好地定义接口和使用嵌入式结构来重用功能来清理我的代码库。就我而言,我有许多可以链接到各种对象的实体类型。我想定义接口来捕获实现接口的需求和结构,然后可以嵌入到实体中。

// 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
}

所以我的第一个想法是让 FooLinkerEntity 和 BarLinkerEntity 只实现 Entity 接口。

// 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
}

但是,对于可以链接 Foos 和 Bars 的任何类型,这最终会导致模棱两可的错误。

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

构建此类代码的正确 Go 方法是什么?我是否只是在LinkFoo()LinkBar() 中进行类型断言以获取Identifier()Type()?有没有办法在编译时而不是运行时得到这个检查?

【问题讨论】:

  • 为什么是FooLinkerEntity doesn't implement Entity?似乎FooLinkerEntityEntity 的子类型
  • Enitty 是 FooLinkerEntity 不直接实现的接口(或者如果它实现了,我最终会出现歧义错误)。
  • 我知道它现在没有实现,但我的意思是,正如你所说的All entities implement this(Entity ) interface,为什么不让它实现Entity 接口呢?或者您可以发布一个完整的代码以使细化更清晰。
  • 这就是整个问题。看看上面的 Baz 结构。如果我让 FooLinkerEntity 和 BarLinkerEntity 实现 Entity 接口,我不能再将它们嵌入到其他实体中而不会产生歧义问题。 FooLinkerEntity 永远不会单独使用,我只是用它来封装可以嵌入到其他实体中的功能。

标签: oop go interface composition embedding


【解决方案1】:

Go 是not (quite) an object oriented language:它没有类,它是does not have type inheritance;但它在struct 级别和interface 级别都支持称为embedding 的类似构造,并且它确实具有methods

所以你应该停止思考 OOP 并开始思考组合。既然你在你的 cmets 中说过 FooLinkerEntity 永远不会单独使用,这有助于我们以一种干净的方式实现你想要的。

我将使用新名称和更少的功能来专注于问题和解决方案,这会导致代码更短,也更容易理解。

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

实体

简单的Entity 及其实现如下所示:

type Entity interface {
    Id() int
}

type EntityImpl struct{ id int }

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

美食和酒吧

在您的示例中,FooLinkerEntityBarLinkerEntity 只是 装饰器,因此它们不需要嵌入(扩展在 OOP 中)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 第 2 轮

如果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 的值将用于多个复合文字。由于我们只创建了一个Entity (EntityImpl) 并且我们在所有地方都使用了它,所以在不同的实现类中只有一个一个 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

【讨论】:

  • 这是我试图绕过的丑陋部分x := FooBarEntityImpl{e, FooImpl{e}, BarImpl{e}}。我只希望 FooBarEntityImpl 有一个 id,而不是需要初始化的三个不同的。如果可组合部分依赖于共享属性,则组合似乎不起作用。
  • @Bill 丑陋的部分并没有那么丑:FooBarEntityImpl 只有 一个 id,它在 e 中。初始化只是将 same e 传递给所有需要/使用 Entity: reference 的实现,而不是复制/复制。这是你无法避免的,因为没有继承。
  • 不幸的是,您需要了解 FooBarEntityImpl 的内部结构才能进行序列化,因为您需要初始化组合。我真的只想访问不可能的父实体。所以我想我需要做一百次剪切和粘贴,以便每个实体直接实现 SayFoo 和 SayBar 而不是尝试使用组合。这样他们就可以访问 Id(),因为父元素正确地实现了它。
  • @Bill 请参阅有关FooBarEntity 创建的编辑后答案。另外:“我......想要访问父实体” - Go 中没有继承(因此没有父实体),请尝试不同的想法。
  • @Bill 避免所有这些继承和组合的另一种方法是使用一个函数,在这种情况下为Entity 接口提供任意数量的参数,并通过调用相应的接口方法返回一个序列化的字符串。这避免了关系,也更加清晰。如果纯粹的意图是序列化,那么组合和继承关系似乎是一种矫枉过正(不管是 Go 还是 Java 之类的继承)。
【解决方案2】:

在我看来,在一个结构中具有三个 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()
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多