策略模式的孿生兄弟——對狀態模式的深度複習總結

前言

前面有總結——策略模式,以前早就以爲策略和狀態設計模式有一些類似……參考:繼承、組合和接口用法——策略模式複習總結 該模式其實也很經常使用,我常常把它和策略模式結合着用,來減小大量的 if-else 代碼片斷。html

策略模式是對象的行爲模式,其實就是對一系列級別平等的算法的封裝,它不關心算法實現,讓客戶端去動態的依靠 「環境」 類去選擇須要的算法,由於他們能互相替換,能夠說策略模式使能一系列算法能夠平滑的切換。那麼狀態(State)模式,也是對象的行爲設計模式的一種。java

狀態模式的演進和實現

官方教科書是這樣定義的:正則表達式

狀態模式容許經過改變對象的內部狀態而改變對象的行爲,這個對象表現得就好像修改了它的類同樣。算法

呵呵,記得早以前學設計模式,學到狀態模式的概念,這麼一看,這是解釋的雞毛啊…… 先直接看個小例子,順着它的定義來推演:編程

/**
 * Person*/
public class Person {
    /**
     * 這我的有一個鬧錶,靠它的時間變化(狀態修改)來決定什麼時候作什麼(改變行爲)
     */
    private int hour;

    public int getHour() {
        return hour;
    }

    public void setHour(int hour) {
        this.hour = hour;
    }

    /**
     * 人的一個行爲
     *
     * 狀態模式容許經過改變一個對象的內部狀態,來改變對象的行爲,就像修改了對象的類同樣!
     */
    public void doSth() {
        // 那麼我就模擬修改類的對象的內部狀態
        if (this.hour == 7) {
            System.out.println("起牀啦!");
        } else if (this.hour == 11) {
            System.out.println("吃中午餐了!");
        } else if (this.hour == 19) {
            System.out.println("吃晚飯了!");
        } else if (this.hour == 22) {
            System.out.println("睡覺咯!");
        } else {
            System.out.println("學習呢!");
        }
    }
}

public class MainState {
    public static void main(String[] args) {
        Person person = new Person();

        person.setHour(7);
        person.doSth();// 起牀啦!

        person.setHour(11);
        person.doSth();// 吃中午餐了!

        person.setHour(19);
        person.doSth();// 吃晚飯了!

        person.setHour(22);
        person.doSth();// 睡覺咯!

        person.setHour(10);
        person.doSth();// 學習呢!
    }
}

這個例子,確實就是狀態模式描述的場景,有一個Person類,它有一個時間對象——鬧錶,經過時間的變化(修改對象的內部狀態)來改變對象的行爲(人的一些睡覺,學習的行爲),這個對象表現的就比如修改了它的類同樣。設計模式

可是,這個例子並無使用狀態模式來實現,Person類設計的很low,由於有大量的 if-else 不易維護……那麼這個場景下,應該使用本文提到的狀態模式。嘗試實現:數組

public abstract class State {
    /**
     * 抽象狀態(接口)角色,封裝了和環境類(Person類)的對象的狀態(鬧錶時間的變化)相關的行爲
     */
    public abstract void doSth();
}

public class GetUp extends State {
    /**
     * 各個具體的狀態角色,實現狀態類,
     */
    @Override
    public void doSth() {
        System.out.println("起牀啦!");
    }
}

public class HaveDinner extends State {
    @Override
    public void doSth() {
        System.out.println("吃晚飯了!");
    }
}

public class HaveLunch extends State {
    @Override
    public void doSth() {
        System.out.println("吃中午餐了!");
    }
}

public class Sleep extends State {
    @Override
    public void doSth() {
        System.out.println("睡覺咯!");
    }
}

public class Study extends State {
    @Override
    public void doSth() {
        System.out.println("學習呢!");
    }
}

public class Person {
    /**
     * 這我的有一個鬧錶,靠它的時間變化(狀態修改)來決定什麼時候作什麼(改變行爲)
     */
    private int hour;

