在Go语言中正确使用多态代码

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

Getting polymorphic code right in Go

问题

我已经多次重构了我的树包,但都没有找到满意的解决方案,所以我想请教一下关于继续进行的最佳方式。

我试图将问题简化,并制作了一个简单的树的示例,该树由节点组成。所有节点都具有一组共同的功能(在示例中表示为打开/关闭状态)。此外,有几种类型的节点,每种节点都具有特定的行为(在示例中表示为实现EditorInterface的可编辑节点,并具有可见/隐藏状态)。

在我的示例中,我们尝试满足所需的行为 - 任何节点都可以打开,当它打开时,如果它是可编辑的,应该使编辑器可见。

我的示例定义了两种类型的节点,文件夹和文档。文档是可编辑的。

我的直觉是为节点定义一个结构体,并将共同功能作为成员和方法包含在内。然后为文件夹和文档定义结构体,每个结构体中都嵌入了一个匿名的节点结构体。

然而,这会导致一个问题,这个问题将在我的第一个示例中突显出来。我创建了一个简单的测试,测试失败了:

示例1:https://play.golang.org/p/V6UT19zVVU

在这个示例中,测试失败了,因为我们无法在SetNodeState中访问接口。

package main

import "testing"

func TestTree(t *testing.T) {
	n := getTestNode()
	n.SetNodeState(true)
	if !n.(*document).visible {
		t.Error("document is not visible")
	}
}

func getTestNode() NodeInterface {
	doc := &document{node: &node{}, content: "foo"}
	folder := &folder{node: &node{children: []NodeInterface{doc}}, color: 123}
	return folder.children[0]
}

type NodeInterface interface {
	SetNodeState(state bool)
}

type EditorInterface interface {
	SetEditState(state bool)
}

type node struct {
	open     bool
	parent   NodeInterface
	children []NodeInterface
}

func (n *node) SetNodeState(state bool) {

	n.open = state

	// TODO: obviously this isn't possible.
	//if e, ok := n.(EditorInterface); ok {
	//	e.SetEditState(state)
	//}
}

type folder struct {
	*node
	color int
}

var _ NodeInterface = (*folder)(nil)

type document struct {
	*node
	visible bool
	content string
}

var _ NodeInterface = (*document)(nil)
var _ EditorInterface = (*document)(nil)

func (d *document) SetEditState(state bool) {
	d.visible = state
}

我尝试了多次重构以实现所需的行为,但没有一种方法让我满意。我不会将它们全部粘贴到问题中,但我创建了Go playground链接:

示例2:https://play.golang.org/p/kyG-sRu6z-
在这个示例中,测试通过了,因为我们将接口添加为嵌入结构体的“self”成员。这似乎是一个不好的修补方法。

示例3:https://play.golang.org/p/Sr5qhLn102
在这个示例中,我们将SetNodeState移动到一个接受接口的函数中。这种方法的缺点是我们无法访问嵌入的结构体,因此所有成员都需要在接口上公开getter和setter。这使得接口变得过于复杂。

示例4:https://play.golang.org/p/P5E1kf4dqj
在这个示例中,我们提供了一个getter来返回整个嵌入的结构体,然后在SetNodeState函数中使用它。这似乎也是一个不好的修补方法。

示例5:https://play.golang.org/p/HMH-Y_RstV
在这个示例中,我们将接口作为参数传递给每个需要它的方法。同样,这种方法不太合适。

示例6:https://play.golang.org/p/de0iwQ9gGY
在这个示例中,我们删除了NodeInterface,并从基本结构体和实现ItemInterface的对象构建节点。这可能是这些示例中问题最少的方法,但我仍然希望有一个更好的解决方案。

也许有人可以提出一个更好的解决方案?

英文:

I've refactored my tree package several times, and not found a solution I'm happy with, so I'd like a little advice on the best way to proceed.

