【设计模式】2. 策略模式

设计模式学习笔记


版权声明:本文由 Hov 所有,发布于 https://chenhy.com ,转载请注明出处。



1 策略模式


1.1 简介

策略模式:通过定义算法族,封装不同算法的具体实现。使用算法的用户只需调用已封装好的算法即可,无需重写算法代码,增加了算法代码的复用性。同时,可以动态选择特定的算法实现。

1.2 思路

策略模式将每一个算法封装起来,实现算法族(业务规则),这些算法可互换代替。

策略模式通过分离变化部分,封装接口,基于接口实现不同的功能。也就是说,对于同一类型的操作,将复杂多样的处理方式分离开来,有选择的实现各自特有的操作。通常,在超类放行为接口对象,在子类设定具体行为对象

1.3 使用场景

对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如,买东西要付款,但是付款的方式有多种,比如有的人现金支付,有的人网上支付,有的人找人代付。

  • 对于同一类型问题的多种处理方式,仅仅具体行为有差别。
  • 需要安全的封装多种同一类型的操作。
  • 出现同一抽象多个子类,又需要使用 if 或 switch 选择语句来选择。

1.4 优缺点

优点:

  1. 算法可自由切换;
  2. 避免使用多重条件判断;
  3. 扩展性良好。
  4. 可替换继承关系

缺点:

  1. 策略类会增多
  2. 所有策略类都需要对外暴露。


2 实例讲解


我们引入模拟鸭子这个例子(这是 Head First设计模式中的例子,简单易懂,推荐)。

2.1 原始需求

现在,我们要通过程序模拟鸭子。假设我们的需求是这样的:鸭子有绿头鸭红头鸭两种,它们都会嘎嘎叫游泳

根据需求,借助面向对象思维,我们可以设计如下代码:

(1)首先设计一个抽象类:Duck

/* Duck.java */

public abstract class Duck{
    public Duck(){
    
    }

    public abstract void display();     //外观
    
    public void quack(){                //嘎嘎叫
        System.out.println("--gaga--");
    }

    public void swim(){                 //游泳
        System.out.println("--swimming--");
    }
}

(2)绿头鸭 GreenHeadDuck 继承抽象父类 Duck

因为父类 Duck 中有抽象方法 display ,所以子类 GreenHeadDuck 需要重写 display 方法,这里我们通过重写 display 方法实现了不同鸭子(子类)外观的差异化。

/* GreenHeadDuck.java */

public class GreenHeadDuck extends Duck{
    @Override
    public void display(){
        System.out.println("**GreenHeadDuck**");    //我是绿头鸭
    }
}

(3)同理,红头鸭也抽象父类 Duck

因为父类 Duck 中有抽象方法 display ,所以子类 RedHeadDuck 需要重写 display 方法,这里我们通过重写 display 方法实现了不同鸭子(子类)外观的差异化。

/* RedHeadDuck.java */

public class RedHeadDuck extends Duck{
    @Override
    public void display(){
        System.out.println("**RedHeadDuck**");      //我是红头鸭
    }
}

(4)模拟绿头鸭、红头鸭

/* StimulateDuck.java */

public class StimulateDuck {
    public static void main(String[] args) {
        GreenHeadDuck mGreenHeadDuck = new GreenHeadDuck(); //绿头鸭
        RedHeadDuck mRedHeadDuck = new RedHeadDuck();       //红头鸭
        
        //绿头鸭出场
        mGreenHeadDuck.display();
        mGreenHeadDuck.quack();
        mGreenHeadDuck.swim();
        
        //红头鸭出场
        mRedHeadDuck.display();
        mRedHeadDuck.quack();
        mRedHeadDuck.swim();
    }
}

程序输出结果如下:

绿头鸭嘎嘎叫和游泳,红头鸭嘎嘎叫和游泳。

**GreenHeadDuck**
--gaga--
--swimming--
**RedHeadDuck**
--gaga--
--swimming--

可见,我们通过程序成功模拟鸭子。
嘎嘎叫和游泳是所有鸭子的共同技能,但是鸭子的外观(种类)有可能不同。
所以,在超类 Duck 将 display 方法定义成抽象方法,由子类(不同鸭子)去实现 display 方法,实现了不同鸭子的差异化。

2.2 新的需求

