function FSM() {
this.stateList = [];
this.transitionSet = {};
this.currentState = false;
this.initialState = false;
this.isValidTransitionInfo = function(transitionInfo) {
if (typeof transitionInfo != 'object' || transitionInfo == null) return false;
var needAttribute = ['process', 'fromState', 'toState'];
for(var i = 0, j = needAttribute.length; i < j; i++) {
if (typeof transitionInfo[needAttribute[i]] != 'string' || transitionInfo[needAttribute[i]].length < 1) {
return false;
}
}
return true;
};
this.setTransition = function(transition) {
this.transitionSet[transition['process']] = this.transitionSet[transition['process']] || {};
if (this.transitionSet[transition['process']][transition['fromState']])
throw new Exception('FSM # don`t overwrite transition @ process : ' + transition['process'] + ' / from state : ' + transition['fromState']);
this.transitionSet[transition['process']][transition['fromState']] = {};
this.transitionSet[transition['process']][transition['fromState']]['state'] = transition['toState'];
this.transitionSet[transition['process']][transition['fromState']]['before'] = transition['before'] || false;
this.transitionSet[transition['process']][transition['fromState']]['callback'] = transition['callback'] || transition['after'] || false;
};
this.addTransition = function(transition) {
if (!(transition instanceof Array)) {
transition = [transition];
}
for (var i = 0, j = transition.length; i < j; i++) {
if (this.isValidTransitionInfo(transition[i])) {
var elm = transition[i];
this.addState(elm['fromState']);
this.addState(elm['toState']);
this.setTransition(elm);
} else {
throw new Exception('FSM # invalid transition info');
}
}
};
this.addState = function(state) {
if (!this.stateList.isExistValue(state, true)) {
this.stateList[this.stateList.length] = state;
return true;
}
return false;
};
this.getStateList = function() {
return this.stateList;
};
this.getCurrentState = function() {
if (!this.currentState)
throw new Exception('FSM # not initialized @ get current state fail');
return this.currentState;
};
this.setInitalState = function(state) {
if (!this.stateList.isExistValue(state, true))
throw new Exception('FSM # unknown state appointed : ' + String(state) + ' @ FSM.stateList : [ ' + this.stateList.join(', ') + ' ]');
this.currentState = state;
this.initialState = state;
};
this.resetState = function() {
if (!this.initialState)
throw new Exception('FSM # not initialized @ reset fail');
this.currentState = this.initialState;
};
this.availableTransition = function(process) {
var isAvailable = true && typeof process == 'string';
isAvailable = isAvailable && this.transitionSet;
isAvailable = isAvailable && this.transitionSet[process];
return isAvailable && this.transitionSet[process][this.currentState];
};
this.transition = function(process) {
if (this.availableTransition(process)) {
var transition = this.transitionSet[process][this.currentState];
if (transition['before'] && transition['before'] instanceof Function)
transition['before']();
this.currentState = this.transitionSet[process][this.currentState]['state'];
if (transition['callback'] && transition['callback'] instanceof Function)
transition['callback']();
} else {
throw new Exception('FSM # cannot transition @ process : ' + process + ' / from state : ' + this.currentState);
}
};
};
테스트 (qunit)
module("fsm");
//-----------------------------------------------------------------------------
test('fsm basic action', function() {
var fixture = new FSM();
var transitionInfo = { process : 'testa', fromState : 'ready', toState : 'run' };
fixture.addTransition(transitionInfo);
deepEqual(fixture.getStateList(), ['ready', 'run'], '2개의 상태가 추가됨');
notDeepEqual(fixture.getStateList(), ['Raady', 'run'], '상태는 문자열로서 case-sensitive하게 비교함');
fixture.setInitalState('ready');
equal(fixture.getCurrentState(), 'ready', 'setInitalState 메서드로 주어진 상태는 FSM의 첫번째 상태로 기억함');
fixture.transition('testa');
equal(fixture.getCurrentState(), 'run', '등록된 testa 라는 일을 하게 되면 상태가 ready에서 run으로 전이함');
fixture.resetState();
equal(fixture.getCurrentState(), 'ready', 'resetState 메서드를 통해서 최초 상태인 ready로 상태를 강제로 전이시킬 수 있음');
});
//-----------------------------------------------------------------------------
test('fsm illegal use-case', function() {
raises(function() {
var fixture = new FSM();
fixture.addTransition(false);
}, 'transition 정보가 object가 아니므로 예외를 던짐');
raises(function() {
var fixture = new FSM();
fixture.addTransition({blah : 'blah'});
}, 'transition 정보를 줄때 process[문자열], fromState[문자열], toState[문자열]의 3개의 프로퍼티 중 하나라도 주지 않으면 예외를 던짐');
raises(function() {
var fixture = new FSM();
fixture.addTransition({ process : 'testa', fromState : 'ready', toState : 'run' });
fixture.resetState();
}, 'resetState 메서드를 사용할때 fsm에 최초 상태를 주지 않았다면 예외를 던짐');
raises(function() {
var fixture = new FSM();
fixture.addTransition({ process : 'testa', fromState : 'ready', toState : 'run' });
fixture.getCurrentState();
}, 'getCurrentState 메서드를 사용할때 fsm에 최초 상태를 주지 않았다면 예외를 던짐');
raises(function() {
var fixture = new FSM();
fixture.addTransition({ process : 'testa', fromState : 'ready', toState : 'run' });
fixture.setInitalState('exception');
}, 'transition 정보에 쓰이지 않은 것을 최초 상태로 주면 예외를 던짐');
raises(function() {
var fixture = new FSM();
fixture.addTransition({ process : 'testa', fromState : 'ready', toState : 'run' });
fixture.setInitalState('ready');
fixture.transition('testb');
}, 'process로 등록되지 않은 것을 사용하면 예외를 던짐');
raises(function() {
var fixture = new FSM();
fixture.addTransition([
{ process : 'step1', fromState : 'ready', toState : 'run' },
{ process : 'step2', fromState : 'run', toState : 'end' },
{ process : 'step3', fromState : 'end', toState : 'ready' }
]);
fixture.setInitalState('ready');
fixture.transition('step1');
fixture.transition('step3');
}, '현재 상태에서 사용할 수 없는 process를 주면 예외를 던짐');
});
//-----------------------------------------------------------------------------
test('fsm event callback', function() {
var fixture = new FSM();
var nowState = '';
var transitionInfo = [
{ process : 'step1', fromState : 'ready', toState : 'run',
before : function() { nowState = 1; }
},
{ process : 'step2', fromState : 'run', toState : 'ready',
after : function() { nowState = 2; }
},
{ process : 'step3', fromState : 'ready', toState : 'run',
after : function() { nowState = 2; },
callback : function() { nowState = 3;}
},
{ process : 'step3', fromState : 'run', toState : 'ready',
before : function() { nowState = 'a'; },
callback : function() { nowState = nowState + 'b';}
}
];
fixture.addTransition(transitionInfo);
fixture.setInitalState('ready');
fixture.transition('step1');
strictEqual(nowState, 1, '`before` 속성은 상태가 전이되기 직전에 실행됨');
fixture.transition('step2');
strictEqual(nowState, 2, '`after` 속성은 상태가 전이된 후에 실행됨');
fixture.transition('step3');
strictEqual(nowState, 3, '`callback` 속성은 after와 같이 쓰였을때 after를 무시하고 상태가 전이된 후에 실행됨');
fixture.transition('step3');
strictEqual(nowState, 'ab', '`before`와 `after` 속성은 반드시 순차적으로 일어남');
});
댓글 없음:
댓글 쓰기