深入浅出学设计模式(一)之 状态模式

状态模式概要

在日常的开发中,状态模式会经常用到,特别是对于一些流程性的功能,比如:订单系统,电子流审批系统等。接下来我将和大家一起来学习状态模式的定义与实际使用。

定义

状态模式的定义:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
如何理解这句话呢? 首先我们可以感觉到 这个对象必须是要有状态的,并且它的状态是可以改变的,还有就是当对象的状态改变时,它的行为(也就是它提供的方法)也会跟着变化。

接下来,我们可以举两个例子来感受一下这个定义。

例子 一:我们每个人在年龄上都有各个阶段,我们可以把不同的阶段试想成不同的状态。那么我们可以抽象出 4 个状态:小时候、青年、壮年和老年。小时候,由于我们的身体、心智都不够成熟,所以我们的行为大概有:在沙滩上玩泥巴、在幼儿园滑滑梯、或者晚上睡觉做梦拉尿。当我们再大一些到青年时,我们的身体、心智等都发育和成长后,我们的行为也会发生变化。我们的行为大概有:在球场打球、准备高考、谈恋爱等,当然,也不排除有的人依旧怀恋小时候在沙滩上玩泥巴的快乐时光,所以在青年时,他也在沙滩上玩泥巴。经过青年,我们将步入中年,大概的行为可能就是:陪孩子在沙滩上玩泥巴、认真工作养家等。最后老年的时候,可能在大树底下下象棋、打太极等。这个例子很好的展示人的一生(年龄维度)经历过的一些状态,在不同的年龄我们做不同的事情,有不同的行为,但是无论你在哪个阶段,你还是你,你这个对象依然不变,变的是你当前的状态和状态改变使你的行为发生改变。

例子二:我们举一个电梯的例子,电梯大概有:运行状态、停止状态。在运行状态电梯门肯定是关闭的并且肯定是不能开门的,不然人直接摔死了,所以在运行状态的行为可以有:选择楼层、停止运行。在停止状态,电梯未运行,那么可以进行开门、关门、运行和选楼层操作。我们需要理解的一点就是:在不同的状态,就只能有该状态该提供的行为,不能有其他的行为,就比如在电梯运行状态时,不能提供开门方法一样。在状态模式中,一个状态可以切换到另一个状态,运行状态可以切换到停止状态;停止状态可以切换到运行状态。

理解状态模式最重要的就是要清楚:不同的状态有不同的行为,完成一个行为后可以由当前状态切换到另一个状态。

状态模式解决的问题

如果我们要根据对象的不同状态来做不同的事情的时候,不用状态模式的话,我们可能会使用很多 if -else 来判断当前状态应该做什么。

public static void main(String[] args) {
       int state = 2;//订单当前状态

       if(state == 0){//初始状态
           //创建订单
       }else if(state == 1){//已创建
           //两个行为:商家接单、用户取消
       }else if(state == 2){//已接单
           //在商家接单后,该订单 的行为:商家取消、用户取消、商家发货
       }else if(state == 3){//已发货
           // 用户取消、用户收货
       }else if(state == 4){//已收货
           //行为:用户进行评价、用户退货
       }else{
           //等等
       }
   }

所以当状态达到一定数量后,if else 就会很多,对于代码的维护也变得很难,比如你想修改商家发货相关的业务逻辑,由于代码太多,有可能误改了商家接单部分的代码。使用状态模式就可以利用多态的特性将 if else 的判断逻辑分散到各个状态子类中,在某一个状态子类中只提供在该状态下所拥有的行为。

UML 图

uml 图示例

在这里插入图片描述
从类图中,我们可以看到有三个角色:
State:状态接口,该接口用于定义每一个状态的行为,将这些行为都抽象到该接口中。比如:电梯例子中,运行状态的行为:停止运行 和 选择楼层;停止状态的行为:开门、关门、选择楼层。
ConcreteState :具体的状态类, 主要实现某状态下应该有的行为。比如:电梯例子中的 RunStateStopState 就是 ConcreteState类,在这两个类中分别实现了各自的行为(见 uml 图)。一般情况下,使用一个抽象类 AbstractState 来实现 State 接口,然后再让 各个ConcreteState 类继承 AbstractState ,并重写对应的方法。
Context :上下文对象, 该类拥有一个具体状态类的引用,并为客户端提供相应的操作方法。该类的 state 变量用来存储当前的状态。

电梯示例

在前面,我们已经结合电梯的例子,画出了状态模式的 UML 图,接下来,我们使用代码来实现该例子,来实际应用一下状态模式。

State.java :主要用于定义行为集合:

public interface State {

   //选择楼层
   void chooseFloor();

   //关闭电梯门
   void closeDoor();

   //打开电梯门
   void openDoor();

   //使电梯运行
   void run();

   //使电梯停止运行
   void stop();
}

