浅谈策略模式

设计模式之策略模式

首先,给出策略模式的定义:策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式可以让算法的变化独立于使用算法的客户。

例子

为了引出为什么使用策略模式,先说说不用这个模式之前是怎么做的。从简单的模拟鸭子应用开始,首先需要设计一套模拟鸭子的游戏,游戏中会出现各种类型的鸭子,一边游泳戏水,一边呱呱叫。系统内部使用标准的OO技术,设计了一个鸭子超类(SuperClass),并让各种鸭子继承超类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Duck{
void quack();
void swim();
void display();
}
class MallardDuck extends Duck{
void display(){
//外观是绿头
}
}
class RedheadDuck extends Duck{
void display(){
//外观是红头
}
}

然后公司因为竞争压力巨大。客户需要改需求了,他们说要让鸭子会飞;主管也表示会飞的鸭子来将竞争者抛在后头。所以这个时候首先想到最直观的方法当然就是直接在超类中添加飞的方法:

1
2
3
4
5
6
class Duck{
void quack();
void swim();
void display();
void fly();
}

但是这样一来问题出现了,一些本来不应该会飞的鸭子也在空中飞来飞去了,比如之前继承超类实现的橡皮鸭。直接在超类中加上新的行为会使得某些并不适合该行为的子类也具有该行为。

分开变化和不变的部分

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

意思就是说如果每次新的需求一来,都会使某方面的代码发生变化,那就可以确定这部分代码需要被抽离出来,和其他稳定的代码有所区分。

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

所以这里以鸭子为例,把变化的和不变化的部分分开来,就目前所知除了fly()和quack()的问题之外,其他行为还算正常,没有特别需要改变的地方。所以现在准备建立两组类(完全远离Duck类),一个是“fly”相关的,一个是“quack”相关的,每组类将实现各自的动作。比如叫声不一样实现“呱呱叫”、“吱吱叫”、“安静不叫”。

设计鸭子的行为

那我们如何设计实现飞行和呱呱叫的行为类呢?我们希望一切能有弹性,我们想能够“指定”行为到鸭子的实例。比如,想要产生一个新的绿头鸭的示例,并指定特定“类型”的飞行行为给他。干脆顺便让鸭子的行为可以动态地改变好了。

所以这里提出了第二个设计原则:针对接口编程,而不死针对实现编程。

利用接口代表每个行为,比如FlyBehavior与QuackBehavior,而行为的实现都将实现其中的一个接口。所以鸭子类不会负责实现Flying与Quacking接口,反而制造一组其他类专门实现这两个行为。

所以新的设计中,鸭子的子类将使用接口(FlyBehavior与QuackBehavior)所表示的行为,实际的“实现”不会被绑死在鸭子的子类中。

这里所说的针对接口编程的接口并不是一定要用到Java的interface构造,其关键在于多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上(向上转型)。举个例子:

针对实现编程:

1
2
Dog d = new Dog();
d.bark();

但是,针对接口/超类型编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal{
void makeSound();
}
class Dog extends Animal{
void makeSound(){
bark();
}
void bark();
}
class Cat extends Animal{
void makeSound(){
meow();
}
void meow();
}
Animal animal = new Dog();
animal.makeSound();

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

鸭子的行为

所以为了将鸭子的叫和飞这两种行为分离开来,实现了两个接口:FlyBehavior和QuackBehavior,还有他们对应的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface FlyBehavior{
void fly();
}
class FlyWithWings implements FlyBehavior{
void fly(){
//实现鸭子飞行
}
}
class FlyNoway implements FlyBehavior{
void fly(){
//什么都不做,不会飞
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface QuackBehavior{
void quack();
}
class Quack implements QuackBehavior{
void quack(){
//实现鸭子呱呱叫
}
}
class Squeak implements QuackBehavior{
void quack(){
//橡皮鸭子吱吱叫
}
}
class MuteQuack implements QuackBehavior{
void quack(){
//什么都不做,不会叫
}
}

这样的设计,可以让飞行和呱呱叫的动作被其他对象复用,因为这些行为已经和鸭子类无关了。而可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。有了继承的好处,却没有继承所带来的包袱

整合鸭子行为

这样做的关键在于,鸭子现在将飞行和呱呱叫的动作“委托”给别人处理,而不是使用定义在Duck()类(或子类)内的呱呱叫和飞行方法。

  • 首先,在Duck类中加入两个示例变量,分别为“flyBehavior”与“quackBehavior”,声明为接口类型,每个鸭子对象动态的设置这样变量以在运行时引用正确的行为类型(FlyWithWings、Squeak等)。

  • 然后实现表现类:

    1
    2
    3
    4
    5
    6
    7
    class Duck {
    QuackBehavior quckBehavior;
    //其他省略
    void performQuack(){
    quckBehavior.quack();
    }
    }
  • 最后设定flyBehavior与quackBehavior的实例对象。以MallardDuck为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MallardDuck extends Duck {
    public MallardDuck(){
    quckBehavior = new Quack();
    flyBehavior = new FlyWithWings();
    }

    void display(){
    //绿头鸭
    }
    }
动态设定行为

假设在鸭子类中通过设定方法(setter method)来设定鸭子的行为,而不是在鸭子的构造器内部实例化。

在Duck类中加入下面方法:

1
2
3
4
5
6
void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}

这样一来就可以随时调用这两个方法改变鸭子的行为。

比如,一开始制造一个新的鸭子类型,是不会飞的:模型鸭(ModelDuck)

1
2
3
4
5
6
7
8
9
class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
void display(){
//xxxxxx
}
}

此时建立一个新的飞行方式,火箭。。(FlyRocketPowered):

1
2
3
4
5
class FlyRocketPowered implements FlyBehavior {
void fly() {
//火箭飞行
}
}

然后使用setter方法将该飞行模式加到模型鸭上去,使其具有火煎动力:

1
2
Duck model = new ModelDuck();
model.setFlyBehavior(new FlyRocketPowered());
“有一个”的关系

上面的例子每一个鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和叫委托给他们代为处理。这种将两个类结合起来使用的方式就是组合,这种做法和继承的不同之处在于:鸭子的行为不是继承来的,而是和适当的行为对象组合来的。

这里有一个很重要的设计原则:多用组合,少用继承

使用组合建立系统具有很大的弹性,不仅可以将算法封装成类,更可以“在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准即可。

0%