乱砍设计模式
 

2009-08-19 作者:junguo 来源:vckbase.com

 

乱砍设计模式之零

有时候想知道偶然会为人生带来什么样的意义?作为一个怀疑论者,我对人生充满了疑虑,对于偶然所起的作用也不是那么确定。但还是可以总结一些自己并不确定的结论。大学期间,成天旷课的我,那天偶然上了一堂软件工程课(我不是计算机专业的,我们开这堂课本身就有些古怪)。那天老师不知道是一时兴起,还是早有准备,在下课前他在黑板上画了一个图,标注了学习计算机的进阶图。这堂偶然的课,给我带来了后来的失落和彷徨。

            一个编程工具(VC,Delphi)
        一门编程语言(C,Pascal)
    常用软件的熟练使用
计算机硬件(说白了就是装机的水平)

他提供的进阶图如上所示,他似乎没有说C和Pascal应该学到什么样子。而当时的我每天泡在图书馆或者宿舍里,看的是数据结构,编译原理一类的书。我为自己绘制的进阶曲线是学习计算机系的所有基础课程,然后考高级程序员。那时候对VC等工具并不感兴趣,我觉得还是打好基础容易进阶。但听完这堂课后,我的思路被打断了,放弃了自己原有的理念,跑到书店买了一本VC基础的书,那是我学生时代买的最贵的书(70多或者80多块,书借人了),书上都是一个一个的例子,其实学完后我都没弄明白MFC是怎么回事。只是学会了拖动不同的控件,在界面上做出不同的效果。凭良心说,那本书译文的文笔不错,也挺适合入门的,但它确实是本不折不扣的破书(国外图书也不都是精品,垃圾也不少),在不懂C++的情况下,它可以教会你在界面上拖拉的本事,屏蔽了你学习的路线。而我们老师的进阶图无疑也是一张误导图,编程还是以数据结构等内容为基础的,没有了基础,你也就没有了前进的依仗。经过多年的失落彷徨,终于感觉找到了入门的通道。我也想总结一幅进阶图,但我发现自己总结不出来。庞庞杂杂接触了太多的东西:汇编,破解,操作系统,C++,ASP,MFC,COM,ATL,VB,STL,数据库;但接触的东西都没有到精通的地步,只是感觉现在理解东西没有以前那么复杂了,但为什么会这样,我自己也说不清楚。只是隐隐约约感觉理解这些东西和汇编有些关系,但这些都是在我单纯学习汇编时候并没有感觉到的。我如今也只是处于从初级向中级攀升的阶段,真要搞出一个进阶路线,可能效果就和我们老师的进阶图一样只能误人子弟了。所以如今放弃了这方面的努力,也许将来有一天我会去做。如今我想做的就是提供给那些和我一样在从初级向中级攀升的人们一些共同感兴趣的话题。而设计模式绝对是这样一个话题,设计模式是帮助你真正理解OO设计的一把钥匙,也许只有它能帮助你真正进入OO设计之门。在没接触设计模式之前,对于OO只知其形,不知其意。很多OO设计的原则,不知道我买的那本巨著《C++ Primer》中有没有,反正我是一条也没记住。如果你觉得你懂了面向对象的基本原理,懂得了虚函数,需要继续紧阶,那么我们可以一起探讨这里谈到的设计模式。

GOF出的《设计模式》买了很长时间了,翻看了也不下三篇,但每次总感觉收获不大。虽被众人奉为经典,但那本书不适合我,写的太过抽象,没有完整的例子,每次看过都没留下多少印象。又是一个偶然,给我带来了惊喜。我在网上找到了一份电子版的《Head First Design Patterns》,尽管只有第三章,但我发现这本书文笔清新,事例翔实,以前看多次没记住的东西,这儿看一次就留下了深刻的印象,实是一本入门的好书。所以豪不犹豫买了本纸版的,不过如今该书只有影印版,但并不影响阅读。以我大学英语四级没过的水平都可以基本看懂这本书,我想搞计算机的应该都不成问题。

刚看到这本书的时候,第一个想法是自己能不能把它翻译一下。与大家共享,但后来想想版权什么的东西,自己并不清楚。加之文笔拙劣,怕毁了原文的意境,所以放弃了这个念头(毕竟我不是人民教师)。但总感觉有些失落,那就自己写吧,把我能理解的总结出来,配以我想到例子。经过这么一番折腾,也帮助自己加深理解,因为看书的时候,还是会忽略太多的细节,只有经过了自己的手,才会抠出很多容易忽略的东西。这就是我写这个系列的原因了。但也希望有同样兴趣的同仁共同讨论了。

