策略模式的雙胞胎:狀態模式

来源:https://www.cnblogs.com/hongdouni/archive/2018/01/19/state_parttern.html
-Advertisement-
Play Games

Simple Demo 假設我有一部iPhoneX,又非常喜歡玩游戲,那麼我這部破手機主要存在兩種狀態:待機和游戲中。 此時手機的狀態圖非常簡單: 將這個狀態圖轉換為代碼: 每一個狀態用不同的整數代表,將每一個動作整合成方法,每一個動作都可能造成狀態的轉換。 測試代碼: 更改需求 但存在一種特殊情況 ...


Simple Demo

假設我有一部iPhoneX,又非常喜歡玩游戲,那麼我這部破手機主要存在兩種狀態:待機和游戲中。

此時手機的狀態圖非常簡單:

 

  

 

將這個狀態圖轉換為代碼:

每一個狀態用不同的整數代表,將每一個動作整合成方法,每一個動作都可能造成狀態的轉換。

public class MyiPhoneX {
    public final static int    STANDBY    = 0;        // 待機狀態
    public final static int    PLAYING    = 1;        // 游戲進行中狀態
    private int                state    = STANDBY;    // 持有當前狀態的實例變數

    public MyiPhoneX() {
    }

    public void startGame() { // 打開游戲
        if (state == STANDBY) {
            System.out.println("Game is loading...");
            state = PLAYING;
        } else if (state == PLAYING) {
            System.out.println("Game is already in progress!");// 已存在該游戲進程
        }
    }

    public void exitGame() { // 結束游戲
        if (state == STANDBY) {
            System.out.println("There is no game process!");// 不存在游戲進程,這是此狀態的一個不恰當動作
        } else if (state == PLAYING) {
            System.out.println("Game is exiting...");
            state = STANDBY;
        }
    }
}

 測試代碼:

public class MyiPhoneXTest {
    public static void main(String[] args) {
        MyiPhoneX iPhoneX = new MyiPhoneX();
        iPhoneX.startGame();
        iPhoneX.exitGame();
        iPhoneX.exitGame();
        iPhoneX.startGame();
        iPhoneX.exitGame();
    }
}
Game is loading...
Game is exiting...
There is no game process!
Game is loading...
Game is exiting...

 

更改需求

但存在一種特殊情況:遇上了某款“渣渣游戲”,其在響應網路方面的功能沒有很完善,我的iPhoneX在載入這款游戲的過程中某個瞬間網路狀況不太好,進入了“游戲無限載入”狀態,最後只能選擇退出游戲後再重新嘗試打開游戲。

下麵是加入“無限載入狀態”的手機狀態圖:

 

  

 

如果在第原版本的代碼進行維護, 需要作如下修改:

 

  

 

可這樣的代碼不易於擴展,我的iPhoneX每新增一個狀態,需要MyiPhoneX類做出的改變地方可能會更多,這樣的設計不符合“對修改關閉,對擴展開放”的原則、也不夠面向對象。

而且這還只是一個只有兩三個狀態的簡單例子,如果是狀態及操作方法較多的程式,按照以上的設計,代碼臃腫混亂,每新增一個狀態,需要在每個操作方法里都新增一個 else if 判斷,所有的代碼維護都堆積在一個類中。

 

新的設計

為了更容易的維護和修改程式,我們需要對原代碼重新設計:

我們將每個狀態的行為都放在各自的類中(將會改變的那部分封裝起來),那麼每個狀態類只要實現它自己的動作就可以了,對iPhoneX執行的動作只需要委托當前狀態狀態對象具體執行就好了。我們可以這麼做:

