策略模式:定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的用户。
以一个模拟鸭子的游戏来举例说明:
- 需求说明
要求一个池塘里面有很多鸭子,我们可以灵活的添加各种鸭子,这些鸭子有很多属于自己的特征,比如叫声、飞翔等等,总之就是一个池塘里面游满了各种各样的鸭子,而我们的程序可以灵活的添加鸭子到这些池塘,同时这些鸭子还具有属于自己的特征。
- 系统设计
其实这种设计有一个很大的问题:我们很难抽象出所有鸭子的公共行为。对于一只鸭子,拥有的行为可以列举出为:鸣叫、游泳、走路、飞翔,吃饭、睡觉、繁衍等等,这些行为不是所有的鸭子都具备的,因为鸭子只是一个统称,往下细分就会有各种各样的鸭子:真的、假的、会飞的、不会飞的、吃稻谷的、吃虫的.....,直白的说就是,我们如果把某一个行为定义在父类Duck中的话,肯定会出现在某一个导出类中不适用的情况。
第二种设计方法是为每一种行为都抽象一个接口,某一种鸭子如果有某一种行为就实现这个接口。这种设计的问题在于:①我们可能要在一个类中实现很多个接口,这样的程序不美观;②如果多个鸭子都具备某一种行为,但是这些鸭子不属于同一个种类,那么我们就得在每一个导出类中都实现相同的方法,这样的话代码重复率会很高,同样使得程序不美观。
这里我们采用策略模式来设计这个系统。策略模式在这里的设计思想可以概括为:我们只搭一个鸭子的框架,然后我们去实现很多种不同的行为,如果我们当前希望构建的鸭子具有某种行为,那么我们就找到已经实现的这种行为,拼装到这个鸭子的骨架上,当我们把所有的这个鸭子应该具有的行为都瓶装好之后,这个鸭子也就成型了。这样的设计是站在所有鸭子的高度之上的,而不是针对具体的某一只鸭子的。相同的功能我们只需要实现一次,接下来就是根据不同的需要来进行拼装,而且如果某一天某只鸭子的行为发生了变化,我们需要做的只是“拆下当前零件,换另外一个零件装上”即可,而不需要对鸭子本身伤筋动骨。
- 系统实现
上图为鸭子游戏的UML类图,说明如下:
- 接口
FlyBehavior:飞行行为接口,其中声明了一个方法fly()
- 接口实现类
QuackWithZhizhi:定义了吱吱叫的行为方式,鸣叫行为实现类之一
QuackWithNull:定义了不会鸣叫的行为方式,鸣叫行为实现类之一
FlyWithWings:定义了用翅膀飞的行为方式,飞行行为实现类之一
FlyWithNull:定义了不会飞行的行为方式,飞行行为实现类之一
- 抽象类
1 /** 2 * 鸭子抽象类 3 * @author Apache_xiaochao 4 * @version 2014-8-9 10:51:23 5 */ 6 public abstract class Duck { 7 8 //这两个域可以看作是行为的装配接口,接口上装配不同的组件来实现同一类行为的不同实现方式 9 protected FlyBehavior flyBehavior; 10 protected QuackBehavior quackBehavior; 11 12 /** 13 * 飞行方法 14 */ 15 protected void fly(){ 16 if(flyBehavior != null) 17 flyBehavior.fly(); //这里是对行为调用的通用框架,具体的表现方式由具体实现而定 18 } 19 20 /** 21 * 鸣叫方法 22 */ 23 protected void quack(){ 24 if(quackBehavior != null) 25 quackBehavior.quack(); //这里是对行为调用的通用框架,具体的表现方式由具体实现而定 26 } 27 28 /** 29 * 显示当前鸭子的特性 30 */ 31 public abstract void display(); //抽象方法,由导出类实现来进行特征展示 32 33 }
- 导出类
GeneralDark:普通的鸭子类,Duck的具体化
RubberDuck:橡皮鸭类,Duck的具体化
1 以GeneralDark为例,实现代码如下: 2 3 /** 4 * 普通的鸭子:用翅膀飞,会嘎嘎叫 5 * 6 * @author Apache_xiaochao 7 * 8 */ 9 public class GeneralDark extends Duck { 10 11 /* 12 * 在这里完成具体的拼接,将应有的行为装配到骨架上 13 */ 14 public GeneralDark() { 15 flyBehavior = new FlyWithWings(); 16 quackBehavior = new QuackWithGaga(); 17 } 18 19 @Override 20 public void display() { 21 System.out.println("我是一只普通的鸭子"); 22 fly(); 23 quack(); 24 } 25 26 }
整个架构中代码执行过程说明:
父类Duck中定义了一些域和方法,Duck依赖于行为接口,利用面向接口编程的思想,可以在导出类中规定具体的行为方式,我们都知道在继承中,父类的非private修饰的域也是被导出类所拥有的,而且Duck中的域不是static的,这也就是说每一个子类拥有的父类的域都是私有的,不会共享。父类在通过接口已经规定了行为的调用的方式,但是具体调用行为的哪一种实现方式,这是由导出类去自己选择的。这样的父类就是一个骨架,是通用的,而导出类可以根据实际来进行组合,从而可以达到不同的效果。
- 总结:
- 针对接口编程,这里的接口并不仅仅指java中的interface,任何超类型都可以被看做是一个接口,只要他是所有导出类的超类(接口、抽象类、普通类都可以)
- 多用组合,少用继承,这样的代码更加灵活,耦合性更小
- 封装变化,应该将需要变化的东西封装起来,约定好接口,当变化发生时可以灵活的替换
- 良好的OO设计必须具备可复用、可扩充、可维护三个特性