AbstractState.java : 主要用于给各个行为默认实现:
一般情况下,行为的默认实现就是抛出一些异常信息,用于告诉调用者当前状态不能调用该行为。
比如在电梯运行过程中,如果调用了 openDoor() 方法,就会给出提示信息 “现在不能打开电梯门”。

public abstract class AbstractState implements State {

    @Override
    public void chooseFloor() {
        System.out.println("现在不能选择楼层");
    }

    @Override
    public void closeDoor() {
        System.out.println("现在不能关闭电梯门");
    }

    @Override
    public void openDoor() {
        System.out.println("现在不能打开电梯门");
    }

    @Override
    public void run() {
        System.out.println("现在不能运行");
    }

    @Override
    public void stop() {
        System.out.println("现在不能停止");
    }
}

Context.java:上下文对象:主要用于向客户端类提供各种可调用的方法。
当调用该类中不同的方法时,currentState 将切换为不同的状态(运行状态、停止状态),达到同一个 context 对象,同一个方法却有不同的行为效果的作用。

public class Context {

    private State currentState;//当前状态

    public Context(){
        //开始时的默认状态为 停止状态
        currentState = new StopState(this);
    }

    public void chooseFloor() {
        currentState.chooseFloor();
    }

    public void closeDoor() {
        currentState.closeDoor();
    }

    public void openDoor() {
        currentState.openDoor();
    }

    public void run() {
        currentState.run();
    }

    public void stop() {
        currentState.stop();
    }

    public void setCurrentState(State currentState) {
        this.currentState = currentState;
    }
}

RunState.java:运行状态:提供一些只能在运行状态下才能调用的方法。

public class RunState extends AbstractState {

    private Context context;

    public RunState(Context context){
        this.context = context;
    }

    @Override
    public void chooseFloor() {
        System.out.println("选择了楼层");
        context.setCurrentState(new RunState(context));
    }

    @Override
    public void stop() {
        System.out.println("电梯停止");
        context.setCurrentState(new StopState(context));
    }
}

StopState.java:停止状态:提供一些只能在停止状态下才能调用的方法。

public class StopState extends AbstractState {

    private Context context;

    public StopState(Context context){
        this.context = context;
    }

    @Override
    public void chooseFloor() {
        System.out.println("选择了楼层");
        //在停止状态,选择楼层后,电梯将切换到运行状态
        context.setCurrentState(new RunState(context));
    }

    @Override
    public void closeDoor() {
        System.out.println("关闭电梯门");
        context.setCurrentState(new StopState(context));
    }

    @Override
    public void openDoor() {
        System.out.println("打开电梯门");
        context.setCurrentState(new StopState(context));
    }

    @Override
    public void run() {
        System.out.println("电梯运行");
        context.setCurrentState(new RunState(context));
    }
}

RunState 和 StopState 属于 ConcreteState ,即具体的实现类。继承自 AbstractState 类,并且各自只从父类重写在各自状态下才具有的行为。

ClientTest.java:测试类。
在测试类中,可以自定义一个流程:电梯开门、电梯关门、电梯运行、选择楼层、选择楼层、电梯停止、电梯开门。这个流程应当符合一个既定的业务流程规范,比如在电梯运行时,不能打开电梯门,否则就是错误的流程会提示错误信息。

1、正确流程:

public class ClientTest {

    public static void main(String[] args) {
        Context context = new Context();//创建上下文对象
        context.openDoor();//打开电梯门,用户进入电梯
        context.closeDoor();//用户关闭电梯门
        context.run();//电梯开始运行
        context.chooseFloor();//选择楼层
        context.chooseFloor();//选择楼层
        context.stop();//电梯停止
        context.openDoor();//电梯开门
    }
}

结果截图:
在这里插入图片描述
2、错误流程:

public static void main(String[] args) {
        Context context = new Context();//创建上下文对象
        context.openDoor();//打开电梯门,用户进入电梯
        context.closeDoor();//用户关闭电梯门
        context.run();//电梯开始运行
        context.chooseFloor();//选择楼层
        context.openDoor();
    }
   

结果截图:
在这里插入图片描述
调用 chooseFloor() 方法后,将 currentState 设置为了 RunState 。在 RunState 中没有重写 openDoor() 方法,是因为我们大家都知道在电梯运行时,电梯门不能打开的这个既定的业务流程,所以此处将会调用 AbstractState 类中的 openDoor() 。

对于不同的业务功能,它们的业务流程肯定也是不同的,具体什么业务要采用什么样的流程,是根据需求来决定的。

在这个电梯的例子中,简单的讨论了一下状态模式的使用,代码中有些地方也有问题,不过不影响用来理解状态模式的涵义。

订单系统示例

在订单系统中,一个订单从创建到完成将经历很多状态,例如:已创建、已接单、已发货等。现在,我们已一个简单的订单系统的示例来加深对状态模式的理解,只是列举了其中的一些状态和行为。

我们的订单系统简单抽象为如下的流程:

