重构背景
最近在开发这样一个管理系统:将公司多台服务器上的查询SQL相关信息(用户,查询语句,查询类型,起始时间,结束时间等)收集起来,持久化到数据库,并在页面上统一展示。其中最基本的也是最重要的部门就是从每个服务器上获取SQL信息,然后进行相应的处理。最初的程序伪代码如下所示:
if (SQL完成) {
//存入数据库
} else {
while (true) {
//获取最新SQL查询状态
if (SQL完成) {
//存入数据库
break;
}
}
}
只需要从服务器获取SQL最新的查询信息,然后判断该SQL是否执行完成,执行完成之后存入数据库中。处理逻辑也非常简单,一个if-else判断就能搞定。但是在往后开发的时候,需求又进行了更新,还有一些其他的情况需要进行处理,此时程序的伪代码如下所示:
if (SQL完成) {
//存入数据库
} else {
while (true) {
//获取最新SQL查询状态
if (SQL完成) {
//存入数据库
break;
} else if (SQL正在执行) {
if (执行时间 > 执行时间阈值) {
//取消SQL执行
//存入数据库
}
} else if (SQL等待) {
if (等待时间 > 等待时间阈值) {
//取消SQL等待
//存入数据库
}
}
}
}
在新的处理逻辑中加入了两个新的判断分支:分别是执行超时和等待状态。对于这两种情况我在这里就不解释其含义了。对于执行超时,用户可以设置一个执行时间的阈值。当SQL执行时间超过该阈值时,就取消该SQL的执行,以释放资源;而对于等待状态,当SQL已经在等待超时状态时,如果等待时间超过该阈值,则取消该SQL的等待,直接进行后续处理。
对于每个服务器上的每一条SQL查询都会进行这样一个循环判断,从伪代码就不难看出,逻辑处理比较复杂,而且代码看起来不优雅。此时,我们可以明显发现,其实上面的各个if-else分支判断,本质上就是一个SQL的各个状态之间的转移。因此我们很自然的就想到了使用状态机编程的方式,对这部分代码进行重构。
开始重构
与一般的编程方法不同,状态机编程主要就是将程序划分为各个不同的状态,并且定义了每个状态对应的行为以及相关的状态转换关系。说起来可能比较抽象,下面就结合上面所说的例子,来具体了解下什么是状态机编程。
首先,对于上面的伪代码我们可以看到,SQL在执行的过程中一共有四种状态:完成,运行中,执行超时和等待超时。这几种状态之间有相互转换的可能,例如运行中的SQL可能完成,也可能执行超时或者等待超时。因此,我们根据SQL的这些执行状态定了如下的枚举:
public enum QueryStatus {
START, RUNNING, WAITING, TIMEOUT, FINISH, STOP
}
下面来对上面的各个执行状态进行解释:
- START:表示刚开始时,从服务器获取SQL的查询信息;
- RUNNING:表示SQL正在执行过程中;
- WAITING:表示此时SQL已经处于等待的状态;
- TIMEOUT:表示SQL执行超时或者等待超时;
- SUCCESS:表示SQL执行完成,此时可以持久化到数据库中;
- STOP:结束状态,不做具体操作。
解释完了各个状态的含义之后,我们来看看各个状态之间的状态转换图:
关于每个状态之间的状态转移情况就再赘述了,大家只要知道这些状态之间有这样的转换关系即可。
下面为了使用状态机编程,还定义了另外一个类用于保存SQL的状态及查询信息,如下所示:
public class QueryEntity {
private QueryStatus status;
private QueryInfo info;
//省略余下的get和set方法
}
其中,QueryInfo就是用于保存SQL的查询信息。然后,我们就可以根据上面的状态转换图进行编码了,这里只展示关键部分的代码,对于相关的上下文则略去:
public void handle(QueryInfo info) {
QueryEntity entity = new QueryEntity(QueryStatus.START, info);
while (entity.getStatus() != QueryStatus.STOP) {
switch(entity.getStatus()) {
case START:
start(entity);
break;
case RUNNING:
running(entity);
break;
case WAITING:
waiting(entity);
break;
case TIMEOUT:
timeout(entity);
break;
case FINISH:
finish(entity);
break;
default:
logger.error("Unknown query status: ");
break;
}
}
}
private void start(QueryEntity entity) {
//初始的获取信息操作
}
private void running(QueryEntity entity) {
//运行状态的处理逻辑,如果执行超时则转换为TIMEOUT;否则为FINISH
}
private void waiting(QueryEntity entity) {
//等待状态的处理逻辑,如果等待超时则转换为TIMEOUT;否则为FINISH
}
private void timeout(QueryEntity entity) {
//取消SQL的执行或者等待,然后转换为FINISH
}
private void finish(QueryEntity entity) {
//SQL处理完毕,执行持久化操作
//只有在这里,状态才会转换为STOP,并退出状态机
}
至此,本次重构就已经完成。我们可以看到,使用状态机编程方法进行重构之后,代码逻辑变得更加清晰和易懂,而且状态之间的转换也不容易出错。代码也更加优雅。然后就是相关的代码review和测试过程了。这就不是本文的重点了。
延伸学习–状态模式
状态机最早并不是来源于软件开发,但是现在的应用非常广泛,例如音乐播放器之间的各个状态变换也是使用了状态机编程。在设计模式中有一种状态模式,就是使用了这种思想。
定义:状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
这里一共包含了两部分的含义:
- 状态模式将状态封装成了独立的类,并通过代表当前状态的对象来进行相应的动作;
- 从用户的角度来看,如果对象能够完全改变其行为,那么该对象是从别的类实例化来的。然而,状态模式是使用组合,通过引用不同的状态对象来造成类改变的假象。
下面来看看状态模式的类图:
从类图可以看出,我们在上面所使用的状态机编程与这里有一些不同。状态模式首先抽象了一个状态类的接口,然后为每个状态实现了一个具体的状态类。下面就根据状态模式的定义将上面的例子改为状态模式的编码。
首先,定义一个抽象状态接口,并定义一些通用的方法,然后对每一种状态都实现一个具体的状态类。这里只写了START和STOP对应的状态类作为演示:
public interface State {
public void handler(QueryInfo queryInfo);
}
public class StartState implements State {
QueryContext context;
public StartState(QueryContext context) {
this.context = context;
}
public void handler(QueryInfo queryInfo) {
//和上面的start()方式执行相同的处理逻辑
//可以通过context.setState()来改变状态
}
}
public class StopState implements State {
QueryContext context;
public StopState(QueryContext context) {
this.context = context;
}
public void handler(QueryInfo queryInfo) {
//退出本次处理
context.setStop = true;
}
}
下面就是查询对应的Context类,通过调用Context的handler方法可以将动作委托给具体的状态来执行:
public class QueryContext {
State startState;
State runningState;
State waitingState;
State timeoutState;
State finishState;
State stopState;
State state = startState;
boolean isStop;
public QueryContext() {
startState = new StartState(this);
runningState = new RunningState(this);
waitingState = new WaitingState(this);
timeoutState = new TimeoutState(this);
finishState = new FinishState(this);
stopState = new StopState(this);
isStop = false;
}
public void handler(QueryInfo queryInfo) {
state.handler(queryInfo);
}
public void setState(State state) {
this.state = state;
}
public State setStop(boolean isStop) {
this.isStop = isStop;
}
public State isStop() {
return isStop;
}
}
下面是一个简单的测试类,用于验证我们的这个状态处理机:
public class QueryContextTest {
public static void main(String[] args) {
QueryContext context = new QueryContext();
QueryInfo queryInfo = GetTestQueryInfo();
while (!context.isStop()) {
context.handler(queryInfo);
}
}
}
可以看到,使用状态模式对上面的例子进行修改之后,代码量反而增加了不少,而且逻辑看上去也复杂了。所以说,上面的情况并不适合于使用状态模式进行处理。状态模式比较适合对于每种状态,都有好几个操作进行,而上面的例子中,就只有一个handler方法。因此,只需要定一个状态枚举即可。在实际开发过程中,我们也应该灵活处理问题,而不是生搬硬套。