首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > 软件开发 >

设计模式菜鸟教程(下)

2012-11-01 
设计模式初学者教程(上)Good cooking takes time. If you are made to wait, it is to serve you better,

设计模式初学者教程(上)

Good cooking takes time. If you are made to wait, it is to serve you better, and to please you.
美酒的酿造需要年头,美食的烹调需要时间;片刻等待,更多美味,更多享受。—— 《人月神话》


写在前面

前段时间,部门要求对新人进行设计模式方面的培训,因此就有了本文的雏形。当时由于时间原因,只做了ppt,讲完之后觉得意犹未尽,趁这几天项目完成,将讲稿完善后分享给各位。相关的ppt可在此处下载http://grunt1223.iteye.com/blog/549893


最后,祝开卷有益。

欢迎来到模式的世界
且慢,我们为什么需要模式?大学里开发“Hello World”我们用到了模式吗?在GOF的设计模式之前设计的程序难道都不是好的程序吗?

王尔德曾说,百折不挠的决心若与科学法则相抵触,犹如江心补漏劳而无益。若想翘起地球,必先懂得杠杆原理和使用规律。当然哲学上来说,一切规律都是相对静止而绝对变化的,因此那些较为稳定、变化不那么频繁的规律更是我们追求的目标,毕竟,大家都想要掌握一些一劳永逸的东西。

有兴趣的同学可以去看看《IT十大死对头》这篇文章,里面讲述了Linux单挑Windows以及Google对抗所有人等等,但传达给我们开发人员的精神只有一点,就是技术日新月异懂的程度堪比摩尔定律。尽管我们不提倡重新发明轮子,但是谁也不知道下一个轮子是谁发明的,并且不同“厂家”的轮子使用起来总会有一点差异;因此抛开具体的轮子不说,较为理想的是我们能够从宏观(设计模式角度)上掌握轮子的适用场景、改进它的使用方法等,或是能够从微观(算法角度)上了解轮子的具体结构、工作原理甚至DIY。

好了,你已经知道设计模式是一种相对稳定、适于宏观把握的规律,那么使用设计模式究竟有什么好处呢?有些文章将鼓吹设计模式无所不能,在我看来,设计模式主要有两个好处:

1、以经验复用替代代码复用
一切皆可复用!技术复用范畴很广,由低到高分别包括设计复用、组件复用、类库复用、代码复用,而设计模式提供了的“经验复用”,则是最高层次的复用。GOF《设计模式》所做的就是总结了面向对象设计中最有价值的经验,并且用简洁且可复用的形式表达出来。

2、共享词汇的威力

假设去肯德基吃东西,你可能会对负责点餐的服务员说:
给我一对用黑胡椒和新奥尔良秘制酱烤制的鸡翅,一个北京风味的、包含鸡肉、胡萝卜、黄瓜的特大春卷,一份夹了炸鸡腿、色拉酱和蔬菜的面包,一杯350ml的、加了冰块的百事可乐,另外把土豆打碎放在杯子里给我 @¥#%!¥
当然,服务员小姐很有礼貌的说:重复一遍,您要的是一对新奥尔良烤翅、一份老北京鸡肉卷、一个田园脆鸡堡、一杯中可乐还有一份土豆泥,对吗?

得到你的默许之后,她又会对后面负责送餐的小弟说:9527,这边来一对烤翅、一份老北、一个田园、一杯中可、一杯泥。


同样的意思源自于不同的表述,这就是人类语言的魅力;但现实中往往存在的是,哪怕是略微的表述不同也会造成别人极大的误解,那是沟通的陷阱。为了解决这个问题,各行各业产生了形形色色的“行话”,它们不仅将纷繁复杂的语言精炼化,还最大程度上避免了误解的产生。如果你认为肯德基点餐这个例子离软件设计太远的话,请看下面一个例子:

华仔:我建立了一个专门对付琛哥监视类。它能够联系所有的探员,而且任何时候只要韩琛有轻举妄动,它就会通知每个人。最棒的是,任何警员都可以随时加入或退出这套系统。这样的设计可谓相当的面向对象
伟仔:华仔,只要你说用了“观察者模式”,我就懂了

总得来说,共享词汇具有如下作用:

    用更少的词汇作更充分的沟通 避免误解 在设计阶段,尽可能停留在设计层次,而排除编码阶段的影响 帮助初级开发人员迅速成长


既然你已经了解了共享词汇的威力,以后碰到“张口闭口模式”的人,千万不要武断地以为他是在炫耀或是显摆,有可能他在他的圈子里已经习惯于共享词汇了设计模式菜鸟教程(下)