    private State state;

    public int getHour() {
        return hour;
    }

    public void setHour(int hour) {
        this.hour = hour;
    }

    /**
     * 人(環境類)的個行爲
     *
     * 狀態模式容許經過改變一個對象的內部狀態,來改變對象的行爲,就像修改了對象的類同樣!
     */
    public void doSth() {
        if (this.hour == 7) {
            state = new GetUp();
            state.doSth();
        } else if (this.hour == 11) {
            state = new HaveLunch();
            state.doSth();
        } else if (this.hour == 19) {
            state = new HaveDinner();
            state.doSth();
        } else if (this.hour == 22) {
            state = new Sleep();
            state.doSth();
        } else {
            state = new Study();
            state.doSth();
        }
    }
}

public class MainStateA {
    public static void main(String[] args) {
        Person person = new Person();

        person.setHour(7);
        person.doSth();// 起牀啦!

        person.setHour(11);
        person.doSth();// 吃中午餐了!

        person.setHour(19);
        person.doSth();// 吃晚飯了!

        person.setHour(22);
        person.doSth();// 睡覺咯!

        person.setHour(10);
        person.doSth();// 學習呢!
    }
}

確實有了變化,把以前的Person類對象的內部狀態的改變對應的Person行爲的變化作了封裝,變成了類來表示,可是並無什麼實質上的改變。安全

Person類依然有大量不易維護的if-else語句,而狀態模式的使用目的就是控制一個對象狀態轉換的條件表達式過於複雜時的狀況——把狀態的判斷邏輯轉譯到表現不一樣狀態的一系列類當中,能夠把複雜的判斷邏輯簡化。網絡

上一版本沒有把對應狀態的判斷邏輯同時轉移,仍是留在了環境類(Person類)裏……繼續優化:數據結構

public abstract class State {
    /**
     * 抽象狀態(接口)角色,封裝了和環境類(Person類)的對象的狀態(鬧錶時間的變化)相關的行爲
     */
    public abstract void doSth(PersonB personB);
}

public class GetUp extends State {
    /**
     * 各個具體的狀態角色,實現狀態類,
     */
    @Override
    public void doSth(PersonB personB) {
        if (personB.getHour() == 7) {
            System.out.println("起牀啦!");
        } else {
            // 轉移狀態
            personB.setState(new HaveLunch());
            // 必需要調用行爲
            personB.doSth();
        }
    }
}

public class HaveDinner extends State {
    @Override
    public void doSth(PersonB personB) {
        if (personB.getHour() == 19) {
            System.out.println("吃晚飯了!");
        } else {
            personB.setState(new Sleep());
            personB.doSth();
        }
    }
}

public class HaveLunch extends State {
    @Override
    public void doSth(PersonB personB) {
        if (personB.getHour() == 11) {
            System.out.println("吃中午餐了!");
        } else {
            personB.setState(new HaveDinner());
            personB.doSth();
        }
    }
}

public class Sleep extends State {
    @Override
    public void doSth(PersonB personB) {
        if (personB.getHour() == 22) {
            System.out.println("睡覺咯!");
        } else {
            personB.setState(new Study());
            personB.doSth();
        }
    }
}

public class Study extends State {
    @Override
    public void doSth(PersonB personB) {
        // 如此,不再須要向下傳遞狀態了!
        System.out.println(personB.getHour() + "點,正學習呢!");
    }
}

把以前放到環境類裏的對當前對象狀態的邏輯判斷(條件表達式……),隨着不一樣的狀態放到了對應的狀態類裏,且同時讓狀態動態的遷移——這裏又有責任鏈模式的影子。並且繼承的抽象狀態類的行爲方法里加上了環境類的對象做爲參數。以起牀狀態爲例:

public void doSth(PersonB personB) {
        if (personB.getHour() == 7) {
            System.out.println("起牀啦!");
        } else {
            // 轉移狀態
            personB.setState(new HaveLunch());
            // 必需要調用行爲
            personB.doSth();
        }
    }

當getup狀態類的if判斷不知足時,就轉移狀態到下一個——set 一個新狀態去覆蓋舊狀態……同時記得調用下一個狀態的行爲(執行doSth方法)。

PS:這裏很是像責任鏈(職責鏈)模式。最後一個狀態——學習類,沒有轉移的其餘狀態了,那麼就不須要轉移,直接設置爲終結狀態(在責任鏈模式裏是依靠判斷get到的連接對象是否爲null來判斷職責鏈條的終點的)。

以下:

public class Study extends State {
    @Override
    public void doSth(PersonB personB) {
        // 如此,最後一個狀態(或者說表明其餘的狀態)不再須要向下傳遞狀態了!
        System.out.println(personB.getHour() + "點,正學習呢!");
    }
}

再看環境類,和客戶端(客戶端代碼不須要變化)

public class PersonB {
    /**
     * 這我的有一個鬧錶,靠它的時間變化(狀態修改)來決定什麼時候作什麼(改變行爲)
     */
    private int hour;

    private State state;

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public int getHour() {
        return hour;
    }

    public void setHour(int hour) {
        this.hour = hour;
    }

    public PersonB() {
        // 在構造器裏初始化狀態,從早晨起牀開始
        this.state = new GetUp();
    }

    /**
     * 人(環境類)的個行爲
     *
     * 狀態模式容許經過改變一個對象的內部狀態,來改變對象的行爲,就像修改了對象的類同樣!
     */
    public void doSth() {
        // 傳入的是PersonB的對象
        state.doSth(this);
    }
}

public class MainStateB {
    public static void main(String[] args) {
        PersonB personB = new PersonB();

        personB.setHour(7);
        personB.doSth();

        personB.setHour(11);
        personB.doSth();

        personB.setHour(19);
        personB.doSth();

        personB.setHour(22);
        personB.doSth();

        personB.setHour(10);
        personB.doSth();
    }
}

打印:

起牀啦!
吃中午餐了!
吃晚飯了!
睡覺咯!
10點,正學習呢!

貌似 ok。。。睡覺到次日,早晨又該起牀……給客戶端順序增了一個7點的狀態

public class MainStateB {
    public static void main(String[] args) {
        PersonB personB = new PersonB();

        personB.setHour(7);
        personB.doSth();

        personB.setHour(11);
        personB.doSth();

        personB.setHour(19);
        personB.doSth();

        personB.setHour(22);
        personB.doSth();

        personB.setHour(10);
        personB.doSth();

        personB.setHour(7);
        personB.doSth();// 有問題
    }
}

發現打印以下:

起牀啦!
吃中午餐了!
吃晚飯了!
睡覺咯!
10點,正學習呢!
7點,正學習呢!

相對完美的狀態模式實現

分析前面的問題:7點應該是「起牀啦!」。

這說明以前的狀態模式的實現代碼有問題,問題出在環境類(Person類)的初始化上,客戶端 new 了一我的,則person的構造器自動初始化狀態爲getup,把對象的內部狀態修改,會去尋找對應的狀態類,找不到就遷移到下一個狀態,它的狀態遷移是單向不可逆的……如圖:

優化以下,只需修改環境類——Person,每次搜索,都要重置狀態,即從getup 開始搜索,核心思想是每次對象內部狀態改變以後,都把狀態遷移復位一下。記住是搜索一次以後復位

public class PersonB {
    /**
     * 這我的有一個鬧錶,靠它的時間變化(狀態修改)來決定什麼時候作什麼(改變行爲)
     */
    private int hour;

    private State state;

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public int getHour() {
        return hour;
    }

    public void setHour(int hour) {
        this.hour = hour;
    }