前面我们实现了不同鸭子可以有不同的外观(绿头鸭、红头鸭),而且都能嘎嘎叫和游泳。现在我们有了新的需求,我们要让一些鸭子会飞

(1)按照前面的解决方案:像游泳这个技能一样,在超类 Duck 中增加会飞这个技能,这样继承自超类 Duck 的鸭子都会飞。

修改 Duck.java 代码如下:

/* Duck.java */

public abstract class Duck{
    public Duck(){
    
    }
    
    public abstract void display();     //外观
    
    public void quack(){                //嘎嘎叫
        System.out.println("--gaga--");
    }

    public void swim(){                 //游泳
        System.out.println("--swimming--");
    }
    
    /* 增加 fly 方法 */
    public void fly(){                  //飞
        System.out.println("--flying--");
    }
}

但是,这样会带来比较大的问题,继承自这个超类的鸭子都会飞

这也是继承存在的问题:对类的局部改动,尤其超类的局部改动,会影响其它部分

事实上,并不是所有的鸭子都会飞,所以我们能不能让有的鸭子会飞,有的鸭子不会飞呢。

(2)还是按照之前的思路来解决上面的问题:我们在子类中重写 fly 方法,比如绿头鸭不会飞,我们就在绿头鸭这个子类中重写 fly 方法。

修改 GreenHeadDuck.java 代码如下:

/* GreenHeadDuck.java */

public class GreenHeadDuck extends Duck{

    @Override
    public void display(){
        System.out.println("**GreenHeadDuck**");
    }
    
    /* 重写超类 Duck 中的 fly 方法 */
    @Override
    public void fly() {             //不会飞
        System.out.println("--can not fly--");  
    }

}

这样就可以让有些鸭子能飞,有些鸭子不能飞了。
遗憾的是,这样降低了代码的复用性
如果我们又新增一类鸭子,那是不是在子类上也重写 fly 方法呢?
显然,每次增加新的鸭子,我们可能就需要重写 fly 方法或其它方法,来实现不同鸭子的差异化

2.3 策略模式

在新的需求下,我们要让有的鸭子能飞,有的鸭子不能飞。
在 2.2 中,我们遗留了一些问题:

  1. 如果只在超类中定义新的 fly 方法,那么所有鸭子都会飞,不符合要求;
  2. 在超类定义 fly 方法,在子类重写 fly 方法。但是,每新增一个子类(新的鸭子)我们都要重写 fly 方法,代码复用性不高,而且操作繁琐。

有了策略模式,上面的问题不复存在。

(1)通过接口封装行为(算法族)

FlyBehavior 接口(算法族):飞行行为族

/* FlyBehavior.java */

public interface FlyBehavior {      // FlyBehavior 行为族
    void fly();
}

GoodFlyBehavior 类(算法实现类)实现 FlyBehavior 接口(算法族):会飞

/* GoodFlyBehavior.java */

public class GoodFlyBehavior implements FlyBehavior {   //会飞
    @Override
    public void fly() {
        System.out.println("--GoodFly--");  
    }
}

BadFlyBehavior 类(算法实现类)实现 FlyBehavior 接口(算法族):不会飞

/* BadFlyBehavior.java */

public class BadFlyBehavior implements FlyBehavior {    //不会飞
    @Override
    public void fly() {
        System.out.println("--BadFly--");   
    }
}

GoodFlyBehavior 、 BadFlyBehavior 类都实现了 FlyBehavior 接口。
我们通过 FlyBehavior 接口将飞行行为封装起来, FlyBehavior 就是我们所说的行为族(算法族)
GoodFlyBehavior 、 BadFlyBehavior 算法实现类是 FlyBehavior 行为族下的不同行为策略,分别对应会飞、不会飞。
GoodFlyBehavior 、 BadFlyBehavior 算法实现类可被用来实例化不同的鸭子,这样就可以让鸭子有不同的行为策略,即有的鸭子会飞,有的鸭子不会飞。

(2)修改超类 Duck

接下来,我们需要修改父类 Duck ,以符合策略模式。值得注意的是,以后我们新增鸭子时不需要修改父类 Duck 的代码,因为父类 Duck 已经按照策略模式封装好代码。

/* Duck.java */

public abstract class Duck{
    FlyBehavior mFlyBehavior;   //声明 FlyBehavior 接口的引用变量    
    