策略模式??????? 上兵伐谋,其次供交,其次伐兵,其下攻城。—— 孙子


在讲解策略模式的定义之前,请看下面的需求:

全聚德烤鸭集团需要一个可以展示他们可爱鸭子的平台,希望用户可以通过这个可视化的平台“亲眼”看到全聚德的鸭子游泳或是呱呱叫,以此来提升他们低迷的股价。他们最重要的需求就是这个平台要有较强的灵活性和扩展性,因为没有任何预算可以支付任何升级或重构的费用。
让我们先来看看第一个“面向对象”的设计方案(继承):

设计模式菜鸟教程(下)

所有的鸭子都既会呱呱叫(quack),也会游泳(swim),所以这一部分的代码由超类(Duck)负责实现。但每一种鸭子的外观 都是不同的,所以Duck类中的Display方法是抽象的,将其具体实现延迟到子类(MallardDuck、RedheadDuck)中去。

正当一切看起来都很好的时候,新的需求来了。由于金融海啸的影响,全聚德公司的竞争压力骤增;在为期一周的头脑风暴会议之后,公司高层决定向潜在的客户展示一些令人印象深刻的元素,来提高公司低迷不振的股价,譬如说展示一些会飞的鸭子……

幸好当初我们采用了面向对象的设计,只需要在超类(Duck)中添加fly()方法,然后所有的鸭子(MallardDuck、RedheadDuck)都会继承它,这是展示OO强大继承威力的时候了……

但是,可怕的事情发生了,测试人员在你的系统中看到了一些在天空中飞行的橡皮鸭子(RubberDuck,陪伴鸭子玩耍的玩具),它们虽然是鸭子,挤压它可以发出怪叫(quack),也可以浮在水面上(swim),但是却不会飞翔(fly);并非所有的Duck子类都会飞。如果在基类中加上新的行为,将会迫使某些不适合该行为的子类,也具有该行为;对超类代码所作的局部修改,影响层面可不只是局部!

一个想当然的解决方法是,可以直接覆盖橡皮鸭类中的fly方法为空,神不知、鬼不觉,就好像一切都没有发生过一样;但这样做会有以下问题:
1、如果存在大量的不会飞行的鸭子子类,您将被迫多次覆盖fly方法为空,这样就失去了继承的意义了;即便你的功能可以正常实现,你是否闻到了代码的坏味道?每当有重复代码的时候,就应该考虑重构……
2、可是如果以后加入诱饵鸭(DecoyDuck),又会如何呢?诱饵鸭是木头假鸭,不会飞也不会叫。您从来都不是生物学分类专家,鬼才知道下一次需求方会不会再创造唐老鸭(不会飞、不会游泳、会叫)……

抽象超类继承可能不是答案,因为您可能不是业务领域专家,无法事先预料到需求会如何改变;按照上述方案,每当有新的鸭子具体实现类出现,开发人员就要被迫检查并可能需要覆盖fly()和quack()......这简直是无穷无尽的噩梦。

重新整理一下思路,我们想要的是一种更清晰的实现方案,让“某些”(而不是全部)鸭子的子类型可飞或可叫。究竟谁来决定它的行为呢?当然是具体实现类自己啦。请看下面的实现方案:

设计模式菜鸟教程(下)

我们可以把fly()从超类中取出来,放进一个“Flyable”接口中,只有会飞的鸭子才实现此接口。同样的方式,也可以设计一个”Quackable”的接口,因为不是每一个鸭子都会叫。每当加入一种新的鸭子后,可以自行决定是否需要实现Flyable或Quackable,而不会去影响原有的代码,这是多么美好的事啊……

可是,该系统的维护团队可不这么想!他们认为,这真是一个超笨的主意,这么一来重复的代码会增多,如果你认为覆盖几个方法不算什么,那么对于48个Duck的子类都需要稍微修改一下飞行的行为,你认为如何?!

让我们来总结一下失败的教训:并非所有的子类都具有飞行(fly)和呱呱叫(quack)的行为,鸭子的行为在子类里会不断地改变,所以继承显然不是最适当的解决方案。虽然Flyable与Quackable接口可以解决“一部分”问题(即不会再有会飞的橡皮鸭),但Java接口不具有实现代码因此造成了代码的无法复用,从而失去了面向对象的意义。这意味着:无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,就会犯下大错。

