Inheritance 我如何组织这段Go代码,将方法从嵌入式类型重新定义为更少冗余和更易于维护?

Inheritance 我如何组织这段Go代码,将方法从嵌入式类型重新定义为更少冗余和更易于维护?,inheritance,go,methods,embedding,organization,Inheritance,Go,Methods,Embedding,Organization,我在其中声明了一个类型foo,其中包含一些相互调用的方法(比如:foo.get,由foo.double和foo.toString调用) 我有另一种类型,bar,它嵌入了foo,并重新定义了get。我被迫重新定义bar上的double和toString,这样他们就可以看到bar.get(而不仅仅是foo.get),但这些函数的主体基本上与原始函数相同 是否有更好的方法来组织此代码,以避免冗余,同时仍有bar实现与foo相同的接口 注: 上述组织的准则适用;这很难维护,因为当我在嵌入foo的类型上

我在其中声明了一个类型
foo
,其中包含一些相互调用的方法(比如:
foo.get
,由
foo.double
foo.toString
调用)

我有另一种类型,
bar
,它嵌入了
foo
,并重新定义了
get
。我被迫重新定义
bar
上的
double
toString
,这样他们就可以看到
bar.get
(而不仅仅是
foo.get
),但这些函数的主体基本上与原始函数相同

是否有更好的方法来组织此代码,以避免冗余,同时仍有
bar
实现与
foo
相同的接口

注:

  • 上述组织的准则适用;这很难维护,因为当我在嵌入
    foo
    的类型上重新定义最初在
    foo
    上声明的方法时,我必须仔细检查
    foo
    上有哪些其他方法调用它,并确保重新定义所有这些方法。事实证明,很容易不小心漏掉一个
  • 在基于此的实际项目中,有近十几种类型嵌入了“
    foo
    ”,每种类型上都有相似数量的相互连接的方法
  • 我可以向foo添加一个接口值成员,该成员包含一个指向嵌入它的最外层封闭结构的指针,并让
    foo.double
    调用
    f.outermost.get()
    ,而不是
    f.get()
    ,但这似乎有点浪费,意味着
    foo
    的零值无效。(另外,仅当嵌入类型为结构时才可能。)
在围棋中有,但没有。如果在结构中嵌入类型,则嵌入类型的所有方法都将升级,并将位于包装器结构类型中。但是您不能“覆盖”提升的方法。当然,您可以使用相同的名称添加自己的方法,并且在包装结构上使用该名称调用方法将调用您的方法,但是如果此方法是从嵌入类型调用的,它将不会被分派到您的方法,它仍将调用定义为嵌入类型的“原始”方法

请在此处阅读更多信息:和此处:

看起来您只想“继承”
double()
toString()
方法(在Go中应称为
String()
),而不是
get()
方法,因为它的实现随着类型的变化而变化

所以基本上你应该重构一点。您的
foo
类型应该具有/获取提供
get()
方法的值。您可以使用
getter
界面捕捉到这一点:

type getter interface {
    get() int
}
以及
foo
实现:

type foo struct {
    g getter
}

func (f foo) double() int {
    return f.g.get() * 2
}

func (f foo) toString() string {
    return fmt.Sprintf("%d", f.g.get())
}
以及嵌入
foo
bar
类型,只提供“缺少的”
get()

用法示例:

b := bar{}
b.foo = foo{g: b}
fmt.Println(b.double())
fmt.Println(b.toString())
输出与预期一致(请在上尝试):

用一个简单的函数值 使用上面的
getter
接口很好,因为它在将来需要添加其他方法时提供了灵活性

如果情况并非如此,并且您只需要一个函数,那么您可以省去接口而只使用函数值

这就是它的样子:

type foo struct {
    get func() int
}

func (f foo) double() int {
    return f.get() * 2
}

func (f foo) toString() string {
    return fmt.Sprintf("%d", f.get())
}

type bar struct {
    foo
}

func (b bar) get() int {
    return 69
}
使用它:

b := bar{}
b.foo = foo{get: b.get}
fmt.Println(b.double())
fmt.Println(b.toString())
输出是相同的。试穿一下

请注意,虽然我们使用了
bar.get()
方法(
b.get
),但并不要求
get()
应该是一种方法。它可以是一个普通函数值,甚至是一个函数值,例如:

b := bar{foo: foo{get: func() int { return 69 }}}
fmt.Println(b.double())
fmt.Println(b.toString())

在上试试这个。

是的:我希望我的基类更脆弱一些……我喜欢这种方法;这无疑使代码更易于维护。遗憾的是,它的代价是使
foo
bar
比接口变量的大小更大(如果有很多实例,这会很昂贵),并使零值无效-/接口变量的大小只有几个字节;如果内存消耗是您最关心的问题,那么您应该以牺牲设计为代价来关注它。但需要很多很多实例才能产生重大影响,因为接口值只会增加两个字的开销(64位系统中为16字节)。据推测,到目前为止,您的数据结构使这一点黯然失色。噢:我必须收回我以前的评论(但我不会删除它,因为Adrain的回复将缺少上下文):我现在看到额外的成本只是指针的大小,而不是接口值。我用“tsk”来表示我自己的不专心阅读。@Adrian:在这种情况下,我实际的
foo
对象只有40个字节(其中7个是对齐填充),因此再加上16个字节并不重要。幸运的是,我想出了一个聪明的方法来重新安排事情,添加了一个接口值,但在最常见的情况下,在大小上没有净变化。
b := bar{}
b.foo = foo{get: b.get}
fmt.Println(b.double())
fmt.Println(b.toString())
b := bar{foo: foo{get: func() int { return 69 }}}
fmt.Println(b.double())
fmt.Println(b.toString())