组合模式相比于仅使用数组有哪些优势?

6
我最近在处理一棵树形结构,有多个节点,多个可增加的层级和一个print()方法。 起初,我认为这应该是一个组合模式,然后我写下了一些可能的设计和代码: alt text
$struc = new Node(‘name0’, ‘id0’, ‘desc0’);
$node1 = new Node(‘node1’, ‘id1’, ‘desc1’);
$node2 = new Node(‘node2’, ‘id2’, ‘desc2’);
$node3 = new Node(‘node3’, ‘id3’, ‘desc3’);
$leaf1 = new Leaf(‘leaf1’, ‘ld1’, ‘lesc1’);
$leaf2 = new Leaf(‘leaf2’, ‘ld2’, ‘lesc2’);
$leaf3 = new Leaf(‘leaf3’, ‘ld3’, ‘lesc3’);
$leaf4 = new Leaf(‘leaf4’, ‘ld4’, ‘lesc4’);

$struc.add($node1);
$struc.add($node3);

$node1.add($leaf1);
$node1.add($leaf2);
$node1.add($node2);

$node2.add($leaf3);    
$node3.add($leaf4);

看起来不错,我开始编码了,print()方法可能后续会遵循迭代器模式。 但是在编码过程中,我感觉这些简单的节点是否太复杂了?而且我不得不实例化很多具体类(超过50个以上,还在增加)。于是我停下来思考,用数组找一个简单的类似方式:

-- Structure Class --
//To be more readable and clear, array here could be
//divided to 3 arrays(root/nodes/leafs), then connect
//in a similar way Composite does.
$struc = array('name0', 'id0', 'desc0',

           'children'=>array(

               array('node1', 'id1', 'desc1',
                  'children' => array(
                     array('leaf1', 'ld1', 'lesc1'),
                     array('leaf2', 'ld2', 'lesc2'),
                     array('node2', 'id2', 'desc2',
                        'children'=>array(array('leaf3', 'ld3', 'lesc3'))
                     )
                  )
               ),

               array('node3', 'id3', 'desc3',
                  'children' => array(array('leaf4', 'ld4', 'lesc4'))
               )
           )
);

function print($node = $this->struct) {
    ...
    if(isset($node['children'])) $this->print($node['children']);
    ...
}

这两个设计看起来非常相似,现在我有点困惑,组合模式的价值是什么,我是否错过了这种模式的重要内容?


2
我的结论是:设计模式很好,但并不意味着它们总是首选。在这个例子中,考虑到树有1000多个节点,一个组合实现需要客户端实例化1000多个对象,那么怎么办?你必须使用另一种模式——享元模式来提高性能和节省资源。程序变得越来越复杂,最后需要使用组合+迭代器+享元(有时还要用访问者)实现。而对于一个可以通过一个类的数组和一个方法解决的简单问题来说,最终的答案是,数组实现胜过模式实现。 - Edward
3个回答

7

组合的价值在于你可以通过交换一些复杂性来避免破坏封装性

在你的数组版本中,你正在破坏封装性,因为你正在测试节点是否不是叶子节点:

if(isset($node['children'])) $this->print($node['children']);

使用复合类型,你可以这样说:

print();

那么运行时多态性将调用正确的方法。在这种情况下(我不是PHP程序员,所以让我使用类似Java的语法):

class Node {

   void print() {
       for (child in children) {
          child.print();
       } 
   }

   ...
}

class Leaf {

   void print() {
       // print it! 
   }
}

与普通数组相比,另一个优点是你可以隐藏实现细节(数据结构等)


我尝试理解,但仍然无法抓住精髓。以下是我的想法,请随意指出我的错误,我也纠正了一些代码中的错误。
  1. “打破封装”,我困惑的是如果我不将节点视为实体,它是否仍然是打破封装?最终会导致什么问题?
  2. 我认为实现细节隐藏在类结构中,不是这样吗?
  3. 我仍然找不到组合模式的优势,无论我如何修改这棵树,采用两种方式所需的代码更改量似乎都相同。
- Edward
现在代码看起来更好了:1. 使用组合模式,您不再区分对节点的操作和对叶子的操作。2. 现在清楚地表明您的数组被封装在一个类中;在您原始的问题中并没有这样。 - dfa

6
组合模式的目的是将一组对象视为单个对象(例如,用于显示或写入文件)。正如您自己所写“print()方法可能后续遵循Iterator模式” - 组合模式的重点在于,您可以调用print()而不必担心是否正在打印单个Component还是必须遍历整个Tree。
但是看起来您对面向对象编程并不清楚,因为您考虑使用嵌套数组。使用对象而不是哈希(PHP数组)的价值在于类型安全性,这使得调试和维护程序变得更加容易。

你是对的,我曾经是一名长期C程序员,在面向对象编程中,我遵循许多原则和模式,因为我知道它们很重要,但我真的不知道它们为什么重要。在我看来,面向对象主要是使程序易于维护和更改的方法,但通常我看不到,甚至不知道它是否有效。例如,在这个例子中,我仍然看不到组合模式应该提供的较低价格如何添加/删除节点,更改顺序,两个代码更改是否相同? - Edward
这样想:使用数组而不是对象类似于使用void*作为所有方法参数的C程序。 - Michael Borgwardt
我感受到你的不同思维方式,考虑到数组的实现,我认为它是一种面向对象的实现,只是不是组合模式,对吗? 我能理解你所说的例子,但是用对象代替数组并不能帮助我理解,在这个例子中,有什么好处呢? - Edward
正如我所说:使用对象的好处是类型安全。基于数组的实现具有相同的结构,但它完全失去了类型安全性,因此失去了面向对象编程以及组合模式的大部分优势。在这种简单情况下可能并不重要,但当结构变得更加复杂时,对所有内容使用数组意味着您更有可能犯错误(因为对于您、编译器和运行时来说,一个数组看起来像其他任何数组),而且调试起来更加困难。 - Michael Borgwardt
哇,太棒了!通过在谷歌上搜索,我发现了一些有趣的关于JAVA数组、PHP类型安全的讨论。还有这句话:"PHP是一种弱类型语言。这意味着任何变量(例如$x)都可以是任何类型。在一行中,它可能是一个整数,但在下一行中,你可以将它视为字符串(我经常在我的程序中这样做)。虽然松散的类型从运行时完整性的角度来看可能不太“安全”,但它允许PHP使用许多有用的技巧来处理数组,使它们在某些情况下非常高效。"感谢您强调这个有价值的观点,我现在对它有了更深入的了解,并且对PHP也更加熟悉了。 - Edward

0

对于你来说,实现数组看起来不是更加复杂吗?如果我看着它,我必须仔细研究才能理解你在做什么。你要处理索引,将条目分配给特定的索引等等...看起来非常复杂。

另一方面,你在使用组合模式时,代码看起来简单自然。你有一个节点,并附加其他节点或叶子。没有什么复杂、奇怪的东西,我不必关心/记住索引等等...这就是为什么组合模式在你的情况下很有用。当然,如果你不习惯使用它,它的实现可能看起来有点复杂,但重要的点(正如其他人所说)是你隐藏了细节。这是非常重要的。在简单的例子中,你的数组仍然可以工作,但当你在生产环境中实现这样的代码时,可能还会有其他开发人员编辑/维护你的代码。如果有人必须修改你的“数组”解决方案,他会引入新的bug的可能性要大得多。


我纠正了以前代码中的一些错误,现在它应该更接近我的想法了。 - Edward

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接