贾哇技术指南

V1

2022/05/06阅读:32主题:默认主题

详解状态模式以及Spring状态机

详解状态模式以及Spring状态机

前言

讲设计模式之前我们先来了解下设计模式的SOLID原则:

  • S(Single Responsibility Principle):单一职责原则,接口职责应该单一,不要承担过多的职责。
  • O(Open Closed Principle):开闭原则,即对扩展开放,对修改关闭。简单来说就是代码的设计要达到:当别人要修改扩展功能的时候,最好能不要修改我们原有代码,而是新增代码来实现空能的扩展。这也是我们设计时要达到的目标。
  • L(Liskov Substitution Principle):里氏替换原则,子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
  • I(Interface Segregation Principle):接口隔离原则,类之间的依赖关系应该建立在最小的接口上,它不需要的接口不应该被依赖。
  • D(Dependence Inversion Principle):依赖倒置原则,应该依赖一个抽象的服务接口,而不是去依赖一个具体的服务,从依赖具体实现转向到依赖抽象接口,倒置过来。

1.什么是状态模式,它能解决什么问题?

状态模式是行为模式的一种,它是指:当一个对象在状态改变时允许改变其行为,也就是说它可以通过改变对象内部的状态来帮助对象控制自己的行为。

阿里巴巴《Java开发手册》中提到: 超过3层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式来实现。 Java开发手册

状态模式和策略模式很相似,它也能解决多层 if-else 嵌套的问题。但是它们的意图不太一样,策略模式会控制对象使用什么策略,而状态模式会自动改变状态。下面我用一个案例来介绍下状态模式。

2.状态模式实战

如果我们要开发oa系统里面的一个请假模块,功能很简单:员工可以提交请假单,领导来审批该请假单据是通过还是不通过。我们应该怎么开发这么功能呢? 首先我们创建一个请假单的实体类:

public class LeaveBill {

    /**
     * 请假单id
     */

    private long id;

    /**
     * 请假原因
     */

    private String reason;

    /**
     * 单据状态
     */

    private int state;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public LeaveBill(long id, String reason, int state) {
        this.id = id;
        this.reason = reason;
        this.state = state;
    }
}

下面来看请假单处理类:

    /**
     * 假装这是个数据库
     */

    Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1Lnew LeaveBill(1L"心情不好,申请请假一天", waitLeaderApproval));
        put(2Lnew LeaveBill(2L"家里有事,申请请假", waitLeaderApproval));
    }};

    /**
     * 等待领导审批
     */

    final int waitLeaderApproval = 1;

    /**
     * 审批成功,请假成功
     */

    final int approvalSucceeded = 2;

    /**
     * 万恶的领导不给批假,审批不通过
     */

    final int approvalFailed = 3;

    /**
     * 审批通过
     *
     * @param id 请假单id
     */

    public void pass(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitLeaderApproval) {
            doPass(leaveBill);
        } else if (state == approvalSucceeded) {
            // 已经审批过不做任何操作
            System.out.println("我什么都没做");
        } else if (state == approvalFailed) {
            System.out.println("已审批未通过,不能再次审批");
        }
    }

    /**
     * 审批不通过
     *
     * @param id 请假单id
     */

    public void failed(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitLeaderApproval) {
            doFailed(leaveBill);
        } else if (state == approvalSucceeded) {
            System.out.println("已经审批通过的单据还想再给我审批不通过,门也没有!");
        } else if (state == approvalFailed) {
            // 如果已经失败了,就不做操作
            System.out.println("我什么都没做");
        }
    }

    /**
     * 审批通过,不加任何条件判断
     *
     * @param leaveBill 请假单
     */

    public void doPass(LeaveBill leaveBill) {
        // 将状态设置为审批通过
        leaveBill.setState(approvalSucceeded);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    /**
     * 审批不通过,不加任何状态判断
     *
     * @param leaveBill 请假单
     */

    public void doFailed(LeaveBill leaveBill) {
        // 将状态设置为审批不通过
        leaveBill.setState(approvalFailed);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人审批未通过
        System.out.println("亲爱的xxx,你的请假单未审批通过,未通过原因为:xxx");
    }
}

为了看起来简单,这里我用一个 map 来作为数据库。

简单测试:

@Test
    public void test() {
        // 1.2 审批通过
        pass(1L);
        System.out.println("=========================================");
        // 2.2 审批未通过
        failed(1L);
    }

