乱砍设计模式之一-STRATEGY

来源:百度文库 编辑:神马文学网 时间:2024/03/29 19:53:10
乱砍设计模式之一:
STRATEGY 模式—赵子龙单骑救主
作者:junguo
源代码下载
STRATEGY在中文中被译成了策略,我感觉这个意思并不妥切,但翻英文词典能得到的翻译也只有这个,我的词典比较简单,不知道是否还有其它意思?如果没有,那么我想可能和中国研制的CPU在研发阶段被定名为“狗剩”一样,它只是一个名字而已,并不能确切的代表真实的意义。经典著作《设计模式》中将策略模式定义为:定义一系列的算法,把它们一个个的封装起来,并且使它们可以相互转换。这个定义还是比较抽象,下面我将通过一个例子来具体的讲解策略模式。感觉这样更容易帮助新手理解模式,这也是《Head First Design Patterns》中讲解模式的方法。先来描述一下用到的例子的背景资料:
话说三国时期,刘备失去徐州之后,四处奔逃,寄人篱下。先投袁绍,后附荆州刘表,屯兵新野。好不容易有些转折,三顾茅庐请出了诸葛亮。但此后不久,曹操率领大军猝然而至,意欲扫平江南。刘备不及防范,更加之区区数千军马根本不是五十万大军的对手。无奈之下,率同新野军民逃奔襄阳。不意赶上刘表身亡,刘表之妻蔡夫人及蔡夫人之兄蔡瑁决议投降曹操,不给刘备开门。再度无奈,只好继续向南奔逃,但刘备带领数万百姓,行动迟缓,被曹军追上。刘备安排张飞断后,让赵云保护家小,自己继续向当阳逃窜。终还是被曹军包围了起来。赵云苦战之中走失了刘备家小,遂于乱军之中左奔右突寻找刘备妻小。不期碰到曹操随身背剑之将夏侯恩,子龙抢挑夏侯恩,夺了青釭宝剑(曹操有两把宝剑,另一把名为倚天剑)。子龙继续四处寻找幼主。终在一堵断墙边找到刘备之妻糜夫人和刘备幼子刘禅,糜夫人为了不拖累赵云护刘禅突围,跳井而死。赵云将刘禅护于胸中,纵马向外奔突。枪挑曹军数将,而后曹军一拥而上。短兵相接,子龙拔出了青釭宝剑,左挥右砍,勇不可挡,杀退曹军众将。由于曹操爱惜赵云人才,不许放冷箭,赵云幸而冲出了曹军包围圈。抱阿斗去见刘备。而后刘备摔了阿斗,有了我们都知道的谚语:刘备摔孩子——收买人心。
后人有诗赞曰:血染征袍套甲红,当阳谁敢与争锋!古来冲阵扶危主,只有常山赵子龙。声明一下,该诗完全是抄写罗贯中的。刚刚看到一件乐事:高晓松要起诉韩寒,原因是韩寒多年前写的小说《三重门》中引用了高的歌词,也真能想的出来。如今互联网上的趣事真多。
刘备五虎将中,赵云排名最后,确最受欢迎。雄姿英发,英勇强悍,确又心细如丝,体贴入微,实为千古男人之典范。以至千载之下犹有众多美女粉丝。如果设计一个以子龙为原型的游戏,定会吸引众多玩家,说不定还会拥有众多的女性玩家。玩过以前大型游戏机上的三国志,赵云挺帅,但太过简单,没有情节,感觉不过瘾。加上不是国产的,失去了不少感情。不知道现今的三国志一类的游戏是否有单骑救主这样的情节?由于本人游戏IQ太差,不是太关心这些。不管这些了,先来设计一下我们的这段情节,简单实现之(呵呵,说清楚了,该程序只是一个简单的文字输出,图形版的我还没这水平,这里只是教你如何使用策略模式)。
我们来看看要实现的功能,赵云手握长枪与曹军众将武斗,由于他夺来了青釭宝剑,所以他的兵器可以随时更换,但每次都只能使用一种兵器(不要和我抬杠,说他可以左手持剑,右手握枪;绞尽脑汁才想起这么一个例子,容易吗,我?)。而每种兵器的杀伤力并不相同。我们要实现的就是这么一个简单的功能。
首先我们帮赵云提炼出一个他所属的类——武将类,该类拥有武将名字,所使兵器等信息;还包括前进,冲锋等方法。先来看看类图:

