设计模式之策略模式(小鸭子的故事)

引言

当我们完成一个复杂的业务中常常会面临一个问题:针对一个对象的某个行为,不同的情境下有不同的处理方式;
就比如今天我要去上班,那么我需要以哪种交通方式去上班呢?可以有下面几种选择:

  • 步行
  • 公交
  • 地铁
  • 自行车
  • 开车

当然还会有更多的选择,这只是列举了几种;我上班时会在不同的情况下选择不同的交通工具,这就是不同处理方式;
如果在代码中体现,我们可以选择用 if…else 或者 switch 来对不同的情境进行判断,从而选择相应的交通工具;这样想当然很简单,但是写出来的代码会很复杂,而且后期进行维护是很难的;
那么就需要想一个办法将这个对象和这个行为分开,这个行为就是一个算法,这样对于修改维护只需要针对这个算法就可以了;

这里就需要用到设计模式中的策略模式;

策略模式定义:
策略模式(Strategy Pattern)中,定义算法族(策略组),分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户;

下面我将会用一个模拟鸭子的例子来介绍这个模式;

张三的问题

某公司有一个模拟鸭子的业务,这个业务中将会定义多种不同类型的鸭子,有的会飞,有的会叫,并且飞行方式和叫声可能也会有不同;现在这个业务交给了张三去做,张三以熟练的OO技术,设计了一个父类为Duck,然后让各种不同的鸭子去继承这个父类,便轻而易举的完成了这个项目;

类图:
在这里插入图片描述
可以看到,红头鸭和绿头鸭都继承了Dunk并重写了display方法,是不是很简单,我们看看代码:

Duck类:

// 鸭子类(抽象类)
public abstract class Duck {
    // 抽象方法:显示什么鸭子
    public abstract void display();
    // 飞行的方法
    public void fly() {
        System.out.println("I'm flying!!!");
    }
    // 叫的方法
    public void quack() {
        System.out.println("嘎嘎嘎");
    }
}
12345678910111213

MallardDuck类

// 绿头鸭
public class MallardDuck extends Duck{
    @Override
    public void display() {
        System.out.println("我是一只绿头鸭!!");
    }
}
1234567

RedheadDuck类

// 红头鸭
public class RedheadDuck extends Duck{
    @Override
    public void display() {
        System.out.println("我是一只红头鸭!!");
    }
}
1234567

输出测试一下:

// 测试相应的功能
public class MiniDuckSimulator {
    public static void main(String[] args) {
        Duck mallardDuck = new MallardDuck(); // 绿头鸭
        mallardDuck.display();
        mallardDuck.fly();
        mallardDuck.quack();
        System.out.println("-----------------");

        Duck redheadDuck = new RedheadDuck(); // 红头鸭
        redheadDuck.display();
        redheadDuck.fly();
        redheadDuck.quack();
        System.out.println("-----------------");
    }
}
12345678910111213141516

结果如下:

我是一只绿头鸭!!
I'm flying!!!
嘎嘎嘎
-----------------
我是一只红头鸭!!
I'm flying!!!
嘎嘎嘎
-----------------
12345678

就在张三沾沾自喜的时候,这时产品经理提出了新的要求:要加一种新的鸭子:橡皮鸭;
张三一想:橡皮鸭不能飞,而且橡皮鸭的叫声是“吱吱吱”,那么只要创建一个橡皮鸭的类,依然继承Dunk,然后重写Dunk中的fly和quack方法不久可以了嘛;
于是他又加了一个类:

RubberDuck类

// 橡皮鸭不会飞,叫声是吱吱吱,所以需要重写所有方法
public class RubberDuck extends Duck{
    @Override
    public void display() {
        System.out.println("我是一只橡皮鸭!!");
    }
    @Override
    public void fly() {
        System.out.println("I can't fly!!!");
    }
    @Override
    public void quack() {
        System.out.println("吱吱吱");
    }
}
123456789101112131415

这时张三突然意识到,这里使用继承好像并不是特别完美,虽然实现了功能,但是RubberDuck类把父类中的所有方法都重写了一遍,这样的代码就出现了重复啊;

张三又想:
Duck类中的fly和quack好像并不适用于所有鸭子的情况,有的鸭子不会飞,有的不会叫,有的叫声不一样,有的飞行方式不一样。。。。这样看来,这个父类并不是完美的;如果产品经理让我给鸭子多加一个游泳的行为,那么一旦加到Duck类中后,所有种类的鸭子可能都会面临修改的可能,这样也太麻烦了吧!那该怎么办呢?

