11. 组合模式
一、组合模式
对于树形结构,当容器对象的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象)并调用执行,牵一而动百,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性。
组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构以表示 “部分-整体” 的层次结构,使得客户端对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。
在组合模式中,你可以创建表示叶子节点和容器节点的类。叶子节点表示层次结构中的基本对象,而容器节点则包含其他叶子节点或容器节点。通过这种方式,你可以构建复杂的树形结构,并对其进行统一的操作。组合模式的关键在于将对象视为具有共同接口的树形结构,使得客户端代码可以透明地处理单个对象和组合对象。这使得你可以以统一的方式操作整个层次结构,而无需关心对象的具体类型。
在组合模式中,通常涉及以下角色:
- 抽象根节点(Compoent):为叶子对象和容器对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象根结点中定义了访问及管理它的子构件的方法。
- 树枝节点(Composite):容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点。它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
- 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。它实现了在抽象根结点中定义的行为。对于那些访问及管理子构件的方法,可以通过捕获异常等方式进行处理。
组合模式的关键是定义了一个抽象根结点类,它既可以代表叶子,又可以代表容器。客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
二、C++实现组合模式
一般将抽象根结点类设计为接口或抽象类,将所有子类共有方法的声明和实现放在抽象构件类中。对于客户端而言,将针对抽象构件编程,而无须关心其具体子类是容器对象还是叶子对象。
作为抽象根结点类的子类,在叶子对象中需要实现在抽象根结点类中声明的所有方法,包括业务方法以及管理和访问子对象的方法。由于叶子对象不能再包含子对象,因此在叶子对象中实现子对象管理和访问方法时需要提供异常处理或错误提示。当然,这无疑会给叶子对象的实现带来麻烦。
// 抽象根节点,菜单组件
class MenuComponent
{
private:
std::string name;
int level;
public:
MenuComponent(std::string name, int level);
virtual void add(MenuComponent * menuComponent);
virtual void remove(MenuComponent * menuComponent);
virtual void print(void) = 0;
std::string getName(void);
void setName(std::string name);
int getLevel(void);
void setLevel(int level);
};
MenuComponent::MenuComponent(std::string name, int level) : name(name), level(level) {}
void MenuComponent::add(MenuComponent * menuComponent)
{
throw std::runtime_error("不能进行操作哦");
}
void MenuComponent::remove(MenuComponent * menuComponent)
{
throw std::runtime_error("不能进行操作哦");
}
std::string MenuComponent::getName(void)
{
return name;
}
void MenuComponent::setName(std::string name)
{
this->name = name;
}
int MenuComponent::getLevel(void)
{
return level;
}
void MenuComponent::setLevel(int level)
{
this->level = level;
}
树枝节点提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
// 树枝节点
class Menu : public MenuComponent
{
private:
std::list<MenuComponent *> menuComponents;
public:
using MenuComponent::MenuComponent;
void add(MenuComponent * menuComponent) override;
void remove(MenuComponent * menuComponent) override;
void print(void) override;
};
void Menu::add(MenuComponent * menuComponent)
{
menuComponents.push_back(menuComponent);
}
void Menu::remove(MenuComponent * menuComponent)
{
menuComponents.remove(menuComponent);
}
void Menu::print(void)
{
std::cout << "菜单:" << getName() << std::endl;
for (auto & menuComponent : menuComponents)
{
menuComponent->print();
}
}
叶子节点实现了在抽象根结点中定义的行为。对于那些访问及管理子构件的方法,可以通过捕获异常等方式进行处理。
// 叶子节点,菜单项类
class MenuItem : public MenuComponent
{
public:
using MenuComponent::MenuComponent;
void print(void) override;
};
void MenuItem::print(void)
{
std::cout << "菜单项:" << getName() << std::endl;
}
main() 函数:
#include <iostream>
int main(void)
{
// 创建菜单树
MenuComponent * fileMenu = new Menu("文件", 1);
MenuComponent * editMenu = new Menu("编辑", 1);
// 菜单项
MenuItem * openItem = new MenuItem("打开", 2);
MenuItem * saveItem = new MenuItem("保存", 2);
MenuItem * closeItem = new MenuItem("关闭", 2);
MenuItem * copyItem = new MenuItem("复制", 2);
MenuItem * cutItem = new MenuItem("剪切", 2);
MenuItem * pasteItem = new MenuItem("粘贴", 2);
// 添加菜单
fileMenu->add(openItem);
fileMenu->add(saveItem);
fileMenu->add(closeItem);
editMenu->add(copyItem);
editMenu->add(cutItem);
editMenu->add(pasteItem);
// 打印菜单
fileMenu->print();
std::cout << std::endl;
editMenu->print();
std::cout << std::endl;
// 打印菜单项
openItem->print();
// 删除菜单项
fileMenu->remove(openItem);
editMenu->remove(cutItem);
// 打印菜单
fileMenu->print();
std::cout << std::endl;
editMenu->print();
std::cout << std::endl;
delete openItem;
delete saveItem;
delete closeItem;
delete copyItem;
delete cutItem;
delete pasteItem;
delete fileMenu;
delete editMenu;
return 0;
}
三、透明组合模式与安全组合模式
组合模式有 透明组合模式 和 安全组合模式。
透明组合模式 中,抽象根结点中声明了所有用于管理成员对象的方法,包括 add()、remove() 以及 getChild() 等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式。
透明组合模式 的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 以及 getChild() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。
安全组合模式 中,在抽象根结点中没有声明任何用于管理成员对象的方法,而是在树枝节点类中声明并实现这些方法。这种做法是安全的,因为根本不向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法。
安全组合模式 的缺点是不够透明。因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
四、组合模式的总结
4.1、组合模式的优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次。它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案。通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
4.2、组合模式的缺点
组合模式的主要缺点是:在增加新构件时很难对容器中的构件类型进行限制。有时希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件。使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自相同的抽象层。在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
4.3、组合模式的适用场景
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致性地对待它们。
- 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
- 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,将来需要增加一些新的类型。