1. 组合模式(Composite Pattern)的定义
(1)将对象组合成树型结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
(2)组合模式的结构和说明
①Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。
②Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其他子节点对象。
③Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。(注意:Composite类也是继承自Component,这样的好处是让组合对象和Leaf对象一样,也可以加入到另一组合对象中,形成一个更复杂的复合对象!)
④Client:客户端,通过组件接口来操作组合结构里面的组件对象。
(3)典型的Composite组合对象的结构
(4)组合模式的思考
①组合模式的本质:统一叶子对象和组合对象。它把叶子对象和组合对象都当成Component对象,有机地统一了叶子对象和组合对象。
②组合模式的目的:让客户端不用区分操作的是组合对象还是叶子对象,而是以统一的方式来操作。
③组合模式中存在天然的递归,这里所说的递归不是常说的“方法调用自己”的递归。而是指对象的递归组合。
2. 安全性和透明性——到底在Component接口还是Composite类中定义子组件操作方法?
(1)透明性的实现(建议使用的方式!)
①把子组件的操作定义在Component中,那么客户端只需要面对Component,而无须关心具体的组件类型,这种实现方法就是透明性的实现。(见前面的类图结构)
②这种透明性是以安全性为代价的,因为Component中定义的一些方法,对于叶子对象来说没有意义,如增加、删除子组件对象。而客户端不知道这些区别,因为对客户来说是透明的,而客户端调用这些方法是不安全的。
③以防止客户端调用这些方法出现安全问题,一般在Component中为这些方法提供默认的实现,通常是抛出一个异常,来表示不支持这个功能。
(2)安全性的实现
①把管理子组件的操作定义在Composite中,那么客户端在使用叶子对象时,因为没有这个的操作可供调用,所以是安全的。
②但带来的问题是客户端在使用的时候,必须区分到底使用的是Composite还是Leaf的对象
(3)两种实现方式的选择
①对于组合模式而言,在安全性和透明性上,会更看重透明性,毕竟组合模式的功能就是要让用户对叶子对象和组合对象的使用具有一致性。
②对于安全性的实现,需要区分是组合对象还是叶子对象。有时需要进行强制类型转换,但这本身就是不安全的,所以在Component中,一般会定义一个getComposite方法来判断是叶子对象还是组合对象,这样在使用前先判断,再转换才不会出现安全问题。
【编程实验】Windows资源管理器
//结构型模式:组合模式 //场景:Windows资源管理 #include <iostream> #include <string> #include <list> using namespace std; //************************抽象组件类****************** class AbstractFile { protected: string name; /*文件或目录名*/ public: //显示文件名或目录名 virtual void display() {cout << name << endl;} //判断是文件还是目录 virtual bool isFolder() = 0; //添加子目录或文件 virtual bool addChild(AbstractFile* file) = 0; //删除子目录或文件 virtual bool removeChild(AbstractFile* file) = 0; //获得子节点 virtual list<AbstractFile*>* getChildren() = 0; }; //***********************具体组件角色******************* class File : public AbstractFile { public: File(string name){this->name = name;} list<AbstractFile*>* getChildren() { //对于文件(叶子节点),无子结点 //这里可以抛出异常 return NULL; } bool addChild(AbstractFile* file) { //叶子结点无子结点,可抛异常 return false; } bool removeChild(AbstractFile* file) { //叶子结点无子结点,可抛异常 return false; } bool isFolder() {return false;} }; //*****************************组合组件对象********************** class Folder : public AbstractFile { list<AbstractFile*> childList; public: Folder(string name){this->name = name;} list<AbstractFile*>* getChildren() { return &childList; } bool addChild(AbstractFile* file) { childList.push_back(file); return true; } bool removeChild(AbstractFile* file) { childList.remove(file); return true; } bool isFolder(){ return true;} }; void displayTree(AbstractFile* root,int deep = 0) { for(int i = 0; i< deep; i++) { cout << "--"; } //显示自身名称 root->display(); //获取子目录 list<AbstractFile*>* childList = root->getChildren(); if(childList != NULL) { list<AbstractFile*>::iterator iter = childList->begin(); while (iter != childList->end()) { if((*iter)->isFolder()) { displayTree(*iter, deep + 1); } else { for(int i=0;i<=deep;i++) cout << "--"; (*iter)->display(); } ++iter; } } } int main() { AbstractFile* rootFolder = new Folder("C:\"); AbstractFile* compositeFolder = new Folder("Composite"); AbstractFile* windowsFolder = new Folder("Windows"); AbstractFile* file = new File("Test.cpp"); //组织成树状结构 rootFolder->addChild(compositeFolder); rootFolder->addChild(windowsFolder); compositeFolder->addChild(file); //显示根目录及其子目录(或文件) displayTree(rootFolder, 0); delete rootFolder; delete compositeFolder; delete windowsFolder; delete file; return 0; }
3. 父组件和环状引用问题
3.1 父组件的引用:在子组件对象中保存有父组件对象的引用
(1)使用场景:如删除某个商品类型A,但如果它有子类型B,这时会涉及到子类型的处理,是连带全部删除,还是子类型上移一层,即成为B.setParent(A.parent);
(2)引用的定义:通常会在Component中定义对父组件的引用。组合对象和叶子对象都可以继承这个引用。
(3)引用的维护:在Composite的实现中,当组合对象添加子组件对象时设置其父对象,删除子组件对象时重新设置父组件的引用(可能被删除组件的子组件对象涉及到上移一层问题)。
3.2 环状引用
(1)概念:在对象结构中,某个对象包含的子对象,或子对象的子对象,或子对象的子对象的子对象……如此经过N层后,出现所包含的子对象中有这个对象本身,从而构成了环状引用。比如,A包含B,B包含C,而C又包含A,就构成了环状引用。
(2)组合模式一般是当用构建树状结构的,通常要避免环状引用的出现(当然有些特殊需求,也需要环状引用),否则很容易造引起死循环。
(3)检测和处理环状引用的方法:
①记录下每个组件从根节点开始的路径,在这条路径上,某个对象出现两次,就会构成环状引用。
②先在Component中设置一个字段,专门用来记录本组件从根结点开始到Component本身的路径。
③当Composite对象的添加子组件方法中,先检测要添加的子组件是否出现在上面所说的路径中,如果出现环状引用,则抛出异常。
(4)说明:
①上面的环路检测方法很简单,但没考虑到如果删除了某个路径上的某个组件对象,那么所有该组件对象的子组件对象所记录的路径都要更新。
②可以考虑动态计算路径的方式,每次添加一个组件的时候,动态的递归寻找父组件,然后父组件再找父组件,直到根组件。这样就能避免某个组件被删除后,路径发生了变化而需修改所有相关路径记录的情况。
4. 使用时的注意事项
(1)子组件列表(list<Component*>)应放在Component接口还是Composite类中?
①大多数情况下,一个Composite对象会持有子节点集合。因为叶子节点是没有子组件的。
②但是也可以将子组件列表放在Component接口中定义(aps.net的容器类,就是这样定义的),这可以简单叶子结点的操作。但对于叶子节点来说,会导致空间的浪费,因为叶子节点本身不需要子节点,因此只有当组合结构中的叶子对象数目较少的时候,才使用这种方法。
(2)最大化Component定义
Component中的方法是两种对象对外方法之和,换句话说,有点大杂烩的意思,组件里面既有叶子对象需要的方法,也有组合对象需要的方法。这会造成“接口污染”,也与类的设计原则相冲突。
(3)子组件排序
①当需要按照一定的顺序来使用子组件对象时(如分析语法树时),设计时需要把组件对象的索引考虑进去,并设计对子节点的访问和管理接口。
②通常会结合Iterator模式来实现按照顺序来访问组件对象。
5. 组合模式的优缺点
(1)优点:
①高层模式调用简单,因为统一了叶子对象和组合对象的操作。
②节点自由增加,只要找到它的父节点,就很容易扩展,符合开闭原则。
③可以组合成复杂的对象,从而构成一个统一的组合对象的类层次结构
(2)缺点
很难限制组合中的组件类型,需要检测组件类型时,不能依靠编译期的类型来约束,必须在运行期间动态检测。
6.组合模式的使用场景
(1)通常,组合模式会组合出树型结构来,这意味着所有可以使用对象树来描述或操作的功能,都可以考虑使用组合模式,如UI界面设计中的容器对象、读取XML或对语句进行语法分析、OA系统中组织结构的处理、操作系统的资源管理器等。
(2)如果想表示对象的部分——整体层次结构,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也容易。
(3)如果希望统一地使用组合结构中的所有对象,可以选用组合模式。
【编程实验】绘制基本图形和复合图形对象
//结构型模式:组合模式(安全型) //场景:绘图(基本图形和复合图形) #include <iostream> #include <string> #include <list> using namespace std; //************************抽象组件类****************** class Graphics { protected: string name; /*名称*/ public: virtual void draw() = 0; //绘图 }; //***********************具体组件角色******************* //线 class Line : public Graphics { public: Line(string name){this->name = name;} void draw() { cout << "draw a " << name << endl; } }; //圆 class Circle : public Graphics { public: Circle(string name){this->name = name;} void draw() { cout << "draw a " << name << endl; } }; //矩形 class Rectangle : public Graphics { public: Rectangle(string name){this->name = name;} void draw() { cout << "draw a " << name << endl; } }; //*****************************组合组件对象********************** class Picture : public Graphics { list<Graphics*> childList; public: Picture(string name){this->name = name;} list<Graphics*>* getChildren() { return &childList; } bool addChild(Graphics* file) { childList.push_back(file); return true; } bool removeChild(Graphics* file) { childList.remove(file); return true; } void draw() { cout << "draw Composite Object: " << name << endl; list<Graphics*>::iterator iter = childList.begin(); while(iter != childList.end()) { (*iter)->draw(); ++iter; } } }; int main() { Picture* root = new Picture("Root"); Graphics* line = new Line("Line"); Graphics* circle = new Circle("Circle"); Graphics* rectangle = new Rectangle("Rectangle"); root->addChild(line); root->addChild(circle); root->addChild(rectangle); root->draw(); delete line; delete circle; delete rectangle; delete root; return 0; }