①   先定義一個State介面,iPhoneX可以執行的每一個動作都有對應的方法在這個介面內。(面向介面編程

②   iPhoneX的每個狀態都有一個對應的實現了State介面的具體狀態類

③   對iPhoneX執行動作委托給具體狀態類來執行。

狀態介面和其具體實現類的類圖如下:

 

  

 

實際代碼:

public interface State {
    void startGame();

    void exitGame();
}

 

具體狀態實現類主要做的是實現適合所在的這個狀態的具體動作。

 

public class Standby implements State {  //“待機”狀態
    private MyiPhoneX    iPhoneX;

    public Standby(MyiPhoneX iPhoneX) {
        this.iPhoneX = iPhoneX;
    }

    @Override
    public void startGame() {
        System.out.println("Game is loading..."); // 游戲正在載入
        iPhoneX.setState(iPhoneX.getPlaying()); // iPhone修改為“游戲中狀態”
    }

    @Override
    public void exitGame() {
        System.out.println("There is no game process!");// 不存在該游戲進程,這是此狀態的一個不恰當動作
    }
}

 

 

public class Playing implements State {  //“游戲中”狀態
    private MyiPhoneX iPhoneX;

    public Playing(MyiPhoneX iPhoneX) {
        this.iPhoneX = iPhoneX;
    }

    @Override
    public void startGame() {
        System.out.println("Game is already in progress!");// 已存在該游戲進程
    }

    @Override
    public void exitGame() {
        System.out.println("Game is exiting..."); // 退出游戲中
        iPhoneX.setState(iPhoneX.getStandby()); // iPhone修改為“待機狀態”
    }
}

 

 

我的iPhoneX實體類:

public class MyiPhoneX {
    private State    standby;
    private State    playing;
    private State    state    ;    // 持有當前狀態的實例變數

    public MyiPhoneX() {
        standby = new Standby(this);
        playing = new Playing(this);
        state = standby;
    }

    public void startGame() { // 打開游戲
        state.startGame();
    }

    public void exitGame() { // 結束游戲
        state.exitGame();
    }

    public void setState(State state) { // 用於修改手機當前狀態
        this.state = state;
    }

    public State getStandby() {
        return standby;
    }

    public State getPlaying() {
        return playing;
    }
}

 

實現需求

將新的“游戲無限載入”狀態加上,代碼修改如下:

增加遇到渣渣游戲的“游戲無限載入”狀態類:

public class RubbishGame implements State { //“游戲無限載入”狀態
    private MyiPhoneX iPhoneX;

    public RubbishGame(MyiPhoneX iPhoneX) {
        this.iPhoneX = iPhoneX;
    }

    @Override
    public void startGame() {
        System.out.println("Game is already in progress!");// 已存在該游戲進程
    }

    @Override
    public void exitGame() {
        System.out.println("Game is exiting..."); // 退出游戲中
        iPhoneX.setState(iPhoneX.getStandby()); // iPhone修改為“待機狀態”
    }
}

 

在MyiPhoneX類中添加引用“游戲無限載入”狀態的實例變數:

public class MyiPhoneX {
    private State    standby;
    private State    playing;
    private State    rubbishGame; //添加遇到渣渣游戲的“游戲無限載入”狀態
    private State    state;        // 持有當前狀態的實例變數

    public MyiPhoneX() {
        standby = new Standby(this);
        playing = new Playing(this);
        rubbishGame = new RubbishGame(this); //初始化“游戲無限載入”狀態
        state = standby;
    }

    public void startGame() { // 打開游戲
        state.startGame();
    }

    public void exitGame() { // 結束游戲
        state.exitGame();
    }

    public void setState(State state) { // 用於修改手機當前狀態
        this.state = state;
    }

    public State getStandby() {
        return standby;
    }

    public State getPlaying() {
        return playing;
    }

    public State getRubbishGame() { //提供獲取“游戲無限載入”狀態的方法
        return rubbishGame;
    }
}

 

修改“待機”狀態類,我們用隨機數代表遇到“渣渣游戲”的機會,增加從“待機”到“游戲無限載入”狀態的轉換的代碼:

public class Standby implements State {
    private MyiPhoneX    iPhoneX;
    private Random        randomRubbish    = new Random(); //用於產生隨機數

    public Standby(MyiPhoneX iPhoneX) {
        this.iPhoneX = iPhoneX;
    }

    @Override
    public void startGame() {
        int rubbish = randomRubbish.nextInt(10);
        if (rubbish <= 1) {
            System.out.println("Rubbish Game!"); // 游戲正在載入
            iPhoneX.setState(iPhoneX.getRubbishGame()); // iPhone修改為“游戲中狀態”
        } else {
            System.out.println("Game is loading..."); // 游戲正在載入
            iPhoneX.setState(iPhoneX.getPlaying()); // iPhone修改為“游戲中狀態”
        }

    }

    @Override
    public void exitGame() {
        System.out.println("There is no game process!");// 不存在該游戲進程,這是此狀態的一個不恰當動作
    }

}

測試代碼:

 

public class MyiPhoneXTest {
    public static void main(String[] args) {
        MyiPhoneX iPhoneX = new MyiPhoneX();
        iPhoneX.startGame();
        iPhoneX.exitGame();
        iPhoneX.startGame();
        iPhoneX.exitGame();
        iPhoneX.startGame();
        iPhoneX.exitGame();
        iPhoneX.startGame();
        iPhoneX.exitGame();
    }
}
Rubbish Game!
Game is exiting...
Game is loading...
Game is exiting...
Rubbish Game!
Game is exiting...
Game is loading...
Game is exiting...

 

這樣就為我的iPhoneX增加了一個新的狀態,並實現了這個新的狀態。要做的只是新增這個狀態類及增加能夠轉換到這個狀態的代碼。

相對最早的代碼版本,刪除了容易產生問題的if語句,日後更好維護,讓大多數狀態類“對修改關閉”,讓MyiPhoneX“對擴展開放”,而且這樣的代碼結構,對應起相關的狀態圖也更容易閱讀和理解。

這樣設計的好處在這個比較簡單的例子里看起來可能不是很明顯;假如是在多個狀態的程式,我們新增一個狀態,只需新增這個狀態的實體類、在主體類中加入這個新狀態類的實例引用、在能夠轉換到這個狀態的其他狀態類加入轉換的邏輯代碼就可以了。

 

狀態模式:

以上的新設計採用了狀態模式,狀態模式允許對象在內部狀態改變時改變它的行為,對象看起來好像修改了它的類。

這個模式將狀態封裝成獨立的類,並將動作委托到代表當前狀態的對象,我的iPhoneX在“待機”和“游戲中”兩種不同狀態時,如果執行“開始游戲”的動作,就會得到不同的具體行為(提示已開啟游戲和啟動游戲)。

我們使用組合通過簡單引用不同的狀態對象來造成類改變的假象(執行一個動作時,當前的狀態對象的具體動作里可能會轉換內部的狀態,而客戶的視角是主體對象自己發生了改變,沒有意識到是狀態對象起的作用)。

狀態模式的類圖:

 

  

 

我們可以發現,它和策略模式的類圖是一樣的。

兩個模式的差別在於它們的“意圖”不同。

①對狀態模式而言

將具體行為封裝在狀態類中,Context的行為委托給當前具體狀態對象來執行,執行具體行為後可能會改變當前狀態,這樣Context執行的具體行為也在改變(每個狀態類對每種行為都有各自的具體實現)。而使用Context的客戶對於狀態對象卻是陌生的、甚至是未知的。這個方案可以去掉原本存在Context內的許多的條件判斷,通過Context對象內簡單的改變狀態對象來改變行為。

②對策略模式而言

客戶對於策略對象是很熟悉的,通常由客戶主動選擇Context需要組合的策略對象,而且雖然策略模式可以在運行時改變策略,但對於某個Context對象來說,通常只有一個最適當的策略對象。一般用來作為除了繼承之外的一種彈性替代方案,通過組合不同對象來改變行為。

 

參考書籍:《Head First 設計模式》


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

-Advertisement-
Play Games
更多相關文章
  • Vue.js之路由 以前的跳轉都是使用a標簽,a標簽里有一個屬性叫href,給他一個對應的網路地址或者一個路徑的話,它就會幫助跳轉到對應的頁面。 Vue.js的路由,其實跟我們的a標簽實現的功能是一樣的,我們也是實現一個對應的跳轉,只不過路由的性能更優化,a標簽不管點擊多少次,都會發生對應的網路請求 ...
  • 偽元素和偽類之所以這麼容易混淆,是因為他們的效果類似而且寫法相仿,但實際上 css3 為了區分兩者,已經明確規定了偽類用一個冒號來表示,而偽元素則用兩個冒號來表示。 :Pseudo-classes ::Pseudo-elements 但因為相容性的問題,所以現在大部分還是統一的單冒號,但是拋開相容性 ...
  • 預設字體大小是16px 在<html>中設置字體大小 與em的區別: em是在父級設置字體大小受影響 移動端適配 首先獲取屏幕的寬度 計算當前屏幕寬度和640的比例 計算出font-size的值 改變html的font-size的值 <!DOCTYPE html> <html lang="en" s ...
  • 在我們程式員的日常開發中,總會時不時的需要用到地圖開發,我也在多次碰到之後,寫下我對地圖開發的理解經驗和總結。 一、地圖的選擇 回想一下我們生活中用到的地圖工具,數了一下,百度地圖,高德地圖,騰訊地圖,谷歌地圖,其他。 1、作為開發者,我們應該選擇普遍被大眾認可的地圖平臺,所以其他這個選項中,除去最 ...
  • // 全圖預設背景 // backgroundColor: ‘rgba(0,0,0,0)’, // 預設色板 color: ['#ff7f50','#87cefa','#da70d6','#32cd32','#6495ed', '#ff69b4','#ba55d3','#cd5c5c','#ffa5 ...
  • 原地址:http://blog.csdn.net/she_lover/article/details/51448967theme = { // 全圖預設背景 // backgroundColor: ‘rgba(0,0,0,0)’, // 預設色板 color: ['#ff7f50','#87cefa ...
  • 在使用`React Native`開發中,我們熟練的採用`JavaScript`的方式發送請求的方式發送一個請求到服務端,但是處理這個請求的過程其實和處理`Web`應用中發送的請求的過程是不一樣的。因為處理這個請求的目標不是瀏覽器,而是嵌入這個應用的原生操作系統。 ![banner](https... ...
  • <!doctype html><html><head><meta charset="utf-8"><title>無標題文檔</title></head><style>#div1{position:relative;}#div1 div{width:50px;height:50px;position: ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...