第一次看到 State 设计模式第一个想到的场景就是我们工作中经常会涉及到的“状态机”,State 模式确实比较适合处理“状态机”这样的场景,它可以使不同状态的处理逻辑更内聚。
笔者最近的参与的项目中需要将门店的作业场景进行数字化,例如门店工作人员的日常拣货作业、定期库存盘点作业等。每个作业我们都会用状态来控制它的行为,例如“已领取”的作业不能被其他人领取和执行,“已关闭”的作业不能再次执行,“已完成”的作业会用于员工的薪水计算等等,所以针对作业的每个行为我们都需要进行状态的校验,并且要控制好状态流转:
这是一个非常典型的用“状态机”来控制对象行为的案例。第一版的代码是这个样子:
/**
* 作业
*
* @author 0xZzzz
* @date 2020/11/4
*/
public class Task {
/**
* 作业ID
*/
private Long id;
/**
* 作业状态
*/
private TaskStatus status;
/**
* 操作人ID
*/
private String operatorId;
/**
* 实际开始时间
*/
private Date actualStartTime;
/**
* 实际结束时间
*/
private Date actualEndTime;
/**
* 领取作业
*/
private void claimedBy(String operatorId) {
if (status != TaskStatus.CREATED) {
if (status == TaskStatus.CLOSED) {
throw new RuntimeException("该作业已取消,不能领取!");
}
if (status == TaskStatus.FINISHED) {
throw new RuntimeException("该作业已完成,不能领取!");
}
if (status == TaskStatus.STARTED) {
throw new RuntimeException("该作业正在执行中,不能领取!");
}
if (status == TaskStatus.CLAIMED && !operatorId.equals(this.operatorId)) {
throw new RuntimeException("该作业被其他人领取!");
}
}
this.operatorId = operatorId;
status = TaskStatus.CLAIMED;
}
/**
* 开始作业
*/
private void start(String operatorId, Date actualStartTime) {
if (status != TaskStatus.CLAIMED) {
if (status == TaskStatus.CLOSED) {
throw new RuntimeException("该作业已取消,不能执行!");
}
if (status == TaskStatus.FINISHED) {
throw new RuntimeException("该作业已完成,无需再次执行!");
}
if (status == TaskStatus.STARTED) {
throw new RuntimeException("该作业正在执行中,无需再次执行!");
}
if (status == TaskStatus.CREATED) {
throw new RuntimeException("该作业还未被认领,请领取后再执行!");
}
}
if (!operatorId.equals(this.operatorId)) {
throw new RuntimeException("您不能执行他人领取的作业!");
}
// 作业执行逻辑 ...
status = TaskStatus.STARTED;
this.actualStartTime = actualStartTime;
}
/**
* 完成作业
*/
private void finish(String operatorId, Date actualEndTime) {
if (status != TaskStatus.STARTED) {
if (status == TaskStatus.CLOSED) {
throw new RuntimeException("该作业已取消,不能完成!");
}
if (status == TaskStatus.FINISHED) {
throw new RuntimeException("该作业已完成!");
}
if (status == TaskStatus.CLAIMED) {
throw new RuntimeException("该作业还未执行!");
}
if (status == TaskStatus.CREATED) {
throw new RuntimeException("该作业还未被认领!");
}
}
if (!operatorId.equals(this.operatorId)) {
throw new RuntimeException("您不能完成他人领取的作业!");
}
// 作业完成逻辑 ...
status = TaskStatus.FINISHED;
this.actualEndTime = actualEndTime;
}
}
我们看到,代码中充斥着各种各样的状态条件判断语句,很复杂,当我们要为 Task 扩展一个新的状态时,几乎所有的方法都要改动,要增加各种条件判断语句,这样的改动出错几率和成本都很难接受,我们可以引入 State 模式进行一次重构:
上图为 State 模式的类图展现,在门店作业的场景中,Context 即为 Task,我们为作业状态抽象一个接口 TaskState,具体的每个状态分别定义一个实现类实现这个接口,然后我们将 Task 中的 status 属性移除,增加一个 TaskState 类型的属性,Task 对象初始化时的状态为 CreatedState,后续状态的变更由 Task 的行为来触发(因为是真实的生产场景,代码做了一些删减和脱敏):
/**
* 作业状态
*
* @author 0xZzzz
* @date 2020/11/6
*/
interface TaskState {
/**
* 返回作业状态枚举
*
* @return 状态枚举
*/
TaskStatus status();
/**
* 领取作业
*
* @param operatorId 操作人ID
*/
void claimedBy(String operatorId);
/**
* 开始作业
*
* @param operatorId 操作人ID
* @param actualStartTime 作业实际开始时间
*/
void start(String operatorId, Date actualStartTime);
/**
* 完成作业
*
* @param operatorId 操作人ID
* @param actualEndTime 作业实际完成时间
*/
void finish(String operatorId, Date actualEndTime);
}
/**
* 作业状态抽象类
*
* @author 0xZzzz
* @date 2020/11/6
*/
abstract class AbstractTaskState implements TaskState {
/**
* 作业模型
*/
protected Task task;
AbstractTaskState(Task task) {
this.task = task;
}
}
import java.util.Date;
/**
* 已创建状态
*
* @author 0xZzzz
* @date 2020/11/6
*/
class CreatedState extends AbstractTaskState {
CreatedState(Task task) {
super(task);
}
@Override
public TaskStatus status() {
return TaskStatus.CREATED;
}
@Override
public void claimedBy(String operatorId) {
task.operatorId = operatorId;
task.changeState(new ClaimedState(task));
}
@Override
public void start(String operatorId, Date actualStartTime) {
throw new RuntimeException("该作业批次还未被认领,请领取后再执行!");
}
@Override
public void finish(String operatorId, Date actualEndTime) {
throw new RuntimeException("该作业批次还未被认领!");
}
}
/**
* 已开始状态
*
* @author 0xZzzz
* @date 2020/11/6
*/
class StartedState extends AbstractTaskState {
StartedState(Task task) {
super(task);
}
@Override
public TaskStatus status() {
return TaskStatus.STARTED;
}
@Override
public void claimedBy(String operatorId) {
throw new RuntimeException("该作业正在执行中,不能领取!");
}
@Override
public void start(String operatorId, Date actualStartTime) {
throw new RuntimeException("该作业正在执行中,无需再次执行!");
}
@Override
public void finish(String operatorId, Date actualEndTime) {
if (!operatorId.equals(task.operatorId)) {
throw new RuntimeException("您不能完成他人领取的作业!");
}
task.actualEndTime = actualEndTime;
task.changeState(new FinishedState(task));
}
}
/**
* 已领取状态
*
* @author 0xZzzz
* @date 2020/11/6
*/
class ClaimedState extends AbstractTaskState {
ClaimedState(Task task) {
super(task);
}
@Override
public TaskStatus status() {
return TaskStatus.CLAIMED;
}
@Override
public void claimedBy(String operatorId) {
if (!operatorId.equals(task.operatorId)) {
throw new RuntimeException("该作业已被其他人领取!");
}
throw new RuntimeException("您已领取该作业,无需再次领取!");
}
@Override
public void start(String operatorId, Date actualStartTime) {
if (!operatorId.equals(task.operatorId)) {
throw new RuntimeException("您不能执行他人领取的作业!");
}
task.actualStartTime = actualStartTime;
task.changeState(new StartedState(task));
}
@Override
public void finish(String operatorId, Date actualEndTime) {
throw new RuntimeException("该作业还未执行!");
}
}
/**
* 已完成状态
*
* @author 0xZzzz
* @date 2020/11/6
*/
class FinishedState extends AbstractTaskState {
FinishedState(Task task) {
super(task);
}
@Override
public TaskStatus status() {
return TaskStatus.FINISHED;
}
@Override
public void claimedBy(String operatorId) {
throw new RuntimeException("该作业已完成,不能领取!");
}
@Override
public void start(String operatorId, Date actualStartTime) {
throw new RuntimeException("该作业已完成,无需再次执行!");
}
@Override
public void finish(String operatorId, Date actualEndTime) {
throw new RuntimeException("该作业已完成!");
}
}
/**
* 作业
*
* @author 0xZzzz
* @date 2020/11/4
*/
@Getter
@ToString
public class Task {
/**
* 作业ID
*/
private Long id;
/**
* 作业状态
*/
private TaskState state;
/**
* 实际开始时间
*/
Date actualStartTime;
/**
* 实际结束时间
*/
Date actualEndTime;
/**
* 操作人ID
*/
String operatorId;
/**
* 领取作业
*/
public void claimedBy(String operatorId) {
state.claimedBy(operatorId);
}
/**
* 开始作业
*/
public void start(String operatorId, Date actualStartTime) {
state.start(operatorId, actualStartTime);
}
/**
* 完成作业
*/
public void finish(String operatorId, Date actualEndTime) {
state.finish(operatorId, actualEndTime);
}
/**
* 切换状态
*/
void changeState(TaskState state) {
this.state = state;
}
/**
* 获取作业状态
*/
public TaskStatus getStatus() {
return state.status();
}
}
重构过后我们将状态相关的处理逻辑分散到各个具体的 State 子类中,相对于重构前冗杂的代码结构,可读性得到了很大的提升,而且每个状态更能专注于自己的逻辑处理,不会相互影响,后续我们在改动状态处理逻辑时,改动范围也会更加集中,如果我们只扩展新的状态不扩展新的行为,可以完全满足OCP原则。State 模式有很多优点,不过我们并不建议所有的涉及到“状态机”的场景都马上用 State 模式来解决,如果你的状态处理逻辑已经足够清晰并且易于理解,那就不要强行引入 State 模式来增加设计的复杂度,灵活运用很重要~