设计模式之一:策略模式(Strategy pattern)
定义:
定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
The strategy pattern definesa family of algorithms, encapsulates each one, and makes them interchangeable.Strategy lets the algorithm vary independently from clients that use it.
要点:
1. 知道OO基础,并不足以让你设计出良好的OO系统。
2. 良好的OO设计必须具备可复用、可扩充、可维护三个特性。
3. 模式可以让我们建造出具有良好OO设计质量的系统。
4. 模式被认为是历经验证的OO设计经验。
5. 模式不是代码,而是针对设计问题的通用解决方案。你可以把它们应用到特定的应用中。
6. 模式不是被发明,而是被发现。
7. 大多数的模式和原则,都着眼于软件变化的主题。
8. 大多数的模式都允许系统局部改变独立于其他部分。
9. 我们常把系统中会变化的部分抽出来封装。
10. 模式让开发人员之间有共享的语言,能够最大化沟通的价值。
模式介绍
策略模式是一个很简单的模式,也是一个很常用的模式,可谓短小精悍。废话不多说了,下面开始介绍策略模式。
Joe的公司做了一套相当成功的模拟鸭子游戏:SimUDuck.游戏中会出现各种鸭子,一边游泳戏水,一遍哌哌叫。系统的核心类图如下所示:
如图所示,在Duck基类里实现了公共的quack()和swim()方法,而MallardDuck和RedheadDuck可以分别覆盖实现自己的display()方法,这样既重用了公共的部分,又支持了不同子类的个性化扩展。
随着公司竞争压力的加剧,公司主管决定设计出会飞的鸭子来将竞争对手抛在后头。Joe拍着胸脯保证一个星期就可以解决。Joe发现只要在Duck类中加上fly()方法,然后所有鸭子都会继承fly().
Joe很高兴的带着自己的产品到股东会议上去展示,有很多“橡皮鸭子”飞来飞去。这是怎么回事?原来Joe忽略了一件事:并非Duck所有的子类都会飞。Joe在Duck超类中加上新的行为,会使得某些并不适合该行为的子类 也具有该行为。现在可好了!SimUDuck程序中有了一个无生命的会飞的东西。他意会到了一件事:当涉及“维护”时,为了“复用”目的而使用继承,结局并不完美。Joe很郁闷!他突然想到:如果在RubberDuck类里把fly()方法重写一下会如何?在RubberDuck类的fly()里让橡皮鸭子什么都不做,不就一切OK了吗!那以后再增加一个木头鸭子呢?它不会飞也不会叫,那不是要再重写quack()和fly()方法,以后再增加其它特殊的鸭子都要这样,这不是太麻烦了,而且也很混乱。
最终,Joe认识到使用继承不是办法,因为他的上司通知他,董事会决定以后每6个月就会升级一次系统,以应对市场竞争,所以未来的变化会很频繁,而且还不可预知。如果以后靠逐个类去判断是否重写了quack()或fly()方法来应对变化,显然混不下去!
那么用接口能不能解决这个问题吗?把fly()从超类中取出来,放进一个“Flyable接口”中。这么一来只有会飞的鸭子才实习该接口。同样的方式,也可以用来设计一个“Quackable接口”,因为不是所以的鸭子都会叫。
但是这种方法会出现代码无法重用的问题,如果鸭子的类特别多的话,就这么几个鸭子还好说,但是我们有几十、上百个鸭子的时候你怎么办?如果某个方法要做一点修改,就需要重复修改上百遍。
呵呵!如果你是Joe,你该怎么办?
我们知道,并不是所有的鸭子都会飞、会叫,所以继承不是正确的方法。但是虽然上面的使用Flyable接口的方法,可以解决部分问题(不再有会飞的橡皮鸭子),但是这个解决方案却彻底破坏了重用,它带来了另一个维护的噩梦!而且还有一个问题我们前面没有提到,难道所有的鸭子的飞行方式、叫声等行为都是一模一样的吗?不可能吧!
说到这里,为了能帮助Joe摆脱困境,我们有必要先停下来,重新回顾一些面向对象设计原则。请您告诉我:“什么东西是在软件开发过程中是恒定不变的?”,您想到了吗?对,那就是变化本身,正所谓“计划没有变化快”,所以直面“变化这个事实”才是正道!Joe面对的问题是,鸭子的行为在子类里持续不断地改变,所以让所有的子类都拥有基类的行为是不适当的,而使用上面的接口的方式,又破坏了代码重用。现在就需要用到我们的第一个设计原则:
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
换句话说,如果每次心的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他文档的代码有所区分。这个原则的另一种思考方式是:把会变化的部分取出并封装起来,以便以后可以轻易地盖栋或扩充此部分,而不影响不需要变化的其他部分。
OK!现在我们已经有了一条设计原则,那么Joe的问题怎么办呢?就鸭子的问题来说,变化的部分就是子类里的行为。所以我们要把这部分行为封装起来,省得它们老惹麻烦!从目前的情况看,就是fly()和quack()行为总是不老实,而swim()行为是很稳定的,这个行为是可以使用继承来实现代码重用的,所以,我们需要做的就是把fly()和quack()行为从Duck基类里隔离出来。我们需要创建两组不同的行为,一组表示fly()行为,一组表示quack()行为。为什么是两组而不是两个呢?因为对于不同的子类来说,fly()和quack()的表现形式都是不一样的,有的鸭子嘎嘎叫,有的却呷呷叫。有了这两组行为,我们就可以组合出不同的鸭子,例如:我们可能想要实例化一个新的MallardDuck(野鸭)实例,并且给它初始化一个特殊类型的飞行行为(野鸭飞行能力比较强)。那么,如果我们可以这样,更进一步,为什么我们不可以动态地改变一个鸭子的行为呢?换句话说,我们将在Duck类里包含行为设置方法,所以我们可以说在运行时改变MallardDuck的飞行行为,这听起来更酷更灵活了!那么我们到底要怎么做呢?回答这个问题,先要看一下我们的第二个设计原则:
面向接口编程,而不是针对实现编程。
针对接口编程真正的意思是针对超类型编程。“针对接口编程”关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被蚌寺在超类型的行为上。“针对超类型编程”这句话,可以更明确地说成“变量的生命类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型”
根据面向接口编程的设计原则,我们应该用接口来隔离鸭子问题中变化的部分,也就是鸭子的不稳定的行为(fly()、quack())。
第一步:我们要给Duck类增加两个接口类型的实例变量,分别是flyBehavior和quackBehavior,它们其实就是新的设计里的“飞行”和“叫唤”行为。每个鸭子对象都将会使用各种方式来设置这些变量,以引用它们期望的运行时的特殊行为类型(使用横着飞,吱吱叫,等等)。
第二步:我们还要把fly()和quack()方法从Duck类里移除,因为我们已经把这些行为移到FlyBehavior和QuackBehavior接口里了。我们将使用两个相似的PerformFly()和PerformQuack()方法来替换fly()和qucak()方法,后面你会看到这两个新方法是如何起作用的。
第三步:我们要考虑什么时候初始化flyBehavior和quackBehavior变量。最简单的办法就是在Duck类初始化的时候同时初始化他们。但是我们这里还有更好的办法,就是提供两个可以动态设置变量值的方法SetFlyBehavior()和SetQuackBehavior(),那么就可以在运行时动态改变鸭子的行为了。
修改后的Duck类如下图所示:
测试代码:
public abstract class Duck { FlyBehavior flyBehavior; QuackBehavior quackBehavior; public Duck(){} public void performFly() { flyBehavior.fly(); } public void performQuack() { quackBehavior.quack(); } public void swim() { System.out.println("All ducks float,even decoys!"); } public void setFlyBehavior(FlyBehavior fb) { flyBehavior = fb; } public void setQuackBehavior(QuackBehavior qb) { quackBehavior = qb; } }
public class ModelDuck extends Duck{ public ModelDuck() { flyBehavior = new FlyNoWay(); quackBehavior = new Quack(); } public void display() { System.out.println("I'm a model duck"); } }
public class MallardDuck extends Duck{ public MallardDuck() { quackBehavior = new Quack(); flyBehavior = new FlyWithWings(); } public void display() { System.out.println("I'm a real Mallard duck"); } }
public class MuteQuack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("<<Silence>>"); } }
public interface FlyBehavior { public void fly(); }
public class FlyNoWay implements FlyBehavior { public void fly(){ System.out.println("I can't fly"); } }
public class FlyRocketPowered implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("I'm flying with a rocket!"); } }
public class FlyWithWings implements FlyBehavior { public void fly(){ System.out.println("I'm flying"); } }
public interface QuackBehavior { public void quack(); }
public class Quack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Quack"); } }
public class Squeak implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Squeak"); } }
public class MiniDuckSimulator { public static void main(String[] args){ Duck mallard = new MallardDuck(); mallard.performQuack(); mallard.performFly(); Duck model = new ModelDuck(); model.performQuack(); model.performFly(); model.setFlyBehavior(new FlyRocketPowered()); model.performFly(); } }
程序运行结果如下:
应用场景和优缺点
上面我们已经看过了Strategy模式的详细介绍,下面我们再来简单说说这个模式的优缺点吧!怎么说呢,人无完人,设计模式也不是万能的,每一个模式都有它的使命,也就是说只有在特定的场景下才能发挥其功效。我们要使用好模式,就必须熟知各个模式的应用场景。
对于Strategy模式来说,主要有这些应用场景:
1、 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。(例如FlyBehavior和QuackBehavior)
2、 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。(例如FlyBehavior和QuackBehavior的具体实现可任意变化或扩充)
3、 对客户(Duck)隐藏具体策略(算法)的实现细节,彼此完全独立。
对于Strategy模式来说,主要有如下优点:
1、 提供了一种替代继承的方法,而且既保持了继承的优点(代码重用)还比继承更灵活(算法独立,可以任意扩展)。
2、 避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展。
3、 遵守大部分GRASP原则和常用设计原则,高内聚、低偶合。
对于Strategy模式来说,主要有如下缺点:
1、 因为每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量。
封装行为的大局观

最后更新:2017-04-04 07:03:51