冠以乱砍的名字,是因为我不想把技术的东西搞得太枯燥。尽量加一些自己感兴趣的内容进去,呵呵,有同样爱好的同仁,我们也可以一起讨论。但对于技术的内容,我还是尽最大的努力做到正确表述。

好了,要说的差不多了。但声明一下,今天看到自己以前写的一篇文章被转载,但被斩头去尾,还删除了署名。虽然不是太在意,但还是感觉不舒服。所以希望有仁兄要转载的话,请保留署名;不要删除任何内容,如果你不喜欢我的废话,那就不要转了。

乱砍设计模式之一: STRATEGY 模式—赵子龙单骑救主

源代码下载

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等著 东南大学出版社
  • 道法自然——面向对象实践指南 王咏武 王咏刚著 电子工业出版社
  • 三国演义 网上找到的电子档

乱砍设计模式之二: STATE 模式—履胡之肠涉胡血,悬胡青天上,埋胡紫塞旁

源代码下载

STATE模式的中文名称是状态模式。在《设计模式》一书中的定义是:允许一个对象在其内部状态改变的时候改变它的行为。对象看起来似乎修改了它的类(中文译书上的原话,不过我觉得这句话应该翻译成——对象显现出来的是改变了它所属的类)。看了定义还是感觉有些抽象,好的,我们还是通过一个例子来学习该模式。还是先从情节设计开始:

严风吹霜海草凋,筋干精坚胡马骄。 
汉家战士三十万,将军兼领霍嫖姚。 
流星白羽腰间插,剑花秋莲光出匣。 
天兵照雪下玉关,虏箭如沙射金甲。 
云龙风虎尽交回,太白入月敌可摧。 
敌可摧,旄头灭,履胡之肠涉胡血。 
悬胡青天上,埋胡紫塞旁。 
胡无人,汉道昌。 

李白这首诗豪气万丈的诗篇,描述的是汉骠骑将军霍去病率领大军,出陇西,突袭匈奴部落的战争场面,千载之下,犹让人感觉豪迈异常。匈奴和汉朝的争端从汉高祖刘邦开始。楚汉之争过后,刘邦统一了中国。此时,匈奴部族也被冒顿(这两字发音应为mo du)单于统一了起来。匈奴逐渐强大的同时,开始窥觑汉朝的疆土。冒顿单于领军进犯太原,包围晋阳。刘邦亲率30万大军进击匈奴,不期竟中匈奴之计,被困于白登。幸亏用陈平之计,送重礼于冒顿夫人,走枕边风路线,才让匈奴大军让开一条路,逃了出来,狼狈之极。这场战役的结果就是汉朝采取和亲政策,送汉宗室之女给匈奴王为妾,并借机进贡。但这也避免不了匈奴侵犯汉族边界,杀戮汉民,抢夺牲畜。这样的历史持续了数十年,直到汉武帝的时候还保持着。而雄才大略的武帝,岂能容忍此奇耻大辱。坚毅勇猛的他,筹划着对匈奴的打击,经过一系列的改革(个人觉得他改革兵种,将以车兵步兵为主的军队改为轻骑兵为主,对这场战役意义最为重大),开始了对匈奴的穷追猛打。这段历史造就了中国历史上两颗耀眼的将星:卫青,霍去病。霍去病更加夺目,他18岁的时候,随卫青大军出征。他率领800轻骑,远离大军,奔赴敌人腹地,斩敌二千余人。被汉武帝封为冠军侯。后又率1万大军出陇西,转战千余里,斩获8000于人。再于同年秋天,出北地,攻祁连山,大获全胜,斩获三万余人。汉朝与匈奴的最后一次大型战役也由卫青,霍去病完成。霍去病亲率5万骑兵远离代郡,对匈奴人穷追猛打,歼敌七万余人。乘胜追击到狼居胥山(今在蒙古境内),并在此举行祭天仪式(封狼居胥)。此时的霍去病仅仅21岁,但二年后英年早逝。在他短暂的一生中四次与匈奴做战,转战数千余里,灭敌十余万人,彻底熄灭了匈奴人的嚣张气焰,也给大汉的边境带来了安宁。不仅如此,他还留下了“匈奴未灭,何以家为”的千古豪言。真是铮铮男儿形象,为万世铁骨男儿楷模。

