代码改变世界

策略模式

2012-07-13 17:02  youxin  阅读(326)  评论(0编辑  收藏  举报

   先从模拟鸭子游戏说起,游戏中会出现各种鸭子,一边游泳,一边瓜瓜叫,由此设计了一个superclass,并让各种鸭子继承此超类。

 

现在要增加会飞的鸭子,如果在Duck类增加了fly(),则出现问题,不是所有鸭子都会飞。当然,你可以再不会飞的鸭子中把fly覆盖掉,并且方法体为空。但是以后你每加不会飞的鸭子,都必须重新覆盖一遍。

我们这里意识到:

   当涉及“维护”时,为了复用而使用“继承”,结局并不完美。

 

利用接口如何?

    注意我们上面的超类是有问题的,并不是所有的鸭子都会叫,而且quack方式不同。所以我们想到写2个接口。

Flyable  { fly(){ } }

Quackable{ quack(){ } )

 

用接口是一个stupid的注意,想想看,这一来重复的代码比覆盖的方法还要多,对于48会飞的鸭子,必须实现接口类,如果要修改一下飞行的行为,这些都要改变。

 

软件开发的一个不变真理:Change

 不管当初软件设计得多好,一段时间后,总是需要成长和改变。否则软件就会“死亡”。

 

把问题归零

   现在我们意识到采用接口不是一个很好的办法,因为java接口不具有代码实现,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,你必须往下追踪并在定义此行为的类中修改它,一不小心,可能会造成新的错误。幸运的是,有一个设计原则,恰好适用于此状况:

设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

即把变化的部分取出并“封装”起来,好让其他部分不受影响。使系统变得更有弹性。这个概念很简单,几乎是每个设计模式背后的精神所在,所以的模式都提供了一套方法让“系统中的某部分改变不会影响其他部分”。

  现在来分开变化和不变的部分。为了分开,我们建立了2组类(完全远离Duck类),一个和fly相关,一个和quack相关。每一组类将实现各自的动作。

 为了把这两个行为从Duck类分开,我们从Duck去取出来,建立一组新类来代表每个行为。

 

设计鸭子的行为

    如何设计那组实现fly和quack的行为的类呢?

  我们希望一切具有弹性。正因为一开始鸭子行为没有弹性,才让我们走上现在这条路,我们还想能够“指定”行为到鸭子的实例。换句话说,我们应该在鸭子类中包含设定行为的方法,这样,我们就可以在“运行时”动态地“改变”飞行行为。

设计原则: 针对接口编程,而不是针对实现编程。

  我们利用接口代表每个行为,比方说,FlyBehavior与QuackBehavior,而行为的每个实现都放在其中的一个接口。

  所以这次鸭子类不会复制实现Flying与Quacking接口,反而是由我们制造的一组其他类专门实现FlyBehavior与QuackBehavior,这称为“行为”类,由行为类而不是Duck类实现行为接口。

  这样的做法不同于以往,以前的做法是:行为来自Duck超类的具体实现,或是继承某个接口并由子类自行实现而来,这两种做法都是依赖于“实现,我们被实现绑的死死的,没办法更改行为(除非写更多代码)

 从现在开始,鸭子的行为将被放在分开的类中,此类专门提供某行为接口的实现。这样,鸭子类就不在需要知道行为的实现细节了。

为什么非要把FlyBehavior设计成接口,而不是使用抽象超类,这样不就可以使用多态了吗?

针对”接口编程“真正的意思是”针对超类型supertype编程“;

  这里的”接口“有多个函数,接口是一个”概念“,也是一种java的interface构造,你可以在不涉及java interface的情况下,”针对接口编程“。关键在于多态,利用多态,程序可以针对超类型编程,执行时会根据实际情况来执行真正的行为,不会被绑死在超类型的行为上。针对超类型编程“这句话,可以更明确说成”变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量,这也意味着,声明类时不用理会以后执行时的真正对象类型

 举个简单例子,假设有一个抽象类Animal,有2个具体的实现Dog和Cat继承Animal,做法如下:

“针对实现编程”

Dog d=new Dog(); 

d.bark();

但是,针对接口/超类型编程做法如下:

Animal animal=new Dog();

animal.makeSound();

更好的是,子类实例化的动作不在需要在代码中硬编码,例如new Dog(),而是在运行时才指定具体实现的对象。

a=getAnimal();

a.makeSound(); 我们不知道实际子类型是什么, 我们只关心她知道如何正确地进行makesound就够了。

 

问题:用一个类代表一个行为,感觉是否有点奇怪?类不是应该代表某种“东西”吗?

  在oo中,类代表的东西一般都是既有专题(实例变量)又有方法,不过在本例中,恰好“东西”是个“行为”,但是即使是行为,也可以由状态和方法,例如,飞行的行为可以有实例变量,记录飞行行为的属性等等。

 

整合鸭子的行为

   关键在于,鸭子现在将fly和quack的动作“委托”delegate别人除了,而不是使用定义在Duck或子类的方法。

做法是这样的:首先在Duck中加入两个实例变量,flyBehavior与quackBehavior,声明为接口类型(而不是具体实现类型),

将原来在Duck类中的fly与quack换成performFly和performQuack。

package headfirst.strategy;

public abstract class Duck {
    FlyBehavior flyBehavior; //实例变量在运行时持有特定的行为的引用,每只鸭子都是引用实现接口的对象
    QuackBehavior quackBehavior;
 
    public Duck() {
    }
 
    abstract void display();
 
    public void performFly() {
        flyBehavior.fly();  //
    }
 
    public void performQuack() {
        quackBehavior.quack();//鸭子不亲自处理quakc行为,而是委托给quackBehavior引用的对象
    }
 
    public void swim() {
        System.out.println("All ducks float, even decoys!");
    }
}

 

接着,来设定flyBehavior与quackBehavior的实例变量

package headfirst.strategy;

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");
    }
}

