设计模式初学者指南Word文件下载.docx
《设计模式初学者指南Word文件下载.docx》由会员分享,可在线阅读,更多相关《设计模式初学者指南Word文件下载.docx(23页珍藏版)》请在冰豆网上搜索。
给我一对用黑胡椒和新奥尔良秘制酱烤制的鸡翅,一个北京风味的、包含鸡肉、胡萝卜、黄瓜的特大春卷,一份夹了炸鸡腿、色拉酱和蔬菜的面包,一杯350ml的、加了冰块的百事可乐,另外把土豆打碎放在杯子里给我@¥#%!
¥
当然,服务员小姐很有礼貌的说:
重复一遍,您要的是一对新奥尔良烤翅、一份老北京鸡肉卷、一个田园脆鸡堡、一杯中可乐还有一份土豆泥,对吗?
得到你的默许之后,她又会对后面负责送餐的小弟说:
9527,这边来一对烤翅、一份老北、一个田园、一杯中可、一杯泥。
同样的意思源自于不同的表述,这就是人类语言的魅力;
但现实中往往存在的是,哪怕是略微的表述不同也会造成别人极大的误解,那是沟通的陷阱。
为了解决这个问题,各行各业产生了形形色色的“行话”,它们不仅将纷繁复杂的语言精炼化,还最大程度上避免了误解的产生。
如果你认为肯德基点餐这个例子离软件设计太远的话,请看下面无间道的例子:
华仔:
我建立了一个专门对付琛哥监视类。
它能够联系所有的探员,而且任何时候只要韩琛有轻举妄动,它就会通知每个人。
最棒的是,任何警员都可以随时加入或退出这套系统。
这样的设计可谓相当的面向对象
伟仔:
华仔,只要你说用了“观察者模式”,我就懂了
总得来说,共享词汇具有如下作用:
1.用更少的词汇作更充分的沟通
2.避免误解
3.在设计阶段,尽可能停留在设计层次,而排除编码阶段的影响
4.帮助初级开发人员迅速成长
既然你已经了解了共享词汇的威力,以后碰到“张口闭口模式”的人,千万不要武断地以为他是在炫耀或是显摆,有可能他在他的圈子里已经习惯于共享词汇了☺
策略模式
上兵伐谋,其次供交,其次伐兵,其下攻城。
——孙子
在讲解策略模式的定义之前,请看下面的需求:
全聚德烤鸭集团需要一个可以展示他们可爱鸭子的平台,希望用户可以通过这个可视化的平台“亲眼”看到全聚德的鸭子游泳或是呱呱叫,以此来提升他们低迷的股价。
他们最重要的需求就是这个平台要有较强的灵活性和扩展性,因为现有情况表明,没有任何预算可以支付任何升级或重构的费用。
让我们先来看看第一个“面向对象”的设计方案(继承):
所有的鸭子都既会呱呱叫(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接口不具有实现代码因此造成了代码的无法复用,从而失去了面向对象的意义。
这意味着:
无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,就会犯下大错。
莎士比亚说:
不要指着月亮起誓,它是变化无常的,每个月都盈亏圆缺;
你要是指着它起誓,也许你的爱情也像它一样的无常。
的确,不变只是愿望,变化才是永恒。
幸好,设计模式的宝库里有一个设计原则可以帮助我们去更好使用地适应它(而不是去对抗它):
封装变化,找出应用中可能需要变化的部分,把它们独立出来,不要和那些固定不变的代码混合起来。
这句话的概念似乎很简单,但它几乎是每个设计模式背后的精神所在。
基本上,所有的模式都提供了一套方法让“系统中的某部分改变不会影响其他部分”。
我们知道Duck类中的fly()和quack()会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把它们从Duck类中取出来,建立一组新类来改变每个行为。
依照上面的思路,如何封装飞行和呱呱叫的行为呢?
如果我们将具体的fly和quack直接绑定给duck类的话,我们就丧失了动态改变fly和quack行为的能力,试想一下,如果某一只鸭子(的实例)翅膀受伤了,它的fly行为就会改变,这意味我们最好是使用弹性的设计原则,即:
接口编程原则:
针对接口编程,而不是针对实现编程
现在,看一下我们对鸭子行为的封装:
使用上述的设计,可以让飞行和呱呱叫的动作被其他对象复用,因为现在它们是具有代码实现的类而非接口了,并且这些行为已经与鸭子类无关了;
而且我们可以新增一些行为(比如喷气式飞行、骑着扫帚飞行等),不会影响到既有的行为类(FlyWithWings、FlyNoWay),也不会影响“使用”到已有行为的类(各种Duck的具体子类)。
好,让我们看看最终的成果:
恩,抽象超类(Duck)与之前的设计相比,多了以下的内容
1、以接口的形式封装了行为(FlyBehavior、QuackBehavior),并以成员变量的形式(flyBehavior、quackBehavior)保存在类中,同时提供相应的setter方法(setFlyBehavior、setQuackBehavior)以达到动态改变其具体实现的目的。
2、由超类来负责行为的调用。
performQuack和performFly内部分别调用了quackBehavior.quack()和flyBeavior.fly()。
这并不等于说子类对行为的调用没有控制权(否则子类的实现就完全一样了),子类可以通过setter方法(setFlyBehavior、setQuackBehavior)来绑定具体的行为类,从而控制具体的行为。
我们往往认为“面向对象==继承”,在我们的例子当中,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的,并且有一个原则可以支持这种做法:
组合原则:
多用组合,少用继承。
恩,你可能奇怪,讲了这么多怎么还没有切入“设计模式的正题”,其实你已经学会了第一个设计模式:
策略模式定义了一系列算法族,并加以封装,让它们之间可以互相替换;
此模式让算法的变化独立于使用算法的客户。
——
GOF
如果觉得鸭子的例子不够深刻的话,让我们来开发一款类似于Diablo的ARPG冒险游戏吧:
Blizzard邀请您参与最新款的动作冒险游戏《圆桌骑士》的设计工作。
您将能利用开发商提供的代表游戏角色的类和代表武器的类,每名角色一次只能使用一种武器,但在游戏过程中可以切换、买卖、装备、舍弃武器;
您可以提出比较好的架构方案吗?
一群“富有策略”的圆桌骑士看上去应该是这样的:
看吧,每个Character“有一个”WeaponBehavior,在冒险过程中切换、装备、买卖或是丢弃武器时,只要调用setWeapon方法即可。
每一个具体的Character(King、Queen等)都包含一个weapon的属性以及一个fight方法,后者在其内部调用WeaponBehavior接口的useWeapon方法,任何武器都可以实现WeaponBehavior接口,包括拳头、弹弓、口水,只要它实现了useWeapon方法。
不同的武器的useWeapon可能千差万别,比如劈刺、挥砍、射击、喷吐等等。
现在,您应该对策略模式有所了解了吧,下面做一下小结:
策略模式的优点:
1.策略模式提供了管理相关的算法族的办法。
策略类的等级结构定义了一个算法或行为族。
恰当使用继承可以把公共的代码移到超类里面,从而避免重复的代码。
2.策略模式提供了可以替换继承关系的办法。
继承可以处理多种算法或行为。
如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。
3.使用策略模式可以避免使用多重条件转移语句。
多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。
策略模式的缺点:
客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。
策略模式造成很多的策略类
源代码:
publicclassContext{
privateStrategystg;
publicContext(StrategytheStg)
{
this.stg=theStg;
}
publicvoidDoAction()
this.stg.AlgrithmInterface();
}
publicclassConcreteStrategyAimplementsStrategy{
publicvoidAlgrithmInterface()
System.out.println("
ConcreteStrategyA.AlgrithmInterface"
);
publicclassConcreteStrategyBimplementsStrategy{
ConcreteStrategyB.AlgrithmInterface"
publicclassClient{
publicstaticvoidmain(String[]args){
StrategystgA=newConcreteStrategyA();
Contextct=newContext(stgA);
ct.DoAction();
本文来自CSDN博客,转载请标明出处:
心得:
策略模式可以结合工厂模式,将策略聚合在Context中,通过传入参数判定使用哪个策略。
适配器模式
Whileinrome,doasromedose
假设您所维护的应用暴露出来的接口与现有服务商提供的接口相冲突……
您可以通过实现一个适配器来使两者“相匹配”
现在,由于适配器的引入,您的应用又可以正常工作了,而不用去修改现有系统或厂商类的代码了
还记得全聚德的鸭子们吗?
它们现在又碰到了麻烦。
由于现在鸭肉紧缺,公司准备用火鸡来代替,但却发觉之前的系统似乎失灵了……恩,这似乎是显而易见的,之前的系统是针对鸭子接口的,当然不能用于火鸡的实例,要搞清楚的事,火鸡从不呱呱叫(quack),只会咯咯叫(gobble);
火鸡由于体型的缘故,飞行距离很短;
另外,您见过会游泳的火鸡吗?
鸭子和火鸡的抽象超类和具体实现类如下所示:
publicinterfaceDuck{
//鸭子的接口……
publicvoidquack();
publicvoidfly();
publicvoidswim();
publicclassMallardDuckimplementsDuck{
//绿头鸭的具体实现……
publicvoidquack(){
Quack"
publicvoidfly(){
fly"
publicvoidswim(){
swim"
publicinterfaceTurkey{
//火鸡的接口……
publicvoidgobble();
publicvoidfly();
publicclassWildTurkeyimplementsTurkey{
//野生火鸡的具体实现……
publicvoidgobble(){
System.out.println("
Gobble"
publicvoidfly(){
Flyingashortdistance"
由于之前的系统只能识别Duck接口,因此我们实现了一个适配器:
publicclassTurkeyAdapterimplementsDuck{
//首先,你必须实现你想要转换成的、客户所期望看到的类型接口(Duck)
Turkeyturkey;
publicTurkeyAdapter(Turkeyturkey){
//必须采用某种方式,获得需要适配的对象的引用,比如构造器注入…
this.turkey=turkey;
publicvoidquack(){
//有些方法需要简单转换,比如用火鸡的咯咯叫(gobble)来代替鸭子的呱呱叫(quack)
turkey.gobble();
publicvoidfly(){
//有些方法需要复杂抓换,比如,由于火鸡的飞行距离较短,要达到鸭子的飞行效果,火鸡需要加倍的努力哦(调用fly五次)
for(inti=0;
i<
5;
i++)
turkey.fly();
publicvoidswim(){
//还有些方法无法适配,这是可以覆盖为空、或者抛出异常、或者干脆像火鸡一样说:
“这个我干不了”☺
NoTurkeycanSWIM!
!
"
虽然很简单,但这里其实就用到了一个设计模式:
适配器模式将一个类的接口,转换成客户期望的另一个接口。
适配器让原来不兼容的类可以合作无间。
——
当然,设计模式本身就是语言无关的,您正使用支持多重继承(C++)的语言,可能还会看到所谓的“类适配器”,它采用同时继承目标和被适配类、而不是组合被适配类的方法来适配被适配者
下面举一个现实生活中的例子:
我们常常碰到遗留代码,它们往往暴露出Enumeration接口,但我们有希望在新的代码中使用Iterator……
这个时候,您可以考虑使用一个适配器,如下所示:
我们要使旧代码中的枚举变得像是新代码中的迭代器,新代码依然使用迭代器,虽然实际上背后隐藏的事枚举器。
下面看一下这个适配器的具体实现:
publicclassEnumerationimplementsIterator{
//因为我们要将枚举适配成迭代器,因此必须要实现迭代器(被适配者)接口,让它看起来就像是一个迭代器
Enumerationenum;
publicEnumerationIterator(Enumerationenum){
//利用组合的方式,保存一个枚举的引用
this.enum=enum;
publicbooleanhasNext(){
//迭代器的hasNext方法其实是委托给枚举的hasMoreElements方法
returnenum.hasMoreElements();
publicobjectnext(){
//迭代器的next方法其实是委托给枚举的nextElement方法
returnenmu.nextElement();
publicvoidremove(){
//很不幸,枚举不支持remove方法,所以必须放弃,在这里,我们抛出一个UnsupportedOperationException
thrownewUnsupportedOperationException();
}
适配器模式的小结如下:
1.客户通过目标接口调用适配器的方法对适配器发出请求
2.适配器使用被适配者接口吧请求转换为被适配者的一个或多个调用接口
3.客户接收到调用的结果,但并未察觉到这一切是适配器在起转换作用
4.当客户没有指定所需要的接口时,我们仍然可以应用Adapter模式,并使用现有类的实例来创建一个新的客户子类。
这种方法会创建一个