「补课」进行时:设计模式(19)——状态模式

762 阅读6分钟

1. 前文汇总

「补课」进行时:设计模式系列

2. LOL 中的状态

感觉我天天在用 LOL 举例子,没办法,都已经 S11 了,而我依然在玩这个游戏。

LOL 中的英雄有很多状态,有正常状态,有吃了伟哥一样的加速状态,有被对方套了虚弱的虚弱状态,还有被对方控制的眩晕状态。

下面来看下,在 LOL 中,初始的英雄状态:

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;
    //设置状态
    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(new Runnable() {
            public void run() {
                while (!runThread.isInterrupted()) {
                    try {
                        hero.run();
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        });
        System.out.println("--------------开始跑动---------------");
        runThread.start();
    }
    private boolean isRunning(){
        return runThread != null && !runThread.isInterrupted();
    }
    //英雄类开始奔跑
    private void run() throws InterruptedException{
        if (state == SPEED_UP) {
            System.out.println("--------------加速跑动---------------");
            Thread.sleep(2000);//假设加速持续2秒
            state = COMMON;
            System.out.println("------加速状态结束,变为正常状态------");
        }else if (state == SPEED_DOWN) {
            System.out.println("--------------减速跑动---------------");
            Thread.sleep(2000);//假设减速持续2秒
            state = COMMON;
            System.out.println("------减速状态结束,变为正常状态------");
        }else if (state == SWIM) {
            System.out.println("--------------不能跑动---------------");
            Thread.sleep(1000);//假设眩晕持续2秒
            state = COMMON;
            System.out.println("------眩晕状态结束,变为正常状态------");
        }else {
            //正常跑动则不打印内容
        }
    }
}

场景类:

public class Client {
    public static void main(String[] args) throws InterruptedException {
        Hero hero = new Hero();
        hero.startRun();
        hero.setState(Hero.SPEED_UP);
        Thread.sleep(2000);
        hero.setState(Hero.SPEED_DOWN);
        Thread.sleep(2000);
        hero.setState(Hero.SWIM);
        Thread.sleep(2000);
        hero.stopRun();
    }
}

可以看到,我们的英雄在跑动过程中随着状态的改变,我们的英雄会以不同的状态进行跑动。

但是问题也随之而来,我们的英雄类当中有明显的 if else 结构,这并不是我们希望看到的,接下来,我们看下状态模式。

3. 状态模式

3.1 定义

状态模式的定义如下:

Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.(当一个对象内在状态改变时允许其改变行为, 这个对象看起来像改变了其类。)

3.2 通用类图

  • State 抽象状态角色:接口或抽象类, 负责对象状态定义, 并且封装环境角色以实现状态切换。
  • ConcreteState 具体状态角色:每一个具体状态必须完成两个职责: 本状态的行为管理以及趋向状态处理, 通俗地说,就是本状态下要做的事情, 以及本状态如何过渡到其他状态。
  • Context 环境角色:定义客户端需要的接口, 并且负责具体状态的切换。

状态模式从类图上看比较简单,实际上还是比较复杂的,它提供了一种对物质运动的另一个观察视角, 通过状态变更促使行为的变化。

类似水的状态变更一样, 一碗水的初始状态是液态, 通过加热转变为、气态, 状态的改变同时也引起体积的扩大, 然后就产生了一个新的行为: 鸣笛或顶起壶盖,瓦特就是这么发明蒸汽机的。

3.3 通用代码:

抽象环境角色:

public abstract class State {
    // 定义一个环境角色,提供子类访问
    protected Context context;
    // 设置环境资源
    public void setContext(Context context) {
        this.context = context;
    }
    // 行为1
    abstract void handle1();
    // 行为2
    abstract void handle2();
}

具体环境角色:

public class ConcreteState1 extends State {
    @Override
    void handle1() {
        //本状态下必须处理的逻辑
    }

    @Override
    void handle2() {
        //设置当前状态为stat2
        super.context.setCurrentState(Context.STATE2);
        //过渡到state2状态, 由Context实现
        super.context.handle2();
    }
}

public class ConcreteState2 extends State {
    @Override
    void handle1() {
        //设置当前状态为stat2
        super.context.setCurrentState(Context.STATE1);
        //过渡到state2状态, 由Context实现
        super.context.handle1();
    }

    @Override
    void handle2() {
        // 本状态下必须处理的逻辑
    }
}

具体环境角色:

public class Context {
    final static State STATE1 = new ConcreteState1();
    final static State STATE2 = new ConcreteState2();

    private State concreteState;

    public State getCurrentState() {
        return concreteState;
    }
    //设置当前状态
    public void setCurrentState(State currentState) {
        this.concreteState = currentState;
        //切换状态
        this.concreteState.setContext(this);
    }
    public void handle1(){
        this.concreteState.handle1();
    }
    public void handle2(){
        this.concreteState.handle2();
    }
}

环境角色有两个不成文的约束:

  • 把状态对象声明为静态常量, 有几个状态对象就声明几个静态常量。
  • 环境角色具有状态抽象角色定义的所有行为, 具体执行使用委托方式。
public class Client {
    public static void main(String[] args) {
        //定义环境角色
        Context context = new Context();
        //初始化状态
        context.setCurrentState(new ConcreteState1());
        //行为执行
        context.handle1();
        context.handle2();
    }
}

这里我们已经隐藏了状态的变化过程, 它的切换引起了行为的变化。 对外来说, 我们只看到行为的发生改变, 而不用知道是状态变化引起的。

3.4 优点

  • 避免了过多的 if else 语句的使用,避免了程序的复杂性,提高系统的可维护性。
  • 使用多态代替了条件判断,这样我们代码的扩展性更强,比如要增加一些状态,会非常的容易。
  • 状态是可以被共享的,状态都是由 static final 进行修饰的。

3.5 缺点

有优点的同事也会产生缺点,有时候,优点和缺点的产生其实是同一个事实:

状态模式最主要的一个缺点是:子类会太多,也就是类膨胀。因为一个事物有很多个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理。

4. 案例完善

前面那个 LOL 的例子,如果使用状态模式重写一下,会是这样的:

首先创建一个跑动的接口:

public interface RunState {
    void run(Hero hero);
}

接下来是4个实现类,分别实现不同状态的跑动结果:

public class CommonState implements RunState {
    @Override
    public void run(Hero hero) {
        // 正常跑动则不打印内容,否则会刷屏
    }
}

public class SpeedUpState implements RunState{
    @Override
    public void run(Hero hero) {
        System.out.println("--------------加速跑动---------------");
        try {
            Thread.sleep(2000);//假设加速持续2秒
        } catch (InterruptedException e) {}
        hero.setState(Hero.COMMON);
        System.out.println("------加速状态结束,变为正常状态------");
    }
}

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

public class SwimState implements RunState {
    @Override
    public void run(Hero hero) {
        System.out.println("--------------不能跑动---------------");
        try {
            Thread.sleep(1000);//假设眩晕持续1秒
        } catch (InterruptedException e) {}
        hero.setState(Hero.COMMON);
        System.out.println("------眩晕状态结束,变为正常状态------");
    }
}

最后是一个 Hero(Context) 类:

public class Hero {
    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;//跑动线程
    //设置状态
    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 Hero hero = this;
        runThread = new Thread(new Runnable() {
            public void run() {
                while (!runThread.isInterrupted()) {
                    state.run(hero);
                }
            }
        });
        System.out.println("--------------开始跑动---------------");
        runThread.start();
    }

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

可以看到,这段代码和开头那段代码虽然完成了一样的功能,但是整个代码的复杂度缺以肉眼可见的级别提高了,一般而言,我们牺牲复杂性去换取的高可维护性和扩展性是相当值得的,除非增加了复杂性以后,对于后者的提升会乎其微。


文章持续更新,可以微信搜一搜「 极客挖掘机 」第一时间阅读,回复关键字有我准备的各种教程,欢迎阅读。