设计模式之策略模式

2020-08-01   1,109 次阅读


设计模式之策略模式

在了解了设计模式的概念后,下面我们对设计模式中的“策略模式”进行详细讲解。

诞生缘由

对象的某些行为方式,多种多样,而不同的对象可能又拥有相同的行为方式。为了适应对象行为方式的多变性与复用性,“策略模式”诞生了,
它将对象与对象的行为方式相互独立,任何对象都可以使用已经封装好的行为方式,从而达到代码的复用性。
当某一种行为方式需要更改的时候,它的变化是独立于调用它的对象,不需要更改对象类的代码;
当有新的对象需要新的行为方式的时候,可以在不改动原来已有的行为的前提下为新对象扩展新的行为方式,该行为方式也可以被其他任意对象使用。

啊....哦.....好像不太明白?别担心,这纯属正常。

这么说吧,人的行走方式,小宝宝的时候行走是靠“爬”;刚会走路时候,行走是两条腿“跌跌撞撞,还时不时摔个跟头”;青少年的时候,行走是“稳定,随风奔跑”;
年老后,行走是“与蜗牛同行,弯腰驼背亦感累”。都是行走,但是一个人不同阶段,他的行走方式是不一样。

“策略模式”就是要解决人们拥有可变性的行为。

大部分宝宝都是靠爬,如果创建一批对象,假设都是宝宝阶段,为每个对象都设置“爬”的行走方式,那代码的重复率就很高。

“策略模式”就是要解决这一群“爬”的行为,让这个行为的代码进行复用。

在“策略模式”中,以上的行为方式,我们称之为“算法”。下面给出策略模式的定义:

定义

为对象定义一系列行为(算法),并对算法进行封装,对象可以随意切换自己的行为方式(切换策略),策略模式让算法的变化独立于调用该算法的对象。

通关文牒

或许你现在对策略模式有一些初步的认识了,那就带上通关文牒当一次唐僧吧!不取经不罢休,要做一个知其然而知其所以然的“佛”。本次关卡如下:

  • 如何抽取算法来适应多变性?
  • 如何封装算法使得代码复用?

背景

A公司打算上市一款叫“欢乐汪汪队”的游戏,目前该游戏只有2名成员,斑点汪与泰迪汪。
斑点汪与泰迪汪都属于狗狗类,具有狗狗的一些特性,都会叫,会摇尾巴,还有自己独特的外观。

此系统使用了Java标准的OO技术,对所有的狗狗进行了超类的封装,代码如下:

/**
 *  所有狗狗的父类
 */
public abstract class Dog {
    public void bark(){
        System.out.println("汪汪汪");
    }

    public void wagging(){
        System.out.println("摇尾巴");
    }

    // 外观,由于不同的狗狗,外观不一样,所以需要子类自行去实现
    public abstract void display();
}
/**
 * 斑点汪
 */
public class SpeckleDog extends Dog {
    @Override
    public void display() {
        System.out.println("我是斑点汪,外观是浑身有斑点");
    }
}
/**
 * 泰迪汪
 */
public class PoodleDog extends Dog {
    @Override
    public void display() {
        System.out.println("我是泰迪汪,外观是小巧可爱长不大");
    }
}

增加需求

现在,公司产品决定给狗狗加上狗刨式游泳的功能,用于游戏中进行狗狗游泳比拼。这还不简单??键盘伺候!!!

我们只需要在所以狗狗的父类中加一个游泳的方法就可以了,这样继承了该类的所有子类都具备了游泳的功能。

/**
 *  所有狗狗的父类
 */
public abstract class Dog {
    public void bark(){
        System.out.println("汪汪汪");
    }

    public void wagging(){
        System.out.println("摇尾巴");
    }

    // 外观,由于不同的狗狗,外观不一样,所以需要子类自行去实现
    public abstract void display();
    
    // 本次新增方法,游泳
    public void swim(){
       System.out.println("狗刨式游泳");
    }
}

完美解决,就在大家游戏开始进行游泳比拼的时候,一个用户的汪汪竟然挂掉了,检查发现,公司不知道何时上了一只柯基汪,该狗狗由于腿短屁股大,不能进行游泳,下水没几下就会挂掉。

问题引出

接下来该怎么办呢?要不让柯基汪覆盖父类的swim方法,变成在岸边呆着,不参加游泳,给其他狗狗鼓掌加油就好。
貌似没问题,可是如果后期公司又上橡皮狗,会游泳,会叫但是不会摇尾巴。该怎么办呢?

橡皮狗,会游泳,会叫但是不会摇尾巴;电子狗会叫,会摇尾巴,但是不会游泳

大家第一反应是不是想到了Java的接口,我们可以把游泳这个行为设计成“Swimable接口”,
同样也本可以把摇尾巴这个动作设计成“Waggingable接口”,因为游戏里的所有狗狗不一定都会摇尾巴。

欲哭无泪

不改不知道啊,这真是一个超级笨的主意,这么一来,重复的代码很多。因为接口没有具体的实现方法,无法达到代码复用的目的,
就是说如果想要修改一下游泳,你必须进到每个实现类里面去修改,如果你认为覆盖几个方法就算差劲,
那么对于30个狗狗让你都要修改一下游泳这个行为,你又怎么处理?