输出结果:

亲爱的xxx,你的请假单已审批通过
=========================================
已经审批通过的单据还想再给我审批不通过,门也没有!

比如说这个时候添加了一个需求,需要一个保存草稿的功能,就是请假单我可以先保存个草稿,下次再提交。那请假单是不是要多个待提交的状态呢?那就又要添加一堆 if-else 逻辑了。

更改后的代码如下:

public class LeaveService2 {
    /**
     * 假装这是个数据库
     */

    Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1Lnew LeaveBill(1L"心情不好,申请请假一天", waitLeaderApproval));
        put(2Lnew LeaveBill(2L"家里有事,申请请假", waitLeaderApproval));
    }};

    /**
     * 添加 待提交 状态
     */

    final int waitSubmit = 0;

    /**
     * 等待领导审批
     */

    final int waitLeaderApproval = 1;

    /**
     * 审批成功,请假成功
     */

    final int approvalSucceeded = 2;

    /**
     * 万恶的领导不给批假,审批未通过
     */

    final int approvalFailed = 3;

    /**
     * 新增提交方法
     */

    public void submit(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitSubmit) {
            doSubmit(leaveBill);
        } else if (state == waitLeaderApproval) {
            System.out.println("已提交审批的单据不能再次提交");
        } else if (state == approvalSucceeded) {
            System.out.println("已审批通过的单据不能再次提交");
        } else if (state == approvalFailed) {
            System.out.println("已审批未通过的单据不能再次提交");
        }
    }

    /**
     * 审批通过
     *
     * @param id 请假单id
     */

    public void pass(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitSubmit) {
            System.out.println("未提交的单据不能审批");
        } else if (state == waitLeaderApproval) {
            doPass(leaveBill);
        } else if (state == approvalSucceeded) {
            // 已经审批过不做任何操作
            System.out.println("我什么都没做");
        } else if (state == approvalFailed) {
            System.out.println("已审批未通过,不能再次审批");
        }
    }

    /**
     * 审批不通过
     *
     * @param id 请假单id
     */

    public void failed(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitSubmit) {
            System.out.println("未提交的单据不能审批");
        } else if (state == waitLeaderApproval) {
            doFailed(leaveBill);
        } else if (state == approvalSucceeded) {
            System.out.println("已经审批通过的单据还想再给我审批不通过,门也没有!");
        } else if (state == approvalFailed) {
            // 如果已经失败了,就不做操作
            System.out.println("我什么都没做");
        }
    }

    /**
     * 提交审批,不加任何条件判断
     *
     * @param leaveBill 请假单
     */

    public void doSubmit(LeaveBill leaveBill) {
        // 将状态设置为审批通过
        leaveBill.setState(waitLeaderApproval);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已提交成功");
    }
    
    /**
     * 审批通过,不加任何条件判断
     *
     * @param leaveBill 请假单
     */

    public void doPass(LeaveBill leaveBill) {
        // 将状态设置为审批通过
        leaveBill.setState(approvalSucceeded);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    /**
     * 审批不通过,不加任何状态判断
     *
     * @param leaveBill 请假单
     */

    public void doFailed(LeaveBill leaveBill) {
        // 将状态设置为审批不通过
        leaveBill.setState(approvalFailed);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人审批未通过
        System.out.println("亲爱的xxx,你的请假单未审批通过,未通过原因为:xxx");
    }

    @Test
    public void test() {
        // 1.2 审批通过
        pass(1L);
        System.out.println("=========================================");
        // 2.2 审批未通过
        failed(1L);
    }
}

新增了 waitSubmit 状态,新增了 submit 方法,每个方法里面都加了 waitSubmit 的判断逻辑。

那我上面写的这段代码有什么问题呢?你可以暂停下来思考一下。







上文代码缺点:

  1. 状态混乱,各种 if-else 判断,状态转换不明显
  2. 可扩展性差,如果加状态,又要在每个方法里面都加入if else逻辑,没有遵守开放-关闭原则(对扩展开放,对修改关闭)
  3. 没有把会改变的那部分代码包起来,没有遵守”封装变化“原则

那我们用状态模式我们应该怎么解决这个问题呢?我们应该把会变得那部分代码封装起来,将每一种状态都单独抽成一个类,将每个状态的行为都放在各自的类中,那么每个状态只需要实现它自己的动作就可以了,也就是将操作委托给当前状态的状态对象。这样我们添加新的状态时,添加一个对应的状态类,然后实现具体的操作就好了。这就是状态模式。