    public PersonB() {
        // 在構造器裏初始化狀態,從早晨起牀開始
        this.state = new GetUp();
    }

    /**
     * 人(環境類)的個行爲
     *
     * 狀態模式容許經過改變一個對象的內部狀態,來改變對象的行爲,就像修改了對象的類同樣!
     */
    public void doSth() {
        // 傳入的是PersonB的對象
        state.doSth(this);
        // 每次都從頭開始搜索狀態類
        this.state = new GetUp();
    }
}

這樣就ok了。

小結:狀態模式隱含着責任鏈模式的部分思想,而UML類圖的設計上和策略模式很是類似,下面繼續分析。

狀態模式的實戰寫法——結合單例模式

前面的例子確實是狀態模式,可是每次復位狀態的時候,還有搜索狀態的時候,都要new 一個狀態對象,太浪費內存了,故每每實際工程裏,都會結合單例模式,把每一個狀態類都搞成單例,若是實在搞不成,就要從新思考設計了。

參考:最簡單的設計模式——單例模式的演進和推薦寫法(Java 版)

狀態模式都有哪些角色?畫出類圖?

Context:用戶對象,擁有(聚合)一個State類型的成員,以標識對象的當前狀態,就是Person類

State:接口或基類,封裝與Context的特定狀態相關的行爲;

ConcreteState:接口實現類或子類,實現了一個與Context某個狀態相關的行爲。

是否是和策略模式的類圖很像很像:一樣的一個抽象類(接口),包含一個行爲,和N個具體實現的類,外加一個環境類(聚合了接口引用)……

狀態模式和策略模式的比較

兩個模式的實現類圖雖然一致,可是實現目的不同。

首先,策略模式是一個接口的應用案例,一個很重要的設計模式,簡單易用,通常用於單個算法的替換,客戶端事先必須知道全部的可替換策略,由客戶端去指定環境類須要哪一個策略,注意一般都只有一個最恰當的策略(算法)被選擇。其餘策略是同級的,可互相動態的在運行中替換原有策略。

而狀態模式的每一個狀態類須要包含環境類(Context)中的全部方法的具體實現——條件語句。經過把行爲和行爲對應的邏輯包裝到狀態類裏,在環境類裏消除大量的邏輯判斷,而不一樣狀態的切換由繼承(實現)State的狀態子類去實現,當發現修改的當前對象的狀態不是本身這個狀態所對應的參數,則各個狀態子類本身給Context類切換狀態(有職責鏈模式思想),且客戶端不直接和狀態類交互,客戶端不須要了解狀態。

這點和策略模式不同,策略模式是直接依賴注入到Context類的參數進行選擇策略,不存在切換狀態的操做,客戶端須要瞭解策略。

聯繫;狀態模式和策略模式都是爲具備多種可能情形設計的模式,把不一樣的處理情形抽象爲一個相同的接口(抽象類),符合對開閉原則,且策略模式更具備通常性,在實踐中,能夠用策略模式來封裝幾乎任何類型的規則,只要在分析過程當中聽到須要在不一樣實踐應用不一樣的業務規則,就能夠考慮使用策略模式處理,在這點上策略模式是包含狀態模式的功能的

狀態模式的使用場景

狀態模式主要解決的是:控制一個對象內部的狀態轉換的條件表達式過於複雜時的狀況,且客戶端調用以前不須要了解具體狀態。它把狀態的判斷邏輯轉到表現不一樣狀態的一系列類當中,能夠把複雜的判斷邏輯簡化。維持開閉原則,方便維護,還有重要一點下面會總結,狀態模式是讓各個狀態對象本身知道其下一個處理的對象是誰,即在狀態子類編譯時在代碼上就設定好了。

狀態模式的優缺點  

優勢,前面說了不少了……

一、狀態模式使得代碼中複雜而庸長的邏輯判斷語句問題獲得瞭解決,並且狀態角色將具體的狀態和他對應的行爲及其邏輯判斷封裝了起來,這使得增長一種新的狀態顯得十分簡單。

