状态机在开发中的应用

简介

我们开发过程中会有各种不同状态,围绕这些状态之间的转换方式会有很多业务逻辑,依赖这些状态又会有更多的业务逻辑,如果这些逻辑代码不使用状态模式进行归类统一,会以各种难以理解和难以修改的方式存在各个地方。

本文也以银行账户为例,说明状态机是响应事件在状态之间的转换的,比如银行账户一开始是open状态:

  1. 在open状态时,银行账户可以响应close事件,并将状态切换到closed状态;
  2. 当在closed状态时,银行账户会对reopen事件响应,并切换到open状态;
  3. 当在open状态时,银行账户会响应扣款和存款的事件。

对于这段规则,我们如果直接根据功能要求进行设计编码也能实现需求,但是如果有新的动作导致新的状态,那么我们就会增加新的方法行为,这样增加下去代码会变得复杂,扩展性很差。

上代码

我们可以先提取通用状态机基类:

public abstract static class Flow {

    private String state; // 当前状态
    private static Map<String, List<String>> stateAction = new HashMap<>(); // 状态 -> 动作列表
    private static Map<String, String> actionStates = new HashMap<>(); // 动作 -> 目标状态

    public Flow(String state) {

        if(!stateAction.containsKey(state))
            throw new IllegalArgumentException("不存在的 state : " + state);
        this.state = state;
    }

    public static void initAction(String action, String from, String to) {

        if(!stateAction.containsKey(from)) stateAction.put(from, new ArrayList<String>());
        stateAction.get(from).add(action);
        actionStates.put(action, to);
    }

    public List<String> getActions() {

        return stateAction.containsKey(state) ? stateAction.get(state) : new ArrayList<>(0);
    }

    public void doAction(String action) {

        if(!cando(action))
            throw new IllegalArgumentException("不存在的 action : " + action);
        this.state = actionStates.get(action);
    }

    public boolean cando(String action) {

        return stateAction.get(state).contains(action);
    }

    public String getState() {

        return state;
    }

}

在基类中主要负责基础状态与动作的维护,提供当前状态查询,当前状态下的后续动作查询以及动作执行时的状态切换逻辑。

这个类的后续完善可以将静态代码部分初始化到数据库中,可以将动作分组分权限,实际就已经类似一个简版工作流了,此处不作阐述。

在此基础上编写 账户 类:

public static class Account extends Flow {

    static {
        initAction("close", "open", "closed");
        initAction("reopen", "closed", "open");
        initAction("deposit", "open", "open");
        initAction("withdraw", "open", "open");
    }

    private int balance = 0;

    public Account(String state) {

        super(state);
    }

    public void deposit(int amount) {

        if(cando("deposit")) this.balance += amount;
        else System.out.println("Can't deposit with illegal state :" + this.getState());
    }

    public void withdraw(int amount) {

        if(cando("withdraw")) this.balance -= amount;
        else System.out.println("Can't withdraw with illegal state :" + this.getState());
    }

    public int getBalance() {

        return this.balance;
    }

    public String toString() {

        return "当前状态 : " + this.getState() + ", 账号余额:" + balance + ", 可用操作:" + this.getActions();
    }
}

账户类中的静态代码块部分进行了基础状态和动作字典的数据初始化,函数部分实现了状态机之外的实体方法 存取钱查余额等。

测试代码:

public static void main(String[] args) {

    Account account = new Account("open");
    System.out.println("初始化:" + account);

    account.deposit(12);
    System.out.println("存钱:" + account);
    account.withdraw(3);
    System.out.println("取钱:" + account);

    account.doAction("close");
    account.deposit(12);
    System.out.println("关闭后存钱:" + account);
    account.withdraw(3);
    System.out.println("关闭后取钱:" + account);

    account.doAction("reopen");
    System.out.println("重新打开:" + account);
}

可以观测到日志:

初始化:当前状态 : open, 账号余额:0, 可用操作:[close, deposit, withdraw]
存钱:当前状态 : open, 账号余额:12, 可用操作:[close, deposit, withdraw]
取钱:当前状态 : open, 账号余额:9, 可用操作:[close, deposit, withdraw]
Can't deposit with illegal state :closed
关闭后存钱:当前状态 : closed, 账号余额:9, 可用操作:[reopen]
Can't withdraw with illegal state :closed
关闭后取钱:当前状态 : closed, 账号余额:9, 可用操作:[reopen]
重新打开:当前状态 : open, 账号余额:9, 可用操作:[close, deposit, withdraw]

All Done !

到此结束,通过这样重构,可以将状态和动作尽最大量解绑,不用到处判断状态,新加动作或者状态后也不需要到处查找引用。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注