    public Duck(){
    
    }
    
    public void quack(){
        System.out.println("--gaga--");
    }

    public abstract void display();

    public void swim(){
        System.out.println("--swimming--");
    }
    
    //注意这里我们没有设定鸭子会不会飞,而是让 mFlyBehavior 去考虑
    public void fly(){
        mFlyBehavior.fly();     
    }
    
    //动态设置 FlyBehavior ,这里可根据 Duck 的子类来动态设置行为族
    public void SetFlyBehavior(FlyBehavior fb) {
        mFlyBehavior = fb;
    }
}

(3)在子类设定绿头鸭、红头鸭会不会飞

因为 GreenHeadDuck 和 RedHeadDuck 都继承了 Duck ,所以它们也继承了 FlyBehavior 类型的 mFlyBehavior 变量。
我们可以在它们的构造器中根据不同的飞行行为来实例化 mFlyBehavior。不同的 FlyBehavior 实例对应不同的飞行行为,比如 GoodFlyBehavior(会飞)、BadFlyBehavior(不会飞)。

/* GreenHeadDuck.java */

public class GreenHeadDuck extends Duck{
    public GreenHeadDuck() {
        mFlyBehavior = new GoodFlyBehavior();   //实例化为 GoodFlyBehavior(会飞)
    }
    
    @Override
    public void display(){
        System.out.println("**GreenHeadDuck**");
    }
}
/* RedHeadDuck.java */

public class RedHeadDuck extends Duck{
    public RedHeadDuck() {
        mFlyBehavior = new BadFlyBehavior();    //实例化为 BadFlyBehavior(不会飞)
    }
    
    @Override
    public void display(){
        System.out.println("**RedHeadDuck**");
    }
}

(4)测试

/* StimulateDuck.java */

public class StimulateDuck {
    public static void main(String[] args) {
        Duck mGreenHeadDuck = new GreenHeadDuck();  //绿头鸭
        Duck mRedHeadDuck = new RedHeadDuck();      //红头鸭
        
        //绿头鸭出场
        mGreenHeadDuck.display();
        mGreenHeadDuck.quack();
        mGreenHeadDuck.swim();
        mGreenHeadDuck.fly();
        
        //红头鸭出场
        mRedHeadDuck.display();
        mRedHeadDuck.quack();
        mRedHeadDuck.swim();
        mRedHeadDuck.fly();
        
        //前面在构造函数中定义鸭子会不会飞,这里动态的将绿头鸭从 GoodFly 变成 BadFly 
        mGreenHeadDuck.display();
        mGreenHeadDuck.SetFlyBehavior(new BadFlyBehavior());
        mGreenHeadDuck.fly();
    }

}

程序运行结果:

首先,绿头鸭和红头鸭都嘎嘎叫、游泳,但是绿头鸭会飞(GoodFly),红头鸭不会飞(BadFly)。

最后,我们动态的将绿头鸭从会飞设置成不会飞,所以绿头鸭输出了 BadFly 。

**GreenHeadDuck**
--gaga--
--swimming--
--GoodFly--
**RedHeadDuck**
--gaga--
--swimming--
--BadFly--
**GreenHeadDuck**
--BadFly--

这就是策略模式的解决思路。
通过封装行为族,超类不考虑具体的行为,而让子类去选择各自的行为。
这样,我们增加新的鸭子,超类的代码无需改动,子类也不需覆盖超类的代码,只需构造不同的实例即可实现子类的行为差异化


3 小结


策略模式的核心是对算法族的封装,将变化和不变化的部分分离。其针对接口编程(代码复用),并通过组合代替继承(增加灵活性)。

假设我们从父类继承并重写特定的方法,来实现子类行为的差异化。这样在每次新增加的子类很可能都需要重写这些方法(代码复用率低),而且不够灵活(有些子类并不需要父类的方法)。

有了策略模式,我们将这些差异化的算法实现提取出来,封装成不同的算法实现类(实现自算法族接口)。通过父类来使用算法族接口,而子类只需实例化不同的算法实现类,即可实现子类行为的差异化。这样的好处是在实现子类差异化的同时,不需要修改父类的代码,在增加新特性时只需增加具体的算法实现类(并实现算法族接口),然后在子类中通过 Setter 设置不同的算法实现类。


 
comments powered by Disqus