策略模式

书中以模拟鸭子的应用为例引出策略模式,方便理解。

模拟鸭子游戏:游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫。此系统设计了一个鸭子超类(Superclass),并让各种鸭子继承此超类。

abstract class Duck {
/**
* 所有的鸭子都会呱呱叫(Quack)也会游泳(Swim),
* 所以由超类负责处理这部分的实现代码。
*/
public void quack() {};
public void swim() {};
//每一种鸭子的外观都不同,所以display()方法是抽象的。
public abstract void display();
}
/**
* 每个鸭子子类型(subtype)负责实现自己的display()行为,
* 在屏幕上显示其外观。
*/
class MallardDuck extends Duck {
@Override
public void display() {
//外观是绿头
}
}
/**
* 许多其他类型的鸭子继承Duck类。
*/
class RedheadDuck extends Duck {
@Override
public void display() {
//外观是红头
}
}

之后,主管希望鸭子能飞。

设想:只需要在Duck类中加上fly()方法,然后所有鸭子都会继承fly()。

abstract class Duck {
public void quack() {};
public void swim() {};
public abstract void display();
//所有的子类都会继承fly()。
public void fly() {};
}

但是,可怕的问题发生了:有很多“橡皮鸭子”在屏幕上飞来飞去。

并非Duck所有的子类都会飞,在Duck超类中加上新的行为,会使得某些并不适合该行为的子类也具有该行为。
对代码所做的局部修改,影响层面可不只是局部(会飞的橡皮鸭)!

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

这时,想到继承:把橡皮鸭类中的fly()方法覆盖掉。

class RubberDuck extends Duck{
@Override
public void quack(){
//吱吱叫
}

@Override
public void display(){
//橡皮鸭
}

@Override
public void fly(){
//覆盖,变成什么事都不做
}
}

可是,如果以后加入诱饵鸭(DecoyDuck),诱饵鸭是木头假鸭,不会飞也不会叫。

/**
* 这是继承层次中的另一个类。
* 注意,诱饵鸭既不会飞也不会叫,
* 可是橡皮鸭不会飞但会叫。
*/
class RubberDuck extends Duck{
@Override
public void quack(){
//覆盖,变成什么事都不做
}

@Override
public void display(){
//诱饵鸭
}

@Override
public void fly(){
//覆盖,变成什么事都不做
}
}

利用继承来提供Duck的行为,这会导致下列缺点:

  • 代码在多个子类中重复。
  • 运行时的行为不容易改变。
  • 很难知道所有鸭子的全部行为。
  • 改变会牵一发动全身,造成其他鸭子不想要的改变。

认识到继承可能不是答案,每当有新的鸭子子类出现,就要被迫检查并可能需要覆盖fly()和quark()……

所以需要一个更清晰的方法,让“某些”(而不是全部)鸭子类型可飞或可叫。

可以把fly()从超类中取出来,放进一个“Flyable接口”中。这么一来,只有会飞的鸭子才实现此接口。同样的方式,也可以用来设计一个“Quackable接口”,因为不是所有的鸭子都会叫。

interface Flyable{
public void fly();
}
interface Quackable{
public void quack();
}
abstract class Duck {
public void swim() {};
public abstract void display();
}
class MallardDuck extends Duck implements Flyable, Quackable {
@Override
public void display() {
}

@Override
public void fly() {
}

@Override
public void quack() {
}
}

虽然Flyable和Quackable可以解决“一部分”问题(不会再有会飞的橡皮鸭),但是却造成代码无法复用,这只能算是从一个噩梦跳进另一个噩梦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化……

软件开发的一个不变真理:不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会“死亡”。

驱动改变的因素很多:

  1. 我们的顾客或用户需要别的东西,或者想要新功能。
  2. 我的公司决定采用别的数据库产品,又从另一家厂商买了数据,这造成数据格式不兼容。
  3. 应对技术改变,我们必须更新代码,适用于新协议。
  4. 我们学到了足够的构建系统的知识,希望回去把事情做得更好。
  5. ……

把问题归零

现在我们知道使用继承并不能很好地解决问题,因为鸭子的行为在子类里不断地改变,并且让所有的子类都有这些行为是不恰当的。Flyable与Quackable接口一开始似乎还挺不错,解决了问题(只有会飞的鸭子才能继承Flyable),但是Java接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,可能会造成新的错误!

幸运的是,有一个设计原则,恰好适用于此状况。

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

把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的的其他部分。

这样的概念很简单,几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让“系统中的某部分改变不会影响其他部分”。

分开变化和不会变化的部分

准备建立两组类(完全远离Duck类),一个是“fly”相关的,一个是“quack”相关的,每一组类将实现各自的动作。

设置鸭子的行为

我们希望一切能有弹性,还想能够“指定”行为到鸭子的实例。比方说,我们想要产生一个新的绿头鸭实例,并指定特定“类型”的飞行行为给它。干脆顺便让鸭子的行为可以动态地改变好了。换句话说,我们应该在鸭子类中包含设定行为的方法,这样就可以在“运行时”动态地“改变”绿头鸭的飞行行为。

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

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

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

在我们的新设计中,鸭子的子类将使用接口(FlyBehavior与QuackBehavior)所表示的行为,所以实际的“实现”不会被绑死在鸭子的子类中,即特定的具体行为编写在实现了FlyBehavior与QuackBehavior的类中。

interface FlyBehavior{
void fly();
}

class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
//实现鸭子的飞行动作
}
}

class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
//什么都不做,不会飞
}
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注