前年看到的一部连续剧《汉武大帝》,总体感觉还行。不过太过小气,为了一些戏剧效果,把很多历史事件强塞了进去。如曹操为人捉刀的故事,被安排到了汉武帝身上;为了那么一段冒顿单于鸣镝杀父的情节,让他晚生了数十年,居然他还成了汉武帝的死敌(为汉武帝安排这么一个对手,我总觉得太小觑了汉武帝)。

还有些事,本不想写了,后来想想还是写吧。反正咱这是乱砍,就随性而来吧。网上有个臭名昭著的网站,叫做汉网。集聚着一群民族主义分子,提倡汉服(不过他们好像也不是提倡每天都穿),说这样可以帮助中国人恢复民族自尊心。让我想起阿Q说过的:老子原前也阔过。还有明显意淫古人的意味,翻看历史,我们可以看到武帝之前,汉族一直受匈奴人的欺凌。而武帝之后,国力衰落,虽然周边的小国还算顺服,但汉朝还是失去了武帝时的雄霸之气。试想想如果当时情况下的皇帝不是汉武,而是一个软弱无能,如明朝的那个建文帝,估摸着汉朝内部都搞不定。更甭谈打击匈奴了,而消灭匈奴取得成功,也和武帝破格提拔的将领有关。试想没有卫青,霍去病又会是什么情况?第一次攻击匈奴,四路人马出击,两路被败(著名的飞将军李广被俘,而后逃脱),一路无功,只有卫青有所斩获。而后的多次出击中,除卫青,霍去病外的其他将领多有败绩。在电视《汉武大帝》中,李广的死被安排成了为了成全卫青的计策,吸引匈奴,他主动陷于匈奴包围圈中,英勇战死(这段情节,描写的匈奴人也太弱智了)。我一哥们看到这情节后,哈哈大笑。然后他津津有味的为别人讲解李广真正的死因:李广是路痴死的。李广出击匈奴的战争中多次迷路,最后一次因为卫青派人问他迷路的原因,李广自杀身亡。李广是汉朝名将,但我觉得他不适合那个年代。从战国时代起,名将都以智略取胜。因为战争基本都以攻城拔地的形式进行,都有相对固定的战争场地。武器也以战车,重骑兵为主,行动较为缓慢,主要打的是阵地战。而当时匈奴,采取的是打得过就打,打不过就跑得游击套路(呵呵,应该是我军游击战的雏形了)。面对这样的对手,更需要的就是霍去病这种有锐气,深沉勇猛,敢于追着敌人背后猛打的将领(我觉得国民党将领薛岳也属于此类战将)。当汉武让霍去病学习古兵法的时候,他的回答是:顾方略何如耳,不至学古兵法。也许正是这种藐视权威的气概成就了霍去病,也成就了“犯大汉者,虽远必诛”的大汉豪情。但这段历史的演绎里离不开汉武帝,也离不开霍去病(战功最著的是他,单靠卫青可能这场战争很难如此彻底),是他们的组合成就了这段历史。没有这样的组合,也许大汉天下会被匈奴的铁蹄践踏,而汉文明也许会象希腊文明那样被游牧民族践踏而消亡。历史充满了偶然性,历史本身并不值得我们拿来炫耀。我觉得如今的中国人没有什么不自信的,我也不觉得现在的中国人没古代好,伟大的时代总离不开伟大的人物。也许我们的民族只是现在缺少一个这样的人物,那也就不好怨天尤人了,因为身处这个时代的我们,没有一个是伟人,我不是,你也不是,那你怪谁去?好了,不扯了,还是认真做我们的Coding生涯吧。

我们以骠姚将军深入漠北追歼匈奴人的情节作为我们的例子,这次我们将五万骑兵作为我们的对象。这五万骑兵作为一个作战整体,会有一些这样的一些状态:休整,前进,攻击。整个作战过程中都在这些状态下进行着。我们先来看看下图:

这里我们看到一个转换图:共有三种状态Advaneing(前进),Assaulting(攻击状态),Resing(休息状态)。三个状态之间可以进行转换,Advaneing和Resing可以直接转换,Advanceing和Resing可以直接转换到Assaulting状态,而Assaulting只能在敌军被全歼的时候才能转化到Resing状态。