状态模式的UML图如下:

这里对其中的类做下解释:

  • State:可以是抽象类或者接口,在这个接口/抽象类 中定义每个动作的方法
  • ConcreteStateA、ConcreteStateB:State 具体的实现类,对应某一种状态的实现
  • Context:串联所有状态的封装类,封装的目的是为了让内部状态的变化不被调用类知晓(迪米特法则)

下面我按照状态模式来改造下代码:

首先建一个抽象类 LeaveState,里面包含了 Context 和所有方法。

由于这里面没有需要统一进行处理的操作,所以你将抽象类改为接口也是完全OK的

public abstract class LeaveState {

    protected Context context;

    public void setContext(Context context) {
        this.context = context;
    }

    public abstract void pass(long id);

    public abstract void failed(long id);
}

将所有的状态都抽成一个类,然后继承抽象类 LeaveState

待领导审批状态:

public class WaitLeaderApprovalState extends LeaveState {
    @Override
    public void pass(long id) {
        // 修改要处理的状态类
        super.context.setState(super.context.APPROVAL_SUCCEEDED);
        // 动作委派
        super.context.getState().pass(id);
    }

    @Override
    public void failed(long id) {
        // 修改要处理的状态类
        super.context.setState(super.context.APPROVAL_FAILED);
        // 委派给要处理的状态类
        super.context.getState().failed(id);
    }
}

审批成功状态:

public class ApprovalSucceededState extends LeaveState {

    @Override
    public void pass(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = super.context.database.get(id);
        // 将状态设置为审批通过
        leaveBill.setState(super.context.approvalSucceeded);
        // 更新数据库
        super.context.database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    @Override
    public void failed(long id) {
        System.out.println("已经审批通过的单据还想再给我审批不通过,门也没有!");
    }
}

审批失败状态:

public class ApprovalFailedState extends LeaveState {
    @Override
    public void pass(long id) {
        System.out.println("已审批未通过,不能再次审批");
    }

    @Override
    public void failed(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = super.context.database.get(id);
        // 将状态设置为审批未通过
        leaveBill.setState(super.context.approvalFailed);
        // 更新数据库
        super.context.database.put(leaveBill.getId(), leaveBill);
        System.out.println("您好,您的请假单审批未通过");
    }
}

状态上下文封装类:

public class Context {

    public Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1Lnew LeaveBill(1L"心情不好,申请请假一天", waitLeaderApproval));
        put(2Lnew LeaveBill(2L"家里有事,申请请假", waitLeaderApproval));
    }};

    /**
     * 等待领导审批
     */

    final int waitLeaderApproval = 1;

    /**
     * 审批成功,请假成功
     */

    final int approvalSucceeded = 2;

    /**
     * 万恶的领导不给批假,审批未通过
     */

    final int approvalFailed = 3;

    protected final WaitLeaderApprovalState WAIT_LEADER_APPROVAL = new WaitLeaderApprovalState();

    protected final ApprovalSucceededState APPROVAL_SUCCEEDED = new ApprovalSucceededState();

    protected final ApprovalFailedState APPROVAL_FAILED = new ApprovalFailedState();

    private LeaveState state;


    public LeaveState getState() {
        return state;
    }

    public void setState(LeaveState state) {
        this.state = state;
        // 将当前环境通知到各个类中
        this.state.setContext(this);
    }
}

这个类里面包含了所有的状态类,

测试:

@Test
public void test(){
    Context context = new Context();
    context.setState(context.WAIT_LEADER_APPROVAL);
    context.getState().pass(1L);
    context.getState().failed(1L);
}

结果:

亲爱的xxx,你的请假单已审批通过
已经审批通过的单据还想再给我审批不通过,门也没有!

那么此时,保存 草稿的需求来了,我们应该怎么改动呢?

  1. 在 LeaveState 中添加 submit() 方法
  2. 添加待提交的状态类 WaitSubmitState,继承 LeaveState 并实现里面的抽象方法。
  3. 给 LeaveState 所有的子类添加 submit() 方法的实现。
  4. Context 类中添加 WaitSubmitState 的依赖。

各个类改动的代码如下(为了看起来简洁,这里只贴做了新增的代码,原来的代码就不贴了):

