策略模式
例:设计一个模拟鸭子游戏,游戏中有各种鸭子,一边戏水一边嘎嘎叫。
所以学习设计模式前,我们最先想到的就是设置一个超类,并让其他子类去继承这个类,UML图如下:
*
*
但是,程序需求是会经常变动的,若给游戏中加入飞行方法以及玩具小黄鸭呢?并不是所有鸭子都可以飞,也并不是所有鸭子都是嘎嘎叫。
此时你可能会想,在父类添加相应方法,在子类重写就可以让不同鸭子的同一方法具有不同表现了!
然而当子类数量剧增时你都要到对应子类去逐个修改,这将是个复杂又无聊的工作!单独使用接口也同样会使代码量变多,出现许多重复性代码。


那么,还有其他更好的方式来添加飞行方法吗?
有,就是委托。我们可以把委托者和被委托者想成has-a(有一个)的关系。比如上面这个例子,鸭子(被委托者)有一个飞行行为(委托者)
把飞行想成Duck的一个功能,Duck具有该功能,即Duck本身含有飞行行为的实例,可以调用飞行行为的飞行方法。
找出易于变化的部分,把他们独立起来
把会变化的部分取出封装起来,好让其他部分不会受到影响。代码变化引起的不经意后果变少,系统将变得更有弹性。

针对接口编程,而不是针对实现编程
将会变化的部分放在类中,此类专门提供某行为接口的实现,这样主类就不需要知道行为的实现细节。

多用组合,少用继承
相应的UML图

实际代码
设计两个接口类让所有飞行类及所有叫声类都实现对应的接口
接口 FlyBehavior
| 1 2 3 | public interface flyBehavior{ public void fly(); } |
接口QuackBehavior
| 1 2 3 | public interface QuackBehavior{ public void quack(); } |
这样设计接口让飞行与叫声的动作可以被其他对象复用又可以新增一些行为类而不会影响既有的行为类,也不会影响到“使用”行为的类。
编写对应的类实现接口,让这些类负责实现具体的行为。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class FlyNoWay implements FlyBehavior { @Override public void fly() { System.out.println("我不会飞"); } } * * * * * * * * * * * * * * * * * * * * * * *分割线 * * * * * * * * * * * * * * * * * * * * public class Quack implements QuackBehavior { @Override public void quack() { System.out.println("Quack"); } } |
在此省略其他行为类的展示...
在Duck类中怎加两个实例变量,声明为我们刚刚建立的接口类型,这样Duck都能在运行时动态地设置这些变量并保证引用正确的行为类型。
Duck类:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //抽象化 public abstract class Duck { //接口类型的实例变量 QuackBehavior quackBehavior; FlyBehavior flyBehavior; public Duck() { } //设定display()方法为抽象的,让每个继承的类都必须实现它 public abstract void display(); public void performFly() { //将行为的实现委托给flyBehavior引用的对象 flyBehavior.fly(); } public void performQuack() { //将行为的实现委托给quackBehavior引用的对象 quackBehavior.quack(); } public void swim() { System.out.println("所有鸭子都能会游泳"); } //建立一个seter,方便动态地设定行为 public void setFlyBehavior(FlyBehavior fb) { flyBehavior=fb; } public void setQuackBehavior(QuackBehavior qb) { quackBehavior=qb; } } |
MallardDuck类:
| 1 2 3 4 5 6 7 8 9 10 11 | public class MallardDuck extends Duck{ public MallardDuck() { quackBehavior=new Quack(); flyBehavior=new FlyWithWings(); } public void display() { System.out.println("我是只野鸭"); } } |
MiniDuckSimulator类:
| 1 2 3 4 5 6 7 8 9 10 11 12 | public class MiniDuckSimulator { public static void main(String[] args) { Duck mini=new MallardDuck(); /*这会调用MallardDuck类继承来的performFly方法, *进而委托给FlyBehavior对象处理(即调用继承来的 *对象flyBehavior引用的FlyWithWings()。 */ mini.performFly(); mini.performQuack(); } } |
动态设定行为
模型鸭既不会飞也不会叫,但我们在Duck类中添加了seter方法以便动态设定行为,给模型鸭添加一个火箭助推器,让它飞起来。
ModelDuck类
| 1 2 3 4 5 6 7 8 9 10 11 | public class ModelDuck extends Duck{ @Override public void display() { System.out.println("我是模型鸭"); } public ModelDuck() { flyBehavior= new FlyNoWay(); quackBehavior= new Quack(); } } |
对MiniDuckSimulator类稍作改动,
| 1 2 3 4 5 6 7 8 9 | public class MiniDuckSimulator { public static void main(String[] args) { Duck model=new ModelDuck(); model.performFly(); //FlyRocketPower()是继承飞行接口的一个飞行行为类 model.setFlyBehavior(new FlyRocketPower()); model.performFly(); } } |
输出将是:
总结
策略模式的三项原则:
1、找出应用中可能需要变化之处,把它们独立出来,不要和那些无需变化的代码混在一起。
2、针对接口编程,而不是针对实现编程
3、多用组合,少用继承
分离变与不变。回忆之前的需求,可以发现,不变的是swim,变的是fly与quack。虽然swim和fly都是鸭子的行为,但是swim不怎么变化,它可以利用继承来实现代码复用,而飞行方式却有很多种,无法继承,因此需要将飞行行为抽出来,各自实现,让Duck实现类拥有飞行行为实例,达到操作飞行行为的目的。这种思想就像零件的组装,主体是不变的,零件有各种相同功能不同性能的各种款式,这样可以做出不同的产品。
另外策略模式还有一个好处是可以动态变化行为,比如木头鸭子原来是不会飞的,它用setFlyBehavior设置不会飞行,后来设计师给他装上了引擎翅膀,它可以再次通过setFlyBehavior在运行时动态变更飞行行为。