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

重构背景

最近在开发这样一个管理系统:将公司多台服务器上的查询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和测试过程了。这就不是本文的重点了。

延伸学习–状态模式

状态机最早并不是来源于软件开发,但是现在的应用非常广泛,例如音乐播放器之间的各个状态变换也是使用了状态机编程。在设计模式中有一种状态模式,就是使用了这种思想。
定义:状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
这里一共包含了两部分的含义:

  1. 状态模式将状态封装成了独立的类,并通过代表当前状态的对象来进行相应的动作;
  2. 从用户的角度来看,如果对象能够完全改变其行为,那么该对象是从别的类实例化来的。然而,状态模式是使用组合,通过引用不同的状态对象来造成类改变的假象。

下面来看看状态模式的类图:
状态模式
从类图可以看出,我们在上面所使用的状态机编程与这里有一些不同。状态模式首先抽象了一个状态类的接口,然后为每个状态实现了一个具体的状态类。下面就根据状态模式的定义将上面的例子改为状态模式的编码。
首先,定义一个抽象状态接口,并定义一些通用的方法,然后对每一种状态都实现一个具体的状态类。这里只写了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方法。因此,只需要定一个状态枚举即可。在实际开发过程中,我们也应该灵活处理问题,而不是生搬硬套。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值