二、把容易出錯的if-else語句在環境類 or 客戶端中消除,方便維護

三、每個狀態類都符合「開閉」原則——對狀態的修改關閉,對客戶端的擴展開放,能夠隨時增長新的Person的狀態,或者刪除。

四、State類在只有行爲須要抽象時,就用接口,有其餘共同功能能夠用抽象類,這點和其餘一些(策略)模式相似。

缺點,我的認爲微不足道

使用狀態模式時,每一個狀態對應一個具體的狀態類,使結構分散,類的數量變得不少,使得程序結構變得稍顯複雜,閱讀代碼時相對以前比較困難,不過對於優秀的研發人員來講,應該是微不足道的。

由於想要獲取彈性,就必須付出代價,除非程序是一次性的,用完就丟掉……若是不是,那麼假設有一個系統,某個功能須要不少狀態,若是不使用狀態模式優化,那麼在環境類(客戶端類)裏會有大量的整塊整塊的條件判斷語句。這才尼瑪是真正的變得很差理解,lz我是實習生的時候,在xx公司(匿名了)就見過有人寫這樣的代碼,一個方法或者一個類,動不動幾千行代碼……重要的是裏面一大塊一大塊的if-else……還倍感優越。。。看,我寫的快不快。。。

狀態模式偏偏是看着類多了,實際上是讓狀態變的清晰,讓客戶端和環境類都彼此乾淨,更加方便理解和維護。

實際編程中,面對大量的if-else,switch-case邏輯判斷,如何優化?

有時業務不是很複雜,參數校驗不是不少的時候,固然可使用if或者if-else邏輯塊或者switch-case塊來進行編碼,可是一旦擴展了程序,增長了業務,或者開始就有不少不少的邏輯判斷分支,這並非一件好事,它首先不知足OCP——開閉原則,一旦須要修改判斷方法或者類,那麼牽一髮動全身,經常整個邏輯塊都須要大改,責任沒有分解,對象內部狀態的改變和對應邏輯都雜糅在了一塊兒,也不符合單一職責原則,偏偏此時,我但願分解整個判斷過程,分離職責,把狀態的判斷邏輯轉移到表示不一樣狀態的一系列類當中,把複雜的判斷邏輯簡化,這就是剛剛說的狀態模式。狀態模式把當前類對象的內部的各類狀態轉移邏輯分佈到State抽象類的子類中,這樣減小了各個邏輯間的依賴,客戶端也不須要實現瞭解各個狀態。

不過,綜上總結,我發現,狀態模式是讓各個狀態對象本身知道其下一個處理的對象是誰!即在編譯時在代碼上就設定好了!好比以前例子的狀態子類:

public class GetUp extends State {
    /**
     * 各個具體的狀態角色,實現狀態類,
     */
    @Override
    public void doSth(PersonB personB) {
        if (personB.getHour() == 7) {
            System.out.println("起牀啦!");
        } else {
            // 轉移狀態,明確知道 要轉移到哪一個 已有 的狀態!
            personB.setState(new HaveLunch());
            // 必需要調用對應狀態的行爲
            personB.doSth();
        }
    }
}

 

若是有一種複雜邏輯判斷,好比公司考勤系統處理員工請假的流程,不一樣級別,類型,部門等的員工的請假流程是不同的,咱們沒法知道員工該狀態的下一個狀態是什麼。。。

好比老王是臨時工,請假只須要直接領導批准,老李是正式工,請假須要先讓直接領導審批,再交給主管批准,老張是安所有門的員工,請假須要的流程更復雜……或者哪天系統變化升級,請假制度修改了……換句話說就是請假系統裏請假相關的各個對象並不指定(也不知道)其下一個處理的對象究竟是誰,只有在客戶端才設定。這怎麼辦?這就須要責任鏈設計模式解決,二者類圖不同,具體解耦責任,轉移對象的流程略微的不同,可是總的目標一致:參考:大量邏輯判斷優化的思路——責任鏈模式複習總結及其和狀態模式對比

