乱砍设计模式之四

最后更新于:2022-04-01 14:36:50

COMPOSITE与BUILDER模式 —— 忠义堂石碣受天文 梁山泊英雄排座次 [junguo](#) Composite模式的中文名字是组合模式,该模式的目的是使单个对象和它的对象组合(一般是数组或者链表的结构)拥有统一的操作方式,这样可以简化客户的使用。我们还是通过具体的例子来理解该模式。还是先来一段例子背景介绍: 话说,宋江带人攻陷东平东昌两郡,收降了双枪将董平和没羽箭张清,而后皇甫端来归。梁山聚集了武将,书生,医生以至兽医等各色人等,成为了一个成熟的社区。着点人数正好一百单八人,是个吉利数字,为此宋江组织了浩大的祭天仪式。在祭天仪式进行期间,发现了埋于地下的石碣,石碣上排定了三十六天罡,七十二地煞的座次。次序以定,梁山也开始走上正规,开始划分各位好汉的职责。依据各人特长,分别任职于马军,步军,军师等角色。有了规矩,才成方圆,自此梁山告别了单挑的时代,作战有了一些固定的组合。如李逵出战,必有项充,李衮护在身边;这位头脑简单的杀人魔王的典型特点是攻强守弱,而且不着盔甲,如若无人保护估摸早死了。还有另外一些组合:如矮脚虎王英和一丈青扈三娘这样的夫妻组合(扈三娘嫁给了王英,你能想通吗?我想不通);还有解宝解珍这样的兄弟组合等。 来考虑一下,如何实现这样的功能,武将可以单独出击,也可以成群的出击?该如何去实现这样的功能呢?还是一步一步来,先看类图: 这是我们建立的武将类,该类和我们第一讲《赵子龙单骑救主》中建立的武将类相同(为了突出中心功能,简化了功能)。考虑一下在这种情况下,我们如果想让多个武将一起出击,该如何实现?最单纯的做法就是在调用该类的地方生成一个数组,然后客户端通过该数组来调用各个对象的操作。大概代码如下: ~~~ General general[10]; for( int i = 0; i<10; i++ ) general[i].Assault(); ~~~ 客户端也可以将这些操作封装在不同的函数中,但如果武将类填加新的函数的时候,客户端也需要相应的增加此类德函数。这是典型的封装不好的例子,客户端需要增加特殊的函数来支持对象。我自己的工作中就遇到了这样的问题,类没有封装好,针对于这些类的操作,你不得不填加一些函数,接着就可以发现在不同的文件里有很多功能相似的函数(一个类往往在多处用到,而这些往往是由不同的人写的,这样很容易造成重复代码)。整个程序被改得乱码七糟的,改这样的东西真是头痛万分,更加上你如果要用到这些外部的函数的时候,都不知道用那个。那如何来解决该问题呢?找到组合模式,就找到了解决之道。先来看看组合模式的类图描述: Component是为Leaf和Composite抽象出来的共同接口,可以通过它来统一操作单个对象或者组合对象。 Leaf是我们具体要用到的类,它用来完成具体的功能。Composite是我们用来组合对象的类,并实现对子对象的管理;它通过Add,Remove和GetChild来实现对子对象的管理。Composite中会含有一个Component型的对象列表childers,它也会重载Leaf中所需要的函数,然后通过遍历执行所有childer的函数。该childer也可以是Composite型的,这样就可以有一个递归的结构。 我们用Composite模式来重新设计我们的类图,如下: 理解模式最有效的方式,还是看具体的代码: ~~~ #include #include #include #include using namespace std; //抽象部件类,是Leaf和Composite的共有接口 class GeneralComponent { public: //为了确保调用到析构函数,此处声明为虚函数 virtual ~GeneralComponent(){} //由于General中不实现Add,Remove,GetChild,此处需要一个缺省的实现,但此处抛出异常更合适一些,为了简单,省略了。 virtual void Add(GeneralComponent* pComonent) {} virtual void Remove(GeneralComponent* pComonent) {} virtual GeneralComponent* GetChild(int i) {return NULL;} virtual void Assault() = 0; }; //具体用到的类 class General : public GeneralComponent { private: string m_strName; public: General(string strName):m_strName(strName){} void Assault() { cout << m_strName << " 进入战斗!" << endl; } }; //组合类 class GeneralComposite : public GeneralComponent { private: string m_strName; //用来存放需要组合的对象 vector m_pComponents; public: GeneralComposite(string strName):m_strName(strName){} virtual ~GeneralComposite() { vector::iterator pos; for( pos = m_pComponents.begin();pos::iterator ivite= find(m_pComponents.begin(),m_pComponents.end(),pGeneral); m_pComponents.erase(ivite); } GeneralComponent* GetChild(int i) { return m_pComponents[i]; } void Assault() { cout << m_strName << "战斗序列" << endl; //需要调用所有的组合对象的操作 vector::iterator pos; for( pos = m_pComponents.begin();posAssault(); } } }; ~~~ 我们再来看一下,客户端如何来调用该对象: ~~~ int main(int argc, char* argv[]) { GeneralComposite pArmy("梁山大军"); GeneralComposite* pHorseArmy = new GeneralComposite("梁山马军"); GeneralComposite* pPaceArmy = new GeneralComposite("梁山步军"); General *pWusong = new General("好汉武松"); General *pHuaheshang = new General("侠客鲁智深"); General *pLida = new General("莽汉李逵"); General *pLinchong = new General("英雄林冲"); General *pGuansheng = new General("大刀关胜"); pHorseArmy->Add(pLinchong); pHorseArmy->Add(pGuansheng); pPaceArmy->Add(pWusong); pPaceArmy->Add(pHuaheshang); pPaceArmy->Add(pLida); pArmy.Add(pHorseArmy); pArmy.Add(pPaceArmy); pArmy.Assault(); return 0; } ~~~ 运行结果如下: 我们可以看到,通过组合模式,对于对象的调用变得简单了起来,只要通过一个函数就可以实现对所有对象的统一调用。这样做最大的好处就是将所有的代码统一成了一份,可以避免在不同的地方出现类似的代码。在多人合作的情况下,由于交流不充分的问题,很多时候开发人员各自写各自的代码,而不关心别人的代码,极容易产生相同功能的代码。当我们的类进行扩展的时候,可能很多地方都需要进行修改。    我们再来看看组合模式可能存在的问题。Leaf类继承了Component中的所有方法,但对于Add,Remove,GetChild等操作,这些操作对它来说是无意义的。我们也可以把这些操作放到Composite中,但在这种情况下,我们将无法通过基类来直接操作Composite对象。个人觉得这个问题可以根据具体问题权衡解决。我们也可以为Component扩展更多的Leaf或者Composite,在这种情况下,如果我们想控制Composite中只能添加限定的Leaf或者Composite在静态情况下是不可能的,必须在运行时刻去判断。 还有,我们应该最大化Component接口,我们使用该接口的目的就是使用户不知道它具体操作的是哪个Leaf或者Composite,所以我们应该尽量多定义一些它们公共的操作。这样还是会造成我们上面提到的会给一些类带来无意义的功能。 我们再来简单比较一下Composite和Decorator模式,这两个模式都会有一个子类拥有基类的指针,不同之处是Composite会拥有多个组件,而Decorator只拥有一个组件。Composite主要目的是对象的聚集,使它们的调用统一化;而Decorator不是此目的,它的目的是给对象添加一些额外的功能。 好了,Composite的讲解先到此为止。如果你感兴趣的话,可以试着把我们第一章用到的策略模式应用到该处。不过我们下面看另外一个问题。在main中,我们需要很多代码去完成对象的创建。我们是否可以将对象的创建部分也做一些封装呢?我们可以找找创建型的模式中是否有适合我们的模式? 在《设计模式》中,我们可以找到Builder模式,它的定义是:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。和我们的意图有点象,先来看看《设计模式》中为我们提供的类图: 作一下解释:    1, Director是具体要用到复杂对象的部分(如GeneralComponent)的类(我们程序中是main中调用)。它会通过Builder来建造自己需要的对象,而不需要自己通过new来得到所要的对象。    2, 而Builder的是一个抽象的创建对象的接口。    3, ConcreteBuilder是我们用来具体创建对象的类。它可能有多个,用来帮我们建造不同类型的对象。如我们把我们的General派生出不同的风格,如先秦风格和罗马风格的时候,我们可能需要生成不同的类。而每次创建一种风格的对象,我们就可以扩展Builder而生成不同的创建类。    我们的例子比较简单,不需要这么多东西。Builder的重要特征是:它是用来一步一步创建复杂对象类型的。我们的组合类由于需要组合不同的组件,所以整个创建过程比较复杂,那么可以借用Builder模式的特点来创建我们的对象。好的,看看具体的代码: ~~~ class Builder { public: static GeneralComponent* CreateCompise(GeneralComponent *pParent,string strArr[],int iLength = 1) { if ( pParent != NULL ) { for( int i = 0 ; i < iLength ; i++ ) { pParent->Add(new GeneralComposite(strArr[i])); } return pParent; } else { return new GeneralComposite(strArr[0]); } } static GeneralComponent* CreateGeneral(GeneralComponent *pParent,string strArr[],int iLength = 1) { if ( pParent != NULL ) { for( int i = 0 ; i < iLength ; i++ ) { pParent->Add(new General(strArr[i])); } return pParent; } else { return new General(strArr[0]); } } }; ~~~ 这边的代码略显简单,只重意了,你可以看到,创建过程被封装成CreateCompise和CreateGeneral两个方法,通过它们来创建我们所需要的对象。只要将所需要的Composite(为它创建Leaf和子Composite的),还有创建对象所需要的参数(例子有些简单,只要一个初始化参数)数组和数组长度传进函数就可以帮我们创建所需要的对象了。由于Builder没有成员变量,将两个函数设置成了静态函数,想当于一个单件类型。我们再看看它的具体用法: ~~~ int main(int argc, char* argv[]) { string strArmy[1] = {"梁山大军"}; string strArmyChild[2] = {"梁山马军","梁山步军"}; string strSpecLeader[3] = {"好汉武松","侠客鲁智深","莽汉李逵"}; string strHorseLeader[2] = {"英雄林冲","大刀关胜"}; GeneralComponent *pArmy = Builder::CreateCompise(NULL,strArmy); Builder::CreateCompise(pArmy,strArmyChild,2); Builder::CreateGeneral(pArmy->GetChild(0),strHorseLeader,2); Builder::CreateGeneral(pArmy->GetChild(1),strSpecLeader,3); pArmy->Assault(); delete pArmy; } ~~~ 这样我们就可以统一对象的创建过程。做到一定程度的聚合。我个人感觉模式的学习过程是从形到意(就是从一些具体的例子看起,通过具体的例子来理解模式);而后是重意不重形(明白模式的意义之后,我们就可以随意使用模式了,而不需要硬套公式)。所以此处使用了和书上并不一致的Builder模型。其实Builder模式与Abstract Factory模式很相似,我们下次描述Abstract Factory模式的时候,再回头比较一下。 参考书目: 1, 设计模式——可复用面向对象软件的基础(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英军等译 机械工业出版社 2, Head First Design Patterns(影印版)Freeman等著 东南大学出版社 3, 道法自然——面向对象实践指南 王咏武 王咏刚著 电子工业出版社 4, 水浒传 —— 网上找到的电子档
';