莎士比亚说:不要指着月亮起誓,它是变化无常的,每个月都盈亏圆缺;你要是指着它起誓,也许你的爱情也像它一样的无常。的确,不变只是愿望,变化才是永恒。幸好,设计模式的宝库里有一个设计原则可以帮助我们去更好使用地适应它(而不是去对抗它):



依照上面的思路,如何封装飞行和呱呱叫的行为呢?如果我们将具体的fly和quack直接绑定给duck类的话,我们就丧失了动态改变fly和quack行为的能力,试想一下,如果某一只鸭子(的实例)翅膀受伤了,它的fly行为就会改变,这意味我们最好是使用弹性的设计原则,即:



使用上述的设计,可以让飞行和呱呱叫的动作被其他对象复用,因为现在它们是具有代码实现的类而非接口了,并且这些行为已经与鸭子类无关了;而且我们可以新增一些行为(比如喷气式飞行、骑着扫帚飞行等),不会影响到既有的行为类(FlyWithWings、FlyNoWay),也不会影响“使用”到已有行为的类(各种Duck的具体子类)。

好,让我们看看最终的成果:

设计模式菜鸟教程(下)

恩,抽象超类(Duck)与之前的设计相比,多了以下的内容

    以接口的形式封装了行为(FlyBehavior、QuackBehavior),并以成员变量的形式(flyBehavior、quackBehavior)保存在类中,同时提供相应的setter方法(setFlyBehavior、setQuackBehavior)以达到动态改变其具体实现的目的。由超类来负责行为的调用。performQuack和performFly内部分别调用了quackBehavior.quack()和flyBeavior.fly()。这并不等于说子类对行为的调用没有控制权(否则子类的实现就完全一样了),子类可以通过setter方法(setFlyBehavior、setQuackBehavior)来绑定具体的行为类,从而控制具体的行为。


我们往往认为“面向对象 == 继承”,在我们的例子当中,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的,并且有一个原则可以支持这种做法:

?


如果觉得鸭子的例子不够深刻的话,让我们来开发一款类似于Diablo的ARPG冒险游戏吧:

设计模式菜鸟教程(下)

Blizzard邀请您参与最新款的动作冒险游戏《圆桌骑士》的设计工作。您将能利用开发商提供的代表游戏角色的类和代表武器的类,每名角色一次只能使用一种武器,但在游戏过程中可以切换、买卖、装备、舍弃武器;您可以提出比较好的架构方案吗?

设计模式菜鸟教程(下)

一群“富有策略”的圆桌骑士看上去应该是这样的:

设计模式菜鸟教程(下)

每个Character“有一个”WeaponBehavior,在冒险过程中切换、装备、买卖或是丢弃武器时,只要调用setWeapon方法即可。每一个具体的Character(King、Queen等) 都包含一个weapon的属性以及一个fight方法,后者在其内部调用WeaponBehavior接口的useWeapon方法,任何武器都可以实现WeaponBehavior接口,包括拳头、弹弓、口水,只要它实现了useWeapon方法。不同的武器的useWeapon可能千差万别,比如劈刺、挥砍、射击、喷吐等等。

现在,您应该对策略模式有所了解了吧,下面做一下小结:

策略模式的优点:

策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到超类里面,从而避免重复的代码。策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为。如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。


策略模式的缺点:

客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。策略模式造成很多的策略类

?

参考文献


设计模式菜鸟教程(下)


http://www.jdon.com/

http://www.cnblogs.com/Terrylee/archive/2006/07/17/334911.html

http://sourcemaking.com/design_patterns

http://www.iteye.com/forums/tag/Design-Pattern

http://www.chinajavaworld.com/forum.jspa?forumID=44

http://www.ibm.com/developerworks/cn/java/design/#N100C1

  
好好学习学习。。。 2 楼 terryang 2009-12-16   貌似在哪看到过类似的东东~~ 3 楼 kingdu_12 2009-12-16   学习了:) 4 楼 tianmo2008 2009-12-16   请问你的uml用什么工具画的?挺好看的,好操作吗?我用的几个工具画出来的图都很难看 5 楼 grunt1223 2009-12-16   tianmo2008 写道请问你的uml用什么工具画的?挺好看的,好操作吗?我用的几个工具画出来的图都很难看
visio 6 楼 avanry 2009-12-17   写的很好,学了一个设计模式
楼主继续写啊,就等下了 7 楼 conservatism 2009-12-17   这不就是head first吗 8 楼 piratesean 2009-12-17   谢谢,举得例子非常形象。学习了 9 楼 mqh444 2009-12-18   不错,学习了。