讲设计模式之前我们先来了解下设计模式的SOLID
原则:
状态模式是行为模式的一种,它是指:当一个对象在状态改变时允许改变其行为,也就是说它可以通过改变对象内部的状态来帮助对象控制自己的行为。
阿里巴巴《Java开发手册》中提到:
超过3层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式来实现。
状态模式和策略模式很相似,它也能解决多层 if-else
嵌套的问题。但是它们的意图不太一样,策略模式会控制对象使用什么策略,而状态模式会自动改变状态。下面我用一个案例来介绍下状态模式。
如果我们要开发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(1L, new LeaveBill(1L, "心情不好,申请请假一天", waitLeaderApproval));
put(2L, new 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(1L, new LeaveBill(1L, "心情不好,申请请假一天", waitLeaderApproval));
put(2L, new 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
的判断逻辑。
那我上面写的这段代码有什么问题呢?你可以暂停下来思考一下。
上文代码缺点:
那我们用状态模式我们应该怎么解决这个问题呢?我们应该把会变得那部分代码封装起来,将每一种状态都单独抽成一个类,将每个状态的行为都放在各自的类中,那么每个状态只需要实现它自己的动作就可以了,也就是将操作委托给当前状态的状态对象。这样我们添加新的状态时,添加一个对应的状态类,然后实现具体的操作就好了。这就是状态模式。
这里对其中的类做下解释:
下面我按照状态模式来改造下代码:
首先建一个抽象类 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(1L, new LeaveBill(1L, "心情不好,申请请假一天", waitLeaderApproval));
put(2L, new 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,你的请假单已审批通过
已经审批通过的单据还想再给我审批不通过,门也没有!
那么此时,保存
草稿的需求来了,我们应该怎么改动呢?
各个类改动的代码如下(为了看起来简洁,这里只贴做了新增的代码,原来的代码就不贴了):
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语句删除,以方便日后的维护,并且代码更容易阅读和理解。让每一个状态“对修改关闭”,对扩展开放。
缺点:
我们说的状态机一般是指有限状态机,它是一种计算机模型,它是指有限个状态之间的转换。而状态模式是一种设计模式,它们两个不是同一个东西,
很多文章经常都把这两个词混淆使用,也完全可以理解。因为当你要用状态模式实现一个功能的时候,这个功能结构肯定不适合被称为"状态模式",而更适合称为"状态机"。可以理解成状态机是状态模式的一种应用。
本节中我会使用 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<LeaveStateEnum, LeaveStateEvent> {
@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(1L, new LeaveBill(1L, "心情不好,申请请假一天", LeaveStateEnum.WAIT_SUBMIT.ordinal()));
put(2L, new 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官网研究下,这里不再做赘述。
本文通过请假审批的案例来讲解了状态模式:它可以通过改变对象内部的状态来帮助对象控制自己的行为。状态模式将每个状态的行为都收敛到了它自己的类中,将容易产生问题的if语句删除,以方便日后的维护,并且代码更容易阅读和理解。让每一个状态“对修改关闭”,对扩展开放。在项目中我们可以使用 Spring StateMachine
来帮助我们更简单的应用状态模式。当然,如果 Spring StateMachine
不能满足你的需求,你也可以根据状态模式的思想来自定义一个自己的状态机。
好了,这篇文章就到这里了,感谢大家的观看!如有错误,请及时指正!欢迎大家关注我的公众号:贾哇技术指南
。
分布式事务解决方案汇总
Java各种内存溢出异常实践
详解JVM运行时数据区
Java线程启动流程