这里总结一下张三通过继承来提供Duck行为存在的问题:

  • 代码在多个子类中重复
  • 运行时的行为不易改变
  • 很难知道不同的鸭子具体的行为
  • 改变会牵一发动全身,造成其他种类鸭子不需要的改变;

该怎么做?

这时张三突然想到:可以使用一个Flyable和一个Quackable接口,只让会飞的鸭子实现Flyable,会叫的鸭子实现Quackable接口不就可以了嘛;

真的可以吗?
这样的话就会重复的代码会更多,如果需要修改飞行的行为,那么就需要对所有实现飞行接口的代码进行修改;一旦需要加入新的行为,如果用接口,那就需要对所有的鸭子进行一个判断并实现该行为;
因为在这里张三只声明了三种类型的鸭子,如果是五十种呢?一百种呢?难道都需要一一修改吗?

其实出现这些问题的本质就是因为鸭子Duck的行为会在子类里不断地改变,并且如果让所有的子类都有这些行为也是不现实的;

且使用接口不能实现代码,就无法达到代码的复用,一旦修改了某个行为,就需要找到所有实现该行为的类去修改,不仅工作量更大,而且可能会出现新的错误;

这时李四给张三提建议:
只需要找到项目中可能需要变换的地方,并把这个变化独立出来,不和那些不会变化的代码混在一起不就可以了吗?

李四的意思其实就是:把Dunk中会变化的部分取出来,单独封装起来,这样就可以轻易实现更该和扩充该部分,且不会影响不会变化的内容;

那么下面张三需要做的就是:将鸭子变化的行为从Duck中取出封装起来了;

问题解决

张三对Duck进行一个分析:既然要分离变化的行为,那么在这个类中也就只有fly和quack行为会改变了,所以只需要把这俩行为拿出来然后封装起来就可以了;

这时又有了一个新的问题:
张三希望代码更有弹性,因为开始的代码没有弹性才这样做的,如果能够动态的改变鸭子的行为,那样一旦有需求改变肯定会容易很多;

张三灵机一动,想到了一个设计原则:
面向接口编程,而不是针对实现编程;

那么就是用接口来抽象这个行为,具体行为的表现模式实现这个接口就可以了;
所以张三准备了一个QuackBehavior接口和一个FlyBehavior接口,然后将他们聚合到Duck类中,这样就可以灵活的修改代码了;

由于产品经理提出了新的需求:增加一个不会飞不会叫的模型鸭,并且给它加一个火箭助力器,让它可以飞;
张三想:正好我在重新设计代码,不如就拿这个来试试代码,看看能不能达到预期要求;

张三先设计了QuackBehavior接口和FlyBehavior接口的类图
在这里插入图片描述
那么Dunk该怎么设计呢?我们可以让Dunk关联于这两个接口,这样就可以让Dunk类使用对应的方法了;
类图:
在这里插入图片描述

张三这次留了个心眼,为了能够实现运行时代码的动态拓展,所以加入了set方法,这样就可以随时随地的设置不同鸭子的行为了;
接下来就是实现代码了;

FlyBehavior接口

// 鸭子飞的接口
public interface FlyBehavior {
    public void fly();
}
1234

实现FlyBehavior接口:

// 用翅膀飞
public class FlyWithWings implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("I'm flying!!!");
    }
}
1234567
// 不能飞
public class FlyNoWay implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("I can't flying");
    }
}
1234567
// 火箭喷射器飞
public class FlyWithRocket implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("Fly with a rocket!!");
    }
}
1234567

QuackBehavior接口

// 鸭子叫的接口
public interface QuackBehavior {
    public void quack();
}
1234

实现QuackBehavior接口

// 鸭子嘎嘎叫
public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("嘎嘎嘎");
    }
}
1234567
// 橡皮鸭吱吱叫
public class Squeak implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("吱吱吱");
    }
}
1234567
// 不会叫
public class MuteQuack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("我不会叫");
    }
}
1234567

下面就是Duck类和具体不同种类的鸭子了
Dunk类

// 鸭子抽象类
public abstract class Duck {
    // Duck方法关联于FlyBehavior和QuackBehavior接口
    // 这两个接口同样聚合在Dunk类中
    private FlyBehavior flyBehavior; // 鸭子飞属性
    private QuackBehavior quackBehavior; // 鸭子叫属性

