行为型模式——状态模式

2020-09-01  本文已影响0人  Doooook

定义

当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

核心要点

  1. 有一个对象,它是有状态的。
  2. 这个对象在状态不同的时候,行为不一样。
  3. 这些状态是可以切换的,而非毫无关系。

类图

image.png
Context:它就是那个含有状态的对象,它可以处理一些请求,这些请求最终产生的响应会与状态相关。
State:状态接口,它定义了每一个状态的行为集合,这些行为会在Context中得以使用。
ConcreteState:具体状态,实现相关行为的具体状态类。

如果针对于人的状态的例子来分析,那么人(Person)就是Context,状态接口依然是状态接口,而具体的状态类,则可以是睡觉、上班、休息,这一系列状态,每一个状态呈现出来的就是不同的行为实现。

应用实例

我们来试着写一个DOTA的例子,玩过的朋友都知道,DOTA里的英雄有很多状态,比如正常、眩晕、加速、减速等等。相信就算没有玩过DOTA的朋友们,在其它游戏里也能见到类似的情况。那么假设我们的DOTA没有使用状态模式,则我们的英雄类会非常复杂和难以维护,我们来看下,原始版的英雄类是怎样的。
首先我们用最常规的方式,即用if...else...的方式通过判断不同的状态而进行不同的行为动作。

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:05
 * @description: 英雄类
 */
public class Hero {

    /**
     * 正常状态
     */
    public static final int COMMON = 1;
    /**
     * 加速状态
     */
    public static final int SPEED_UP = 2;
    /**
     * 减速状态
     */
    public static final int SPEED_DOWN = 3;
    /**
     * 眩晕状态
     */
    public static final int SWIM = 4;
    /**
     * 默认是正常状态
     */
    private int state = COMMON;
    /**
     * 起跑线程
     */
    private Thread runThread;

    /**
     * 设置state
     *
     * @param state
     */
    public void setState(int state) {
        this.state = state;
    }

    /**
     * 停止跑动
     */
    public void stopRun() {
        if (isRunning()) {
            runThread.interrupt();
        }
        System.out.println("--------------停止跑动---------------");
    }

