本文原文連接地址:http://nullpointer.pw/design-patterns-state.htmlhtml
本文以運營活動狀態轉換爲例,結合 Spring 演示狀態模式的實踐應用。java
類型:行爲型模式git
意圖:容許對象在內部狀態發生改變時改變它的行爲,對象看起來好像修改了它的類。github
主要解決:一個對象存在多個狀態,每一個狀態行爲不一樣,狀態能夠相互轉換。算法
使用場景:一、行爲隨狀態改變而改變的場景。 二、減小 switch..case 以及 if...else數據庫
設計模式系列文章目錄設計模式
State:抽象狀態角色,負責對象狀態定義,而且封裝環境角色以實現狀態切換。app
Context:環境角色,定義客戶端須要的接口,而且負責具體狀態的切換。ide
ConcreteState:具體狀態角色,當前狀態要作的事情,以及當前狀態如何轉換其餘狀態。測試
本文以運營活動狀態爲例,結合 Spring 演示狀態模式的實踐應用。
運營活動建立初始狀態爲草稿狀態,編輯好活動以後,運營會後臺啓用活動,此時活動狀態爲已啓用;
當達到活動開始時間時,定時任務會將活動狀態置爲進行中;
當達到活動結束時間時,定時任務會將活動狀態置爲已結束。
進行中的活動也可能會由於某些緣由須要手動停用,此時活動狀態置爲已停用。
狀態之間有着嚴格的前置校驗,好比草稿狀態能夠繼續保存爲草稿,也能夠進行啓動,但不能直接切換爲進行中,能夠直接編輯切換回草稿箱狀態;好比已停用的狀態只有在啓用以後才能被置爲進行中。
活動狀態的切換約束以下圖:
新狀態→ 當前狀態↓ |
草稿箱 | 已啓用 | 進行中 | 已停用 | 已結束 |
---|---|---|---|---|---|
草稿箱 | ✅ | ✅ | ❌ | ❌ | ❌ |
已啓用 | ✅ | ❌ | ✅ | ✅ | ❌ |
進行中 | ❌ | ❌ | ❌ | ✅ | ✅ |
已停用 | ❌ | ✅ | ❌ | ❌ | ❌ |
已結束 | ❌ | ❌ | ❌ | ❌ | ❌ |
若是不採起狀態模式,可能寫出的代碼就是不斷使用 if 判斷前置狀態是否符合規則,當增長了新的狀態,須要改動判斷的地方,從而可能引入了 Bug。
public abstract class ActivityState { // 抽象狀態角色須要持有環境上下文對象 protected ActivityContext activityContext; public void setActivityContext(ActivityContext activityContext) { this.activityContext = activityContext; } public abstract Integer type(); /** * 判斷是不是當前狀態 */ protected boolean isSameStatus(Activity activity) { return type().equals(activity.getStatus()); } /** * 保存草稿 */ public abstract boolean saveDraft(Activity activity); /** * 啓用 */ public abstract boolean enable(Activity activity); /** * 開始 */ public abstract boolean start(Activity activity); /** * 停用 */ public abstract boolean disable(Activity activity); /** * 中止 */ public abstract boolean finish(Activity activity); }
public class ActivityContext { // 持有抽象狀態角色引用 private ActivityState activityState; public void setActivityState(ActivityState activityState) { this.activityState = activityState; this.activityState.setActivityContext(this); } public boolean saveDraft(Activity activity) { // 委託具體的狀態角色 return this.activityState.saveDraft(activity); } public boolean enable(Activity activity) { return this.activityState.enable(activity); } public boolean start(Activity activity) { return this.activityState.start(activity); } public boolean disable(Activity activity) { return this.activityState.disable(activity); } public boolean finish(Activity activity) { return this.activityState.finish(activity); } }
由於本文示例具體狀態角色有不少,所以只列舉一個開啓狀態角色舉例參考,更多代碼能夠參考本文對應的 GitHub 示例代碼
@Component public class ActivityEnableState extends ActivityState { @Resource private ActivityDraftState activityDraftState; @Resource private ActivityStartState activityStartState; @Resource private ActivityDisableState activityDisableState; @Override public Integer type() { return ActivityStateEnum.ENABLE.getCode(); } @Override public boolean saveDraft(Activity activity) { super.activityContext.setActivityState(activityDraftState); return activityContext.saveDraft(activity); } @Override public boolean enable(Activity activity) { // 若是當前狀態已是 enable 了,則沒法再次 enable if (isSameStatus(activity)) { return false; } activity.setStatus(type()); //TODO 更新數據庫 return true; } @Override public boolean start(Activity activity) { super.activityContext.setActivityState(activityStartState); return activityContext.start(activity); } @Override public boolean disable(Activity activity) { super.activityContext.setActivityState(activityDisableState); return activityContext.disable(activity); } @Override public boolean finish(Activity activity) { // 非進行中的活動狀態,不容許直接進行 finish return false; } }
狀態角色應該是單例的,結合 Spring 與工廠模式對實例進行封裝,方便根據數據庫的 status 值獲取對應的狀態角色實例。
@Component public class ActivityStateFactory implements ApplicationContextAware { public static final Map<Integer, ActivityState> STATE_MAP = new HashMap<>(ActivityStateEnum.values().length); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map<String, ActivityState> beans = applicationContext.getBeansOfType(ActivityState.class); beans.values().forEach(item -> STATE_MAP.put(item.type(), item)); } }
@RunWith(SpringRunner.class) @SpringBootTest(classes = App.class) public class BaseTest { @Test public void test1() { // 通常活動都是從數據庫查詢出來了,此處爲方便測試直接 new Activity activity = new Activity() .setId(1L) .setName("測試活動") .setStatus(ActivityStateEnum.DRAFT.getCode()) .setCreateTime(LocalDateTime.now()); ActivityState activityState = ActivityStateFactory.STATE_MAP.get(activity.getStatus()); ActivityContext context = new ActivityContext(); context.setActivityState(activityState); System.out.println("保存草稿: " + (context.saveDraft(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲已啓用: " + (context.enable(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲進行中: " + (context.start(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲已停用: " + (context.disable(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲已啓用: " + (context.enable(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲進行中: " + (context.start(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲已結束: " + (context.finish(activity) ? "成功" : "失敗")); System.out.println("更新活動狀態爲進行中: " + (context.start(activity) ? "成功" : "失敗")); } }
結果輸出:
保存草稿: 成功 更新活動狀態爲已啓用: 成功 更新活動狀態爲進行中: 成功 更新活動狀態爲已停用: 成功 更新活動狀態爲已啓用: 成功 更新活動狀態爲進行中: 成功 更新活動狀態爲已結束: 成功 更新活動狀態爲進行中: 失敗
能夠看到狀態切換路徑:草稿-> 草稿-> 已啓用-> 進行中-> 已停用-> 已啓用-> 進行中-> 已結束-> 進行中,前面都是正確切換,可是已結束沒法切換爲進行中狀態,從而驗證了狀態模式的應用。
看上一篇策略模式的文章中的 UML,和本文的 UML 是相同的。那麼他們的區別是什麼呢?
策略模式是提供了可相互替換的算法,根據客戶端選擇一種算法指定一種行爲;
狀態模式則包含了對象狀態,根據對象狀態不一樣,行爲也不同,即狀態決定行爲,將行爲對應的邏輯封裝到具體狀態類中,在環境類中消除邏輯判斷,且具體實現不可相互替換。
狀態模式中,客戶端角色與狀態對象不須要進行交互,全部的交互都委託給環境角色進行。