I've tried to reduce the problem down to it's essence, and made a simple example of a tree which is composed of nodes. All nodes have a set of common functionality (expressed in the example as an open / closed state). In addition, there are several types of nodes, each with specialist behaviours (expressed in the example as editable nodes implementing EditorInterface and having a visible / hidden state).

In my example we try to satisfy the desired behaviour - any node can be opened, and when it's opened if it's editable, it should make the editor visible.

My example defines two types of node, folders and documents. Documents are editable.

My instinct is to define a struct for node, and include common functionality as members and methods. Then define structs for folder and document, with an embedded anonymous node struct in each.

However, this causes a problem which will be highlighted by my first example. I've created a simple test that fails:

Example 1: https://play.golang.org/p/V6UT19zVVU

// In this example the test fails because we're unable to access the interface in SetNodeState.
package main
import "testing"
func TestTree(t *testing.T) {
n := getTestNode()
n.SetNodeState(true)
if !n.(*document).visible {
t.Error("document is not visible")
}
}
func getTestNode() NodeInterface {
doc := &document{node: &node{}, content: "foo"}
folder := &folder{node: &node{children: []NodeInterface{doc}}, color: 123}
return folder.children[0]
}
type NodeInterface interface {
SetNodeState(state bool)
}
type EditorInterface interface {
SetEditState(state bool)
}
type node struct {
open     bool
parent   NodeInterface
children []NodeInterface
}
func (n *node) SetNodeState(state bool) {
n.open = state
// TODO: obviously this isn't possible.
//if e, ok := n.(EditorInterface); ok {
//	e.SetEditState(state)
//}
}
type folder struct {
*node
color int
}
var _ NodeInterface = (*folder)(nil)
type document struct {
*node
visible bool
content string
}
var _ NodeInterface = (*document)(nil)
var _ EditorInterface = (*document)(nil)
func (d *document) SetEditState(state bool) {
d.visible = state
}

I've tried to refactor this several times to achieve the desired behaviour, but none of the methods makes me happy. I won't paste them all into the question, but I've created Go playground links:

Example 2: https://play.golang.org/p/kyG-sRu6z-
In this example the test passes, because we add the interface as the "self" member of the embedded struct. This seems like a nasty kludge.

Example 3: https://play.golang.org/p/Sr5qhLn102
In this example, we move SetNodeState to a function that accepts the interface. The disadvantage of this is that we don't have access to the embedded struct, so all members need getters and setters exposed on the interface. This makes the interface needlessly complex.

Example 4: https://play.golang.org/p/P5E1kf4dqj
In this example, we provide a getter to return the entire embedded struct, which we use in the SetNodeState function. Again this seems like a nasty kludge.

Example 5: https://play.golang.org/p/HMH-Y_RstV
In this example we pass in the interface as a parameter to every method that needs it. Again, this doesn't feel right.

Example 6: https://play.golang.org/p/de0iwQ9gGY
In this example, we remove NodeInterface, and construct nodes from a base struct and an object implementing ItemInterface. This is perhaps the least problematic of the examples, but still leaves me wanting a better solution.

Perhaps someone can suggest a better solution?

答案1

得分: 0

在这里,我将重新实现SetNodeState的文档节点,并使用d.node.SetNodeState来更新节点的状态;非Go语言术语中,我会将特定类的代码下推到子类中,像这样

package main

import "testing"

func main() {
	tests := []testing.InternalTest{{"TestTree", TestTree}}
	matchAll := func(t string, pat string) (bool, error) { return true, nil }
	testing.Main(matchAll, tests, nil, nil)
}

func TestTree(t *testing.T) {
	n := getTestNode()
	n.SetNodeState(true)
	if !n.(*document).visible {
		t.Error("document is not visible")
	}
}

func getTestNode() NodeInterface {
	doc := &document{node: &node{}, content: "foo"}
	folder := &folder{node: &node{children: []NodeInterface{doc}}, color: 123}
	return folder.children[0]
}

type NodeInterface interface {
	SetNodeState(state bool)
}

type node struct {
	open     bool
	parent   NodeInterface
	children []NodeInterface
}

