狀態模式(State Pattern)指允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類。
一般用來實現狀態機,而狀態機常用在遊戲、工作流引擎等系統的開發中:
有限狀態機(Finite State Machine,FSM),狀態機有三個組成部分:狀態(State)、事件(Event)和動作(Action)。其中事件也叫作轉移條件(Transition Condition),事件主要用於觸發狀態的轉移及動作的執行,動作不是必須的,也可能只轉移狀態,不執行任何動作。
一、狀態模式的介紹
狀態模式又名狀態物件(Objects for States),它是一種物件行為型模式。它的解決思想是當控制一個物件狀態轉換的條件表示式過於複雜時,把相關“判斷邏輯”提取出來,用各個不同的類進行表示,系統處於哪種情況、直接使用相應的狀態類物件進行處理。
1.1 狀態模式的結構
在狀態模式的結構中,通過實現抽象狀態類的具體狀態類來定義多個狀態,每個狀態類僅實現自己的邏輯,上下文類負責切換狀態。其結構類圖如下所示:
State
:抽象狀態類,提供一個方法封裝上下文物件的狀態ConcreteState1、ConcreteState2
:具體狀態類,繼承抽象狀態類,實現狀態下的行為Context
:上下文類,負責對具體狀態進行切換Client
:客戶端,呼叫具體狀態和上下文
1.2 狀態模式的實現
首先是抽象狀態類,具體程式碼如下:
public abstract class State {
/**抽象業務方法,不同的具體狀態可以有不同的實現*/
public abstract void handle();
}
其次是實現抽象狀態類的具體狀態類:
public class ConcreteState1 extends State{
@Override
public void handle(Context context) {
System.out.println("進入ConcreteState1中~");
context.setState(this);
}
@Override
public String toString() {
return "concreteState1";
}
}
public class ConcreteState2 extends State{
@Override
public void handle(Context context) {
System.out.println("進入ConcreteState2中~");
context.setState(this);
}
@Override
public String toString() {
return "ConcreteState2";
}
}
接下來是上下文類,維護當前狀態,並負責具體狀態的切換
public class Context {
private State state;
//設定初始狀態為null
public Context() {
state = null;
}
//實現狀態轉換
public void setState(State state) {
this.state = state;
}
public State getState() {
return state;
}
}
客戶端測試類
public class Client {
public static void main(String[] args) {
Context context = new Context();
System.out.println("現在的狀態是:" + context.getState());
System.out.println("---------------------------------");
State concreteState1 = new ConcreteState1();
concreteState1.handle(context);
System.out.println("現在的狀態是:" + context.getState());
System.out.println("---------------------------------");
State concreteState2 = new ConcreteState2();
concreteState2.handle(context);
System.out.println("現在的狀態是:" + context.getState());
}
}
測試結果:
現在的狀態是:null
---------------------------------
進入ConcreteState1中~
現在的狀態是:concreteState1
---------------------------------
進入ConcreteState2中~
現在的狀態是:ConcreteState2
二、狀態模式的應用場景
狀態模式的應用比較廣泛,比如遊戲中角色狀態的轉換、公文審批中的流轉等等。
以下情況可以考慮使用狀態模式:
- 物件的行為依賴於它的某些屬性值(狀態),而且狀態的改變將導致行為的變化
- 程式碼中包含大量與物件狀態有關的條件語句(if-else),這些條件語句的出現會導致程式碼的可維護性和靈活性變差。
三、狀態模式實戰
本案例中模擬營銷活動稽核狀態流轉場景,在一個活動的上線中是需要多個層級進行稽核才能上線的(案例來源於《重學Java設計模式》)。如下圖中可以看到流程節點中包括各個狀態到下一個狀態扭轉的關聯條件:
因此在審批過程中就難免會包含很多條件語句的判斷,長此以往,隨著狀態數量的增加,會增加程式碼的可維護性和可讀性。下面就利用狀態模式來實現多狀態的審批過程,先來看看狀態模式模型的結構:
State
:狀態抽象類,定義所有狀態的操作介面CheckState、CloseState、DoingState...
:具體狀態類,各種狀態的具體邏輯實現StateHandler
:狀態處理類,相當於之前結構中提到的上下文類,負責對狀態流程進行統一處理
具體程式碼
- 基本活動資訊和活動列舉狀態
public class ActivityInfo {
private String activityId;
private String activityName;
private Enum<Status> status;
private Date beginTime;
private Date endTime;
//get\set\Constructor
}
public enum Status {
Editing,
Check,
Pass,
Refuse,
Doing,
Close,
Open
}
- 活動業務處理
public class ActivityService {
private static Map<String, Enum<Status>> statusMap = new ConcurrentHashMap<>();
public static void init(String activityId, Enum<Status> initStatus) {
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.setActivityId(activityId);
activityInfo.setActivityName("測試活動");
activityInfo.setStatus(initStatus);
activityInfo.setBeginTime(new Date());
activityInfo.setEndTime(new Date());
statusMap.put(activityId, initStatus);
}
/**
* 查詢活動資訊
* @param activityId 活動ID
* @return 查詢後的活動資訊
*/
public static ActivityInfo queryActivityInfo(String activityId) {
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.setActivityId(activityId);
activityInfo.setActivityName("測試活動");
activityInfo.setStatus(statusMap.get(activityId));
activityInfo.setBeginTime(new Date());
activityInfo.setEndTime(new Date());
return activityInfo;
}
/**
* 查詢活動狀態
* @param activityId 活動ID
* @return 查詢後的活動狀態
*/
public static Enum<Status> queryActivityStatus(String activityId) {
return statusMap.get(activityId);
}
public static synchronized void execStatus(String activityId, Enum<Status> beforeStatus, Enum<Status> afterStatus) {
/*如果前後兩個狀態相同,直接返回*/
if (!beforeStatus.equals(statusMap.get(activityId))) {
return;
}
/*反之更新statusMap*/
statusMap.put(activityId, afterStatus);
}
}
2.活動返回格式
public class Result {
private String code;
private String info;
//get/set
}
- 抽象狀態類和具體狀態實現
public abstract class State {
/**提審*/
public abstract Result arraignment(String activityId, Enum<Status> currentStatus);
/**撤審*/
public abstract Result checkRevoke(String activityId, Enum<Status> currentStatus);
/**稽核通過*/
public abstract Result checkPass(String activityId, Enum<Status> currentStatus);
/**拒審*/
public abstract Result checkRefuse(String activityId, Enum<Status> currentStatus);
/**關閉*/
public abstract Result close(String activityId, Enum<Status> currentStatus);
/**開啟活動*/
public abstract Result open(String activityId, Enum<Status> currentStatus);
/**活動中*/
public abstract Result doing(String activityId, Enum<Status> currentStatus);
}
public class CheckState extends State {
@Override
public Result arraignment(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "提審後不能重複提審");
}
@Override
public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, Status.Check, Status.Editing);
return new Result("0000", "活動稽核撤銷回編輯");
}
@Override
public Result checkPass(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, Status.Check, Status.Pass);
return new Result("0000", "活動稽核通過");
}
@Override
public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, Status.Check, Status.Refuse);
return new Result("0000", "活動稽核被拒絕");
}
@Override
public Result close(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "活動稽核後不能直接關閉");
}
@Override
public Result open(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "活動稽核後不能再開啟");
}
@Override
public Result doing(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "活動稽核不通過無法進入活動中");
}
}
//下面依次是其他幾個狀態,較多省略
- 狀態處理類,相當於前面的上下文,負責進行狀態轉移
public class StateHandler {
private Map<Enum<Status>, State> stateMap = new ConcurrentHashMap<>();
public StateHandler() {
stateMap.put(Status.Check, new CheckState());
stateMap.put(Status.Close, new CloseState());
stateMap.put(Status.Doing, new DoingState());
stateMap.put(Status.Refuse, new RefuseState());
stateMap.put(Status.Pass, new PassState());
stateMap.put(Status.Open, new OpenState());
stateMap.put(Status.Editing, new EditingState());
}
public Result arraignment(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).arraignment(activityId, currentStatus);
}
public Result checkPass(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).checkPass(activityId, currentStatus);
}
public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).checkRefuse(activityId, currentStatus);
}
public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).checkRevoke(activityId, currentStatus);
}
public Result close(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).close(activityId, currentStatus);
}
public Result open(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).open(activityId, currentStatus);
}
public Result doing(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).doing(activityId, currentStatus);
}
}
5.測試類
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_Check2Close() {
String activityId = "100001";
ActivityService.init(activityId, Status.Check);
StateHandler stateHandler = new StateHandler();
Result result = stateHandler.close(activityId, Status.Check);
logger.info("測試結果(提審到關閉):{}", JSON.toJSONString(result));
logger.info("活動資訊:{} 狀態:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityStatus(activityId)));
}
@Test
public void test_Refuse2Revoke() {
String activityId = "100001";
ActivityService.init(activityId, Status.Refuse);
StateHandler stateHandler = new StateHandler();
Result result = stateHandler.checkRevoke(activityId, Status.Refuse);
logger.info("測試結果(拒絕To撤審):{}", JSON.toJSONString(result));
logger.info("活動資訊:{} 狀態:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}
}
測試結果:
19:49:48.755 [main] INFO ApiTest - 測試結果(拒絕To撤審):{"code":"0000","info":"拒絕後返回編輯狀態"}
19:49:48.768 [main] INFO ApiTest - 活動資訊:{"activityId":"100001","activityName":"測試活動","beginTime":1649591388759,"endTime":1649591388759,"status":"Editing"} 狀態:"Editing"
參考資料
《重學Java設計模式》
《設計模式》