我已经多次重构了我的树包,并没有找到我满意的解决方案,所以我想就最佳方式提出一些建议。
我试图将问题简化为它的本质,并做了一个由节点组成的树的简单例子。所有节点都具有一组通用功能(在示例中表示为打开/关闭状态)。此外,有几种类型的节点,每种节点都有专门的行为(在示例中表示为实现EditorInterface并具有可见/隐藏状态的可编辑节点)。
在我的示例中,我们尝试满足所需的行为 - 任何节点都可以打开,如果它是可编辑的,它会打开,它应该使编辑器可见。
我的示例定义了两种类型的节点,文件夹和文档。文件是可编辑的。
我的直觉是为节点定义结构,并包含作为成员和方法的通用功能。然后定义文件夹和文档的结构,每个结构中都嵌入了匿名节点结构。
但是,这会导致我的第一个例子突出显示的问题。我创建了一个失败的简单测试:
示例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
}
我曾尝试多次重构以达到理想的行为,但这些方法都没有让我开心。我不会将它们全部粘贴到问题中,但我已经创建了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函数中使用的整个嵌入式结构。再次,这似乎是一个讨厌的kludge。
示例5:https://play.golang.org/p/HMH-Y_RstV 在此示例中,我们将接口作为参数传递给需要它的每个方法。再一次,这感觉不对。
示例6:https://play.golang.org/p/de0iwQ9gGY 在此示例中,我们删除NodeInterface,并从基础结构和实现ItemInterface的对象构造节点。这可能是示例中问题最少的问题,但仍然让我想要更好的解决方案。
也许有人可以建议更好的解决方案?
答案 0 :(得分:0)
在这里,我将重新实现文档节点SetNodeState
,并使用d.node.SetNodeState
来更新节点的状态;在非Go-y术语中,我将特定于类的代码推送到子类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
}
这也允许您编写适用于任何node
without referring to specific node types的常规方法,您可能会发现这些方法比node
方法具有特定类型的类型断言的方法更清晰。
(反过来,这会让你公开Node
/ NodeInterface
并将它们保存在与特定节点类型不同的包中,因为特定的类型只取决于一般类型和从来没有反过来(回想两个Go包can't both depend on each other)。但是将node
类型保存在具有特定节点类型的包中似乎是合理的。)
如果上述方法不适用,那么类似于您的第三个示例(具有接口的功能)可能有所帮助。为了缩短它,该界面可能只提供getNode() *node
而不是setOpen
,appendChild
等,具体取决于具体情况。
Go stdlib导出带有接口的函数,例如io.ReadFull(r, buf)
而不是具有Reader
方法的ReadFull(buf)
。我怀疑它在C ++中被认为是不好的形式,因为代码是在一个简单的函数而不是一个方法,但它在Go中是常见的做法。
所以:有时你可以通过(重新)实现特定类型的方法来获得OO-ish行为;当你不能,接受界面的功能是惯用的。