如何来实现该系统呢?我们还是首先用土鳖似的方式来实现第一个系统,这样更有助于看到模式的优点。我们首先建立一个Army类,如下:

	#define ADVANEING   0
	#define ASSAULTING  1
	#define RESTING      2
	Class Army
	{
		Private:
			int m_iState;
	        int m_iEmptyCout;
	    public:
		Army():m_iState(RESTING){}
	    void Advance();
	    void Assault();
	    void Rest();
	      
	}; 
	void Army::Advance()
	{
		If (m_iState ==  ADVANEING   )
	    {
	       Cout << “Are Advaning!” << endl;
	}
	else if ( m_iState == ASSAULTING )
	{
		Cout << “sorry! Are assauling!Can’t Advace” << endl;
	} 
	else if( m_iState == RESTING )
	{
		m_iState = ADVANING
		Cout << “ok!Go!” << endl;
	}
	}
	void Army:: Assault ()
	{
		If (m_iState ==  ADVANEING   )
	    {
			m_iEmptyCout = 100;
	m_iState == ASSAULTING;
	       Cout << “ok!Assault!” << endl;
	}
	else if ( m_iState == ASSAULTING )
	{
		m_iEmptyCount -= 100;
		Cout << “Are assauling!” << endl;
	} 
	else if( m_iState == RESTING )
	{
	m_iEmptyCout = 100;
		m_iState = ASSAULTING;
		Cout << “ok! Assault!” << endl;
	}
	}
	void Army:: Rest ()
	{
		If (m_iState ==  ADVANEING   )
	    {
	m_iState == RESTING;
	       Cout << “ok!Rest!” << endl;
	}
	else if ( m_iState == ASSAULTING )
	{
		Cout << “Are assauling!can’t Rest” << endl;
	} 
	else if( m_iState == RESTING )
	{
		Cout << “Are Resing!” << endl;
	}
	}

好了这样我们的类就完成了,虽然看起来有些杂乱,但运行应该没有什么问题。这样完成虽说土了一些,但事实上并不影响它的正常运行。但我们需要考虑的一个问题是:当需求变化的时候,我们的程序该如何去改?软件界的一个规律就是需求一直在变更,变更伴随着软件的生存到死亡的过程。如今流行的设计模式,重构,测试驱动开发等技术目的都是为了适应需求的变更,而将程序修改的难度降到最低来。所以我们来考虑这样的情况,由于骠骑将军取得了大胜,举行了祭天仪式,祭天仪式中战士兴奋度提高,杀敌热情暴增。所以骠骑决定将这个仪式加入到战斗安排中,当取得胜利的时候,举行祭天仪式。而这又是一个新的状态,该状态只有在Rest状态下才能切换过去,我们该如何去修改程序呢?以目前的做法,我们需要在每个函数中添加条件,修改函数,这样又与我们在策略模式中提到的规则“一个模块对扩展应该是开放的,而对修改应该是关闭的”背道而驰了。怎么解决呢?还是同样的方法:提炼出一个类。同样为了解决动态改变状态的需求,我们还应该记着另一个规则:尽量针对接口编程,而不要针对实现编程。闲言少叙,我们还是看类图,这样来的快一些。

从类图,我们可以看到Army类中拥有一个Station类的对象,它所有的操作将通过该对象来实现。是不是发觉和策略模式很相似?先不说这个,我们先看完例子再说。看看具体代码:

我们首先看以下State接口,很简单,就是几个纯虚函数。

class State
{
public:
	virtual void Advance() = 0;
	virtual void Assault() = 0;
	virtual void Rest() = 0;
	virtual void Fiesta() = 0;
};

我们再来看一下AdvanceState,我们看到在AdvanceState中有一个Army对象的指针,是因为需要在内部修改状态,具体的代码中可以看到。

class AdvanceState : public State
{
private:
	Army *m_pArmy;
public:
	AdvanceState(Army *pArmy);
	virtual void Advance();
	virtual void Assault();
	virtual void Rest() ;
	virtual void Fiesta();
};

我们再来看一下AdvanceState的具体实现:

AdvanceState::AdvanceState(Army *pArmy):m_pArmy(pArmy){}

