设计模式之一:策略模式(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("<>");
}
}
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、 因为每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量。