Java實現有限狀態機的推薦方案分享

一、背景

平時工作開發過程中,難免會用到狀態機(狀態的流轉)。

如獎學金審批流程、請假審批流程、競標流程等,都需要根據不同行為轉到不同的狀態。

下面是一個簡單的模擬狀態機:

有些同學會選擇將狀態定義為常量,使用 if else 來流轉狀態,不太優雅。

有些同學會考慮將狀態定義為枚舉。

但是定義為枚舉之後,大多數同學會選擇使用 switch 來流轉狀態:

import lombok.Getter;

public enum State {

    STATE_A("A"),
    STATE_B("B"),
    STATE_C("C"),
    STATE_D("D");

    @Getter
    private final String value;

    State(String value) {
        this.value = value;
    }

    public static State getByValue(String value) {
        for (State state : State.values()) {
            if (state.getValue().equals(value)) {
                return state;
            }
        }
        return null;
    }

    /**
     * 批準後的狀態
     */
    public static State getApprovedState(State currentState) {
        switch (currentState) {
            case STATE_A:
                return STATE_B;
            case STATE_B:
                return STATE_C;
            case STATE_C:
                return STATE_D;
            case STATE_D:
            default:
               throw new IllegalStateException("當前已終態");
        }

    }

    /**
     * 拒絕後的狀態
     */
    public static State getRejectedState(State currentState) {
        switch (currentState) {
            case STATE_A:
                throw new IllegalStateException("當前狀態不支持拒絕");
            case STATE_B:
            case STATE_C:
            case STATE_D:
            default:
                return STATE_A;
        }
    }
}

上面這種寫法有幾個弊端:

(1) getByValue 每次獲取枚舉值都要循環一次當前枚舉的所有常量,時間復雜度是
O(N),雖然耗時非常小,但總有些別扭,作為有追求的程序員,應該盡量想辦法優化掉。

(2) 總感覺使用 switch-case 實現狀態流轉,更多的是面向過程的產物。雖然可以實現功能,但沒那麼“面向對象”,既然 State 枚舉就是用來表示狀態,如果同意和拒絕可以通過 State 對象的方法獲取就會更直觀一些。

二、推薦方式

2.1 自定義的枚舉

通常狀態流轉有兩種方向,一種是贊同,一種是拒絕,分別流向不同的狀態。

由於本文討論的是有限狀態,我們可以將狀態定義為枚舉比較契合,除非初態和終態,否則贊同和拒絕都會返回一個狀態。

下面隻是一個DEMO, 實際編碼時可以自由發揮。

該 Demo 的好處是:

1 使用 CACHE緩存,避免每次通過 value 獲取 State都循環 State 枚舉數組

2 定義【同意】和【拒絕】抽象方法,每個 State 通過實現該方法來流轉狀態。

3 狀態的定義和轉換都收攏在一個枚舉中,更容易維護

雖然代碼看似更多一些,但是更“面向對象”一些。

package basic;

import lombok.Getter;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public enum State {
    /**
     * 定義狀態,並實現同意和拒絕的流轉
     */
    STATE_A("A") {
        @Override
        State getApprovedState() {
            return STATE_B;
        }

        @Override
        State getRejectedState() {
            throw new IllegalStateException("STATE_A 不支持拒絕");
        }
    },
    STATE_B("B") {
        @Override
        State getApprovedState() {
            return STATE_C;
        }

        @Override
        State getRejectedState() {
            return STATE_A;
        }
    },
    STATE_C("C") {
        @Override
        State getApprovedState() {
            return STATE_D;
        }

        @Override
        State getRejectedState() {
            return STATE_A;
        }
    },
    STATE_D("D") {
        @Override
        State getApprovedState() {
             throw new IllegalStateException("當前已終態");
        }

        @Override
        State getRejectedState() {
            return STATE_A;
        }
    };

    @Getter
    private final String value;

    State(String value) {
        this.value = value;
    }

    private static final Map<String, State> CACHE;

    static {
        CACHE = Arrays.stream(State.values()).collect(Collectors.toMap(State::getValue, Function.identity()));
    }

    public static State getByValue(String value) {
        return CACHE.get(value);
    }

    /**
     * 批準後的狀態
     */
    abstract State getApprovedState();

    /**
     * 拒絕後的狀態
     */
    abstract State getRejectedState();
}