func (n *node) SetNodeState(state bool) {
	n.open = state
}

type folder struct {
	*node
	color int
}

var _ NodeInterface = (*folder)(nil)

type document struct {
	*node
	visible bool
	content string
}

func (d *document) SetNodeState(state bool) {
	d.node.SetNodeState(state)
	d.SetEditState(state)
}

func (d *document) SetEditState(state bool) {
	d.visible = state
}

这样做还可以让您编写适用于任何node的通用方法,而无需引用特定的节点类型,这可能比在node方法中使用类型断言更清晰。

(反过来,这还可以让您创建一个公共的Node/NodeInterface,并将它们保留在与特定节点类型不同的包中,因为特定类型只依赖于通用类型,而不是相反的方式(请记住,两个Go包不能相互依赖)。但是,根据您的做法,将node类型与特定节点类型放在同一个包中似乎是合理的。)


在上述方法不适用的情况下,像您的第三个示例(具有接受接口的函数)可能会有所帮助。为了简化一些,该接口可能只需提供getNode() *node而不是setOpenappendChild等,具体取决于具体情况。

Go标准库导出了接受接口的函数,例如io.ReadFull(r, buf)而不是Reader具有ReadFull(buf)方法。我怀疑在C++中,将代码放在裸函数而不是方法中会被认为是不好的做法,但在Go中,这是一种常见的做法。


所以:有时您可以通过在特定类型上(重新)实现方法来获得类似面向对象的行为;当无法实现时,接受接口的函数是惯用的做法。

英文:

Here, I'd have document nodes reimplement SetNodeState, and use d.node.SetNodeState to update the node's state; in non-Go-y terms, I'd push the class-specific code down to the subclass, like this:

package main
import "testing"
func main() {
tests := []testing.InternalTest{{"TestTree", TestTree}}
matchAll := func(t string, pat string) (bool, error) { return true, nil }
testing.Main(matchAll, tests, nil, nil)
}
func TestTree(t *testing.T) {
n := getTestNode()
n.SetNodeState(true)
if !n.(*document).visible {
t.Error("document is not visible")
}
}
func getTestNode() NodeInterface {
doc := &document{node: &node{}, content: "foo"}
folder := &folder{node: &node{children: []NodeInterface{doc}}, color: 123}
return folder.children[0]
}
type NodeInterface interface {
SetNodeState(state bool)
}
type node struct {
open     bool
parent   NodeInterface
children []NodeInterface
}
func (n *node) SetNodeState(state bool) {
n.open = state
}
type folder struct {
*node
color int
}
var _ NodeInterface = (*folder)(nil)
type document struct {
*node
visible bool
content string
}
func (d *document) SetNodeState(state bool) {
d.node.SetNodeState(state)
d.SetEditState(state)
}
func (d *document) SetEditState(state bool) {
d.visible = state
}

This also lets you write the general methods that apply to any node without referring to specific node types, which you might find cleaner than an approach where node methods have type assertions for particular types.

(That, in turn, would let you make a public Node/NodeInterface and keep them in a separate package from specific node types, since the specific types would only depend on the general type and never the other way around (recall two Go packages can't both depend on each other). But it seems reasonable to keep the node type together in a package with the specific node types as you're doing.)


Where the above approach doesn't apply, something like your third example (having a function that takes the interface) might help. To shorten it a bit, that interface might be able to provide just getNode() *node rather than setOpen, appendChild, etc., depending on specifics of the situation.

The Go stdlib exports functions that take interfaces, with, e.g., io.ReadFull(r, buf) instead of Readers having a ReadFull(buf) method. I suspect it'd be considered bad form in, say, C++ for the code to be in a bare function rather than a method, but it's a common practice in Go.


So: sometimes you can get OO-ish behavior by (re)implementing a method on the specific type; when you can't, functions accepting an interface are idiomatic.

huangapple
  • 本文由 发表于 2015年11月29日 18:44:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/33981867.html
匿名

发表评论

匿名网友

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

确定