当mallardDuck实例化时,构造器会把继承而来的实例变量quackBehavior与flyBehavior实例化。

 FlyWithWings是FlyBehavior的具体实现类。

等等,不是说过我们将不对具体实现编程吗?但是我们的构造函数里做了什么,制造一个具体的Quack实现类的实例

暂时我们是这么做的,在以后的学习中,有跟好的方法来修正这一点。仍请注意,我们把行为设定成具体的类,但是还是可以在运行时“轻易地”改变它。

  所以,目前的做法还是很有弹性的,只是初始化实例变量的做法不够弹性罢了

 

编写2个行为实行类FlyWithWings.java和FlyNoway.java

public interface FlyBehavior {
    public void fly();
}

 

public class FlyWithWings implements FlyBehavior {
    public void fly() {
        System.out.println("I'm flying!!");
    }
}

 

public class FlyNoWay implements FlyBehavior {
    public void fly() {
        System.out.println("I can't fly");
    }
}

 

最后编写测试类:

public class MiniDuckSimulator {
    
    public static void main(String[] args)
    {
        Duck mallard=new MallardDuck();
        mallard.performFly();
        mallard.performQuack();
        
    }

}

 

我们可以增加功能,能够动态设定行为:

  在Duck增加设setter method 来设定实例变量:

    public void setFlyBehavior (FlyBehavior fb) {
        flyBehavior = fb;
    }
 
    public void setQuackBehavior(QuackBehavior qb) {
        quackBehavior = qb;
    }
 

 

封装行为的大局观

  我们设计了Duck类,飞行行为类实现FlyBehavior接口,呱呱叫行为类实现QuackBehavior接口。注意,我们描述事情的方式稍微改变,不再把鸭子的行为说成是“一组行为”,我们开始把行为想成是“一簇算法”,想想看,在我们设计中,算法代表鸭子能做的事(不提的叫法和飞行法)。

 

has-a 可能比is -a 更好

   "has-a"关系非常有趣:每一组鸭子都有一个flyBehavior和一个QuackBehavior,好将fly和quack委托给他们代为处理

 当你将2个类结合起来使用,如同本例,这就是”组合“composition,这种做法和”继承“不同之处在于:鸭子的行为不是继承来的,而是和适当的行为对象”组合“而来的。

  这是一个很重要的技巧,其实是使用了我们的第3个设计原则:

设计原则;多用组合,少用继承

 组合用在”许多“设计模式中。

 

上面我们用到的是”策略模式“,strategy pattern定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于是用算法的客户