測試代碼

package basic;

import static basic.State.STATE_B;

public class StateDemo {
    public static void main(String[] args) {
        State state = State.STATE_A;

        // 一直贊同
        State approvedState;
        do {
            approvedState = state.getApprovedState();
            System.out.println(state + "-> approved:" + approvedState);
            state = approvedState;
        } while (state != State.STATE_D);


        // 獲取某個狀態的贊同和拒絕後的狀態
        System.out.println("STATE_B approved ->" + STATE_B.getApprovedState());
        System.out.println("STATE_C reject ->" + State.getByValue("C").getRejectedState());
        System.out.println("STATE_D reject ->" + State.getByValue("D").getRejectedState());
    }
}

輸出結果:

STATE_A-> approved:STATE_B
STATE_B-> approved:STATE_C
STATE_C-> approved:STATE_D
—–
STATE_B approved ->STATE_C
STATE_C reject ->STATE_A
STATE_D reject ->STATE_A

本質上通過不同的方法調用實現自身的流轉,而且贊同和拒絕定義為抽象類,可以“強迫”讓狀態的定義方明確自己的狀態流轉。

整體邏輯比較內聚,狀態的定義和流轉都在 State 類中完成。

2.2 外部枚舉

假如該枚舉是外部提供,隻提供枚舉常量的定義,不提供狀態流轉,怎麼辦?

我們依然可以采用 switch 的方式實現狀態流轉:

import static basic.State.*;

public class StateUtils {
    /**
     * 批準後的狀態
     */
    public static State getApprovedState(State currentState) {
        switch (currentState) {
            case STATE_A:
                return STATE_B;
            case STATE_B:
                return STATE_C;
            case STATE_C:
                return STATE_D;
            case STATE_D:
            default:
            throw new IllegalStateException("當前已經是終態");
        }

    }

    /**
     * 拒絕後的狀態
     */
    public static State getRejectedState(State currentState) {
        switch (currentState) {
            case STATE_A:
                throw new IllegalStateException("當前狀態不支持拒絕");
            case STATE_B:
            case STATE_C:
            case STATE_D:
            default:
                return STATE_A;
        }
    }
}

還有更通用、更容易理解的編程方式呢(不用 switch)?

狀態機的每次轉換是一個 State 到另外一個 State 的映射,每個狀態都應該維護贊同和拒絕後的下一個狀態。

因此,我們很容易會聯想到使用【鏈表】來存儲這種關系 。

由於這裡是外部枚舉,無法將狀態流轉在枚舉內部完成(定義),就意味著我們還需要自定義狀態節點來表示流轉,如:

import lombok.Data;

@Data
public class StateNode<T> {

    private T state;

    private StateNode<T> approveNode;

    private StateNode<T> rejectNode;
}

這樣構造好鏈表以後,還需在工具類中要構造 State 到 StateNode 的映射(因為對於外部來說,隻應該感知 State 類,不應該再去理解 StateNode ) , 提供贊同和拒絕方法,內部通過拿到贊同和拒絕對應的 StateNode 之後拿到對應的 State 返回即可。

偽代碼如下:

public class StateUtils{

// 構造 StateNode 鏈表,和構造 cache Map 略
private Map<State, StateNode<State>> cache ;

	public State getApproveState(State current){
		StateNode<State> node = cache.get(current);
		return node == null? null: return node.getApproveNode().getState();
	}

public State getRejectState(State current){
		StateNode<State> node = cache.get(current);
		return node == null? null: return node.getRejectNode().getState();
	}

}

整體比較曲折,不如直接將贊同和拒絕定義在 State 枚舉內更直觀。

下面給出一種 “狀態鏈模式” 的解決方案。

贊同和拒絕底層分別使用兩個 Map 存儲。

為瞭更好地表達每次狀態的方向(即 Map 中的 key 和 value),每一個映射定義為 from 和 to 。

為瞭避免隻有 from 沒有 to ,定義一個中間類型 SemiData,隻有調用 to 之後才可以繼續鏈式編程下去,最終構造出狀態鏈。

