属于“数据结构”模式,数据结构模式还包括迭代器、职责链设计模式。

动机

软件在某些情况下,客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化将引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。

如何将“客户代码与复杂的对象容器结构”解耦?让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一样来处理复杂的对象容器?

代码

组合设计模式

好处:

  1. 将内部数据结构的访问封装在类内部,而不需要在外部判断
  2. 组合设计模式其实就是对树形结构的一种处理方式,只不过在访问时,采用多态的方式把对树访问的放在了树形结构的内部,而不是要把这个数据结构暴露给外界
  1. #include <iostream>
  2. #include <list>
  3. #include <string>
  4. #include <algorithm>
  5. using namespace std;
  6. class Component
  7. {
  8. public:
  9. virtual void process() = 0;
  10. virtual ~Component(){}
  11. };
  12. //树节点
  13. class Composite : public Component{
  14. string name;
  15. list<Component*> elements;
  16. public:
  17. Composite(const string & s) : name(s) {}
  18. //添加子节点
  19. void add(Component* element) {
  20. elements.push_back(element);
  21. }
  22. //移除子节点
  23. void remove(Component* element){
  24. elements.remove(element);
  25. }
  26. //处理
  27. //好处:把内部数据结构的访问封装进来了,而不需要在外部判断区分
  28. void process(){
  29. //1. 处理当前节点
  30. //...
  31. //2. 处理叶子节点
  32. for (auto &e : elements)
  33. e->process(); //多态调用
  34. }
  35. };
  36. //叶子节点
  37. class Leaf : public Component{
  38. string name;
  39. public:
  40. Leaf(string s) : name(s) {}
  41. void process(){
  42. //process current node
  43. }
  44. };
  45. void Invoke(Component & c){
  46. //...
  47. c.process();
  48. //...
  49. }
  50. int main()
  51. {
  52. Composite root("root"); //根节点
  53. Composite treeNode1("treeNode1");
  54. Composite treeNode2("treeNode2");
  55. Composite treeNode3("treeNode3");
  56. Composite treeNode4("treeNode4");
  57. Leaf leat1("left1");
  58. Leaf leat2("left2");
  59. root.add(&treeNode1);
  60. treeNode1.add(&treeNode2);
  61. treeNode2.add(&leaf1);
  62. root.add(&treeNode3);
  63. treeNode3.add(&treeNode4);
  64. treeNode4.add(&leaf2);
  65. Invoke(root); //处理根节点
  66. Invoke(leaf2); //处理叶子节点
  67. Invoke(treeNode3); //处理树节点
  68. }

普通代码

如果没有用组合设计模式,那会怎么做?会带来什么问题?

  1. #include <iostream>
  2. #include <list>
  3. #include <string>
  4. #include <algorithm>
  5. using namespace std;
  6. class Component
  7. {
  8. public:
  9. virtual void process() = 0;
  10. virtual ~Component(){}
  11. };
  12. //树节点
  13. class Composite : public Component{
  14. string name;
  15. list<Component*> elements;
  16. public:
  17. Composite(const string & s) : name(s) {}
  18. //添加子节点
  19. void add(Component* element) {
  20. elements.push_back(element);
  21. }
  22. //移除子节点
  23. void remove(Component* element){
  24. elements.remove(element);
  25. }
  26. //处理
  27. void process(){
  28. //处理当前节点
  29. }
  30. };
  31. //叶子节点
  32. class Leaf : public Component{
  33. string name;
  34. public:
  35. Leaf(string s) : name(s) {}
  36. void process(){
  37. //process current node
  38. }
  39. };
  40. void Invoke(Component & c){
  41. //判断c是Composite树节点,还是Leaf叶子节点呢?
  42. //1. 如果是树节点
  43. //取中树里面的子元素,继续处理(一对多的关系)
  44. //2. 如果是叶子节点
  45. //处理这个叶子节点(一对一的关系)
  46. }
  47. //很麻烦,而且暴露了内部的数据结构
  48. //可以通过组合设计模式,用多态的递归调用,可以将一对多,转变为一对一的操作,使问题变得简单

模式定义

将对象组合成树形结构以表示“部分-整体”的层次结构。
Composite使得用户对单个对象和组合对象的使用具有一致性(稳定)。——《设计模式》GoF

对单个对象和组合对象处理具有一致性:

  1. Invoke(root); //处理根节点
  2. Invoke(leaf2); //处理叶子节点
  3. Invoke(treeNode3); //处理树节点

结构

image.png
最核心的一点:用多态的调用方式,实现在树形结构中,对非叶子节点与叶子节点做一致性的处理。

【有争议的一点】
AddRemoveGetChild这三个函数是放在Component父类中,还是放在Composite子类中。
其实都有不完善的地方。相对来讲,放在子类中是符合逻辑的。否则,在C++的机制中,Leaf::Add、Leaf::Remove是很尴尬的。

一、放在父类Component
其实对于Leaf节点就很尴尬,你既然是叶子节点,那你还有Add、Remove、GetChild行为

  1. 有的朋友说那实现一个空的,但还是不舒服,你能够看到这些行为,而且你能调用,你Add了一个东西,但是你啥也不干
  2. 当然又有朋友说,你throw一个异常呢?一个异常也不合适,父类有虚函数,子类实现虚函数的时候直接丢异常,这违背了is-a关系准则(子类继承自父类,父类定义的接口,应该是能实现的,现在你仍异常,表明你不支持父类的接口)

二、放在子类Composite
但有又一个缺点。到时候添加child时,leaf没有添加能力,只有composite有添加能力。那你这时又要去判断类型了

要点总结

要点一

Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化为“一对一”的关系,使得客户代码可以一致性(复用)处理对象和对象容器,无需关系处理的是单个的对象,还是组合的对象容器。

要点二

将“客户代码与复杂的对象容器结构”解耦是Composite的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的内部实现结构——发生依赖,从而更能“应对变化”

要点三

Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率(树访问算法方面的技巧)。