设计模式:状态模式

2,731 阅读6分钟

基本概念

定义

State is a behavioral design pattern that lets an object alter its behavior when its internal state changes. It appears as if the object changed its class.

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

FSM

状态模式的概念和 FSM (有限状态机)类似。FSM 表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。在任何给定时刻,程序可以处于有限数量的状态。在任何唯一状态下,程序的行为都不同,并且可以将程序从一种状态切换到另一种状态。当然,根据当前状态,程序可能会或者不会切换到某些其他状态。这些称为过渡的切换规则也是有限的和预定的。

举例说明

iPhone 手机 home 键的功能取决于设备的当前状态:

  • 关机状态: 没有反应。
  • 开机后首次启动: 密码解锁。
  • 非首次启动: 密码解锁或者指纹解锁。
  • 启动后:返回主页面。

类图

状态模式建议为对象的所有可能状态创建新的类,并将所有只存在于特定状态的行为提取到这些类中。 原始对象称为上下文,而不是一个对象单独实现所有行为,上下文存储当前状态对象的引用,并将所有与状态相关的工作委托给该对象。

它的核心是封装,状态的变更引起了行为的变更,从外部看起来就好像这个对象对应的类发生了改变一样。状态模式的类图如图所示。

状态模式中的3个角色。

  • State——抽象状态角色

    接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。

  • ConcreteState——具体状态角色

    每一个具体状态必须完成两个职责:本状态的行为管理以及趋向状态处理,通俗地说,就是本状态下要做的事情,以及本状态如何过渡到其他状态。

  • Context——环境角色

    定义客户端需要的接口,并且负责具体状态的切换。

这种结构看起来类似于“策略”模式,但有一个关键的区别。在状态模式中,特定状态可能彼此了解,并开始从一个状态过渡到另一个状态,而策略几乎永远不会彼此了解。

代码示例 - 电梯

V 1.0

我们用程序来实现一个简单的电梯类,电梯有如下动作:开门、关门、运行、停止。先看类图设计:

电梯接口:

public interface ILift {
     public void open();
     public void close();
     public void run();
     public void stop();
}

电梯的实现类:

public class Lift implements ILift {
     public void close() {
        System.out.println("电梯门关闭...");
     }
     public void open() {
        System.out.println("电梯门开启...");
     }
     public void run() {
        System.out.println("电梯运行起来...");
     }
     public void stop() {
        System.out.println("电梯停止了...");
     }
}

场景类调用

public class Client {
     public static void main(String[] args) {
        ILift lift = new Lift();
        lift.open();
        lift.close();
        lift.run();
        lift.stop();
     }
}

V 2.0

为电梯的这4个动作的执行添加前置条件,具体点说就是在特定状态下才能做特定事。

  • 敞门状态

    电梯只能做的动作是关门动作。

  • 闭门状态

    可以进行的动作是:开门(我不想坐电梯了)、停止(忘记按路层号了)、运行。

  • 运行状态

    电梯只能做的是停止。

  • 停止状态

    电梯有两个可选动作:继续运行和开门动作。

在接口中定义4个常量,分别表示电梯的4个状态:敞门状态、闭门状态、运行状态、停止状态,然后在实现类中电梯的每一次动作发生都要对状态进行判断,判断是否可以执行。

电梯实现类:

public class Lift implements ILift {
     private int state;
     public void setState(int state) {
        this.state = state;
     }
     //电梯门关闭
     public void close() {
        //电梯在什么状态下才能关闭
        switch(this.state) {
            case OPENING_STATE:
                this.closeWithoutLogic();  
                this.setState(CLOSING_STATE);
                break;
            case CLOSING_STATE:  
                break;
            case RUNNING_STATE: 
                break;
            case STOPPING_STATE:  
                break;
        }
     }
     //电梯门开启
     public void open() {
        //电梯在什么状态才能开启
        switch(this.state){
            ... ...            
        }
     }
     //电梯开始运行起来
     public void run() {
        switch(this.state){
            ... ...    
        }
     }
     //电梯停止
     public void stop() {
        switch(this.state){
            ... ... 
        }
     }
}