以下結合 Map 的數據結構,結合升級版的 Builder 設計模式,實現鏈式編程

package basic;

import java.util.HashMap;
import java.util.Map;

public class StateChain<T> {

    private final Map<T, T> chain;

    private StateChain(Map<T, T> chain) {
        this.chain = chain;
    }


    public T getNextState(T t) {
        return chain.get(t);
    }

    public static <V> Builder<V> builder() {
        return new Builder<V>();
    }


    static class Builder<T> {

        private final Map<T, T> data = new HashMap<>();


        public SemiData<T> from(T state) {
            return new SemiData<>(this, state);
        }


        public StateChain<T> build() {
            return new StateChain<T>(data);
        }

        public static class SemiData<T> {
            private final T key;
            private final Builder<T> parent;

            private SemiData(Builder<T> builder, T key) {
                this.parent = builder;
                this.key = key;
            }

            public Builder<T> to(T value) {
                parent.data.put(key, value);
                return parent;
            }
        }
    }

}


使用案例:

package basic;

import static basic.State.*;

public class StateUtils {

    private static final StateChain<State> APPROVE;
    private static final StateChain<State> REJECT;

    static {
        APPROVE = StateChain.<State>builder().from(STATE_A).to(STATE_B).from(STATE_B).to(STATE_C).from(STATE_C).to(STATE_D).build();
        
        REJECT = StateChain.<State>builder().from(STATE_B).to(STATE_A).from(STATE_C).to(STATE_A).from(STATE_D).to(STATE_A).build();
    }

    /**
     * 批準後的狀態
     */
    public static State getApprovedState(State currentState) {
         State next = APPROVE.getNextState(currentState);
         if(next == null){
            throw new IllegalStateException("當前已經終態");
         }
         return next;
    }

    /**
     * 拒絕後的狀態
     */
    public static State getRejectedState(State currentState) {
        State next =  REJECT.getNextState(currentState);
         if(next == null){
            throw new IllegalStateException("當前狀態不支持駁回");
         }
         return next;
    }
}

測試方法

import static basic.State.STATE_B;

public class StateDemo {

    public static void main(String[] args) {
        State state = State.STATE_A;

        // 一直贊同
        State approvedState;
        do {
            approvedState = StateUtils.getApprovedState(state);
            System.out.println(state + "-> approved:" + approvedState);
            state = approvedState;
        } while (state != State.STATE_D);
        
        System.out.println("-------");

        // 獲取某個狀態的贊同和拒絕後的狀態
        System.out.println("STATE_B approved ->" + StateUtils.getApprovedState(STATE_B));
        System.out.println("STATE_C reject ->" + StateUtils.getRejectedState(State.getByValue("C")));
        System.out.println("STATE_D reject ->" + StateUtils.getRejectedState(State.getByValue("D")));
    }
}

輸出結果

STATE_A-> approved:STATE_B
STATE_B-> approved:STATE_C
STATE_C-> approved:STATE_D
—-
STATE_B approved ->STATE_C
STATE_C reject ->STATE_A
STATE_D reject ->STATE_A

這種方式更加靈活,可定義多條狀態鏈,實現每個鏈的狀態各自流轉。而且性能非常好。

巧妙地將狀態的轉換定義和 Map 的定義合二為一,既能夠表意(from,to 比較明確),又能獲得很好的性能(獲取贊同和拒絕後的狀態轉化為
通過 key 取 Map 中的 value ),還有不錯的編程體驗(鏈式編程)。

以上隻是 DEMO,實際編碼時,可自行優化。

可能還有一些開源的包提供狀態機的功能,但核心原理大同小異。

三、總結

本文結合自己的理解,給出一種推薦的有限狀態機的寫法。

給出瞭自有狀態枚舉和外部狀態枚舉的解決方案,希望對大傢有幫助。

通過本文,大傢也可以看出,簡單的問題深入思考,也可以得到不同的解法。

希望大傢不要滿足現有方案,可以靈活運用所學來解決實踐問題。

到此這篇關於Java實現有限狀態機的推薦方案的文章就介紹到這瞭,更多相關Java實現有限狀態機內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: