【一起学系列】之状态模式:你听过“流程”模式吗?

2,833 阅读5分钟

意图

允许一个对象在其内部状态改变时改变它的行为

说人话:允许对象在改变自身状态时候,更改绑定的特定方法

状态模式的诞生

【产品】:Hello,开发小哥,我们需要开发一款 娃娃机 ,你可以提前想想怎么设计它啦。

【开发】:娃娃机?我想想奥,它需要投币,用户移动,确认抓取,结束这几个动作,好像很好做欸,用一个变量维护它当前的阶段,然后写四个 if 语句就好啦。

【BOSS】:你准备用一个主方法,四个子方法配合 if 语句外加一个状态变量去做吗?

// 伪代码
public void handle() {
    if (flag == A) {
        a();
    }

    if (flag == B) {
        b();
    }
}

【开发】:对啊,老大,你真是我肚子里的蛔虫!

【BOSS】:蛔你个头,这样做  大错特错! ,你难道想对 投币口,按钮,摇杆都绑定同一个方法吗?

【开发】:对哦,它们应该是 不同的方法,同时暴露给用户,我再思考思考 

HeadFirst 核心代码

定义状态接口,同时封装变化,利用default关键字封装默认方法

public interface State {

    /** 投币 **/
    default void giveMoney() {
        System.out.println("无法投币");
    }

    /** 移动滑杆 **/
    default void move() {
        System.out.println("无法移动滑杆");
    }

    /** 抓取 **/
    default void grab() {
        System.out.println("无法抓取");
    }

    void changeState();
}

投币状态 状态的其中之一

public class MoneyState implements State{

    Context context;

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

    @Override
    public void giveMoney() {
        System.out.println("已投币!");
        changeState();
    }

    @Override
    public void changeState() {
        context.setExecute(new MoveState(context));
    }
}

为了尽量减少代码,只展示了其中一种状态,我们可以看到在 MoneyState 状态类执行所属的业务方法时,更改了上下文持有的状态类,这就产生了 状态的变更 ,同时上下文更加清晰,即:我只用考虑我下一个状态是什么

状态模式的设计思路:

  • Context 上下文环境,持有状态
  • State 状态顶层接口
  • ConcreteState 具体的状态

简单来说,

  1. 必须清晰的认识到共有多少种不同的状态,并通过接口定义其核心方法,封装变化
  2. 状态类持有 Context 上下文,在核心方法处理后更改其状态

如果看着有点模棱两可,建议看完本文后,访问专题设计模式开源项目,里面有具体的代码示例,链接在最下面

状态模式的关键

  • 明确所有可能发生的状态,及其转换关系
  • 明确状态模式中的各个状态是有可能同时暴露给用户的

就好像娃娃机运作的多种状态, 投币,移动摇杆,按下确认按钮等等可能不按先后顺序触发

整一个 “流程” 模式

每个状态的方法名都一样会如何?

上文中我们大概知道了状态模式的特点,把状态封装成类,在调用状态-核心方法时候更改其状态本身,此时考虑的多种状态方法名可能各不相同,假设我们都起一样的名字会如何?

我们会首先遇到一个问题,我们无法得知它需要调用几次方法(因为可能有重复性 A - B 的情况),但如果无限循环,在适当的地方控制其结束点,和是否继续执行的标识,好像就可以解决了。

来一个流程案例

简单描述下即:开始处理订单

  • 正常则进入成功状态,入库,结束执行
  • 失败则进入失败状态,检测是否重新执行,扭转状态为处理订单

上代码

Context 上下文

public class Context {

    /**
     * 最大执行次数
     */
    public static final Integer FAIL_NUM = 3;

    /***
     * 失败次数
     */
    private int failNum;

    /**
     * 是否继续执行的标识
     */
    private boolean isAbandon;

    /***
     * 当前状态
     */
    private StateInterface stateInterface;

    public Context() {
        this.stateInterface = new HandleOrder();
        this.failNum = 1;
        this.isAbandon = false;
    }

    /***
     * 处理方法
     */
    public void handle () {
        stateInterface.doAction(this);
    }
    
    // 省略无用代码...
}

处理订单状态

public class HandleOrder implements StateInterface {

    @Override
    public void doAction(Context context) {
        printCurrentState();

        // do somethings
        int num = (int) (Math.random() * 11);
        if (num >= 8) {
            System.out.println("处理订单完成, 进入成功状态...");
            context.setStateInterface(new SuccessOrder());
        } else {
            System.out.println("处理订单失败, 进入失败状态...");
            context.setStateInterface(new FailOrder());
        }

        CodeUtils.spilt();
    }

    @Override
    public StateEnums getCurrentState() {
        return StateEnums.HANDLE_ORDER;
    }
}

客户端调用方法

public class App {
    
    public static void main(String[] args) {
        // 模拟从队列中取任务按流程循环执行
        Context context = new Context();
        while (true) {

            // 校验是否为废弃 | 已完成任务
            if (context.isAbandon()) {
                System.out.println("此条任务不再执行... ");
                break;
            }
            
            context.handle();
        }
    }
}

测试结果输出:

# 当前状态:订单处理
# 处理订单失败, 进入失败状态...
# ------------------------

# 当前状态:处理订单失败
# 订单处理失败... 当前执行次数: 1
# ------------------------

# 当前状态:订单处理
# 处理订单失败, 进入失败状态...
# ------------------------

# 当前状态:处理订单失败
# 订单处理失败... 当前执行次数: 2
# ------------------------

# 当前状态:订单处理
# 处理订单完成, 进入成功状态...
# ------------------------

# 当前状态:处理订单成功
# 订单处理完成 -> 进入入库逻辑...
# 入库处理完成
# ------------------------

# 此条任务不再执行... 

如果看着有点模棱两可,建议看完本文后,访问专题设计模式开源项目,里面有具体的代码示例,链接在最下面

“流程” 模式适用的场景

在这样的设计中,与其说是状态的变更,不如说是 “流程” 的变更更为贴切,因此它可以作为诸多后台任务的解决方案,尤其是面临很多业务流程场景时,可以极大的提高代码的可维护性: 我只用考虑和我有关的 “流程”

遵循的设计原则

  • 封装变化:在父级接口中提供 default 方法,子类实现其对应的状态方法即可
  • 多用组合,少用继承:状态模式经常和策略模式做对比,它们都是利用组合而非继承增强其变化和能力

什么场景适合使用状态模式

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变其行为
  • 一个操作中含有庞大的多分支条件语句,且这些分支依赖于该对象的状态

最后

附上GOF一书中对于状态模式的UML图:

相关代码链接

GitHub地址

  • 兼顾了《HeadFirst》以及《GOF》两本经典书籍中的案例
  • 提供了友好的阅读指导