这个类拥有两个成员变量m_strName和m_strWeapon,分别表示武将名和武将使用的兵器。另有几个成员函数:Advance表示前进;Assault表示攻击,由于使用的武器不同,武器的使用及杀伤力并不相同,所以在我们现在设计的类中,Assault需要根据m_strWeapon的类型来实现不同的操作;SetWeapon用来设置武器的类型。
我们首先来想象一下Assault的实现,我们需要根据武器类型的不同来实现不同的操作,如果武器有数十种呢?那么最简单的方式就是,在Assault中加入switch … case,然后根据不同的case来实现功能。如果我们可以将各个case条件下的操作提炼成一个个函数,这样也许程序也不会太庞杂。不过我见过笨蛋写的程序,一个函数中有数十个case条件,每个case条件下都有数十上百行代码,整个函数搞到上千行;还好意思拿这样的函数向人炫耀,真是无知者无畏。再接着想,我们的兵器库不断的变更,每当增加新的兵器类型的时候,我们是不是都需要改Assault呢?那么原本已经测试好的东西,经过变动,又需要经历一次测试的洗礼,我们可以确保不给以前的程序带来问题吗?你改没改过这样的程序?我改过,整个过程就一个字:累。有没有方法帮我们避免这样的问题发生呢有,当然有了!
解决这样的麻烦,我们应该牢记面向对象的一个原则:一个模块对扩展应该是开放的,而对修改应该是关闭的。那我们该如何做到在为我们的模块添加新型武器的时候,做到不需要修改原有的类呢?最简单的方法就是通过继承来实现,先看类图:

将类General做了修改,并为它添加了两个子类GeneralWithLance(带长矛的将军)和GeneralWithSword(带剑的将军)。由于使用的兵器不同,Assault的实现不同,所以我们在子类中重载了Assault。这样当我们的程序中需要添加新的兵器类型的时候,我们只需要重新派生新的子类就可以解决问题了。这个世界是不是变得美好了一些,不需要去修改原有的代码,也就意味着我们可以少碰一些别人的代码。有过经验的人都知道,修改别人的代码,是件痛苦至极的事情。但不要高兴的太早,问题马上又来了。这时候有人提出应该考虑将军的坐骑,如水军统领的行动工具是船,而轻骑将军的行动工具应该是马,而且行动工具不同,将军的杀伤力也不同。我靠,整个世界又向黑暗倾斜了。想想我们当前的方法,再按继承的方式作,就需要再扩展类:骑马的带剑将军,乘船的持矛将军….而且每次添加一种兵器就需要相应得组合不同的行动方式(如下图所示)。可怕的现象出现了,随着兵器和行动方式的增多,类都可能成倍的增加。类越来越多,越来越难控制,这就是传说中的类爆炸了。这样的程序,你还如何去维护?不过到目前为止,我还没见过这样的程序。那些用C++写了十多年程序还只会select…case,而不知道用类的笨蛋,我不知道他们是只懂过程化设计?还是看到类膨胀而不敢使用类?不过类膨胀比结构化的程序更为可怕,面向对象也是把双刃剑,达到什么样的效果,就看应用人的水平了。

我们想使用面向对象的特性,而且不想看到类膨胀,该如何办呢?那就应该记住另一条面向对象的原则:优先使用聚合,而不是继承。先来简单看看聚合的概念。
class IDCart {}; //身份证class Person{public:…..private:string name;int age;IDCart idcart;};   (看到这个定义,基本可以确定该套系统是为中国公民做的。身份证对于身处外地打工的人来说是重要的。前段时间一哥们把身份证弄丢了,由于跳槽的缘故,他离开了原来所在的城市。人民警察要他把户口迁移出去,大城市的户口一般工作单位都不给办。迁哪儿去?想想诺大的中国那里是我们的容身之所?派出所百般刁难,不给补办。好不容易弄了一个临时身份证,拿着去银行注销银行卡,银行居然也不给办。哥们郁闷至极,比钱包丢的时候都郁闷。说是要户籍改革,不知道会改些什么?想想一年前,我被小偷顺走了钱包,那时候还是一代身份证,感觉办起来比现在方便了很多。户籍是要改革了吗?会改成什么样呢?)
看我们的定义,一个人拥有名字,年龄,身份证等属性。由于身份证有一些相关的操作:发放,挂失,补办等操作,我们把它提炼成一个单独的类。此处我们使用聚合的方式来完成对于身份证的处理,所有对于身份证的操作,都通过idcart来实现。如:发放身份证的操作,在聚合条件下就变成了:
class IDCart {public: void PutOut(){}};class Person {public: void PutOutIDCart() { idcart->PutOut(); }void SetIDCart(IDCart cart) { idcart = cart; }private: string name; int age; IDCart idcart; };聚合说白了就是在一个类中定义一个另一个类的对象,然后通过该被聚合的对象来实现相应本需要聚合类实现的功能。
使用聚合的优点是:可以帮助我们更好的封装对象,使每个类集中在单一的功能上,使类的继承层次也不会无限制的增加,避免出现类数量的膨胀。而且使用聚合还有一个优点就是可以动态的改变对象(下面会讨论到)。不过聚合相对于继承来说,没有继承直观,理解比较困难。
在确定使用继承还是聚合的时候,有一个原则:继承体现的类之间“是一个”的关系。例如我们需要对学生,工人进行单独的处理。那么我们的例子应该是这样:
Class student : public person
{
};
Class worker : public person
{
};
也就是说学生是一个人,而工人也是一个人。学生和人之间体现的是“是一个”的关系。而工人也一样。
而身份证对于人来说,是人的一个属性。那么我们就可以提炼出来成为一个单一的类,通过聚合来实现。
接着还是回到我们策略模式的例子,同样在我们的例子程序中,可以把武器提炼成一个单独的类,类图如下:

