设计动机
场景:
我们对于这个图片肯定会非常熟悉,这两幅图片我们都可以看做是一个文件结构,对于这样的结构我们称之为树形结构。
- 在数据结构中我们了解到可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点的时候,就可以对叶子节点进行相关的操作。
- 我们可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些对象即可以是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户,它希望能够一致的对待容器对象和叶子对象。这就是组合模式的设计动机:组合模式定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无需区分,可以对它们进行一致的处理
场景:
- 电脑与它的零件也是部分-整体关系
定义
- 组合模式(Composite Pattern) 也称为 整体-部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得客户对单个对象和组合对象的使用具有一致性。
- 用于把一组相似的对象当做一个单一的对象。
- 组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
- 组合模式 一般用来描述 整体 与 部分 的关系,它将对象组织到树形结构中,最顶层的节点称为 根节点,根节点下面可以包含 树枝节点 和 叶子节点,树枝节点下面又可以包含 树枝节点 和 叶子节点。
- 组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组合到树结构中,可以用来描述整体和部分的关系。
- 同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使得客户程序能够与复杂元素的内部结构解耦。
- 这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
- 这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
由上图可以看出,
- 其实根节点和树枝节点本质上是同一种数据类型(蓝色圆圈),可以作为容器使用;
- 而叶子节点与树枝节点在语义上不属于同一种类型,但是在组合模式 中,会把树枝节点和叶子节点认为是同一种数据类型(用同一接口定义),让它们具备一致行为
- 这样,在组合模式中,整个树形结构中的对象都是同一种类型,带来的一个好处就是客户无需辨别 树枝节点 还是 叶子节点,而是可以直接进行操作,给客户使用带来极大的便利。
比如说计算机的文件系统,文件系统由文件和目录组成,目录下面也可以包含文件或者目录,计算机的文件系统是用递归结构来进行组织的,对于这样的数据结构是非常适用使用组合模式的。
组合模式 核心:借助同一接口,使叶子节点和树枝节点的操作具备一致性。
- 在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。
- 这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
模式结构
组合模式角色:
- 抽象根节点(Component):
- 是组合中对象声明接口,实现所有类共有接口的默认行为。
- 树枝节点(Composite):
- 定义有子部件的组合部件行为,存储子部件(组合树枝节点和叶子节点形成一个树形结构;)
- 在Component接口中实现与子部件有关的操作。
- 叶子节点(Leaf):
- 叶子节点对象,其下再无分支,是系统层次遍历的最小单位;
- 客户端(Client):
- 使用 Component 部件的对象。
实现
组合模式 在代码具体实现上,有两种不同的方式:
透明模式
透明模式:把组合(树节点)使用的方法放到统一行为(Component)中,让不同层次(树节点,叶子节点)的结构都具备一致行为;其 UML 类图如下所示:
透明组合模式 把所有公共方法都定义在 Component 中:
- 这样做的好处是客户端无需分辨是叶子节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口;
- 缺点是叶子节点(Leaf)会继承得到一些它所不需要(管理子类操作的方法)的方法,这与设计模式 接口隔离原则 相违背。
- 解决方法:透明组合模式 中,由于 Component 包含叶子节点所不需要的方法,因此,我们直接将这些方法默认抛出UnsupportedOperationException异常。
安全模式
统一行为(Component)只规定系统各个层次的最基础的一致行为,而把组合(树节点)本身的方法(管理子类对象的添加,删除等)放到自身当中;其 UML 类图如下所示:
安全组合模式 把系统各层次公有的行为定义在 Component 中,把组合(树节点)特有的行为(管理子类增加,删除等)放到自身(Composite)中。
- 这样做的好处是接口定义职责清晰,符合设计模式 单一职责原则 和 接口隔离原则;
- 缺点是客户需要区分树枝节点(Composite)和叶子节点(Leaf),这样才能正确处理各个层次的操作,客户端无法依赖抽象(Component),违背了设计模式 依赖倒置原则。
问:透明组合模式 和 安全组合模式 都有各自的优点和缺点,那么我们应该优先选择哪一种呢?
答:既然 组合模式 会被分为两种实现,那么肯定是不同的场合某一种会更加适合,也即具体情况具体分析。
- 透明组合模式 将公共接口封装到抽象根节点(Component)中,那么系统所有节点就具备一致行为,所以如果当系统绝大多数层次具备相同的公共行为时,采用 透明组合模式 也许会更好(代价:为剩下少数层次节点引入不需要的方法);
- 而如果当系统各个层次差异性行为较多或者树节点层次相对稳定(健壮)时,采用 安全组合模式
注:设计模式的出现并不是说我们要写的代码一定要遵循设计模式所要求的方方面面,这是不现实同时也是不可能的。设计模式的出现,其实只是强调好的代码所具备的一些特征(六大设计原则),这些特征对于项目开发是具备积极效应的,但不是说我们每实现一个类就一定要全部满足设计模式的要求,如果真的存在完全满足设计模式的要求,反而可能存在过度设计的嫌疑。同时,23种设计模式,其实都是严格依循设计模式六大原则进行设计,只是不同的模式在不同的场景中会更加适用。设计模式的理解应该重于意而不是形,真正编码时,经常使用的是某种设计模式的变形体,真正切合项目的模式才是正确的模式。
例子
例子1
#include<iostream>
#include <memory>
#include <list>
using namespace std;
class Componet;
typedef shared_ptr<Componet> COMPONET;
class Componet{
public:
explicit Componet(string & _name) : name(_name){}
virtual ~Componet() { };
virtual void Add(COMPONET c) = 0;
virtual void Remove(COMPONET c) = 0;
virtual void Display(int depth){
for (int i = 0; i < depth; i++)
cout << "-";
cout << name << endl;
}
protected:
std::string name;
};
//叶子
class Leaf : public Componet{
public:
explicit Leaf(std::string _name) : Componet(_name){}
~Leaf() override= default;;
void Remove(COMPONET c) override {
throw "Cannot remove from a leaf";
}
void Add(COMPONET c) override{
throw "Cannot add to a leaf";
}
};
class Composite : public Componet{
public:
Composite(string _name) : Componet(_name) {}
~Composite() {}
void Add(COMPONET c) override {
children.push_back(c);
}
void Remove(COMPONET c) override{
children.remove(c);
}
void Display(int depth) override {
Componet::Display(depth);
for (const auto& itr : children)
{
itr->Display(depth + 3);
}
}
private:
std::list<COMPONET> children;
};
int main(int argc,char **argv){
//建立根
COMPONET root(new Composite(std::string("root")));
//添加两片叶子到根
root->Add(COMPONET(new Leaf(std::string("Leaf A"))));
root->Add(COMPONET(new Leaf(std::string("Leaf B"))));
//创建分支
COMPONET compositeX(new Composite(string("composite X")));
compositeX->Add(COMPONET(new Leaf(std::string("Leaf A"))));
compositeX->Add(COMPONET(new Leaf(std::string("Leaf B"))));
//分支挂在根上
root->Add(compositeX);
//创建分支
COMPONET compositeY(new Composite(string("composite Y")));
compositeY->Add(COMPONET(new Leaf(std::string("Leaf A"))));
compositeY->Add(COMPONET(new Leaf(std::string("Leaf B"))));
//分支挂在另一个分支上
compositeX->Add(compositeY);
root->Add(COMPONET(new Leaf(std::string("Leaf C"))));
COMPONET leaf(new Leaf(string("Leaf D")));
root->Add(leaf);//添加叶子
root->Remove(leaf);//移除叶子
//显示树形结构
root->Display(1);
return 0;
}
例子2
角色
- Builder(建造者):
- 负责定义生成实例的接口
- 负责定义生成实例的接口
- ConcreteBuiler(具体的建造者)
- 负责实现Builder的接口
- 定义了最终生成结构的方法
- Director(监工):
- 负责使用Builder角色的方法,它不依赖于ConcreteBuiler的角色。
- 为了确保不论ConcreteBuiler角色是如何定义的,Director都能正常工作,它只调用在Builder定义的方法
例图
时序图
实现
#include<iostream>
#include <memory>
#include <list>
using namespace std;
class Product{
public:
Product(){
cout << "Product..." << endl;
}
};
class Builder{
public:
virtual void BuildPartA() = 0;
virtual void BuildPartB() = 0;
virtual void BuildPartC() = 0;
virtual Product * GetProduct() = 0;
virtual ~Builder(){};
};
class ConcreteBuilder : public Builder{
public:
void BuildPartA() {
cout << "BuildPartA..." << endl;
}
void BuildPartB() {
cout << "BuildPartB..." << endl;
}
void BuildPartC() {
cout << "BuildPartC..." << endl;
}
Product* GetProduct() {
return new Product();
}
};
class Director{
public:
Director(Builder *pBuilder){
_builder = pBuilder;
}
void Construct(){
_builder->BuildPartA();
_builder->BuildPartB();
_builder->BuildPartC();
}
private:
Builder *_builder;
};
int main(int argc,char **argv){
auto builder = new ConcreteBuilder;
auto director = new Director(builder);
director->Construct();
Product* product = builder->GetProduct();
delete product;
delete builder;
delete director;
return 0;
}
场景
-
电脑型号A与各个零件,电脑型号B与各个零件
-
总公司、分公司,部分之间的关系
-
在文件系统中,可能存在很多种格式的文件,如果图片,文本文件、视频文件等等,这些不同的格式文件的浏览方式都不同,同时对文件夹的浏览就是对文件夹中文件的浏览,但是对于客户而言都是浏览文件,两者之间不存在什么差别,现在只用组合模式来模拟浏览文件
-
复制文件:一个个复制,或者整个文件夹复制
-
文本编辑:对单个字操作/整段文字操作