void AdvanceState::Advance()
{
	cout << "Be in Advancing!" << endl;
}
void AdvanceState::Assault()
{
	//设置假想的敌人数
	m_pArmy->SetEmptyCount(200);
	cout << "Ok!Assault!" << endl;
	m_pArmy->SetState(m_pArmy->GetAssaultState());
	
}
void AdvanceState::Rest()
{
	cout << "OK!Rest!" << endl;
	m_pArmy->SetState(m_pArmy->GetRestState());
}
void AdvanceState::Fiesta()
{
	cout << "sorry!can't Fiesta!" << endl;
}

很简单了,就是根据当前状态来处理各个函数。我们看到有这样的函数m_pArmy->SetState(m_pArmy->GetRestState());是用来修改Army所处的状态的。在Army类中,我们可以看到它的具体实现。其它几个状态类的实现类同,就不房到这里了,感兴趣的可以到附件中自己找。我们再来看看Army类的定义:

	class State;
	
	class Army
	{
	private:
		State* m_pState;
	
		//保存各个状态指针便于使用,当有新的状态填加的时候,我们也需要在此处添加
		State* m_pAdvanceState;
		State* m_pAssaultState;
		State* m_pRestState;
		State* m_pFiestaState;
	
		int m_iEmptyCount;
	public:
		Army();
		
		void SetState(State *pState);
		
		State* GetAdvanceState();
		State* GetAssaultState();
		State* GetRestState();
		State* GetFiestaState();
		void Advance();
		void Assault();
		void Rest();
		void Fiesta();
	
		void SetEmptyCount(int iEmptyCount){m_iEmptyCount = iEmptyCount;}
		int GetEmptyCount(){return m_iEmptyCount;}
	};

它的实现:

	Army::Army()
	{
	
		m_pAdvanceState = new AdvanceState(this);
		m_pAssaultState = new AssaultState(this);
		m_pRestState = new RestState(this);
		m_pFiestaState = new FiestaState(this);
	
		m_pState = m_pRestState;
	
		m_iEmptyCount = 0;
	}
	void Army::SetState(State *pState)
	{
	
	
		m_pState = pState;
	}
	
	State* Army::GetAdvanceState() {return m_pAdvanceState;}
	State* Army::GetAssaultState() {return m_pAssaultState;}
	State* Army::GetRestState() {return m_pRestState;}
	State* Army::GetFiestaState() {return m_pFiestaState;}
	void Army::Advance() {m_pState->Advance();}
	void Army::Assault() {m_pState->Assault();}
	void Army::Rest() {m_pState->Rest();}
	void Army::Fiesta() {m_pState->Fiesta();}

其实也没什么了。很容易的理解的。不知道汉武时代有没有过阅兵仪式,如果有,那就会又多一个状态,想想我们该如何解决?挺简单了,为State添加一个新的子类,并为它提供一个阅兵的启动方法,当然相应的子类也需要添加。相应的Army类中也需要添加该方法。这样做,我们只是扩展了原有类的方法,而不会去改动它原有的功能。这样就可以避免给原有功能带来bug了。 再看看该类的调用:

	int main(int argc, char* argv[])
	{
		Army army;
	
		army.Advance();
		army.Assault();
		army.Rest();
		army.Fiesta();
		army.Assault();
		army.Assault();
		army.Rest();
		army.Fiesta();
		system("pause");
		return 0;
	}
创建对象后,我们可以直接调用它的函数来实现状态的转换了。

好了,状态模式,我们先讲到这里了。回想一下上回的策略模式,是不是觉得很象?在《Head First Design Model》中,该模式开篇的扉页上画的是一幅煽情的母亲流泪的图片,说她眼瞅着自己的孩子分离,此处的两个孩子就是策略模式和状态模式。我们可以这两个模式的原理都是将一个类中的属性提炼成一个接口,再通过该接口实现对其子类的调用而完成所属类的功能。它们的不同是状态模式的接口的变化是在接口子类中完成的,而策略模式是通过所属类来完成的。差别只是这一点,具体工作中使用哪个模式,那就的具体问题具体分析了。你只需记住面向对象设计的规则,就可以以不变应万变了。

参考书目

  • 设计模式——可复用面向对象软件的基础(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英军等译 机械工业出版社
  • Head First Design Patterns(影印版)Freeman等著 东南大学出版社
  • 道法自然——面向对象实践指南 王咏武 王咏刚著 电子工业出版社
  • 史记 网上找到的电子档

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织