    public void startRun() {
        if (isRunning()) {
            return;
        }
        final Hero hero = this;
        runThread = new Thread(() -> {
            while (!runThread.isInterrupted()) {
                try {
                    hero.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        System.out.println("--------------开始跑动---------------");
        runThread.start();
    }

    /**
     * @return
     */
    private boolean isRunning() {
        return runThread != null && !runThread.isInterrupted();
    }

    private void run() throws InterruptedException {
        if (state == SPEED_UP) {
            System.out.println("--------------加速跑动---------------");
            // 假设加速持续4秒
            Thread.sleep(4000);
            state = COMMON;
            System.out.println("------加速状态结束,变为正常状态------");
        } else if (state == SPEED_DOWN) {
            System.out.println("--------------减速跑动---------------");
            // 假设减速持续4秒
            Thread.sleep(4000);
            state = COMMON;
            System.out.println("------减速状态结束,变为正常状态------");
        } else if (state == SWIM) {
            System.out.println("--------------不能跑动---------------");
            // 假设眩晕持续2秒
            Thread.sleep(2000);
            state = COMMON;
            System.out.println("------眩晕状态结束,变为正常状态------");
        } else {
            // 正常跑动则不打印内容,否则会刷屏
        }

    }

}

测试:

    /**
     * 行为型模式——状态模式
     */
    @Test
    public void testBehaviorState() throws InterruptedException {
        Hero hero = new Hero();
        hero.startRun();
        hero.setState(Hero.SPEED_UP);
        Thread.sleep(5000);
        hero.setState(Hero.SPEED_DOWN);
        Thread.sleep(5000);
        hero.setState(Hero.SWIM);
        Thread.sleep(5000);
        hero.stopRun();
    }

结果:

--------------开始跑动---------------
--------------加速跑动---------------
------加速状态结束,变为正常状态------
--------------减速跑动---------------
------减速状态结束,变为正常状态------
--------------不能跑动---------------
------眩晕状态结束,变为正常状态------
--------------停止跑动---------------

我们观察上面的写法,我们的英雄在跑动过程中随着状态的改变,会以不同的状态进行跑动。但是这种写法大量的if...else...,如果以后还要增加状态的话,会变得难以扩展和维护。
状态模式解决的问题:状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
状态模式是可以解决我们上面的if...else...结构的,我们采用状态模式,利用多态的特性可以消除掉if else结构。这样所带来的好处就是可以大大的增加程序的可维护性与扩展性。现在对代码进行重构。
定义一个状态接口,这个接口就只有一个方法,就是run

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:26
 * @description:
 */
public interface RunState {
    void run(HeroState hero);
}

与状态模式类图不同的是,我们加入了一个参数HeroState(Context),这样做的目的是为了具体的状态类当达到某一个条件的时候可以切换上下文的状态。下面列出四个具体的状态类,其实就是把if...else...拆掉放到这几个类的run方法中。
CommonState.java

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:27
 * @description: 正常状态
 */
public class CommonState implements RunState {
    @Override
    public void run(HeroState hero) {
        // 正常跑动则不打印内容,否则会刷屏
    }
}

SpeedUpState.java

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:27
 * @description: 加速状态
 */
public class SpeedUpState implements RunState {
    @Override
    public void run(HeroState hero) {
        System.out.println("--------------加速跑动---------------");
        try {
            // 假设加速持续4秒
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        hero.setState(HeroState.COMMON);
        System.out.println("------加速状态结束,变为正常状态------");
    }
}

SpeedDownState.java

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:28
 * @description: 减速状态
 */
public class SpeedDownState implements RunState {

    @Override
    public void run(HeroState hero) {
        System.out.println("--------------减速跑动---------------");
        try {
            // 假设减速持续4秒
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        hero.setState(HeroState.COMMON);
        System.out.println("------减速状态结束,变为正常状态------");
    }
}

SpeedUpState.java

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:27
 * @description: 加速状态
 */
public class SpeedUpState implements RunState {
    @Override
    public void run(HeroState hero) {
        System.out.println("--------------加速跑动---------------");
        try {
            // 假设加速持续4秒
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        hero.setState(HeroState.COMMON);
        System.out.println("------加速状态结束,变为正常状态------");
    }
}

英雄类稍作修改,最主要的改动就是那些if else可以删掉了,如下:

/**
 * @author: Jay Mitter
 * @date: 2020-09-01 22:05
 * @description: 英雄类
 */
public class HeroState {

    /**
     * 正常状态
     */
    public static final RunState COMMON = new CommonState();
    /**
     * 加速状态
     */
    public static final RunState SPEED_UP = new SpeedUpState();;
    /**
     * 减速状态
     */
    public static final RunState SPEED_DOWN = new SpeedDownState();
    /**
     * 眩晕状态
     */
    public static final RunState SWIM = new SwimState();;
    /**
     * 默认是正常状态
     */
    private RunState state = COMMON;
     /**
     * 起跑线程
     */
    private Thread runThread;

    /**
     * 设置state
     *
     * @param state
     */
    public void setState(RunState state) {
        this.state = state;
    }

    /**
     * 停止跑动
     */
    public void stopRun() {
        if (isRunning()) {
            runThread.interrupt();
        }
        System.out.println("--------------停止跑动---------------");
    }

    public void startRun() {
        if (isRunning()) {
            return;
        }
        final HeroState hero = this;
        runThread = new Thread(() -> {
            while (!runThread.isInterrupted()) {
                state.run(hero);
            }
        });
        System.out.println("--------------开始跑动---------------");
        runThread.start();
    }

    /**
     * @return
     */
    private boolean isRunning() {
        return runThread != null && !runThread.isInterrupted();
    }
}

可以看到,现在我们的英雄类优雅了许多,我们使用刚才同样的客户端运行即可得到同样的结果。

    /**
     * 行为型模式——状态模式2
     */
    @Test
    public void testBehaviorState2() throws InterruptedException {
        HeroState hero = new HeroState();
        hero.startRun();
        hero.setState(HeroState.SPEED_UP);
        Thread.sleep(5000);
        hero.setState(HeroState.SPEED_DOWN);
        Thread.sleep(5000);
        hero.setState(HeroState.SWIM);
        Thread.sleep(5000);
        hero.stopRun();
    }

结论

对比我们的原始例子,现在我们使用状态模式之后,有几个明显的优点:
一、我们去掉了if else结构,使得代码的可维护性更强,不易出错,这个优点挺明显,如果试图让你更改跑动的方法,是刚才的一堆if else好改,还是分成了若干个具体的状态类好改呢?答案是显而易见的。
二、使用多态代替了条件判断,这样我们代码的扩展性更强,比如要增加一些状态,假设有加速20%,加速10%,减速10%等等等(这并不是虚构,DOTA当中是真实存在这些状态的),会非常的容易。
三、状态是可以被共享的,这个在上面的例子当中有体现,看下Hero类当中的四个static final变量就知道了,因为状态类一般是没有自己的内部状态的,所有它只是一个具有行为的对象,因此是可以被共享的。
四、状态的转换更加简单安全,简单体现在状态的分割,因为我们把一堆if else分割成了若干个代码段分别放在几个具体的状态类当中,所以转换起来当然更简单,而且每次转换的时候我们只需要关注一个固定的状态到其他状态的转换。安全体现在类型安全,我们设置上下文的状态时,必须是状态接口的实现类,而不是原本的一个整数,这可以杜绝魔数以及不正确的状态码。

状态模式适用于某一个对象的行为取决于该对象的状态,并且该对象的状态会在运行时转换,又或者有很多的if else判断,而这些判断只是因为状态不同而不断的切换行为。

状态模式也有它的缺点,不过它的缺点和大多数模式相似,有两点。

  1. 会增加的类的数量。
  2. 使系统的复杂性增加。

尽管状态模式有着这样的缺点,但是往往我们牺牲复杂性去换取的高可维护性和扩展性是相当值得的,除非增加了复杂性以后,对于后者的提升会乎其微。

状态模式在项目当中也算是较经常会碰到的一个设计模式,但是通常情况下,我们还是在看到if else的情况下,对项目进行重构时使用,又或者你十分确定要做的项目会朝着状态模式发展,一般情况下,还是不建议在项目的初期使用。

参考:https://www.cnblogs.com/peterxiao/p/11127037.html

上一篇下一篇

猜你喜欢

热点阅读