我们提炼出一个Weapon类,将在General中使用。噫!怎么又有一个m_strWeapon?你可能要开骂了:谁他妈是傻子呢?这样做不又回到了过程化设计的鬼样了?别急,提供这个错误的方法,只是为了给你提供另一个面向对象的设计原则:尽量针对接口编程,而不要针对实现编程。
C++中没有象C#或者Java等面向对象语言那样,提供对Interface的语言支持。但接口也不过是一个概念,我们使用纯虚函数类,等同于接口。我们提供一个不被实例化的基类事实上也可以当作接口来用。针对接口编程的意义是:可以不用知道对象的具体类型和实例,这样可以减少实现上的依赖性。可以帮助我们提高程序的灵活性。好了,我们再重新来设计类图:

新的类图中Weapon被抽象成了一个接口,拥有一个虚函数Assault,拥有两个子类Lance和Sword。而在General类中,我们拥有了一个新的成员:m_pWeapon。而攻击的函数变成了performAssault,它是通过调用m_pWeapon->Assault()来实现攻击的。这样一来武器就可以随时变更了。我们来看看简单的代码实现://武器类class Weapon{public:virtual void Assault() = 0; //纯虚函数};//长枪类class Lance : public Weapon{public:virtual void Assault(){cout << " I kill enemy with the Lance and can kill 10 every time!" << endl;}};//宝剑类class Sword : public Weapon{public:virtual void Assault(){cout << " I kill enemy with the sword and can kill 20 every time!" << endl;}};//武将类class General{private:string m_strName;Weapon *m_pWeapon;public://构造函数,初始化m_pWeapon为Lance类型General(string strName):m_strName(strName),m_pWeapon(new Lance()){}//指针是需要删除的~General(){if ( m_pWeapon != NULL ) delete m_pWeapon;}//设置武器类型void SetWeapon(Weapon *pWeapon){if ( m_pWeapon != NULL ) delete m_pWeapon;m_pWeapon = pWeapon;}void performAssault(){m_pWeapon->Assault();}void Advance(){cout << "Go,Go,Go!!!" << endl;}};int main(int argc, char* argv[]){//生成赵云对象General zy("Zhao Yun");//前进zy.Advance();//攻击zy.performAssault();//更换武器zy.SetWeapon(new Sword());zy.Advance();zy.performAssault();return 0;}   其实程序的实现相当简单,就是一个简单的聚合加应用针对接口编程的例子。这就是我们要讲的第一个模式:策略模式。重新看一下它的定义:定义一系列的算法,把它们一个个的封装起来,并且使它们可以相互转换。这里所说的一系列的算法封装就是通过继承把各自的实现过程封装到子类中去(我们的例子中是指Lance和Sword的实现),而所说的相互转换就是我们通过设置基类指针而只向不同的子类(我们的例子上是通过SetWeapon来实现的)。
是不是很简单呢?如果你懂虚函数的话,千万别告诉我没看懂,这是对我无情的打击,也许导致我直接怀疑你的智商。如果你不懂虚函数,那回头找本C++的书看看吧,推荐的是《C++ primer》,第四版出了。
参考书目
设计模式——可复用面向对象软件的基础(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英军等译 机械工业出版社 Head First Design Patterns(影印版)Freeman等著 东南大学出版社 道法自然——面向对象实践指南 王咏武 王咏刚著 电子工业出版社 三国演义 网上找到的电子档