設計模式學習筆記(二十)狀態模式及其實現

来源:https://www.cnblogs.com/EthanWong/archive/2022/04/10/16127258.html
-Advertisement-
Play Games

狀態模式(State Pattern)指允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。 一般用來實現狀態機,而狀態機常用在游戲、工作流引擎等系統的開發中: 有限狀態機(Finite State Machine,FSM),狀態機有三個組成部分:狀態(State)、事件(Eve ...


狀態模式(State Pattern)指允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。

一般用來實現狀態機,而狀態機常用在游戲、工作流引擎等系統的開發中:

狀態機

有限狀態機(Finite State Machine,FSM),狀態機有三個組成部分:狀態(State)、事件(Event)和動作(Action)。其中事件也叫作轉移條件(Transition Condition),事件主要用於觸髮狀態的轉移及動作的執行,動作不是必須的,也可能只轉移狀態,不執行任何動作。

一、狀態模式的介紹

狀態模式又名狀態對象(Objects for States),它是一種對象行為型模式。它的解決思想是當控制一個對象狀態轉換的條件表達式過於複雜時,把相關“判斷邏輯”提取出來,用各個不同的類進行表示,系統處於哪種情況、直接使用相應的狀態類對象進行處理。

1.1 狀態模式的結構

在狀態模式的結構中,通過實現抽象狀態類的具體狀態類來定義多個狀態,每個狀態類僅實現自己的邏輯,上下文類負責切換狀態。其結構類圖如下所示:

image-20220410152059585

  • 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設計模式》)。如下圖中可以看到流程節點中包括各個狀態到下一個狀態扭轉的關聯條件:

狀態模式案例

因此在審批過程中就難免會包含很多條件語句的判斷,長此以往,隨著狀態數量的增加,會增加代碼的可維護性和可讀性。下麵就利用狀態模式來實現多狀態的審批過程,先來看看狀態模式模型的結構:

image-20220410160208977

  • State:狀態抽象類,定義所有狀態的操作介面
  • CheckState、CloseState、DoingState...:具體狀態類,各種狀態的具體邏輯實現
  • StateHandler:狀態處理類,相當於之前結構中提到的上下文類,負責對狀態流程進行統一處理

具體代碼

  1. 基本活動信息活動枚舉狀態
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
}
  1. 活動業務處理
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
}
  1. 抽象狀態類具體狀態實現
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", "活動審核不通過無法進入活動中");
    }
}
//下麵依次是其他幾個狀態,較多省略
  1. 狀態處理類,相當於前面的上下文,負責進行狀態轉移
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設計模式》

《設計模式》

http://c.biancheng.net/view/1388.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 視頻鏈接:【Ray Dalio】三十分鐘看懂經濟機器如何運轉(比爾蓋茨推薦) 影響經濟的三要素 生產率的提高 短期債務周期 長期債務周期 理解交易 每次交易中:買方使用 貨幣/信用 向賣方獲取 商品、服務或股票資產 支出總額 = 貨幣 + 信用 支出總額是經濟的驅動力 價格 = 支出總額 / 銷量 ...
  • 1、首先引用Less 有npm安裝、cdn引用、或者下載Less.js本地引用,我採用的是第三種方法 less.js引用: 下載地址:https://github.com/less/less.js/tree/master/dist <script src="./js/less.js" type="t ...
  • Vue2升級為Vue3之後有很多新內容,但也有很多坑,這裡講下我今天剛學Vue3遇到的坑。可以直接到最後看main.js。 首先就是Element-ui,前端vue一般都使用這個插件,但這個插件在Vue3中就不能用了(應該是暫時,目前2022年4月10日),but它有一個兄弟可以用,它叫elemen ...
  • 2022第十三屆藍橋杯第一次開放了web組賽道,博主作為一名前端小白,參加了這次比賽。一共十個題目,目的均是實現特定的網頁效果,考點包含三件套、jQuery和vue,這裡簡單的進行一下個人的題解記錄。 ...
  • 基於個人寫的以下關於Vue框架基礎學習的三篇隨筆,在此基礎上,做一個階段性的知識總結,以此來檢驗自己對Vue這一段時間學習的成果,內容不多,但很值得一看。(思維導圖詳解) ...
  • VUE生命周期函數 可謂是一個個鮮活的生命在服務於各個在使用VUE框架的碼農~ beforeCreate: 創建實例之前; 初始化 註入&校驗 把data、methods、props、computed、provide、watch...依次掛載到實例上 methods 中普通的方法和computed中 ...
  • 正文 1. 阿裡雲DataV 2. 積木報表jimureport 3. 百度Sugar 4. 帆軟 最經常的工作是將一些項目的數據從資料庫導出,然後分門別類的列到excel表格中,領導看起來眼花繚亂。 要是能以圖表可視化展現出來,領導就可以看到項目近幾個月的走勢,也知道之後要怎麼決策了。 嘗試了使用 ...
  • 前端周刊發表每周前端技術相關的大事件、文章教程、一些框架的版本更新、以及代碼和工具。每周定期發表,歡迎大家關註、轉載。 如果外鏈不能訪問,關註公眾號「前端每周看」,裡面有解決辦法 大事件 Veni,vidi,formatae! 宣佈Rome Formatter:超快速的 JavaScript 格式化 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...