(转载)设计模式之-策略模式(Strategy)
原文:http://blog.sina.com.cn/s/blog_48df74430100t2m7.html
前言
部门组织培训,《Effective Java》,每人每天给大家讲解一节。但十个同事就我一个是做.Net开发的,所以每回我就是听众,前两天的一节是《用函数对象表示策略》,讲的非常短频快,但下来我的感触颇多,对代码不再有当初的激情了,但总想把平时经常用到的,别人已经总结归纳的用文字记录下来,好记性不如烂笔头,只有记录下来的才是属于自己的。于是决定从本篇开始我的设计模式之旅,这个系列3年前就有总结的想法,但一再找各种理由推托,就像戒烟一样,希望对自己来说现在不算太晚。
从Justin老兄的博客看到经典“鸭子模式”的例子,对我的启发非常大,也是参照他的例子自己胡思乱想了“飞机模式”,跟鸭子模式很像,其实也就是自己编个故事给自己听,然后从故事里能总结出来一些道理就好。鸭子模式原文地址:http://www.cnblogs.com/justinw/archive/2007/02/06/641414.html
从简单的例子开始
话说中东X国空军指挥部使用OO语言开发一款作战飞机管理系统,想把所有飞机管理起来。刚开始由于飞机数量有限,仅拥有为数不多的几架从美俄进口的战斗机,于是开发人员构建了如下的类结构:
如图所示,在“飞机”基类里实现了公共的“飞行”和“加油”方法,而“F22战斗机”和“猛禽战斗机”可以分别覆盖实现自己的“外型”方法,这样即重用了公共的部分,又支持不同子类的个性化扩展。从目前的情况看,X国国防部很满意!
过了一段时间,世界石油价格飙涨,X国高层觉得是时候再添置一批运输机了,以配合地面部队的物质补给,于是X国又采购了一批大力神运输机,空军指挥部的开发人员扩展了他们的类结构:
恩,目前仍然不错,开发人员觉得自己的设计非常棒,甚至国防部还将这个项目纳入了X国国家9527火炬计划!
然而世界局势变幻莫测,X的邻国Y国早对X的石油资源垂延三尺,终于有一天按奈不住冲动,一拥而上......大兵压境,X国十万火急向美俄购入一批导弹来武装“F22”和“猛禽”战斗机,开发人员丝毫不敢怠慢,连夜修改了他们的设计:
第二天,有了攻击系统的X国空军准备大规模反攻时,悲剧发生了,之前按战斗机数量采购的导弹居然不够用,有一半以上的战斗机在出发前居然未能装弹。国防部长来到现场发现:每架运输机机翼上居然捆了两枚价值100万美元的“小牛”空对地导弹。
当天损失惨重,总统下了最后通牒:马上修改你们的系统,三天内如果修改不好,所有人都去挖煤!
顶着压力,开发人员终于冷静下来找到了原因,原来在“飞机”基类里增加的“攻击”方法,也同样被子类“大力神运输机”给继承了,而基类里的“攻击”方法包括了装弹步骤,所以就有了运输机悬挂空对地导弹的一幕。那么该怎么办呢?有人提议:能不能在“大力神运输机”里把“攻击”方法重写一下?让它什么都不做,不就行了吗?但马上有人反对,那“F22战斗机”和“猛禽战斗机”是不是也要重写?并且以后如果再增加其它类型的教练机、预警机、运输机,是不是都要重写“攻击”方法?这太麻烦了,而且非常混乱。
大家很困惑,为什么屡试不爽的继承,在系统扩展的时候,无法很好地支持重用呢?最后经过反复讨论,决定使用接口,设计如下:
但空军指挥部马上否决了这个设计:“你们难道希望所有载有导弹的飞机都去重复实现这个方法吗?现在我们飞机少还好说,但有不久后我们有成千上万架战斗机的时候怎么办?如果导弹系统要做一点修改,难道要重复修改上万遍吗?你们是不是疯啦?”
策略模式前奏
如果你是X国的开发人员,你会怎么做?我们知道,并不是所有飞机都能攻击,所以继承不是正确的方法。虽然上面使用“攻击接口”可以解决部分问题(不再给运输机装上“小牛”空对地导弹),但是这个解决方案却彻底破坏了重用,它带来了另一个噩梦----维护!
要解决这个问题,我们必须回到设计模式的第一个原则:
Identify the aspects of your application that vary and separate them from what stays the same.(找到系统中变化的部分,将变化的部分同其它稳定的部分隔开)
什么意思?打个简单的比方,你电脑上有主板有硬盘,主板跟硬盘是可以很方便拆分开的吧。你今天可以用100G的硬盘,明天可以换500G的硬盘,硬盘就是变化部分,它根据用户需求随时都能调整;而主板上的南北桥芯片你能更换不?也许有强人能做到,但起码厂家的设计是不让你轻易更换的吧,所以这就是稳定部分。我们可以发现,其实设计模式不只针对软件,它随处可见。
Program to an interface, not an implementation.(面向接口编程,而不要面向实现编程)
这条原则应该是经常看见,它跟面向对象里的“多态”紧密相连,什么意思?我们还是继续上面电脑主板的例子,其实主板开发商早已把“面向接口”发挥的淋漓尽致了。拿主板上的USB口来说,USB口可能外接数码相机,可能外接打印机,外接键盘鼠标,外接手机,外接五花八门各种各样已经有的和未来会出现的设备。如果主板厂家为每种设备都实现一套独立的交互程序,那后果是灾难性的。于是主板厂家们使用了面向接口开发,接口早期由像IBM这样的大厂商制订,后来由一些标准化组织接管,他们在接口里声明了所有用到的标准,比如数据的传输封装格式,接口的型状,接口能接受的电压等等,主板厂家们只按照接口标准来开发自己的主板,而不用关心接口另一端连接的是什么设备。
策略模式登场
The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.(策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们可以相互替换。策略模式让算法独立于使用它的客户而独立变化。)
X国使用策略模式后的类结构图如上。
1、将变化部分跟稳定部分分离开:飞机一定是要飞行的,所以“飞行”是稳定部分;飞机一定得加油,所以“加油”是稳定部分;飞机一定会有外型,就算是隐形飞机也不例外,所以“外型”也是稳定部分。而“攻击”则有的飞机有,有的飞机没有,比如战斗机能攻击而运输机不能,所以“攻击”属变化部分,需要将其与稳定部分分离开。
2、单单分离开只是策略模式的一部分,还需要将变化部分细化、整理和抽象,此时,面向接口会是理想选择。
不谈理论,还是用实例继续,OK,经过一段时间实战,X国的空军指挥官们发现了一个奇怪的现象:苏制F22战机每次作战几乎毫发无伤,而美制的猛禽战斗机和大力神运输机却每每损失惨重。经过反复研究,他们终于发现原来F22战机拥有性能强悍的雷达系统,而美制飞机的雷达全部是Made In China,200米范围以外根本侦测不到敌机。于是他们决定花重金将所有美制飞机的雷达更换为以色列的天网系列。这回开发人员很轻易扩充了他们的另一策略:
从类图中可以发现,每个策略均是独立的,各种类型的飞机可以任意添加、删除和组合各种不同的策略。
对策略模式的总结
通过X国的模拟实例可以发现,继承的功能确实很强大,但是也存在诸多问题,因为它违背了封装原则。只有当子类和父类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果父类不是为了继承而设计的,那么继承将会导致脆弱性,所以可以用复合来代替继承,这一点在策略模式中尤其重要。同时这也是本文的最后一个非常重要的设计原则:Favor composition over inheritance.(优先使用对象组合,而非类继承)