设计模式之策略模式

《Head First 设计模式》阅读笔记

Posted by wangtiegang on July 20, 2019

《Head First 设计模式》书中对策略模式对定义为:定义了算法簇,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

书中用一个鸭子游戏来循序渐进的讲解策略模式,假设有几种鸭子它们都能叫能游泳,并且鸭子的种类是可以增加的,那么如何设计这个游戏呢?

肯定第一反应就是使用继承了,先定义一个鸭子的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Duck{

    public void display(){
        // 鸭子的外观
    }

    public void quack(){
        // 所有鸭子都默认嘎嘎叫
        System.out.println("嘎嘎嘎");
    }

    public void swin(){
        // 所有鸭子默认都会游泳
    }

}

父类定义好了之后,就是定义各种类型的鸭子了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 红头鸭子
*/
public class RedheadDuck extends Duck {

    /**
    * 重写外观
    */
    public void display(){
        System.out.println("红头鸭子");
    }

}

/**
* 橡皮鸭子
*/
public class RunnerDuck extends Duck {

    /**
    * 重写橡皮鸭子都叫声为吱吱吱
    */
    public void quack(){
        System.out.println("吱吱吱");
    }

}

这样就实现了很好的代码重用,并且各种鸭子也能重写父类的方法或者增加自己的方法来实现自己的特殊行为。但是假设现在需要对鸭子进行升级,让部分鸭子可以飞,要怎么修改设计呢?

  • 在父类Duck中增加默认的fly()方法实现,则所有鸭子都能飞了,如果要纠正这个问题,则需要在所有不应该飞的鸭子中重写这个方法,比如橡皮鸭子。
  • 不在父类中增加默认实现,则需要在应该飞的鸭子中增加自己的实现方法,但是这样重复的代码就很多了。
  • 增加一个飞行行为的接口,需要飞的鸭子则实现这个接口,再实现飞行行为方法,但是一样重复代码很多。

经过上面的例子可以发现,java的继承在某些场景下有一些缺点

  • 很难知道对象的全部行为
  • 修改父类都行为会导致所有子类都改变
  • 运行时的行为不容易改变

既然继承不适合,那要怎么设计这个鸭子游戏呢?作者在这引出了两个设计原则:

找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起

针对接口编程,而不是针对实现编程

根据第一个原则,首先找出变和不变的地方,假设只考虑飞行和叫声。变化的部分很好理解:不同的鸭子飞行和叫声不一样。不变的地方除了外观和游泳,其他的一下就有点难想到了:飞行的行为,叫的行为,就是说不管是怎么飞的,叫声是怎么样的,鸭子总会有这两个行为。

找出了不变的地方就好办了,把不变的地方写到父类中,首先定义外观和游泳方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Duck{

    public void display(){
        // 鸭子的外观
    }

    public void swin(){
        // 所有鸭子默认都会游泳
    }

}

接下来是定义飞和叫这两个行为,但是问题来了,这两个怎么定义呢?根据第二个原则,应该面向接口编程,作者在这提示了此处的接口不能狭义的理解为interface,接口是一个概念,针对接口编程的关键是多态,利用多态,程序可以针对 超类型 编程,执行时会根据实际情况执行到真正的行为实现,不会被绑定死在超类型的行为上。针对超类型编程可以更明确的说成“变量的声明类型应该是超类型,通常是一个抽象类或者接口”,这意味着声明类型时可以不考虑以后执行时真正的对象类型。看到这,就可以想到把鸭子不变的飞和叫行为定义为一个超类型了,不用关心执行时具体的实现了,因此增加两个行为接口到Duck中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Duck{

    /**
    * 飞行行为接口
    */
    IFlyBehavior flyBehavior;

    /**
    * 叫声行为接口
    */
    IQuackBehavior quackBehavior;

    public void display(){
        // 鸭子的外观
    }

    public void swin(){
        // 所有鸭子默认都会游泳
    }

    public void fly(){
        flyBehavior.fly();
    }

    public void quack(){
        quackBehavior.quack();
    }

}

可以为IFlyBehavior,IQuackBehavior设计不同实现,这样变化的部分就单独出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FlyNoWay implements IFlyBehavior {

    public void fly(){
        Sysout.out.println("不能飞");
    }

}

public class FlyWithWings implements IFlyBehavior {

    public void fly(){
        Sysout.out.println("用翅膀飞");
    }

}

public class ZhizhizhiQuack implements IQuackBehavior {

    public void quack(){
        Sysout.out.println("吱吱吱");
    }

}

这样我们在定义每鸭子的时候,就可以继承Duck来实现代码重用,也可以通过给飞和叫接口指定具体的实现来避免继承牵一发而动全身的副作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 橡皮鸭子
*/
public class RunnerDuck extends Duck {

    public RunnerDuck(){
        // 指定具体行为实现,也可以不在构造方法中指定,如果使用setter方法去设置,就可以在运行是动态改变鸭子的行为了。
        flyBehavior = new FlyNoWay();
        quackBehavior = new ZhizhizhiQuack();
    }

}


以上就是策略模式了,将变化的地方分离出来,定义成一簇可以互换的算法,在运行时为不同的类型实例指定不同的算法,这样在软件后续的维护当中可以更好的应对变化,很方便实现修改替换,或者增加新的行为实现。