属于“数据结构”模式,数据结构模式还包括迭代器、职责链设计模式。
动机
软件在某些情况下,客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化将引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。
如何将“客户代码与复杂的对象容器结构”解耦?让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一样来处理复杂的对象容器?
代码
组合设计模式
好处:
- 将内部数据结构的访问封装在类内部,而不需要在外部判断
- 组合设计模式其实就是对树形结构的一种处理方式,只不过在访问时,采用多态的方式把对树访问的放在了树形结构的内部,而不是要把这个数据结构暴露给外界
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
class Component
{
public:
virtual void process() = 0;
virtual ~Component(){}
};
//树节点
class Composite : public Component{
string name;
list<Component*> elements;
public:
Composite(const string & s) : name(s) {}
//添加子节点
void add(Component* element) {
elements.push_back(element);
}
//移除子节点
void remove(Component* element){
elements.remove(element);
}
//处理
//好处:把内部数据结构的访问封装进来了,而不需要在外部判断区分
void process(){
//1. 处理当前节点
//...
//2. 处理叶子节点
for (auto &e : elements)
e->process(); //多态调用
}
};
//叶子节点
class Leaf : public Component{
string name;
public:
Leaf(string s) : name(s) {}
void process(){
//process current node
}
};
void Invoke(Component & c){
//...
c.process();
//...
}
int main()
{
Composite root("root"); //根节点
Composite treeNode1("treeNode1");
Composite treeNode2("treeNode2");
Composite treeNode3("treeNode3");
Composite treeNode4("treeNode4");
Leaf leat1("left1");
Leaf leat2("left2");
root.add(&treeNode1);
treeNode1.add(&treeNode2);
treeNode2.add(&leaf1);
root.add(&treeNode3);
treeNode3.add(&treeNode4);
treeNode4.add(&leaf2);
Invoke(root); //处理根节点
Invoke(leaf2); //处理叶子节点
Invoke(treeNode3); //处理树节点
}
普通代码
如果没有用组合设计模式,那会怎么做?会带来什么问题?
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
class Component
{
public:
virtual void process() = 0;
virtual ~Component(){}
};
//树节点
class Composite : public Component{
string name;
list<Component*> elements;
public:
Composite(const string & s) : name(s) {}
//添加子节点
void add(Component* element) {
elements.push_back(element);
}
//移除子节点
void remove(Component* element){
elements.remove(element);
}
//处理
void process(){
//处理当前节点
}
};
//叶子节点
class Leaf : public Component{
string name;
public:
Leaf(string s) : name(s) {}
void process(){
//process current node
}
};
void Invoke(Component & c){
//判断c是Composite树节点,还是Leaf叶子节点呢?
//1. 如果是树节点
//取中树里面的子元素,继续处理(一对多的关系)
//2. 如果是叶子节点
//处理这个叶子节点(一对一的关系)
}
//很麻烦,而且暴露了内部的数据结构
//可以通过组合设计模式,用多态的递归调用,可以将一对多,转变为一对一的操作,使问题变得简单
模式定义
将对象组合成树形结构以表示“部分-整体”的层次结构。
Composite使得用户对单个对象和组合对象的使用具有一致性(稳定)。——《设计模式》GoF
对单个对象和组合对象处理具有一致性:
Invoke(root); //处理根节点
Invoke(leaf2); //处理叶子节点
Invoke(treeNode3); //处理树节点
结构
最核心的一点:用多态的调用方式,实现在树形结构中,对非叶子节点与叶子节点做一致性的处理。
【有争议的一点】Add
、Remove
、GetChild
这三个函数是放在Component
父类中,还是放在Composite
子类中。
其实都有不完善的地方。相对来讲,放在子类中是符合逻辑的。否则,在C++的机制中,Leaf::Add、Leaf::Remove是很尴尬的。
一、放在父类Component
中
其实对于Leaf节点就很尴尬,你既然是叶子节点,那你还有Add、Remove、GetChild行为
- 有的朋友说那实现一个空的,但还是不舒服,你能够看到这些行为,而且你能调用,你Add了一个东西,但是你啥也不干
- 当然又有朋友说,你throw一个异常呢?一个异常也不合适,父类有虚函数,子类实现虚函数的时候直接丢异常,这违背了is-a关系准则(子类继承自父类,父类定义的接口,应该是能实现的,现在你仍异常,表明你不支持父类的接口)
二、放在子类Composite
中
但有又一个缺点。到时候添加child时,leaf没有添加能力,只有composite有添加能力。那你这时又要去判断类型了
要点总结
要点一
Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化为“一对一”的关系,使得客户代码可以一致性(复用)处理对象和对象容器,无需关系处理的是单个的对象,还是组合的对象容器。
要点二
将“客户代码与复杂的对象容器结构”解耦是Composite的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对象容器的内部实现结构——发生依赖,从而更能“应对变化”
要点三
Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率(树访问算法方面的技巧)。