C++设计模式——组合模式
微信公众号:幼儿园的学霸
目录
定义
定义
Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
将对象组合成树形结构以表示 “部分-整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
组合模式 一般用来描述整体与部分的关系,它将对象组织到树形结构中,最顶层的节点称为 根节点,根节点下面可以包含 树枝节点 和 叶子节点,树枝节点下面又可以包含 树枝节点 和 叶子节点。如下图所示:
由上图可以看出,其实 根节点
和 树枝节点
本质上是同一种数据类型(蓝色圆圈),可以作为容器使用;而 叶子节点
与 树枝节点
在语义上不属于同一种类型,但是在 组合模式
中,会把 树枝节点
和 叶子节点
认为是同一种数据类型(用同一接口定义),让它们具备一致行为。这样,在 组合模式 中,整个树形结构中的对象都是同一种类型,带来的一个好处就是客户无需辨别 树枝节点 还是 叶子节点,而是可以直接进行操作,给客户使用带来极大的便利。
组合模式的设计动机:如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。
组合模的核心:借助同一接口,使叶子节点和树枝节点的操作具备一致性。
组合模式的本质:统一叶子对象和组合对象/树枝对象
结构
组合模式包含以下主要角色:
- 抽象根节点(Component):定义系统各层次对象的共有方法和属性。它的主要作用是为树枝节点和树叶节点声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树节点完成。
- 树叶节点(Leaf):是组合中的叶子节点对象,其下再无子节点,用于实现抽象根节点中 声明的公共接口,它是系统层次遍历的最小单位。
- 树枝节点(Composite):是组合中的分支节点对象,它有子节点。它实现了抽象根节点中声明的接口,它的主要作用是存储和管理子节点,组合树枝节点和叶子节点形成一个树形结构,通常包含
Add()、Remove()、GetChild()
等方法。
组合模式在代码具体实现上,有透明组合模式和安全组模式两种实现。
透明组合模式:
透明组合模式把组合(树枝节点、树叶节点)使用的方法全部放到抽象根节点(Component)中,让不同层次的节点(树枝节点、树叶节点)的结构都具备相同的行为。其UML类图如下:
在透明组合模式中,由于抽象根节点声明了所有子类中的全部方法,所以产生的好处是客户端无须分辨是树叶节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口,对客户端来说是透明的;但其缺点是树叶节点会继承得到一些它所不需要的方法(管理子类操作的方法,如树叶节点本来没有 Add()、Remove() 及 GetChild() 方法,但是由于被继承了下来,因此需要进行实现,实现为空方法或者抛异常),这与设计模式中接口隔离原则相违背。
接口隔离原则:1)客户端不应该依赖它不需要的接口;2)类间的依赖关系应该建立在最小的接口上
安全组合模式:
抽象根节点(Component)只规定各个层次的最基础的一致行为,而把组合(树枝节点)本身的方法(如管理子类对象的添加、删除等)放到自身当中。其UML类图如下:
在该方式中由于将树枝节点特有的行为放到自身当中,树叶节点没有对子对象的管理方法,带来的好处是接口定义职责清晰,符合设计模式的接口隔离原则和单一职责原则,避免了上一种方式的安全性问题;但是由于树枝节点和树叶节点有不同的接口,使得客户端调用时需要区分树枝节点(Composite)和树叶节点(Leaf),这样才能正确处理各个层次的操作,客户端无法依赖抽象(Component),失去了透明性,违背了设计模式的依赖倒置原则。
单一职责原则:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责
依赖倒置原则:程序要依赖于抽象接口,不要依赖于具体实现。或,要对抽象(抽象类)进行编程,不要对实现(具体的实现类)进行编程
代码示例
场景:北京总公司,上海分公司,北京财务部,上海财务部的公司结构。
以北京总部,上海分公司、广州分公司、成都分公司为例。 透明组合模式的代码如下:
#include <bits/stdc++.h>
//
//组合模式:透明组合模式
//
// 抽象的部件类描述将来所有部件共有的行为
class Component {
public:
Component(const std::string &name)
: m_strCompname(name) {
}
virtual ~Component() = default;//基类:虚析构函数
virtual void Operation() = 0;
virtual void Add(const std::shared_ptr<Component> pComponent) = 0;
virtual void Remove(const std::shared_ptr<Component> pComponent) = 0;
//为了讲解方便,此处用了裸指针(Raw Pointer),实际项目建议修改为智能指针
virtual std::vector<std::shared_ptr<Component>> *GetChild() = 0;
virtual const std::string &GetName() const {
return m_strCompname;
}
//打印节点及子节点
//共有行为及实现,该函数放在基类中即可
virtual void Print(int level = 0) {
std::cout << std::string(4*level,'-') <<m_strCompname << std::endl;
//打印其下节点
auto childs = this->GetChild();
if (childs)
for (auto child:*childs)
child->Print(level + 1);
};
protected:
std::string m_strCompname;
};
//树叶节点
class Leaf : public Component {
public:
Leaf(const std::string &name) : Component(name) {
}
void Operation() override {
std::cout << "I'm " << m_strCompname << std::endl;
}
// 叶节点没有Add功能,但这样做能使接口具备一致性,这就是透明方式,
// 如果不加入Add和Remove方法,那就是安全方式。
void Add(const std::shared_ptr<Component> pComponent) override {}
void Remove(const std::shared_ptr<Component> pComponent) override {}
std::vector<std::shared_ptr<Component>> *GetChild() override { return nullptr; }
};
//树枝节点
class Composite : public Component {
public:
Composite(const std::string &name) : Component(name) {
}
~Composite() {}
void Operation() override {
std::cout << "I'm " << m_strCompname << std::endl;
}
void Add(std::shared_ptr<Component> pComponent) override {
m_vecComp.emplace_back(pComponent);
}
void Remove(std::shared_ptr<Component> pComponent) override {
for (auto it = m_vecComp.begin(); it != m_vecComp.end(); ++it) {
if ((*it)->GetName() == pComponent->GetName()) {
m_vecComp.erase(it);
break;
}
}
}
std::vector<std::shared_ptr<Component>> *GetChild() override {
if (m_vecComp.size())
return &m_vecComp;
return nullptr;
}
private:
std::vector<std::shared_ptr<Component>> m_vecComp;
};
int main(int argc, char *argv[]) {
std::shared_ptr<Component> pNode = std::make_shared<Composite>("Beijing Head Office");//北京总部
std::shared_ptr<Component> pNodeHr = std::make_shared<Leaf>("Beijing Department1");
std::shared_ptr<Component> pSubNodeSh = std::make_shared<Composite>("Shanghai Branch");
std::shared_ptr<Component> pSubNodeCd = std::make_shared<Composite>("Chengdu Branch");
std::shared_ptr<Component> pSubNodeGz = std::make_shared<Composite>("Guangzhou Branch");
pNode->Add(pNodeHr);//叶子节点
pNode->Add(pSubNodeSh);//树枝节点 上海分部
pNode->Add(pSubNodeCd);//树枝节点 成都分部
pNode->Add(pSubNodeGz);//树枝节点 广州分部
pNode->Print();
std::cout<<std::string(50,'-')<<std::endl;
//上海分部节点下面添加自己的叶子节点和树枝节点
std::shared_ptr<Component> pSubNodeShHr = std::make_shared<Leaf>("Shanghai Department1");
std::shared_ptr<Component> pSubNodeShCg = std::make_shared<Leaf>("Shanghai Purchasing Department");
std::shared_ptr<Component> pSubNodeShXs = std::make_shared<Leaf>("Shanghai Sales department");
std::shared_ptr<Component> pSubNodeShZb = std::make_shared<Leaf>("Shanghai Quality supervision Department");
pSubNodeSh->Add(pSubNodeShHr);
pSubNodeSh->Add(pSubNodeShCg);
pSubNodeSh->Add(pSubNodeShXs);
pSubNodeSh->Add(pSubNodeShZb);
pNode->Print();
std::cout<<std::string(50,'-')<<std::endl;
// 公司不景气,需要关闭上海质量监督部门
pSubNodeSh->Remove(pSubNodeShZb);
pNode->Print();
std::cout<<std::string(50,'-')<<std::endl;
return 0;
//运行结果如下:
//Beijing Head Office
//----Beijing Department1
//----Shanghai Branch
//----Chengdu Branch
//----Guangzhou Branch
//--------------------------------------------------
//Beijing Head Office
//----Beijing Department1
//----Shanghai Branch
//--------Shanghai Department1
//--------Shanghai Purchasing Department
//--------Shanghai Sales department
//--------Shanghai Quality supervision Department
//----Chengdu Branch
//----Guangzhou Branch
//--------------------------------------------------
//Beijing Head Office
//----Beijing Department1
//----Shanghai Branch
//--------Shanghai Department1
//--------Shanghai Purchasing Department
//--------Shanghai Sales department
//----Chengdu Branch
//----Guangzhou Branch
//--------------------------------------------------
}
总结
使用场景及注意事项
-
场景
系统对象层次具备整体和部分,呈树形结构,且要求具备统一行为(如树形菜单,操作系统目录结构,公司组织架构等);
希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象时。 -
注意事项
有时候系统需要遍历一个树枝结构的子构件很多次,这时候可以考虑把遍历子构件的结构存储在父构件里面作为缓存(类似我上面写的Print打印节点函数)。
客户端尽量不要直接调用树叶类中的方法,而是借用其父类(Graphics)的多态性完成调用,这样可以增加代码的复用性。
优缺点
- 优点:
组合模式使得客户端代码可以一致地处理对象和对象容器,无需关系处理的单个对象,还是组合的对象容器。
将”客户代码与复杂的对象容器结构“解耦。
可以更容易地往组合对象中加入新的构件。 - 缺点: 使得设计更加复杂。客户端需要花更多时间理清类之间的层次关系。(这个是几乎所有设计模式所面临的问题)。