    // 抽象方法:显示什么鸭子
    public abstract void display();
    // 飞行的方法
    public void performFly() {
        flyBehavior.fly();
    }
    // 叫的方法
    public void performQuack() {
        quackBehavior.quack();
    }
    // 设置飞行的方法
    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }
    // 设置叫的方法
    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }
}
1234567891011121314151617181920212223242526
// 红头鸭
public class RedheadDuck extends Duck{
    public RedheadDuck() {
        this.setFlyBehavior(new FlyWithWings());
        this.setQuackBehavior(new Quack());
    }
    @Override
    public void display() {
        System.out.println("我是一只红头鸭!!");
    }
}
1234567891011
// 绿头鸭
public class MallardDuck extends Duck {
    // 默认构造方法,目的是给父类两个接口实例化对象
    public MallardDuck() {
        this.setFlyBehavior(new FlyWithWings()); // 用翅膀飞
        this.setQuackBehavior(new Quack()); // 嘎嘎叫
    }
    @Override
    public void display() {
        System.out.println("我是一只绿头鸭!!");
    }
}
123456789101112
// 橡皮鸭
public class RubberDuck extends Duck{
    public RubberDuck() {
        this.setFlyBehavior(new FlyNoWay()); // 不能飞
        this.setQuackBehavior(new Squeak()); // 吱吱叫
    }
    @Override
    public void display() {
        System.out.println("我是一只橡皮鸭!!");
    }
}
1234567891011
// 模型鸭
public class ModelDuck extends Duck{
    public ModelDuck() {
        this.setFlyBehavior(new FlyWithRocket()); // 火箭喷射器飞
        this.setQuackBehavior(new MuteQuack()); // 不会叫
    }
    @Override
    public void display() {
        System.out.println("我是一只模型鸭!!");
    }
}
1234567891011

终于实现了所有的功能,张三怀着忐忑写了一个测试代码:

// 测试系统
public class MiniDuckSimulator {
    public static void main(String[] args) {
        Duck mallardDuck = new MallardDuck(); // 绿头鸭
        mallardDuck.display();
        mallardDuck.performFly();
        mallardDuck.performQuack();
        System.out.println("-----------------");

        Duck redheadDuck = new RedheadDuck();
        redheadDuck.display();
        redheadDuck.performFly();
        redheadDuck.performQuack();
        System.out.println("-----------------");

        Duck rubberDuck = new RubberDuck();
        rubberDuck.display();
        rubberDuck.performFly();
        rubberDuck.performQuack();
        System.out.println("-----------------");

        Duck modelDuck = new ModelDuck();
        modelDuck.display();
        modelDuck.performFly();
        modelDuck.performQuack();
        modelDuck.setFlyBehavior(new FlyNoWay()); // 动态改变对象行为
        modelDuck.performFly();
    }
}
1234567891011121314151617181920212223242526272829

输出结果:

我是一只绿头鸭!!
I'm flying!!!
嘎嘎嘎
-----------------
我是一只红头鸭!!
I'm flying!!!
嘎嘎嘎
-----------------
我是一只橡皮鸭!!
I can't flying
吱吱吱
-----------------
我是一只模型鸭!!
Fly with a rocket!!
我不会叫
I can't flying
12345678910111213141516

第二版的代码完美的实现了所有的功能,并且代码的弹性和拓展性都很不错,张三想:升职加薪这不就稳稳地嘛;

李四这时说:这个代码就是用到了设计模式之一 ——策略模式,想要升职加薪,光会这一个设计模式可不行,后面的路还长着呢;

体会到了设计模式的好处,张三下定决心好好学习设计模式;

总结

上面张三的例子可以看出策略模式的好处

  • 不需要许多 if …else或者switch 判断语句
  • 代码可拓展性好
  • 符合开闭原则,便于维护

同样策略模式需要注意:每添加一个策略就要增加一个类,当策略过多是会导致策略类膨胀


其实这个例子中还用到了一个设计原则: 多用组合和聚合,少用泛化(继承)---

这里总结一下文中提到的 三种设计原则:* 封装变化的行为

  • 面向接口编程,不针对实现编程
  • 多用组合聚合,少用继承

当然这三个只是这里用到的,对于设计原则可不止这三种,后面会一 一介绍;

再重新看一下策略模式的定义:
策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理。
策略模式结构图:
在这里插入图片描述

其实策略模式在Java源码中也有体现,简单举个例子:Constructor就用到了策略模式,我们可以通过实现它来创造不同的排序规则,感兴趣可以看看源码体验一下;

当然一个例子不足以让你会用策略模式,想要真正的掌握还是需要大量的练习和实践,希望这篇文章能给你带来一些启发!

欢迎大家的点评!

最后修改:2021 年 10 月 23 日 03 : 57 PM
如果觉得我的文章对你有用,请随意赞赏