一旦我们开始添加越来越多的状态和与状态相关的行为,基于条件判断的弱点便会暴露出来。大多数方法将包含糟糕的条件判断,这些条件会根据当前状态选择方法的正确行为。这样的代码很难维护,因为对转换逻辑的任何更改都可能需要在每种方法中更改状态条件。

随着项目的发展,这个问题会变得越来越大。在设计阶段很难预测所有可能的状态和转换。因此,随着时间的推移,这个类可能会变得臃肿。

V 3.0

如何用状态模式来解决这个问题?

抽象电梯代码:

public abstract class LiftState{
     protected Context context;
     public void setContext(Context _context){
        this.context = _context;
     }
     public abstract void open();
     public abstract void close();
     public abstract void run();
     public abstract void stop();
}

敞门状态类:

public class OpenningState extends LiftState {
     //开启当然可以关闭了,我就想测试一下电梯门开关功能
     @Override
     public void close() {
        //状态修改
        super.context.setLiftState(Context.closeingState);
        //动作委托为CloseState来执行
        super.context.getLiftState().close();
     }
     //打开电梯门
     @Override
     public void open() {
        System.out.println("电梯门开启...");
     }
     //门开着时电梯就运行跑,这电梯,吓死你!
     @Override
     public void run() {
        //do nothing;
     }
     //开门还不停止?
     public void stop() {
        //do nothing;
     }
}

关门状态类:

public class ClosingState extends LiftState {
     //电梯门关闭,这是关闭状态要实现的动作
     @Override
     public void close() {
        System.out.println("电梯门关闭...");
     }
     //电梯门关了再打开
     @Override
     public void open() {
        super.context.setLiftState(Context.openningState);  //置为敞门状态
        super.context.getLiftState().open();
     }
     //电梯门关了就运行,这是再正常不过了
     @Override
     public void run() {
        super.context.setLiftState(Context.runningState); //设置为运行状态
        super.context.getLiftState().run();
     }
     //电梯门关着,我就不按楼层
     @Override
     public void stop() {
        super.context.setLiftState(Context.stoppingState);  //设置为停止状态
        super.context.getLiftState().stop();
     }
}

上下文类

public class Context {
     //定义出所有的电梯状态
     public final static OpenningState openningState = new OpenningState();
     public final static ClosingState closeingState = new ClosingState();
     public final static RunningState runningState = new RunningState();
     public final static StoppingState stoppingState = new StoppingState();
     //定义一个当前电梯状态
     private LiftState liftState;
     public LiftState getLiftState() {
        return liftState;
     }
     public void setLiftState(LiftState liftState) {
        this.liftState = liftState;
        //把当前的环境通知到各个实现类中
        this.liftState.setContext(this);
     }
     public void open(){
        this.liftState.open();
     }
     public void close(){
        this.liftState.close();
     }
     public void run(){
        this.liftState.run();
     }
     public void stop(){
        this.liftState.stop();
     }
}

调用方:

public class Client {
     public static void main(String[] args) {
             Context context = new Context();
             context.setLiftState(new ClosingState());
             context.open();
             context.close();
             context.run();
             context.stop();
     }
}

总结

状态模式的优点

  1. 符合单一职责原则:将与特定状态相关的代码组织到单独的类中。

  2. 符合开放封闭原则:在不更改现有状态类或上下文的情况下引入新状态。

  3. 避免了过长的if 或者是switch判断。

状态模式的缺点

  1. 如果有很多种状态,子类会太多,类膨胀。一个事物有很多个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理。
  2. 如果类只有几个状态或很少更改,应用状态模式可能会过度设计。

参考:

  1. 设计模式之禅
  2. State