C++组合模式及其实现(非常详细)
组合模式(Composite Pattern)用于将对象组合成树状结构以表示“整体-部分”层次关系。组合模式使客户端能够以统一的方式处理单个对象和组合对象。它使对单个对象和组合对象的操作具有一致性,从而提供了更高层次的抽象。
组合模式能够简化客户端代码,客户端可以一致地处理单个对象和组合对象,无须区分它们的类型。增加新的组件类型相对简单,无须修改现有的代码,只需创建新的组件类并实现组件接口。通过递归组合可以更方便地表示复杂的层次结构,提供了更高层次的抽象,能够更灵活地组织和操作对象。
组合模式适合对象的层次结构,并希望以统一的方式处理单个对象和组合对象;客户端对单个对象和组合对象的操作具有一致性。需要递归组合来表示层次结构,并且能够灵活地操作和组织对象。
树状模式的组织方式在实际生活中非常常见,例如任何一个团体的组织方式,以及一个植物的分支结构等,而组合模式适合于树状结构的应用场景,计算机中常见的文件目录处理、XML 文件解析等场合都是非常有用的。

图 1 传统组合模式UML简图

图 2 C++11模板组合模式UML简图
组件部分使用模板参数来定义,这样便将实际业务接口留在了接口模块中,可以大大地提高组合模式模块的通用性。
将节点的具体处理内容部分作为模板的参数从组合模式的本身代码中独立出来。通过这样的方式对业务处理部分和组合模式进行解耦。
下面的代码是第 1 部分内容。使用名字 composite_private__ 空间将空类 stCompositeItfcBase__ 隐藏在模块内部。对外使用时使用宏来完成接口的声明,定义私有基类,代码如下:
第 2 部分是宏定义部分,主要由 4 个宏定义组成,分别负责声明接口类、声明接口方法、结束接口类声明和声明一个默认的接口类。
宏 DECLARE_COMPOSITE_ITFC 用于声明接口类,宏的参数 name 是接口名字,声明的接口类型会从 composite_private__::stCompositeItfcBase__ 继承,用来在后面的代码中进行关系检查。
在定义类时声明了一个对应虚析构函数,用来保证接口对应的对象在释放内存时能够被正确地释放,代码如下:
为了方便在实际工程中使用,需要声明一个默认的接口类型,在多数情况下这个默认的接口类型应该能够满足要求。默认接口函数的名字为 operation。需要注意的是,这个默认接口中只有一个接口函数 operation(),但是可以使用不同的参数来对 operation 进行重载,定义默认接口的宏实现代码如下:
当然 composite 除了 DATA_TYPE 对应对象之外,还包含 composite 自身,以此用来实现层次接口,在默认情况下使用 std::vector 保存子节点对象,同时针对 std::vector 进行别名处理 children_t,这种类型名字可以在工程中使用。
std::vector 接口不适合需要频繁地创建和移除子节点的应用场景,如果在实际工程中需要在这种应用场景下完成任务,则可以使用其他容器替代,例如 std::set、std::list 等,代码如下:
add() 函数和 remove() 函数用来添加和移除子节点操作。因为子节点对象使用了 std::shared_ptr 进行保存,所以特别要注意当移除子节点后并不意味着子节点对象就已经真正被销毁了,add() 和 remove() 函数的实现代码如下:
先序遍历函数 preOrderTraversal(std::function<bool(type_t*)>func),采用循环的方法遍历整棵树,并在每次访问节点时调用回调函数 func 处理节点,并且当回调函数的返回值为 false 时结束遍历操作。采用这种方式将数据的处理能力提供到模块外部,可以显著地增加模块的通用性。
采用循环遍历可以避免采用递归调用时调用栈过深而导致的栈溢出问题,代码如下:
for_each() 函数针对当前层的所有子节点进行遍历处理,使用回调函数对数据进行处理。这是一个提供一个简单编程的语法糖,实际上完全可以使用传统的 for 语句来完成,利用类中提供的 begin() 函数和 end() 函数获取本层数据的遍历范围,代码如下:
使用标准库的容器(std::vector)来存储子节点的指针,可以很容易地将它与其他标准库容器组合使用,例如 std::map、std::set 等。这使代码具有很高的可重用性,可以在不同的项目中进行复用。
由于代码中使用了大量的 std::shared_ptr 来管理内存,所以可能会导致交叉引用的情况出现。在使用的过程中需要特别注意,在获取节点或者数据的 std::shared_ptr 指针时最好使用 std::weak_ptr 进行处理,从而保证在外部不使用引用计数的 std::shared_ptr 对象。
当调用 for_each() 函数时遍历所有的子节点,并输出其对应的数据内容,代码如下:
组合模式能够简化客户端代码,客户端可以一致地处理单个对象和组合对象,无须区分它们的类型。增加新的组件类型相对简单,无须修改现有的代码,只需创建新的组件类并实现组件接口。通过递归组合可以更方便地表示复杂的层次结构,提供了更高层次的抽象,能够更灵活地组织和操作对象。
组合模式适合对象的层次结构,并希望以统一的方式处理单个对象和组合对象;客户端对单个对象和组合对象的操作具有一致性。需要递归组合来表示层次结构,并且能够灵活地操作和组织对象。
树状模式的组织方式在实际生活中非常常见,例如任何一个团体的组织方式,以及一个植物的分支结构等,而组合模式适合于树状结构的应用场景,计算机中常见的文件目录处理、XML 文件解析等场合都是非常有用的。
传统组合模式
通常情况下组合模式的主要角色有组件(Component)、叶节点(Leaf)、容器节点(Composite)和客户端(Client),如下图所示。
图 1 传统组合模式UML简图
- 组件定义了组合中对象的共有接口,可以是抽象类或接口。它定义了在组合中可以进行的操作,例如添加、删除、获取子组件等;
- 叶子是不再包含子组件的节点,叶节点只能进行最基本的操作,不具备添加、删除子组件的能力;
- 容器可以包含子组件,可以对子组件进行管理、添加和删除操作;
- 客户端是组件接口操作的组合结构。
C++11元编程下的结构设计
本文里 C++11 元编程方式的组合模式采用将叶节点和树枝节点整合在一起,以一种节点结构来描述两种节点类型。在实际使用时如果需要判断是否是叶节点,则只需检查子节点的长度,当节点长度为 0 时可判定为叶节点,如下图所示:
图 2 C++11模板组合模式UML简图
组件部分使用模板参数来定义,这样便将实际业务接口留在了接口模块中,可以大大地提高组合模式模块的通用性。
将节点的具体处理内容部分作为模板的参数从组合模式的本身代码中独立出来。通过这样的方式对业务处理部分和组合模式进行解耦。
C++组合模式实现和解析
组合模式的实现主要分成 3 部分:- 第 1 部分是一个用来控制节点数据继承关系的空类 stCompositeItfcBase__;
- 第 2 部分是用宏实现的接口定义的辅助工具,这是一套宏定义的接口,实现了类的定义和方法定义;
- 第 3 部分是实际的组合模式实现部分 composite。
下面的代码是第 1 部分内容。使用名字 composite_private__ 空间将空类 stCompositeItfcBase__ 隐藏在模块内部。对外使用时使用宏来完成接口的声明,定义私有基类,代码如下:
//designM/composite.hpp namespace composite_private__ { struct stCompositeItfcBase__{}; }
第 2 部分是宏定义部分,主要由 4 个宏定义组成,分别负责声明接口类、声明接口方法、结束接口类声明和声明一个默认的接口类。
宏 DECLARE_COMPOSITE_ITFC 用于声明接口类,宏的参数 name 是接口名字,声明的接口类型会从 composite_private__::stCompositeItfcBase__ 继承,用来在后面的代码中进行关系检查。
在定义类时声明了一个对应虚析构函数,用来保证接口对应的对象在释放内存时能够被正确地释放,代码如下:
//designM/composite.hpp #define DECLARE_COMPOSITE_ITFC(name) \ struct name:public composite_private__::stCompositeItfcBase__{\ virtual~name(){}宏 COMPOSITE_METHOD 用于声明接口函数,接口函数是纯虚函数,后续需要在应用中实现接口。接口函数的返回值被确定为 bool 类型。这个宏采用了可变宏参的设计,可以用来声明不同参数数量的接口函数,在函数参数表中使用 __VA_ARGS__ 对宏参数进行展开,这个特性也是在 C++11 中才被引入的,代码如下:
//designM/composite.hpp #define COMPOSITE_METHOD(name,...)virtual bool name(__VA_ARGS__) = 0; //关闭接口声明操作 #define END_COMPOSITE_ITFC() };
为了方便在实际工程中使用,需要声明一个默认的接口类型,在多数情况下这个默认的接口类型应该能够满足要求。默认接口函数的名字为 operation。需要注意的是,这个默认接口中只有一个接口函数 operation(),但是可以使用不同的参数来对 operation 进行重载,定义默认接口的宏实现代码如下:
//designM/composite.hpp #define DECLARE_COMPOSITE_ITFC_DEFAULT(itfcName,...) \ DECLARE_COMPOSITE_ITFC(itfcName) \ COMPOSITE_METHOD(operation,__VA_ARGS__) \ END_COMPOSITE_ITFC()最后是组合模式模板类的实现部分,在这个实现中模板参数 DATA_TYPE 是实际的业务对象接口类型。模板类就是 DATA_TYPE 的容器。
当然 composite 除了 DATA_TYPE 对应对象之外,还包含 composite 自身,以此用来实现层次接口,在默认情况下使用 std::vector 保存子节点对象,同时针对 std::vector 进行别名处理 children_t,这种类型名字可以在工程中使用。
std::vector 接口不适合需要频繁地创建和移除子节点的应用场景,如果在实际工程中需要在这种应用场景下完成任务,则可以使用其他容器替代,例如 std::set、std::list 等,代码如下:
//designM/composite.hpp template< typename DATA_TYPE > class composite { public: //对数据类型进行退化和移除指针处理,扩大 DATA_TYPE 的可用范围 using type_t = typename std::remove_pointer< typename std::decay< DATA_TYPE >::type >::type; //这里检查继承关系,如果不满足,则在编译时报出错误 static_assert( std::is_base_of<composite_private__::stCompositeItfBase__, type_t>::value, ""); //引出组合模式的相关别名,方便在应用中使用 using composite_t = composite< type_t >; //使用 std::vector 存储子节点对象,子节点对象使用 std::shared_ptr 保存 using children_t = std::vector< std::shared_ptr< composite< type_t >>>; using iterator = typename children_t::iterator; public: composite(): p_obj__(nullptr) { } composite(std::shared_ptr< type_t > p): p_obj__(p) { } composite(type_t& data){ p_obj__ = std::make_shared<type_t>(data); } virtual ~composite() {} //判断当前节点是否是叶节点,如果是叶节点,则返回值为 true,否则返回值为 false bool isLeaf() { return m_children_.size() > 0; }
add() 函数和 remove() 函数用来添加和移除子节点操作。因为子节点对象使用了 std::shared_ptr 进行保存,所以特别要注意当移除子节点后并不意味着子节点对象就已经真正被销毁了,add() 和 remove() 函数的实现代码如下:
void add(std::shared_ptr< composite_t > child){ if(!child)return; //子节点对象无效返回 m_children__.push_back(child); } void remove(std::shared_ptr< composite_t > child){ if(!child){return;} //子节点对象无效返回 //查找子节点 auto it = std::find(m_children__.begin(),m_children__.end(),child); //找到后删除节点 if(it! = m_children__.end()){ m_children__.erase(it); } }
先序遍历函数 preOrderTraversal(std::function<bool(type_t*)>func),采用循环的方法遍历整棵树,并在每次访问节点时调用回调函数 func 处理节点,并且当回调函数的返回值为 false 时结束遍历操作。采用这种方式将数据的处理能力提供到模块外部,可以显著地增加模块的通用性。
采用循环遍历可以避免采用递归调用时调用栈过深而导致的栈溢出问题,代码如下:
//designM/composite.hpp void preOrderTraversal(std::function<bool(type_t*) > func){ if(isLeaf()){ func(p_obj__->get()); return; } std::stack<iterator > stack; stack.push(m_children__.begin()); while(!stack.empty()){ auto it = stack.top(); stack.pop(); //当回调函数的返回值为false时接续程序的运行 if(func(!it->get()))break; //处理子节点 if(it->get()&&!it->get()->isLeaf()){ stack.push(it->begin()); //子节点入栈 } auto it1 = std::next(it); if(it1! = m_children__.end()){ stack.push(it1); //兄弟节点入栈 } } }
for_each() 函数针对当前层的所有子节点进行遍历处理,使用回调函数对数据进行处理。这是一个提供一个简单编程的语法糖,实际上完全可以使用传统的 for 语句来完成,利用类中提供的 begin() 函数和 end() 函数获取本层数据的遍历范围,代码如下:
//designM/composite.hpp void for_each(std::function< bool(type_t*) > fun){ for(auto item:m_children__){ bool rst = fun(item->get()); if(!rst)break; } } iterator begin(){return m_children__.begin();} iterator end(){return m_children__.end();} //获取当前节点的数据指针 type_t * get(){return p_obj__.get();} protected: children_t m_children__; //子节点记录 std::shared_ptr< type_t > p_obj__; //当前节点指针 };在这个实现中使用了智能指针 std::shared_ptr 来管理子节点的生命周期,不需要对内存进行额外处理。
使用标准库的容器(std::vector)来存储子节点的指针,可以很容易地将它与其他标准库容器组合使用,例如 std::map、std::set 等。这使代码具有很高的可重用性,可以在不同的项目中进行复用。
由于代码中使用了大量的 std::shared_ptr 来管理内存,所以可能会导致交叉引用的情况出现。在使用的过程中需要特别注意,在获取节点或者数据的 std::shared_ptr 指针时最好使用 std::weak_ptr 进行处理,从而保证在外部不使用引用计数的 std::shared_ptr 对象。
C++组合模式应用示例
下面的示例程序实现了一个接口类 Interface,在该接口类中有一个接口函数 performAction(),然后实现了ConcreteClass,ConcreteClass 从 Interface 实现了接口函数 performAction()。最后在 main() 函数中使用 ConcreteClass 按照组合模式实现了一个树状对象。当调用 for_each() 函数时遍历所有的子节点,并输出其对应的数据内容,代码如下:
//composite.cpp #include <iostream> #include <functional> #include "designM/composite.hpp" using namespace wheels; using namespace dm; //定义一个接口类 DECLARE_COMPOSITE_ITFC(Interface) COMPOSITE_METHOD(performAction) END_COMPOSITE_ITFC() //实现具体的接口类 class ConcreteClass : public Interface { private: int m_data; public: ConcreteClass(int idx) : m_data(idx) { } bool performAction() override { std::cout << "Performing action " << m_data << std::endl; return true; } }; int main() { //创建根对象 auto root = std::make_shared<composite<Interface>>(nullptr); //创建子对象 auto child1 = std::make_shared<composite<Interface>>( std::make_shared<ConcreteClass>(1)); auto child2 = std::make_shared<composite<Interface>>( std::make_shared<ConcreteClass>(2)); auto child3 = std::make_shared<composite<Interface>>( std::make_shared<ConcreteClass>(3)); //将子对象添加到根对象中 root->add(child1); root->add(child2); root->add(child3); //调用子对象的接口方法 root->for_each([](Interface * item) ->bool { return item->performAction(); }); return 0; }运行程序将会输出以下结果:
Performing action 1
Performing action 2
Performing action 3