LeaveState:

public abstract class LeaveState {
    // ...
    public abstract void submit(long id);
}

WaitSubmitState:

public class WaitSubmitState extends LeaveState {

    @Override
    public void submit(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = super.context.database.get(id);
        // 将状态设置为待领导审批
        leaveBill.setState(super.context.waitLeaderApproval);
        // 更新数据库
        super.context.database.put(leaveBill.getId(), leaveBill);
        super.context.setState(super.context.WAIT_LEADER_APPROVAL);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单提交审批");
    }

    @Override
    public void pass(long id) {
        System.out.println("请假单未提交不能审批");
    }

    @Override
    public void failed(long id) {
        System.out.println("请假单未提交不能审批");
    }
}

Context:

public class Context {
    /**
     * 待提交
     */

    final int waitSubmit = 0;

    protected final WaitSubmitState WAIT_SUBMIT = new WaitSubmitState();
}

WaitLeaderApprovalState:

public class WaitLeaderApprovalState extends LeaveState {
    @Override
    public void submit(long id) {
        System.out.println("待审批状态不能再次提交");
    }
}

ApprovalSucceededState:

public class ApprovalSucceededState extends LeaveState {

    @Override
    public void submit(long id) {
        System.out.println("已审批通过,不能提交");
    }
}

ApprovalFailedState:

public class ApprovalFailedState extends LeaveState {

    @Override
    public void submit(long id) {
        System.out.println("已审批失败,不能再次提交");
    }
}

我们做的这些操作基本都没有更改原来方法的逻辑,而是添加了新的方法,这就符合了开闭原则。以后再添加别的状态,比如 待经理审批待老板审批等都可以按这种方式来做改动。

通过上面的案例我们来总结下状态模式的优缺点:

优点是:状态模式将每个状态的行为都收敛到了它自己的类中,将容易产生问题的if语句删除,以方便日后的维护,并且代码更容易阅读和理解。让每一个状态“对修改关闭”,对扩展开放。 缺点:

  1. 创建的类多了,每个状态都要对应一个类。
  2. 新增的状态如果有对应的处理方法,则每个子类都要实现该处理方法。

3.状态模式和状态机

我们说的状态机一般是指有限状态机,它是一种计算机模型,它是指有限个状态之间的转换。而状态模式是一种设计模式,它们两个不是同一个东西, 很多文章经常都把这两个词混淆使用,也完全可以理解。因为当你要用状态模式实现一个功能的时候,这个功能结构肯定不适合被称为"状态模式",而更适合称为"状态机"。可以理解成状态机是状态模式的一种应用。

4.Spring StateMachine (Spring 状态机)

本节中我会使用 Spring StateMachine 来完成请假单审批的功能,你可以做个对比。

首先在 pom.xml 中引入 Spring StateMachine

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
</dependency>

将请假单状态定义为枚举:

public enum LeaveStateEnum {

    /**
     * 待提交
     */

    WAIT_SUBMIT,

    /**
     * 等待领导审批
     */

    WAIT_LEADER_APPROVAL,

    /**
     * 审批成功,请假成功
     */

    APPROVAL_SUCCEEDED,

    /**
     * 万恶的领导不给批假,审批未通过
     */

    APPROVAL_FAILED
}

将状态转换的动作定义为枚举类:

public enum LeaveStateEvent {
    /**
     * 提交审批
     */

    SUBMIT,

    /**
     * 审批通过
     */

    PASS,

    /**
     * 审批不通过
     */

    FAILED
}

状态机配置类:

