2011년 11월 21일 월요일

유한 상태 기계 (Finite state machine) with JavaScript

구현

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` 속성은 반드시 순차적으로 일어남');
});

댓글 없음:

댓글 쓰기