我们了解到了,并非所有狗狗都会游泳与摇尾巴,所以继承并不是适当的解决方式。改成接口,可以解决“一部分”问题(不会出现会游泳的柯基汪),但是却造成代码无法复用。
这只能算是刚出了狼山又入了虎口吧。

撸起袖子加油干

别担心,方法总比困难多。我们已经知道继承并不是很好的处理方式,因为狗狗的功能是变化的,那我们把不变的功能抽离到父类里面去肯定是没问题的,对于变化的功能,我们发现接口并不是很好的处理方式,因为接口无法达到代码复用的目的,那我们就想办法让它被复用起来。

设计模式的精神:把不变的行为进行混合,把变化的行为进行独立。

独立变化的行为

我们先解决上面接口的代码无法复用的问题,针对游泳与摇尾巴,我们建立两个接口Swimable与Waggingable,并且为其定义实现类。

游泳行为的接口

/**
 * 游泳行为的接口
 */
public interface Swimable {
    void swim();
}

以下是几种游泳方式的具体实现:

/**
 * 狗刨式游泳
 */
public class SwimDoggyPaddle implements Swimable {
    @Override
    public void swim() {
        System.out.println("狗刨式游泳");
    }
}
/**
 * 自由泳式的游泳
 */
public class SwimFreeStyle implements Swimable {
    @Override
    public void swim() {
        System.out.println("自由泳式的游泳");
    }
}
/**
 * 不会游泳的狗狗
 */
public class SwimNoWay implements Swimable {
    @Override
    public void swim() {
        System.out.println("我不会游泳,我就站在水边为你们加油鼓掌");
    }
}

摇尾巴的接口:

/**
 * 摇尾巴行为的接口
 */
public interface Waggingable {
    void wagging();
}

以下是摇尾巴行为的具体实现

/**
 * 左右摇尾巴
 */
public class WaggingLeftAndRight implements Waggingable {
    @Override
    public void wagging() {
        System.out.println("左右摇摆自己的尾巴");
    }
}
/**
 * 不会摇尾巴的狗狗
 */
public class WaggingNoWay implements Waggingable {
    @Override
    public void wagging() {
        System.out.println("我不会摇尾巴");
    }
}

行为已经有了,具体的实现代码也有了,不过好像和我们的狗狗还没有任何关系,我们怎么让代码复用起来呢?修改父类:

混合不变的行为

/**
 *  所有狗狗的父类
 */
public abstract class Dog {
    // 声明两个成员变量
    private Waggingable waggingable;    // 摇尾巴行为
    private Swimable swimable;          // 游泳行为

    public void bark(){
        System.out.println("汪汪汪");
    }

    // 调用接口的摇尾巴行为
    public void wagging(){
        waggingable.wagging();
    }

    // 外观,由于不同的狗狗,外观不一样,所以需要子类自行去实现
    public abstract void display();

    // 调用接口的游泳行为
    public void swim(){
        swimable.swim();
    }
}

看看斑点汪的处理:

/**
 * 斑点汪
 */
public class SpeckleDog extends Dog {
    public SpeckleDog(){
        this.swimable = new SwimDoggyPaddle();  // 狗刨式的游泳
        this.waggingable = new WaggingLeftAndRight();   // 左右摇摆尾巴
    }
    @Override
    public void display() {
        System.out.println("我是斑点汪,浑身有斑点");
    }
}

柯基汪的处理:

/**
 * 柯基汪
 */
public class CokeyDog extends Dog {
    public CokeyDog(){
        this.swimable = new SwimNoWay();  // 不会游泳
        this.waggingable = new WaggingLeftAndRight();   // 左右摇摆尾巴
    }
    @Override
    public void display() {
        System.out.println("我是柯基汪,腿短屁股大");
    }
}

好了,现在任凭公司产品怎么变,哪天想把尾巴试用转圈圈的方式摇起来都没问题,这种天马行空之旅就交给公司吧!我们已经处理好了后期的应变之策。

总结

案例解析

本例中游泳和摇尾巴的方式,我们可以理解为不同的算法,将其封装后,任何对象(狗)都可以使用和替换这些算法。当需要其他方式的游泳或者摇尾巴
的行为时,我们只需要新增或者修改算法类,而不用去动到使用者本身的代码。

适用场景

“策略模式”适用于对象的行为经常会发生未知的变化,这时候需要将这个行为抽离出来进行封装,让它的改变不影响调用它的对象本身。
比如超市做优惠活动,五一节可能打五折,国庆节打八折,春节既打折还是送礼品,
这样的优惠活动的折扣算法经常会发生变化,可以采用策略模式,不同节日采用不同的策略。

优缺点

  • 优点:很好地适应了对象行为方式的多变,算法的变化独立于调用它的对象,大大系统的降低了耦合;对算法进行封装,使用算法时采用组合的方式,大大提高了代码的复用性。
  • 缺点:策略模式侧重于策略的切换,对算法的延伸性不是很友好,假设需要2种比较类似的算法,为了不影响之前的算法,需要新增一个新的算法。当侧重于算法本身的延伸性时,可以考虑设计模式中的“模板方法模式”。

本文的“策略模式”只是设计模式的入门,仅仅这一种模式就可以让我们清晰地认识到学好设计模式的重要性,加油,努力让自己写出更加优秀的代码。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议