引言
狀態模式你們可能初聽會很陌生,這種模式有什麼用?我就是個CRUD BOY,面對不一樣的狀態,我一個狀態一個狀態的判斷,if else、if else...... 不斷的來寫不一樣的邏輯它不香嗎?java
香! 可是做爲一個傑出的後浪表明,僅僅如此怎能知足我對知識的慾望!web
咱們知道面向對象的設計模式有七大基本原則:spring
- 開閉原則(Open Closed Principle,OCP)
- 單一職責原則(Single Responsibility Principle, SRP)
- 里氏代換原則(Liskov Substitution Principle,LSP)
- 依賴倒轉原則(Dependency Inversion Principle,DIP)
- 接口隔離原則(Interface Segregation Principle,ISP)
- 合成/聚合複用原則(Composite/Aggregate Reuse Principle,CARP)
- 最少知識原則(Least Knowledge Principle,LKP)或者迪米特法則(Law of Demeter,LOD)
簡單理解就是:編程
- 開閉原則是總綱,它指導咱們要對擴展開放,對修改關閉;
- 單一職責原則指導咱們實現類要職責單一;里氏替換原則指導咱們不要破壞繼承體系;
- 依賴倒置原則指導咱們要面向接口編程;接口隔離原則指導咱們在設計接口的時候要精簡單一;
- 迪米特法則指導咱們要下降耦合。
設計模式就是經過這七個原則,來指導咱們如何作一個好的設計。可是設計模式不是一套「奇技淫巧」,它是一套方法論,一種高內聚、低耦合的設計思想。咱們能夠在此基礎上自由的發揮,甚至設計出本身的一套設計模式。設計模式
固然,學習設計模式或者是在工程中實踐設計模式,必須深刻到某一個特定的業務場景中去,再結合對業務場景的理解和領域模型的創建,才能體會到設計模式思想的精髓。若是脫離具體的業務邏輯去學習或者使用設計模式,那是極其空洞的。api
接下來咱們將經過業務的實踐,來探討如何用狀態設計模式來減小if else,實現可重用、易維護的代碼。springboot
狀態模式
不知道你們在業務中會不會常常遇到這種狀況:app
產品:開發哥哥來下,你看我這邊想加個中間流程,這個流程是要怎樣怎樣處理.......,還想區分加了這些操做後的用戶,其餘不符合這個條件的用戶不要影響,能不能實現啊!ide
我:能啊,加個狀態就行啊!因而將原流程加了個狀態,當用戶處於這個狀態時會如何如何......,因而改完上線,過了幾天。函數
產品:開發哥哥再來下,你看我這邊想加個中間流程,這個流程是要怎樣怎樣處理.......,還想區分加了這些操做後的用戶,其餘不符合這個條件的用戶不要影響,能不能實現啊!
我:能啊!心裏OS: 咦,似曾相識燕歸來,這不是以前加過了一個嗎,還加啊!因而吭哧吭哧又給加上了。本想就結束了,可是過了幾天,又來問了!因而就不斷的if else、if else的來判斷裝個修改原流程!最終一次不當心,動了下以前狀態的代碼,悲劇發生了,生產環境報錯了!
這是每一個開發小哥哥都會遇到的問題,隨着業務的不斷髮展,咱們定義表的狀態會不斷的擴展,狀態之間的流轉也會愈來愈複雜,原來的一小塊if else代碼也會更加的多和雜,着實讓人看着摸不着頭腦。
那有沒有一種模式能讓這些業務解耦開,涉及事件的產生和隨之產生的影響(狀態的流轉)。能夠先將事件和事件產生後的狀態變化綁定起來。不一樣事件產生的狀態流轉也是不一樣的,咱們能夠從全局的角度來進行配置。
有的! 固然是咱們今天的主角-狀態模式了
定義
在狀態模式(State Pattern)中,類的行爲是基於它的狀態改變的。這種類型的設計模式屬於行爲型模式。
在狀態模式中,咱們建立表示各類狀態的對象和一個行爲隨着狀態對象改變而改變的 context 對象。
意圖
容許對象在內部狀態發生改變時改變它的行爲,對象看起來好像修改了它的類。
主要解決
對象的行爲依賴於它的狀態(屬性),而且能夠根據它的狀態改變而改變它的相關行爲。
什麼時候使用
代碼中包含大量與對象狀態有關的條件語句。
如何解決
將各類具體的狀態類抽象出來。
關鍵代碼
一般命令模式的接口中只有一個方法。而狀態模式的接口中有一個或者多個方法。並且,狀態模式的實現類的方法,通常返回值,或者是改變實例變量的值。也就是說,狀態模式通常和對象的狀態有關。實現類的方法有不一樣的功能,覆蓋接口中的方法。狀態模式和命令模式同樣,也能夠用於消除 if...else 等條件選擇語句。
優勢
一、封裝了轉換規則。 二、枚舉可能的狀態,在枚舉狀態以前須要肯定狀態種類。 三、將全部與某個狀態有關的行爲放到一個類中,而且能夠方便地增長新的狀態,只須要改變對象狀態便可改變對象的行爲。 四、容許狀態轉換邏輯與狀態對象合成一體,而不是某一個巨大的條件語句塊。 五、可讓多個環境對象共享一個狀態對象,從而減小系統中對象的個數。
缺點
一、狀態模式的使用必然會增長系統類和對象的個數。 二、狀態模式的結構與實現都較爲複雜,若是使用不當將致使程序結構和代碼的混亂。 三、狀態模式對"開閉原則"的支持並不太好,對於能夠切換狀態的狀態模式,增長新的狀態類須要修改那些負責狀態轉換的源代碼,不然沒法切換到新增狀態,並且修改某個狀態類的行爲也需修改對應類的源代碼。
使用場景
一、行爲隨狀態改變而改變的場景。 二、條件、分支語句的代替者。
實際使用代碼
說了一堆的概念,你們確定仍是模糊的,那麼來這個場景看看吧
場景
做爲一個小up,最大的願望就是本身寫的東西能被更多人看到了。投幣,點贊,收藏,一鍵三聯的操做你們應該熟悉吧,你們的熱情直接影響up的更新頻率,那麼此時事件和狀態就出現了:
- 事件:投幣,點贊,收藏
- 狀態:SOMETIME(想起來何時更新就何時更新),OFTEN(會常常更新下),USUALLY(有事也更新),ALWAYS(沒停過的肝)
咱們能夠獲得一個關係:
- 投幣:UpSometimeState -> UpOftenState
- 點贊:UpOftenState -> UpUsuallyState
- 收藏:UpUsuallyState -> UpAlwaysState
- 英文頻率從低到高:Sometime -> Often -> Usually -> Always
瞭解基本信息後,咱們來基於設計模式原則來面向對象開發吧!
代碼
咱們先定義一個狀態的抽象類,用來表示up的更新頻率
package cn.guess.statemachine.one; import lombok.Data; /** * @program: guess * @description: up主更新頻率狀態接口 * @author: xingcheng * @create: 2020-05-10 12:18 **/ @Data public abstract class UpState { /** * 當前up狀態下的上下文 */ protected BlogContext blogContext; /** * 該狀態下的操做 */ protected abstract void doAction(); /** * 切換狀態 */ protected abstract void switchState(); }
接着咱們定義子類,分別表示每一個不一樣的狀態:
package cn.guess.statemachine.one; /** * @program: guess * @description: up主Sometime更新狀態 * @author: xingcheng * @create: 2020-05-10 12:22 **/ public class UpSometimeState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 切換狀態 動做:投幣 狀態流轉:UpSometimeState -> UpOftenState blogContext.setUpState(new UpOftenState()); // 執行動做 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpSometimeState"; } }
package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up主Often更新狀態 * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpOftenState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 切換狀態 動做:投幣 狀態流轉:UpOftenState -> UpUsuallyState blogContext.setUpState(BlogContext.UP_USUALLY_STATE); // 執行動做 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpOftenState"; } }
package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up主Usually更新狀態 * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpUsuallyState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 切換狀態 動做:投幣 狀態流轉:UpUsuallyState -> UpAlwaysState blogContext.setUpState(BlogContext.UP_ALWAYS_STATE); // 執行動做 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpUsuallyState"; } }
package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up主Always更新狀態 * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpAlwaysState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 終態,不切換狀態 // 執行動做 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpAlwaysState"; } }
咱們還須要一個上下文環境來進行狀態的流轉關聯
package cn.guess.statemachine.one; import lombok.Data; import lombok.NoArgsConstructor; /** * @program: guess * @description: 博客上下文相關信息包裝 * 投幣:UpSometimeState -> UpOftenState * 點贊:UpOftenState -> UpUsuallyState * 收藏:UpUsuallyState -> UpAlwaysState * 英文頻率從低到高:Sometime -> Often -> Usually -> Always * @author: xingcheng * @create: 2020-05-10 12:17 **/ @Data @NoArgsConstructor public class BlogContext { public final static UpSometimeState UP_SOMETIME_STATE = new UpSometimeState(); public final static UpOftenState UP_OFTEN_STATE = new UpOftenState(); public final static UpUsuallyState UP_USUALLY_STATE = new UpUsuallyState(); public final static UpAlwaysState UP_ALWAYS_STATE = new UpAlwaysState(); /** * 當前up主狀態 */ private UpState upState; public BlogContext(UpState upState) { this.upState = upState; this.upState.setBlogContext(this); } /** * 用戶對博客內容的動做-投幣 */ public static void throwCoin() { new BlogContext(BlogContext.UP_SOMETIME_STATE).getUpState().switchState(); } /** * 用戶對博客內容的動做-點贊 */ public static void like() { new BlogContext(BlogContext.UP_OFTEN_STATE).getUpState().switchState(); } /** * 用戶對博客內容的動做-收藏 */ public static void collect() { new BlogContext(BlogContext.UP_USUALLY_STATE).getUpState().switchState(); } }
接着咱們寫一個客戶端來模擬調用流程:
package cn.guess.statemachine.one; /** * @program: guess * @description: 狀態切換執行器 * @author: xingcheng * @create: 2020-05-10 15:36 **/ public class UpStateClient { public static void main(String[] args) { // 開始模擬每一個動做事件-會自動進行狀態轉化 // 投幣 System.out.println("投幣動做"); BlogContext.throwCoin(); System.out.println("-----------------------------------------------------------------------"); // 點贊 System.out.println("點贊動做"); BlogContext.like(); System.out.println("-----------------------------------------------------------------------"); // 收藏 System.out.println("收藏動做"); BlogContext.collect(); } }
此時,狀態模式便完成了,能夠看到咱們沒有用到if else,便完成了判斷。
每一個狀態也是由一個類來代替的,咱們對其中一個狀態進行的改動,不會影響其餘的狀態邏輯
經過這樣的方式,很好的實現了對擴展開放,對修改關閉的原則。
咱們看下輸出:
有的小朋友要問了,開發哥哥,咱們如今開發環境幾乎都是springboot了,能不能結合spring這麼強大的生態,來實現這一模式呢?
能! 毋庸置疑,能結合spring強大的IOC和AOP,徹底能夠實現一個狀態自動機啊!!!
SpringBoot狀態自動機
仍是剛剛的場景,咱們經過Spring StateMachine來實現下。
代碼
包的引入:
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>${spring-boot-statemachine.version}</version> </dependency>
我這邊使用的是<spring-boot-statemachine.version>2.2.0.RELEASE</spring-boot-statemachine.version>版本
定義狀態和事件枚舉
package cn.guess.statemachine.tow.enums; import java.util.Objects; /** * up狀態事件枚舉 英文頻率從低到高:Sometime -> Often -> Usually -> Always * @program: guess * @author: xingcheng * @create: 2020-05-10 16:12 **/ public enum UpStateEnum { UP_SOMETIME_STATE(0, "SOMETIME"), UP_OFTEN_STATE(10, "OFTEN"), UP_USUALLY_STATE(20, "USUALLY"), UP_ALWAYS_STATE(30, "ALWAYS"), ; /** * 枚舉編碼 */ private final int code; /** * 枚舉描述 */ private final String value; public int getCode() { return code; } public String getValue() { return value; } UpStateEnum(int code, String value) { this.code = code; this.value = value; } /** * 根據枚舉key值轉化爲枚舉對象 * * @param key 枚舉值 * @return 枚舉對象 */ public static UpStateEnum keyOf(int key) { UpStateEnum[] values = values(); for (UpStateEnum stateEnum : values) { if (Objects.equals(stateEnum.getCode(), key)) { return stateEnum; } } return null; } }
package cn.guess.statemachine.tow.enums; import java.util.Objects; /** * 博客事件枚舉 * @program: guess * @author: xingcheng * @create: 2020-05-10 16:08 **/ public enum BlobEventEnum { THROW_COIN(0, "投幣"), LIKE(10, "點贊"), COLLECT(20, "收藏"), ; /** * 枚舉編碼 */ private final int code; /** * 枚舉描述 */ private final String value; public int getCode() { return code; } public String getValue() { return value; } BlobEventEnum(int code, String value) { this.code = code; this.value = value; } /** * 根據枚舉key值轉化爲枚舉對象 * * @param key 枚舉值 * @return 枚舉對象 */ public static BlobEventEnum keyOf(int key) { BlobEventEnum[] values = values(); for (BlobEventEnum stateEnum : values) { if (Objects.equals(stateEnum.getCode(), key)) { return stateEnum; } } return null; } }
建立狀態機配置類
package cn.guess.statemachine.tow.config; import cn.guess.statemachine.tow.enums.BlobEventEnum; import cn.guess.statemachine.tow.enums.UpStateEnum; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.transition.Transition; import java.util.EnumSet; /** * @program: guess * @description: 該註解用來啓用Spring StateMachine狀態機功能 * @author: xingcheng * @create: 2020-05-10 16:14 **/ @EnableStateMachine @Configuration public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<UpStateEnum, BlobEventEnum> { /** * configure用來初始化當前狀態機擁有哪些狀態 * * @param states * @throws Exception */ @Override public void configure(StateMachineStateConfigurer<UpStateEnum, BlobEventEnum> states) throws Exception { states .withStates() // 定義了初始狀態爲UP_SOMETIME_STATE .initial(UpStateEnum.UP_SOMETIME_STATE) //指定UpStateEnum中的全部狀態做爲該狀態機的狀態定義 .states(EnumSet.allOf(UpStateEnum.class)); } /** * configure用來初始化當前狀態機有哪些狀態遷移動做 * 從其中命名中咱們很容易理解每個遷移動做,都有來源狀態source,目標狀態target以及觸發事件event * 事件和狀態流轉關係綁定:相似BlogContext的throwCoin及UpSometimeState下的switchState的過程 * * @param transitions * @throws Exception */ @Override public void configure(StateMachineTransitionConfigurer<UpStateEnum, BlobEventEnum> transitions) throws Exception { transitions .withExternal() // 投幣:UpSometimeState -> UpOftenState .source(UpStateEnum.UP_SOMETIME_STATE).target(UpStateEnum.UP_OFTEN_STATE) .event(BlobEventEnum.THROW_COIN) .and() .withExternal() // 點贊:UpOftenState -> UpUsuallyState .source(UpStateEnum.UP_OFTEN_STATE).target(UpStateEnum.UP_USUALLY_STATE) .event(BlobEventEnum.LIKE) .and() .withExternal() // 收藏:UpUsuallyState -> UpAlwaysState .source(UpStateEnum.UP_USUALLY_STATE).target(UpStateEnum.UP_ALWAYS_STATE) .event(BlobEventEnum.COLLECT); } /** * configure爲當前的狀態機指定了狀態監聽器,其中listener()則是調用了下一個函數建立的監聽器實例,用來處理各個各個發生的狀態遷移事件。 * 這裏註釋是由於咱們有其餘更好的方法去替代 */ // @Override // public void configure(StateMachineConfigurationConfigurer<UpStateEnum, BlobEventEnum> config) throws Exception { // config // .withConfiguration() // // 指定狀態機的處理監聽器 // .listener(listener()); // } /** * listener()方法用來建立StateMachineListener狀態監聽器的實例, * 在該實例中會定義具體的狀態遷移處理邏輯,上面的實現中只是作了一些輸出, * 實際業務場景會有更嚴密的邏輯,因此一般狀況下,咱們能夠將該實例的定義放到獨立的類定義中,並用注入的方式加載進來。 * 這裏註釋是由於咱們有其餘更好的方法去替代 */ // @Bean // public StateMachineListener<UpStateEnum, BlobEventEnum> listener() { // return new StateMachineListenerAdapter<UpStateEnum, BlobEventEnum>() { // // @Override // public void transition(Transition<UpStateEnum, BlobEventEnum> transition) { // if (transition.getTarget().getId() == UpStateEnum.UP_SOMETIME_STATE) { // System.out.println("up sometime update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_SOMETIME_STATE // && transition.getTarget().getId() == UpStateEnum.UP_OFTEN_STATE) { // System.out.println("user throw coin, up sometime update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_OFTEN_STATE // && transition.getTarget().getId() == UpStateEnum.UP_USUALLY_STATE) { // System.out.println("user like blob, up usually update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_USUALLY_STATE // && transition.getTarget().getId() == UpStateEnum.UP_ALWAYS_STATE) { // System.out.println("user collect blob, up always update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_ALWAYS_STATE) { // System.out.println("up always update blob"); // return; // } // } // // }; // } }
註解監聽器
package cn.guess.statemachine.tow.config; import org.springframework.statemachine.annotation.OnTransition; import org.springframework.statemachine.annotation.OnTransitionEnd; import org.springframework.statemachine.annotation.OnTransitionStart; import org.springframework.statemachine.annotation.WithStateMachine; /** * @program: guess * @description: 該配置實現了cn.guess.statemachine.tow.config.StateMachineConfig類中定義的狀態機監聽器實現 * @author: xingcheng * @create: 2020-05-10 16:31 **/ @WithStateMachine public class EventConfig { @OnTransition(target = "UP_SOMETIME_STATE") public void initState() { System.out.println("up sometime update blob"); } @OnTransition(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoin() { System.out.println("up sometime update blob"); } @OnTransitionStart(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoinStart() { System.out.println("up sometime update blob start"); } @OnTransitionEnd(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoinEnd() { System.out.println("up sometime update blob end"); } @OnTransition(source = "UP_OFTEN_STATE", target = "UP_USUALLY_STATE") public void like() { System.out.println("user like blob, up usually update blob"); } @OnTransition(source = "UP_USUALLY_STATE", target = "UP_ALWAYS_STATE") public void collect() { System.out.println("user collect blob, up always update blob"); } }
建立應用Controller來完成流程
package cn.guess.statemachine.tow.controller; import cn.guess.common.api.ApiResult; import cn.guess.common.web.controller.BaseController; import cn.guess.statemachine.tow.enums.BlobEventEnum; import cn.guess.statemachine.tow.enums.UpStateEnum; import cn.guess.system.web.res.UserSelfCenterInfoRes; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.statemachine.StateMachine; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @program: guess * @description: state檢測相關接口 * @author: xingcheng * @create: 2020-05-10 16:38 **/ @Slf4j @RestController @RequestMapping("/api/state") @Api(value = "state檢測相關接口 API", description = "03.state檢測相關接口") public class StateController extends BaseController { @Autowired private StateMachine<UpStateEnum, BlobEventEnum> stateMachine; @GetMapping("/v1/run") @ApiOperation(value = "state檢測請求") public String stateRun() { // start()就是建立這個up主的發博客流程,根據以前的定義,該up會處於不常常更新(SOMETIME)狀態 stateMachine.start(); // 經過調用sendEvent(Events.THROW_COIN)執行投幣操做 stateMachine.sendEvent(BlobEventEnum.THROW_COIN); // 經過調用sendEvent(Events.THROW_COIN)執行點贊操做 stateMachine.sendEvent(BlobEventEnum.LIKE); // 經過調用sendEvent(Events.THROW_COIN)執行收藏操做 stateMachine.sendEvent(BlobEventEnum.COLLECT); return "OK"; } }
調用結果
說明
咱們能夠對如何使用Spring StateMachine作以下小結:
-
定義狀態和事件枚舉
-
爲狀態機定義使用的全部狀態以及初始狀態
-
爲狀態機定義狀態的遷移動做
-
爲狀態機指定監聽處理器
狀態監聽器
經過上面的入門示例以及最後的小結,咱們能夠看到使用Spring StateMachine來實現狀態機的時候,代碼邏輯變得很是簡單而且具備層次化。
整個狀態的調度邏輯主要依靠配置方式的定義,而全部的業務邏輯操做都被定義在了狀態監聽器中。
其實狀態監聽器能夠實現的功能遠不止上面咱們所述的內容,它還有更多的事件捕獲,咱們能夠經過查看StateMachineListener接口來了解它全部的事件定義:
總結
狀態模式的核心是封裝,將狀態以及狀態轉換邏輯封裝到類的內部來實現,也很好的體現了「開閉原則」和「單一職責原則」。每個狀態都是一個子類,不論是修改仍是增長狀態,只須要修改或者增長一個子類便可。在咱們的應用場景中,狀態數量以及狀態轉換遠比上述例子複雜,經過「狀態模式」避免了大量的if-else代碼,讓咱們的邏輯變得更加清晰。同時因爲狀態模式的良好的封裝性以及遵循的設計原則,讓咱們在複雜的業務場景中,可以遊刃有餘地管理各個狀態。