@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<LeaveStateEnumLeaveStateEvent{

    @Override
    public void configure(StateMachineStateConfigurer<LeaveStateEnum, LeaveStateEvent> states) throws Exception {
        // 初始化状态
        states.withStates().initial(LeaveStateEnum.WAIT_SUBMIT).states(EnumSet.allOf(LeaveStateEnum.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<LeaveStateEnum, LeaveStateEvent> transitions) throws Exception {
        // 定义各个状态之间转换时对应的动作(事件)
        transitions.withExternal().source(LeaveStateEnum.WAIT_SUBMIT).target(LeaveStateEnum.WAIT_LEADER_APPROVAL).event(LeaveStateEvent.SUBMIT).and().withExternal().source(LeaveStateEnum.WAIT_LEADER_APPROVAL).target(LeaveStateEnum.APPROVAL_SUCCEEDED).event(LeaveStateEvent.PASS).and().withExternal().source(LeaveStateEnum.WAIT_LEADER_APPROVAL).target(LeaveStateEnum.APPROVAL_FAILED).event(LeaveStateEvent.FAILED);
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<LeaveStateEnum, LeaveStateEvent> config) throws Exception {
        // 设置状态机id
        config.withConfiguration().machineId("leaveStateMachine");
    }
}

通过 @EnableStateMachine 注解用来启用 Spring StateMachine 状态机功能。

配置状态机对应的动作处理方法:

@WithStateMachine(name = "leaveStateMachine")
@Configuration
public class StateMachineEventConfig {

    public Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1Lnew LeaveBill(1L"心情不好,申请请假一天", LeaveStateEnum.WAIT_SUBMIT.ordinal()));
        put(2Lnew LeaveBill(2L"家里有事,申请请假", LeaveStateEnum.WAIT_SUBMIT.ordinal()));
    }};

    @OnTransition(source = "WAIT_SUBMIT", target = "WAIT_LEADER_APPROVAL")
    public void submit(Message<LeaveStateEvent> msg) {
        Long leaveId = (Long) msg.getHeaders().get("leaveId");
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(leaveId);
        // 将状态设置为待领导审批
        leaveBill.setState(LeaveStateEnum.WAIT_LEADER_APPROVAL.ordinal());
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已提交审批");
    }

    @OnTransition(source = "WAIT_LEADER_APPROVAL", target = "APPROVAL_SUCCEEDED")
    public void pass(Message<LeaveStateEvent> msg) {
        Long leaveId = (Long) msg.getHeaders().get("leaveId");
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(leaveId);
        // 将状态设置为审批通过
        leaveBill.setState(LeaveStateEnum.APPROVAL_SUCCEEDED.ordinal());
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    @OnTransition(source = "WAIT_LEADER_APPROVAL", target = "APPROVAL_FAILED")
    public void failed(Message<LeaveStateEvent> msg) {
        Long leaveId = (Long) msg.getHeaders().get("leaveId");
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(leaveId);
        // 将状态设置为审批未通过
        leaveBill.setState(LeaveStateEnum.APPROVAL_FAILED.ordinal());
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        System.out.println("您好,您的请假单审批未通过");
    }
}

@OnTransition 中 source 指定原始状态,target 指定目标状态,当事件触发时将会被监听到从而调用该方法。

单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class StateTest 
{
    @Autowired
    private StateMachine<LeaveStateEnum, LeaveStateEvent> stateMachine;

    @Test
    public void test() throws Exception {
        stateMachine.start();
        stateMachine.sendEvent(MessageBuilder.withPayload(LeaveStateEvent.SUBMIT).setHeader("leaveId"1L).build());
        stateMachine.sendEvent(MessageBuilder.withPayload(LeaveStateEvent.PASS).setHeader("leaveId"1L).build());
    }
}

可以通过 Message来传递参数。

结果如下:

亲爱的xxx,你的请假单已提交审批
亲爱的xxx,你的请假单已审批通过

可以看到使用 Spring StateMachine 之后,我们只需要配置各个状态之间的流转以及对应的状态,就可以实现相应的功能了。在实际应用中,我们的系统一般不会只有一个状态机在运行,可以通过 StateMachineBuilder 来构建多个状态机,如果你有兴趣可以去Spring官网研究下,这里不再做赘述。

5.小结

本文通过请假审批的案例来讲解了状态模式:它可以通过改变对象内部的状态来帮助对象控制自己的行为。状态模式将每个状态的行为都收敛到了它自己的类中,将容易产生问题的if语句删除,以方便日后的维护,并且代码更容易阅读和理解。让每一个状态“对修改关闭”,对扩展开放。在项目中我们可以使用 Spring StateMachine 来帮助我们更简单的应用状态模式。当然,如果 Spring StateMachine 不能满足你的需求,你也可以根据状态模式的思想来自定义一个自己的状态机。

好了,这篇文章就到这里了,感谢大家的观看!如有错误,请及时指正!欢迎大家关注我的公众号:贾哇技术指南

参考

历史文章回顾

分布式事务解决方案汇总

Java各种内存溢出异常实践

详解JVM运行时数据区

Java线程启动流程

分类:

后端

标签:

后端

作者介绍

贾哇技术指南
V1