用户创建订单
商家接单
商家发货
用户收货
用户取消
用户取消
用户取消
开始状态
已创建
已接单
已发货
已收货
已取消

状态:
开始、已创建、已接单、已发货、已收货、已取消、已完成。

每个状态具有的行为:
开始状态:在该状态时,订单还未创建,所以只有一个行为:创建订单。
已创建:用户下单后,表示该订单为已创建状态,在已创建状态下,可以有商家接单操作和用户取消订单操作。。
已接单:商家发货操作、用户取消订单。
已发货:用户收货操作、用户取消订单。
在本例中,就先不考虑 已收货、已完成、已取消状态下的行为, 大家可以自己发挥一下。

经过上述分析,我们开始写代码。

State.java:定义订单系统的所有行为。

public interface State {

    //用户创建订单
    void create(Context context);

    //商家接单
    void take(Context context);

    //商家发货
    void send(Context context);

    //用户收货
    void receive(Context context);

    //用户取消订单
    void cancel(Context context);
}

AllConcreteState.java:所有的具体状态类。
在本例中,为简单起见,已取消、已收货状态被当做订单的最终状态,所以这两个状态不需要提供行为方法。

/**
 * 初始状态
 */
class StartState extends AbstractState{

    @Override
    public void create(Context context) {
        System.out.println("用户创建订单");
        //订单创建完成后,切换到 已创建 状态
        context.setCurrentState(new CreatedState());
    }

}

/**
 * 已创建 状态
 */
class CreatedState extends AbstractState{

    @Override
    public void take(Context context) {
        System.out.println("商家接单");
        //商家接单后,切换到 已接单 状态
        context.setCurrentState(new TokenState());
    }

    @Override
    public void cancel(Context context) {
        System.out.println("用户取消订单");
        //用户取消订单后, 切换到已取消状态
        context.setCurrentState(new CanceledState());
    }
}

/**
 * 已接单 状态
 */
class TokenState extends AbstractState{
    @Override
    public void send(Context context) {
        System.out.println("商家发货");
        //商家接单后,切换到 已发货 状态
        context.setCurrentState(new SentState());
    }

    @Override
    public void cancel(Context context) {
        System.out.println("用户取消订单");
        //用户取消订单后, 切换到已取消状态
        context.setCurrentState(new CanceledState());
    }
}

/**
 * 已发货状态
 */
class SentState extends AbstractState{
    @Override
    public void receive(Context context) {
        System.out.println("用户收货");
        //用户收货后,切换到 已收货状态
        context.setCurrentState(new ReceivedState());
    }

    @Override
    public void cancel(Context context) {
        System.out.println("用户取消订单");
        //用户取消订单后, 切换到已取消状态
        context.setCurrentState(new CanceledState());
    }
}

/**
 * 已收货 状态
 */
class ReceivedState extends AbstractState{
    //已收货状态为最终状态,没有任何行为
}

/**
 * 已取消 状态
 */
class CanceledState extends AbstractState{
    //已取消状态为最终状态,没有任何行为
}

Context.java:上下文类,向客户端提供操作订单的方法。

public class Context {

    //当前状态
    private AbstractState currentState;

    public Context(){
        //初始状态
        this.currentState = new StartState();
    }

    public void create() {
        currentState.create(this);
    }

    public void take() {
        currentState.take(this);
    }

    public void send() {
        currentState.send(this);
    }

    public void receive() {
        currentState.receive(this);
    }

    public void cancel() {
        currentState.cancel(this);
    }

    public void setCurrentState(AbstractState currentState) {
        this.currentState = currentState;
    }
}

OrderClientTest : 客户端测试类。

public class OrderClientTest {

    public static void main(String[] args) {
        Context context = new Context();

        context.create();//创建订单
        context.take();//接单
        //context.cancel();//取消订单
        context.send();//发货
        context.receive();//收货
    }

}

结果截图:
在这里插入图片描述
将测试类中的 注释行:context.cancel() 打开,当用户取消订单后,订单处于已取消状态,在取消状态下是不能进行发货操作的(在 CanceledState 中没有重写 AbstractState 的 send() 方法,所以就会调用 AbstractState 的 send() 方法),所以程序将抛出异常。
在这里插入图片描述

状态模式的优缺点

优点:

  • 体现了开闭原则和单一职责原则,每个状态都是一个子类,与单一职责原则高度符合,扩展状态只需增加子类,正是开闭原则的体现。
  • 枚举可能的状态,在枚举状态之前需要确定状态种类。

缺点:

  • 如果状态太多的话,会有很多个状态子类。
  • 在新增状态时,也会面临需要修改其他状态类的方法来切换到新状态的问题。

虽然状态模式也有这些缺点,但是我个人觉得使用状态模式会让我们对整个个任务流程有一个清晰的认识,并且在编写代码时,时时刻刻都能清晰的知道某个状态下所具有的行为。

示例代码参考 github

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值