「狀態機模式」( State Pattern ) 是眾多設計模式中的一種,主要是用來處理一種物件可能依照不同的狀態而有不同行為的情況。
可以應用的場景有錄音機、紅綠燈等。若是商業方面的應用話,如包裹運送、文件傳簽等。
本文就以公司裡面會出現的「傳簽」來當作狀態模式的應用場景。
本文
場景一:簡單的傳簽系統
假設現在有一間新創公司剛成立一年,內部設計出如下圖的傳簽流程:
我準備了以下幾項東西來符合需求:
狀態機 ( State Machine )
狀態 ( State )
狀態資訊 ( StateInfo )
1
2
3
4
5
6
7
publicinterfaceStateMachine{/* Switch to next state */voidnextState();/* Print current information */voidinfo();}
1
2
3
4
5
6
7
8
9
10
publicinterfaceState{/* Set next state */voidsetNextState(StatenextState);/* Switch to & return next state */StatenextState();/* Print current state information */voidprintStateInfo();}
publicclassSignatureStateimplementsState{privateStatenextState;privateStateInfoinfo;publicSignatureState(StateInfoinfo){this.info=info;}@OverridepublicvoidsetNextState(StatenextState){this.nextState=nextState;}@OverridepublicStatenextState(){returnnextState;}@OverridepublicvoidprintStateInfo(){System.out.println("Now, the state is "+info.getInfo());}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicenumStateInfo{DRAFT("Draft"),WATING_FOR_BOSS("Waiting for boss"),APPROVED("Approved");privateStringinfo;StateInfo(Stringinfo){this.info=info;}publicStringgetInfo(){returninfo;}}
publicclassSignatureDemo{publicstaticvoidmain(String[]args){StatedraftState=newSignatureState(StateInfo.DRAFT);StatewaitingForBossState=newSignatureState(StateInfo.WATING_FOR_BOSS);StateapprovedState=newSignatureState(StateInfo.APPROVED);draftState.setNextState(waitingForBossState);waitingForBossState.setNextState(approvedState);approvedState.setNextState(null);StateMachinesignSystem=newSignatureStateMachine(draftState);signSystem.info();// "Now, the state is Draft"
signSystem.nextState();signSystem.info();// "Now, the state is Waiting for boss"
signSystem.nextState();signSystem.info();// "Now, the state is Approved"
signSystem.nextState();signSystem.info();// "Already approved!"
signSystem.nextState();signSystem.info();// "Already approved!"
}}
我們沿用上面的狀態機 ( StateMachine ) 介面,但是改寫狀態 ( State ) 介面,並且重新實作它們。
重新定義的狀態介面如下:
1
2
3
4
5
6
7
8
9
10
publicinterfaceState{/* Register a next state */voidregisterNextState(SignActionaction,StatenextState);/* Switch to & return next state */StatenextState(SignActionaction);/* Print current state information */voidprintStateInfo();}
publicclassSignatureStateMachineimplementsStateMachine{privateStatecurrentState;publicSignatureStateMachine(StatecurrentState){this.currentState=currentState;}@OverridepublicvoidnextState(SignActionaction){if(currentState!=null)currentState=currentState.nextState(action);}@Overridepublicvoidinfo(){if(currentState!=null){currentState.printStateInfo();}else{System.out.println("Already approved!");}}/* The static method to generate a custom signature system */publicstaticSignatureStateMachinegenerateSignatureSystem(){StatedraftState=newSignatureState(StateInfo.DRAFT);StatewaitingForLeaderState=newSignatureState(StateInfo.WATING_FOR_LEADER);StatewaitingForBossState=newSignatureState(StateInfo.WATING_FOR_BOSS);StateapprovedState=newSignatureState(StateInfo.APPROVED);draftState.registerNextState(SignAction.SIGN,waitingForLeaderState);waitingForLeaderState.registerNextState(SignAction.SIGN,waitingForBossState);waitingForLeaderState.registerNextState(SignAction.REJECT,draftState);waitingForBossState.registerNextState(SignAction.SIGN,approvedState);waitingForBossState.registerNextState(SignAction.REJECT,waitingForLeaderState);returnnewSignatureStateMachine(draftState);}}
publicclassSignatureStateimplementsState{privateMap<SignAction,State>map=newEnumMap<>(SignAction.class);privateStateInfoinfo;publicSignatureState(StateInfoinfo){this.info=info;}@OverridepublicvoidregisterNextState(SignActionaction,StatenextState){map.put(action,nextState);}@OverridepublicStatenextState(SignActionaction){if(map.containsKey(action)){returnmap.get(action);}else{thrownewStateError.NoSuchStateException();}}@OverridepublicvoidprintStateInfo(){System.out.println("Now, the state is "+info.getInfo()+".");}}
publicclassFile{/* The file's content */privateStringcontent;/* Whether the file is submitted. */privatebooleansubmitted=false;/* The map to store role & password pairs. */privateMap<String,String>roleToTokenMap=Map.ofEntries(Map.entry("Leader","leader-pass-123"),Map.entry("Boss","boss-pass-456"));/* Inner state machine */privateSignatureStateMachinestateMachine=SignatureStateMachine.generateSignatureSystem();publicFile(Stringcontent){this.content=content;}/* Display the file's state information */publicvoidstateInfo(){stateMachine.info();}/* To sign the file */publicvoidsign(Stringrole,Stringpassword,SignActionaction){if(!submitted)thrownewStateError.NotSubmittedException();if(roleToTokenMap.containsKey(role)&&roleToTokenMap.get(role).equals(password)){stateMachine.nextState(action);if(role.equals("Leader")&&SignAction.REJECT.equals(action)){submitted=false;}}}/* To submit the file */publicvoidsubmit(){if(!submitted){stateMachine.nextState(SignAction.SIGN);submitted=true;}else{thrownewStateError.AlreadySubmittedException();}}}
publicclassSignatureDemo2{publicstaticvoidmain(String[]args){StringfileContent="Promotion request.";FilenewFile=newFile(fileContent);newFile.stateInfo();// "Now, the state is Draft."
newFile.submit();newFile.stateInfo();// "Now, the state is Waiting for leader."
newFile.sign("Leader","leader-pass-123",SignAction.SIGN);newFile.stateInfo();// "Now, the state is Waiting for boss."
newFile.sign("Boss","boss-pass-456",SignAction.REJECT);newFile.stateInfo();// "Now, the state is Waiting for leader."
newFile.sign("Leader","leader-pass-123",SignAction.SIGN);newFile.stateInfo();// "Now, the state is Waiting for boss."
newFile.sign("Boss","boss-pass-456",SignAction.SIGN);newFile.stateInfo();// Now, the state is Approved.
}}
更多複雜的場景
真正在處理公司內部的業務邏輯的時候,複雜度往往沒有像上面兩個例子一樣簡單,好比下面傳簽的場景:
在處理複雜場景的時候,還可以考慮以下幾點,讓狀態模式更動態 ( 當然也就更複雜… 😅 )
抽象化轉換過程 ( Transition )。
封裝狀態機 ( State Machine ) 到會改變狀態的物件當中。
使用列舉 ( Enumerate ) 方式來定義狀態資訊 ( State Information )、轉換過程 ( Transition ) 等。
除了可以轉換的狀態之外,也紀錄上一個狀態。
有機會轉換失敗的話,加入回滾 ( Rollback )的轉換。
當然…這篇文章就不寫出如何處理這樣的場景啦! 😉
總結
狀態模式 ( State Machine ) 主要是用來處理一種物件可能依照不同的狀態而有不同行為的情況。