记一次代码重构--状态机编程

xiaoxiao2021-02-28  97

重构背景

最近在开发这样一个管理系统:将公司多台服务器上的查询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方法。因此,只需要定一个状态枚举即可。在实际开发过程中,我们也应该灵活处理问题,而不是生搬硬套。

转载请注明原文地址: https://www.6miu.com/read-24042.html

最新回复(0)