组合模式(Composite Pattern)是一种结构型设计模式,它允许我们将对象组合成树形结构来表现"部分-整体"的层次关系。这种模式使得客户端可以统一地处理单个对象和组合对象,无需关心它们的具体差异。
组合模式最早出现在GoF的《设计模式》一书中,用于解决树形结构数据的处理问题。在实际开发中,我们经常会遇到需要处理树形结构的场景:
这些场景的共同特点是:它们都具有递归的树形结构,且需要对整个结构和单个元素执行类似的操作。组合模式正是为这类场景提供了优雅的解决方案。
组合模式包含三个关键角色:
Component(抽象组件):定义所有组件的通用接口,包括管理子组件和操作子组件的方法。在我们的菜单示例中,MenuComponent就是这个角色。
Leaf(叶子组件):表示树中的叶子节点,没有子节点。在菜单系统中,MenuItem就是叶子组件,它实现了组件的具体行为。
Composite(复合组件):定义有子节点的组件行为,存储子组件,并在抽象组件中实现与子组件相关的操作。我们的Menu类就是这个角色。
提示:在设计组件接口时,需要考虑"透明性"和"安全性"的平衡。透明性是指所有组件(包括叶子)都有相同的接口;安全性则是指只有复合组件才有管理子组件的方法。我们的实现采用了透明性方式,在叶子组件中提供了默认实现(抛出错误)。
在我们的菜单系统中,MenuComponent是所有组件的基类:
cpp复制class MenuComponent {
public:
// 子组件管理方法(复合组件实现)
virtual void add(MenuComponent* menuComponent) {
cerr << "Menu can not add" << endl;
}
virtual void remove(MenuComponent* menuComponent) {
cerr << "Menu can not remove" << endl;
}
virtual MenuComponent* getChild(int i) {
cerr << "Menu can not getChild" << endl;
return nullptr;
}
// 操作方法(叶子组件实现)
virtual const string& getName() const {
cerr << "MenuItem can not getName" << endl;
return nullStr;
}
virtual const string& getDescription() const {
cerr << "MenuItem can not getDescription" << endl;
return nullStr;
}
virtual const double getPrice() const {
cerr << "MenuItem can not getPrice" << endl;
return -1;
}
virtual bool isVegeTarian() const {
cerr << "MenuItem can not isVegeTarian" << endl;
return false;
}
// 公共操作
virtual void print() const = 0;
bool operator==(const MenuComponent* another) {
return this == another;
}
private:
string nullStr;
};
这种设计有几个关键点需要注意:
透明性设计:所有方法都在基类中声明,叶子组件和复合组件都继承自同一个基类。这使得客户端可以一致地对待所有组件。
默认实现:对于不适用于某些组件的方法(如叶子组件的add()),我们提供了默认实现(输出错误信息)而不是纯虚函数。这样避免了强制叶子组件实现不相关的方法。
操作符重载:实现了==操作符,方便在容器中查找和删除组件。
MenuItem是叶子组件的具体实现:
cpp复制class MenuItem : public MenuComponent {
public:
MenuItem(const string& name, const string& description,
bool vegetarian, double price):
_name(name), _description(description),
_vegetarian(vegetarian), _price(price)
{}
const string& getName() const override { return _name; }
const string& getDescription() const override { return _description; }
bool isVegeTarian() const override { return _vegetarian; }
const double getPrice() const override { return _price; }
void print() const override {
cout << "菜单项:" << _name;
if (_vegetarian) cout << "(v)";
cout << ", " << _price << " --" << _description << endl;
}
private:
string _name;
string _description;
bool _vegetarian;
double _price;
};
叶子组件的特点:
getName(), getPrice()等)Menu是复合组件的具体实现:
cpp复制class Menu : public MenuComponent {
public:
Menu(const string& name, const string& description):
_name(name), _description(description)
{}
// 子组件管理方法
void add(MenuComponent* menuComponent) override {
menuComponents.push_back(menuComponent);
}
void remove(MenuComponent* menuComponent) override {
auto it = menuComponents.begin();
while (it != menuComponents.end()) {
if (*it == menuComponent) {
menuComponents.erase(it);
return;
}
it++;
}
}
MenuComponent* getChild(int i) override {
return menuComponents[i];
}
// 自身操作方法
const string& getName() const override { return _name; }
const string& getDescription() const override { return _description; }
void print() const override {
cout << endl << _name << ", " << _description
<< endl << "-------------------" << endl;
for (const auto& e : menuComponents) {
e->print();
}
}
private:
vector<MenuComponent*> menuComponents;
string _name;
string _description;
};
复合组件的特点:
子组件管理:使用vector<MenuComponent*>存储子组件,实现了add()、remove()和getChild()方法。
递归操作:print()方法不仅打印自身信息,还递归调用所有子组件的print()方法,这是组合模式的关键特征。
自身属性:和叶子组件一样,复合组件也有自己的属性(名称、描述等)。
Waitress类作为客户端,展示了如何使用组合结构:
cpp复制class Waitress {
public:
Waitress(MenuComponent* menuComponent):
allMenus(menuComponent) {}
void printMenu() {
allMenus->print();
}
private:
MenuComponent* allMenus;
};
使用方式:
cpp复制int main() {
// 创建各种菜单
MenuComponent* pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "BREAKFAST");
MenuComponent* dinerMenu = new Menu("DINER MENU", "Lunch");
MenuComponent* cafeMenu = new Menu("CAFE MENU", "Dinner");
MenuComponent* dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
// 创建总菜单并添加子菜单
MenuComponent* allMenu = new Menu("ALL MENUS", "All menus combined");
allMenu->add(pancakeHouseMenu);
allMenu->add(dinerMenu);
allMenu->add(cafeMenu);
// 向子菜单添加菜单项
dinerMenu->add(new MenuItem("Pasta", "香啊啊啊啊啊啊", true, 3.89));
dinerMenu->add(dessertMenu);
dinerMenu->add(new MenuItem("Apple Pie", "香哈哈哈哈哈哈", true, 1.39));
cafeMenu->add(new MenuItem("抹茶拿铁", "咖咖咖", false, 20));
dessertMenu->add(new MenuItem("哈根达斯", "甜甜甜", false, 100));
// 使用Waitress打印整个菜单结构
Waitress waitress(allMenu);
waitress.printMenu();
return 0;
}
这段代码展示了如何构建一个复杂的菜单树形结构,并通过统一的接口操作整个结构。
在实际应用中,组合模式常与迭代器模式一起使用,以提供更灵活的遍历方式。我们可以为组件添加创建迭代器的方法:
cpp复制class MenuComponent {
public:
// ... 其他方法 ...
virtual Iterator<MenuComponent*>* createIterator() = 0;
};
class MenuItem : public MenuComponent {
public:
// ... 其他方法 ...
Iterator<MenuComponent*>* createIterator() override {
return new NullIterator(); // 空迭代器
}
};
class Menu : public MenuComponent {
public:
// ... 其他方法 ...
Iterator<MenuComponent*>* createIterator() override {
return new MenuIterator(menuComponents);
}
};
这样,客户端可以统一地遍历任何组件,无论它是叶子还是复合组件。
在我们的实现中,直接使用了原始指针,这可能导致内存泄漏。更安全的做法是使用智能指针:
cpp复制class Menu : public MenuComponent {
public:
// ... 其他方法 ...
void add(std::shared_ptr<MenuComponent> menuComponent) {
menuComponents.push_back(menuComponent);
}
private:
std::vector<std::shared_ptr<MenuComponent>> menuComponents;
};
使用智能指针可以自动管理内存,避免手动删除对象的问题。
根据不同的需求,组合模式有几种常见的变体:
透明模式:所有组件都有相同的接口(如我们的实现)。优点是客户端无需区分叶子与复合组件;缺点是叶子组件需要实现不相关的方法。
安全模式:只有复合组件有管理子组件的方法。优点是避免了叶子组件实现不相关方法;缺点是客户端需要区分组件类型。
带父引用的组合:组件保存指向父组件的指针,方便向上遍历。
简化客户端代码:客户端可以一致地处理简单元素和复杂元素,无需关心具体的类。
易于添加新组件类型:新增组件类型不会影响现有代码,符合开闭原则。
灵活的结构:可以构建任意复杂的树形结构,且结构可以动态变化。
设计复杂性:需要仔细设计组件接口,平衡透明性和安全性。
类型检查问题:在透明模式下,客户端可能需要检查组件类型以确保操作合法。
性能考虑:对于非常深的树结构,递归操作可能导致性能问题。
组合模式特别适用于以下场景:
循环引用问题:在树形结构中,如果允许循环引用(如A是B的子节点,B又是A的子节点),可能导致无限递归。解决方案是添加引用检查。
缓存问题:复合组件可能需要缓存某些操作结果(如子节点数量)。当结构变化时,需要确保缓存失效。
性能优化:对于大型结构,可以考虑使用惰性求值或备忘录模式优化性能。
子组件排序:是否需要保持子组件的特定顺序?如果需要,应该提供什么接口?
组件唯一性:是否允许相同的子组件出现在多个位置?这会影响add()和remove()的实现。
批量操作:是否提供批量添加/删除子组件的方法?对于大型结构,这可以显著提高性能。
测试组合模式时,应特别注意:
与迭代器模式:常一起使用,迭代器可以遍历组合结构(如前文所述)。
与访问者模式:访问者可以对组合结构中的元素执行操作,将算法与结构分离。
与装饰器模式:装饰器可以透明地为组合结构中的组件添加功能。
与享元模式:享元可以共享叶子组件,节省内存。
在实际项目中,组合模式很少单独使用,通常与其他模式结合,形成更强大的解决方案。