控制结构(3): 状态机(state machine)

// 上一篇:卫语句(guard clause)
// 下一篇:局部化(localization)


基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构。

前情回顾

上次分析了guard 控制结构,有人说:“可是阅读很多开源代码也是if/else层层嵌套的”。是的,guard控制结构只是一种风格,语言并没有规定你一定要这么写,也没必要。两种风格都能写出逻辑清晰的代码,但是还是有细微差别,嵌套模式下,要写出清晰的代码需要更好的做好函数的分层(见第1节),例如,在分支里只做函数的转发调用,那么,代码也是清晰可读的。

function doSomething(){
	if(condition1){
		call1();
	}else{
		if(condition2){
			call2();
		}else{
			call3();
		}
	}	
}

我们来看一个新的结构。

典型代码:


let opening = false;
let opened = false;
let closing = false;
let closed = false;
let step1 = false;

function open(onComplete){
	if(closing||closed){
		return onComplete(RESULT.CLOSED)
	}

	if(opening){
		return onComplete(RESULT.PENDING);
	}

	if(opened){
		return onComplete(RESULT.SUCCESS);
	}
	

	opening = true;
	doOpen(function(err){
		onComplete(err);
		opening = false;
		opened = true;
	});
}

function close(onComplete){
	if(!opened){
		return onComplete(RESULT.INVALID_OP);
	}

	if(closing||closed){
		return onComplete(RESULT.PENDING);
	}

	closing = true;
	doClose(function(err){
		onComplete(err);
		closing = false;
		closed = true;
	})
}

function start(onComplete){
	open((err)=>{
		if(err){
			return onComplete(err);
		}
		step1((err)=>{
			if(err){
				return onComplete(err);
			}		
			step2((err)=>{
				onComplete(err);
				close((err)=>{
					log(`close, ret:${err}`);
				})
			})
		})
	})
}

function step1(onComplete){
	if(!opened){
		return onComplete(RESULT.NOT_OPEN);
	}

	doStep1(function(err){
		step1 = true;
		onComplete(err);
	});
}

function step2(onComplete){
	if(!opened){
		return onComplete(RESULT.NOT_OPEN);
	}

	if(!step1){
		return onComplete(RESULT.INVALID_STEP);
	}

	doStep2(function(err){
		onComplete(err);
	});
}

结构分析

这段代码使用了多个bool变量来做状态标记,为了安全有序的做好每个异步动作,需要在适当的地方对每个bool标志变量做判断。如果这个流程继续复杂起来,bool变量之间的状态组合会变的更加繁杂,也很容易一不小心就漏掉状态的组合判断,导致难以定位的BUG。此时,我们可以采用一种重要的控制结构。

...

是的,状态机(state machine)结构。我们先来看看使用状态机控制结构改写后的代码:


/**所有的状态*/
const STATE = {
	INIT: 0,
	OPENING: 1,
	OPENED: 2,
	STEP1: 3,
	STEP2: 4,
	CLOSING: 5,
	CLOSED: 6,
	ERROR: 7
} 

/**每个状态都只允许从指定状态转移过来*/
const stateTransfer = {
	STATE.INIT: [],
	STATE.OPENING: [STATE.INIT],
	STATE.OPENED: [STATE.OPENING],
	STATE.STEP1: [STATE.OPENED],
	STATE.STEP2: [STATE.STEP1],
	STATE.CLOSING: [STATE.INIT, STATE.OPENING, STATE.OPENED, STATE.STEP1, STATE.STEP2],
	STATE.CLOSED: [STATE.CLOSING],
	STATE.ERROR: [STATE.INIT, STATE.OPENING, STATE.OPENED, STATE.STEP1, STATE.STEP2],
}

let state = STATE.INIT;
let stateChangeEvent = null;

function moveTo(err, newState){
	
	let nextState = err ? STATE.ERROR : newState;

	if(stateTransfer[nextState].indexOf(state)===-1){
		return RESULT.INVALID_STATE_TRANSFER;
	}

	let oldState = state;
	state = nextState;
	stateChangeEvent(oldState, state, err);

	do();

	return RESULT.SUCCESS;
}

function do(){
	switch(state){
		case STATE.INIT:
			moveTo(0, STATE.OPENING);
			break;
		case STATE.OPENING:
			doOpen((err)=>{
				moveTo(err, STATE.OPENED);
			});
			break;
		case STATE.OPENED:
			doStep1((err)=>{
				moveTo(err, STATE.STEP1);
			});
			break;
		case STATE.STEP1:
			doStep2((err)=>{
				moveTo(err, STATE.STEP2);
			});
			break;
		case STATE.STEP2:
			moveTo(0, STATE.CLOSING);
			break;
		case STATE.CLOSING:
			doClose((err)=>{
				moveTo(err, STATE.CLOSED);
			});
			break;
		case STATE.CLOSED:
			break;
		default:
			break;
	}
}

function start(onStateChange){
	stateChangeEvent = onStateChange;
	do();
}

语义分析

看上去好像多了很多代码。然而在复杂的异步处理流程中,状态机能清晰地表达逻辑,保证逻辑代码的时序,并在形式推理上发现和消灭漏洞,阅读do里面的逻辑,状态之间的转换直接、明显。编程语言提供了基本的状态标志位:bool变量。这便于让程序员在编程中随意地使用bool变量标记各种状态,但0/1状态标示在稍微复杂一点的状态变化中,就会显得不好用:

  • 出现组合爆炸,你需要对多个bool变量做组合判断,这很容易漏掉某个状态组合。
  • 设计上的失误,导致bool标志位之间不是正交的,因而任一时刻,程序的状态不能确保唯一,这会导致程序出现未知的时序问题。

事实上,任何程序在运行中本身就是一个状态机:

  • 当前在第i行执行
  • 根据条件,进入第j行执行

这里的状态就是:

  • pc, 也就是program counter, 程序计数器
  • condition,改变pc的条件

如果想更清楚地理解这点,可以观看Lamport制作的视频,要看到第2集:
http://lamport.azurewebsites.net/video/intro.html

posted @ 2017-05-02 14:29  ffl  阅读(1882)  评论(0编辑  收藏  举报