狀態模式和職責鏈模式對比

大致上看,責任鏈模式要比狀態模式靈活,雖然職責鏈模式靈活,可是遵循夠用原則,好比前面的狀態模式的例子:Person類的鬧錶記錄一天的狀態及其對應的行爲,各個狀態(判斷邏輯)明確知道其下一個狀態(處理對象)是誰,在內部編碼時就肯定了,狀態模式就ok了,用責任鏈就顯得很呵呵,適合就好。

還有簡單情景下,可使用三元運算符 condition ?  : 代替簡單的if-else語句,或者數組這種隨機存儲乃至查詢性能很好的數據結構替換switch-case。

可是我想的是設計模式的陰暗面,不要爲了用設計模式而用設計模式,對於switch-case語句塊,也不要過分優化,數量不是很大時,switch的性能也不差,不必優化什麼,想起來《Java編程思想》做者和《重構》一書做者都說過的:

等到無可奈何必需要這麼作的時候,再想優化,不要陷入優化和設計模式的陷阱

JDK裏都有哪些類有狀態模式的應用?

常見的就是java.util.Iterator  

拓展:什麼是有限狀態機?在Java中有什麼應用?

先看教科書的具體定義:

(Finite-state machine, FSM),又稱有限狀態自動機,是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學模型。

FSM是一種算法思想,簡單而言,有限狀態機由一組狀態、一個初始狀態、輸入和根據輸入及現有狀態轉換爲下一個狀態的轉換函數組成。

本文總結的State模式(狀態模式)其實本質就是一種面向對象的狀態機思想,能夠適應很是複雜的狀態管理。

它反映從系統開始到如今時刻輸入的變化,以及各個狀態之間轉移的指示變動,並且必須使用能知足狀態轉移的條件來描述,狀態機裏的動做是在給定時刻要進行的活動描述,狀態機裏的狀態存儲的是有關於過去的信息,它有多種類型的動做:

  • 進入動做(entry action):在進入狀態時進行;
  • 退出動做:在退出狀態時進行;
  • 輸入動做:依賴於當前狀態和輸入條件進行;
  • 轉移動做:在特定轉移時進行。

說了那麼多,它到底能幹嗎的呢,其實不論編程仍是生活裏,狀態機無時不在。我知道,編程是對現實的抽象,狀態機也是,當業務邏輯裏有大量邏輯判斷須要各類來回的轉換狀態時,有限狀態機就有用了,本質上其是用查表法把處理邏輯獨立到表中:

能夠用通用的代碼去處理任意複雜的狀態轉換,擴展開來,任何複雜狀態邏輯的處理均可以好比:

  • Java的多線程裏,線程的狀態轉移,就可使用狀態機來描述
  • 常常須要使用的正則表達式,判斷字符串格式和解析字符串內容基本全靠它,正則表達式就是有限狀態機。僅僅表達形式不一樣,正則表達式寫好後能夠經過程序「編譯」成狀態轉換表,就是你們常見的狀態轉換圖。
  • 各類網絡協議,記得上計算機網絡課時老師講過——全部的協議定義都有明確的「有限狀態機」設計,爲此國際電信聯盟專門出了規格描述語言SDL(Specification and Description Language)來描述有限狀態機。
  • 衆所周知的自動客服系統(如10086:接通以後大堆話,按1給查……按2查……按0轉……按xx返回xx……)
  • 編譯器設計中,詞法分析和語法分析都會用到
  • 字符串匹配的 KMP 算法也是自動機算法的一種
  • 遊戲開發和設計中,好比一個NPC就是一個很典型的狀態機,當玩家按下前進鍵時,它會從正常狀態轉移到向前走的狀態……
相關文章
相關標籤/搜索