java(2) java寫狀態機類
曾經有這樣一個腦筋急轉彎:把一頭大象放進冰箱需要幾步?當然了,這是一個老梗了,可能連三歲小孩都能毫不猶豫地回答出來:3步;開啟,塞進去,再關上。或許,作為一個老梗,它已經笑果不佳,但如果我們從新的角度去分析,也能發現新的價值。從把大象塞進冰箱這個過程思考,有三個非常明確的步驟:
1.開啟冰箱門
2.把大象塞進去
3.關上冰箱門
如果從演算法的角度來看,這就是一個典型的演算法,符合了演算法的有窮性,確定性,至於可行性的話,至少理論上是可行的。
再換種角度,如果從冰箱和大象所處的狀態的分解這個問題,我們可以得到一種非常不同的解決問題的思路,先來進行狀態的分析:
1.冰箱門關著,大象在外面 2.冰箱門開啟,大象在外面 3.冰箱門開啟,大象在裡面 4.冰箱門關著,大象在裡面
從上面的分析可以清楚的看到在不同的時刻冰箱和大象處於不同的狀態(冰箱有門或開或閉,大象在裡面或在外面的狀態),由冰箱大象這兩個物件組成的一個整體也因它的組成物件狀態的不同而處於不同的狀態,但是無論是從個體的狀態看,還是從整體的狀態看,在任一時刻都只能有一個狀態,也就是說同一時刻有且只有一個確定的狀態,而不能同時有多個狀態。就好像,在某一時刻,大象只能在裡面或者不在裡面,而不能同時在裡面又不在裡面。另外一個值得注意的現象是,在不同的狀態下我們能夠進行的動作是不同的。當處於“冰箱門關著,大象不在裡面”狀態時,我門能夠進行的動作是“開啟門”;當處於“冰箱門開啟,大象正在進入冰箱”狀態時,我們能進行的動作是“把大象塞進冰箱”,不能進行的動作是“開啟門”……可以看到整個過程是很典型的“狀態驅動行為”,即有什麼行為是由處於什麼狀態決定的,狀態在整個過程中處於核心的地位——這便是有限狀態機的思想。上面提及的大象冰箱的例子就是一個非常簡單的有限狀態機模型。
下面我就用程式設計的方式來實現上面那個有限狀態機模型,並且模仿“把大象塞進冰箱”這個過程。
要實現有限狀態機,首要的就是狀態機圖的設計。或許上面的例子非常簡單,簡單到不需要狀態機圖,但仍建議畫出狀態機圖。
狀態機圖(沒有UML工具,用office的流程圖替代,將就著用吧)
既然已經畫出了狀態機圖,那麼把它轉換成JAVA程式碼就是比較簡單的事了。下面結合具體程式碼來講解一下,講解遵循從抽象到具體的的順序,這也是我當初寫這個示例程式碼遵循的一個順序:
1.com.zyzz.fsm包,這個包下有整個程式的核心實現,包含了狀態介面,狀態介面的初始實現(介面卡類),狀態機的實現,以及狀態機內部的具體狀態子類的實現
2.com.zyzz.ele包,這個包下有對狀態機的簡單測試程式碼
好了,接下來就是各個類:
(1).com.zyzz.fsm.IState
[java] view plain copy
package com.zyzz.fsm;
/**
* 狀態介面,在此介面內定義了用於完成把“把大象塞進冰箱”任務的相關操作
* @author zyzz1995
*
*/
public interface IState {
/**
* 預定義值,這個常量用於標識每個具體子類,在具體的實現中應該被覆蓋,賦予唯一的,不重複的值
* 只有在這種情況下該常量才有意義
*/
public final static int STATE_ID=-1;
/**
* 開啟冰箱
*/
void openFridge();
/**
* 放進大象
*/
void putElephant();
/**
* 關閉冰箱
*/
void closeFridge();
/**
* 列印狀態資訊
*/
void printStateInfo();
}
上面介面的程式碼註釋解釋的很清楚了,這裡就不贅述了。需要的強調一點是在之後這個介面的子類中對STATE_ID這個常量有一個比較巧的運用。
(2).com.zyzz.fsm.StateAdapter
[java] view plain copy
package com.zyzz.fsm;
/**
* 這是一個實現了IState介面的介面卡類,它給出了狀態介面的初始實現,但它並不是直接可用的
*它只是為了避免各個具體的狀態子類直接實現狀態介面而產生的重複的,相同的程式碼
*每個具體的狀態子類都應該繼承自此類,根據需要重寫相關方法
* @author zyzz1995
*/
public class StateAdapter implements IState {
public final static String ERROR_INFO=”狀態下不允許”;
public Machine targetMachine;
private String oprTag=”“;
public String stateTag=”“;
/*public StateAdapter(Machine mac){
targetMachine=mac;
}*/
@Override
public void openFridge() {
oprTag="開啟冰箱";
targetMachine.printErrInfo(stateTag+ERROR_INFO+oprTag);
}
@Override
public void putElephant() {
oprTag="把大象塞進冰箱";
targetMachine.printErrInfo(stateTag+ERROR_INFO+oprTag);
}
@Override
public void closeFridge() {
oprTag="關上冰箱";
targetMachine.printErrInfo(stateTag+ERROR_INFO+oprTag);
}
@Override
public void printStateInfo() {}
}
如註釋所言,這是一個介面卡類,它實現類IState介面,提供了IState介面的預設實現,當然這些實現都是列印一些諸如“在XXX狀態不允許XXX操作”的資訊,如果僅僅把它作為“機器”類的狀態實現的話是不行的,因為任一一個操作只是打印出錯資訊而已,這對於完成任務沒有如何用處。所以,必須它派生出子類,在具體的狀態子類中有選擇地重寫一些方法,使其對完成任務起到應有的作用。至於為什麼要存在這個介面卡類,是因為,不同的狀態子類的差異僅僅在於個別方法的實現不同,大部分方法的實現是一樣的,如果直接現實IState介面,那麼意味著每一個狀態子類都要實現IState介面中的每一個方法,同時意味著相同的程式碼會被重複很多次,而這並不是我們所希望的,所以StateAdapter這個介面卡類的存在是基於實現的需要。
(3).com.zyzz.fsm.Machine
[java] view plain copy
package com.zyzz.fsm;
/**
* “機器類”,是狀態存在的場所,它依靠內部的狀態運轉起來的(由狀態驅動)
*
* @author zyzz1995
*
*/
public class Machine {
private IState presentState;// 當前狀態
private IState[] states;// 所有狀態組成的狀態組
/**
* 構造器
*/
public Machine() {
// 構建所有狀態
states = new StateAdapter[4];
states[FridgeClosedElephantOutState.STATE_ID] = new FridgeClosedElephantOutState();
states[FridgeOpenElephantOutState.STATE_ID] = new FridgeOpenElephantOutState();
states[FridgeOpenElephantInState.STATE_ID] = new FridgeOpenElephantInState();
states[FridgeClosedElephantInState.STATE_ID] = new FridgeClosedElephantInState();
// 設定,初始狀態
this.setState(FridgeClosedElephantOutState.STATE_ID);
}
// 幾個具體的狀態子類,設計成內部類,理由是具體的狀態只與具體的機器有關,這些狀態這裡對於外界是透明的
// 因此,應該把它們封裝在“機器”類的內部,對外界隱藏,外部世界無法直接改變“機器”內部的狀態,確保了“機器”的安全性
// 這些類只使用一次,本來應該用匿名內部類的,但匿名內部類沒有類名,因此無法體現其所代表的狀態,可讀性教差
class FridgeClosedElephantOutState extends StateAdapter {
public static final int STATE_ID = 0x0;
public FridgeClosedElephantOutState() {
this.targetMachine = Machine.this;
this.stateTag = "冰箱關閉,大象在外面";
}
@Override
public void openFridge() {
targetMachine.setState(FridgeOpenElephantOutState.STATE_ID);
}
public void printStateInfo() {
System.out.println("現在:" + stateTag);
}
}
class FridgeOpenElephantOutState extends StateAdapter {
public static final int STATE_ID = 0x1;
public FridgeOpenElephantOutState() {
this.targetMachine = Machine.this;
this.stateTag = "冰箱開啟,大象在外面";
}
@Override
public void putElephant() {
targetMachine.setState(FridgeOpenElephantInState.STATE_ID);
}
public void printStateInfo() {
System.out.println("現在:" + stateTag);
}
}
class FridgeOpenElephantInState extends StateAdapter {
public static final int STATE_ID = 0x2;
public FridgeOpenElephantInState() {
this.targetMachine = Machine.this;
this.stateTag = "冰箱開啟,大象在裡面";
}
@Override
public void closeFridge() {
targetMachine.setState(FridgeClosedElephantInState.STATE_ID);
}
public void printStateInfo() {
System.out.println("現在:" + stateTag);
}
}
class FridgeClosedElephantInState extends StateAdapter {
public static final int STATE_ID = 0x3;
public FridgeClosedElephantInState() {
this.targetMachine = Machine.this;
this.stateTag = "冰箱關閉,大象在裡面";
}
public void printStateInfo() {
System.out.println("現在:" + stateTag);
}
}
/**
* 工具方法:列印錯誤資訊到控制檯
*/
public final void printErrInfo(String errInfo) {
System.out.println("Error:" + errInfo);
}
/**
* 狀態切換方法:設定當前狀態。只有本類可見。
*/
private final void setState(int stateID) {
presentState = states[stateID];
presentState.printStateInfo();
}
// 下面是面向外部開放的介面
public void openTheFridge() {
presentState.openFridge();
}
public void putTheElephantIn() {
presentState.putElephant();
}
public void closeTheFridge() {
presentState.closeFridge();
}
}
好了,上面是重頭戲。Machine類,傳說中的“機器”類。裡面有幾個繼承了StateAdapter的狀態子類,作為驅動這個機器類核心。其實這個類也比較簡單,註釋看看就能理解,不詳述了。
(4).com.zyzz.ele.ElephantVsFridge
[java] view plain copy
package com.zyzz.ele;
import com.zyzz.fsm.Machine;
public class ElephantVsFridge {
/**
* @param args
*/
public static void main(String[] args) {
//下面演示用java+有限狀態機的方式,實現“把大象塞進冰箱”。
//雖然用這個當做例子可能有些“蛋痛”,但也算說明有限狀態機的一個比較簡單的例子
Machine eleMac = new Machine();//建立一個“機器”例項,此時機器內部處於“冰箱關上,大象在外面狀態”
eleMac.openTheFridge();//開啟冰箱門,此時內部處於“冰箱門開啟,大象在外面狀態”
eleMac.putTheElephantIn();//塞進大象,此時內部處於“冰箱門開啟,大象在裡面狀態”
eleMac.closeTheFridge();//關上冰箱門,此時內部處於“冰箱門關上,大象在裡面狀態”
/*
*如果任一換動什麼語句的順序或者註釋掉其中一些語句,機器都會發出警告“xx狀態下不允許xx操作”
*但是從Machine類內部的實現看,並沒有相關的狀態判斷語句,這就是是狀態機另一個好處了:把龐大的條件分支轉移到
*各狀態子類當中去,從而表面類“麵條式”的語句,降低了出錯的風險和後期維護的成本。當然,這一點在本例中不是很明顯,
*但也能說明問題了。
*/
}
}
上面是個對Machine的簡單測試,看看註釋吧!
好了,這個例子就到這裡了。這就是我對有限狀態機的一些個人見解,也不知道對不對,目前也是自己摸索學習中,歡迎高人斧正提攜,也歡迎共同探討學習,願與各位共同分享,共同進步。