設計模式之——狀態模式與策略模式

如果你的簡歷里出現了"設計模式"的字樣,那么作為面試官的我幾乎都會問到一個問題: "狀態模式與策略模式有哪些區別"。很多人一臉懵,我就知道這次愉快的技術交流無疾而終了。可能對于很多人來說,策略模式比較熟悉,可什么是狀態模式,好多人還是比較迷糊的。此篇專題,我們就來聊聊狀態模式與策略模式。

第一部分 狀態模式

考慮這樣的一個場景:一個電梯,有四種操作:運行停止開門關門。每一種操作成功后,都對應著狀態的切換。每一種狀態,又可以隨著操作,向另一種狀態切換。但是狀態與狀態之間又不是隨意切換的。如下表所示:

  • 運行狀態

    • 可以向停止狀態切換
    • 不能再次切換到運行狀態
    • 不能在電梯的運行過程中開門
    • 不能在電梯的運行過程中關門 - 因為運行的過程中,電梯的門必然是關的
  • 停止狀態

    • 可以向運行狀態切換
    • 可以向開門狀態切換
    • 可以向關門狀態切換
    • 不能再次切換到停止狀態
  • 開門狀態

    • 可以向關門狀態切換
    • 不能再次切換到開門狀態
    • 不能在開門的狀態下運行
    • 不能在開門的狀態下停止 - 因為開門狀態下,電梯的狀態必然是停止的
  • 關門狀態

    • 可以向運行狀態切換
    • 可以向開門狀態切換
    • 不能向停止狀態切換 - 因為在關門狀態下,電梯必然是停止的
    • 不能再次切換到關門狀態

說得比較復雜,看一個狀態機圖

有箭頭的,就是允許;沒有箭頭的,就不允許

Lift-State.png

1 錯誤示范

if - else真是個害人精,它讓我們在實現功能的時候,不必過多地思考,很多沒有研習過狀態模式的同學也是可以輕松實現的——只不過沒那么優美罷了。

  • 新建一個枚舉,列出四種狀態
  • 電梯有4個方法
  • 電梯有1種狀態
Lift-Design.png
  • LiftState.java

    package com.futureweaver.enums;
    
    // 電梯狀態
    public enum LiftState {
        // 開門狀態
        Opening,
    
        // 關門狀態
        Closed,
    
        // 運行狀態
        Running,
    
        // 停止狀態
        Stoped
    }
    
  • Lift.java

    package com.futureweaver.domain;
    
    import com.futureweaver.enums.LiftState;
    
    // 電梯
    public class Lift {
        private LiftState state = LiftState.Closed;
    
        // 開門
        public void open() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 無法重復開門");
            } else if (state == LiftState.Closed) {
                state = LiftState.Opening;
                System.out.println("success: 開門");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 運行狀態下不能開門");
            } else/* if (state == LiftState.Stoped)*/ {
                state = LiftState.Opening;
                System.out.println("success: 開門");
            }
        }
    
        // 關門
        public void close() {
            if (state == LiftState.Opening) {
                state = LiftState.Closed;
                System.out.println("success: 關門");
            } else if (state == LiftState.Closed) {
                System.out.println("failure: 無法重復關門");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 運行狀態下,就一定是關門狀態了");
            } else/* if (state == LiftState.Stoped)*/ {
                System.out.println("failure: 停止后就是關門狀態了");
            }
        }
    
        // 運行
        public void run() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 電梯門沒關,不能運行");
            } else if (state == LiftState.Closed) {
                state = LiftState.Running;
                System.out.println("success: 運行");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 無法重復運行");
            } else/* if (state == LiftState.Stoped)*/ {
                state = LiftState.Running;
                System.out.println("success: 運行");
            }
        }
    
        // 停止
        public void stop() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 開門狀態下不會運行,自然也不需要停止");
            } else if (state == LiftState.Closed) {
                System.out.println("failure: 關門狀態下不會運行,自然也不需要停止");
            } else if (state == LiftState.Running) {
                state = LiftState.Stoped;
                System.out.println("success: 停止");
            } else/* if (state == LiftState.Stoped)*/ {
                System.out.println("failure: 無法重復停止");
            }
        }
    }
    
  • LiftTest.java

    package com.futureweaver.domain;
    
    import org.junit.Test;
    
    public class LiftTest {
        @Test
        public void testLift() {
            Lift lift = new Lift();
    
            lift.close();
            lift.close();
            lift.open();
            lift.run();
            lift.open();
            lift.stop();
        }
    }
    
  • 輸出

    failure: 無法重復關門
    failure: 無法重復關門
    success: 開門
    failure: 電梯門沒關,不能運行
    failure: 無法重復開門
    failure: 開門狀態下不會運行,自然也不需要停止
    

結論

從測試的結果可以看出,需求實現了,也沒什么問題。但這是我們編寫的簡單代碼,回過頭再來審視一下Lift.java,我們做了大量的條件判斷。同一個類當中的代碼量又太多——如果狀態不止4種,怎么辦?如果狀態與狀態之間的切換,業務比較復雜,不能一兩條代碼就搞得定,又怎么辦?

2 正確示范

在現實領域中,電梯狀態,自然而然就是電梯的一個屬性。然而在面向對象語言中,所謂萬物皆對象,狀態,自然也可以作為一個對象。既然狀態可以作為對象,那么就可以利用多態來解決了。

  • 新建一個電梯狀態的抽象類,定義4個操作: 打開關閉停止運行
  • 新建四個電梯狀態的子類
  • 每個實際狀態,自己判斷能否向目標狀態切換。如果能切換的話,創建目標狀態對象,并向電梯發送修改狀態的請求
Lift-Design-State-Pattern.png
  • Lift.java

    package com.futureweaver.domain;
    
    // 電梯
    public class Lift {
        private LiftState state = new ClosedState();
    
        public LiftState getState() {
            return state;
        }
    
        public void setState(LiftState state) {
            this.state = state;
        }
    
        // 開門
        public void open() {
            // 由狀態對象自己處理切換行為
            state.open(this);
        }
    
        // 關門
        public void close() {
            // 由狀態對象自己處理切換行為
            state.close(this);
        }
    
        // 運行
        public void run() {
            // 由狀態對象自己處理切換行為
            state.run(this);
        }
    
        // 停止
        public void stop() {
            // 由狀態對象自己處理切換行為
            state.stop(this);
        }
    }
    
  • LiftState.java

    package com.futureweaver.domain;
    
    // 電梯狀態
    public abstract class LiftState {
    
        // 電梯狀態的共同父類,無論向哪一個狀態切換都可以,子類自己覆蓋要阻止的操作
    
        public void open(Lift lift) {
            // 如果成功的話,直接修改電梯的"狀態"屬性
            lift.setState(new OpeningState());
            System.out.println("success: 開門");
        }
    
        public void close(Lift lift) {
            // 如果成功的話,直接修改電梯的"狀態"屬性
            lift.setState(new ClosedState());
            System.out.println("success: 關門");
        }
    
        public void stop(Lift lift) {
            // 如果成功的話,直接修改電梯的"狀態"屬性
            lift.setState(new StopedState());
            System.out.println("success: 停止");
        }
    
        public void run(Lift lift) {
            // 如果成功的話,直接修改電梯的"狀態"屬性
            lift.setState(new RunningState());
            System.out.println("success: 運行");
        }
    }
    
  • OpeningState.java

    package com.futureweaver.domain;
    
    public class OpeningState extends LiftState {
        @Override
        public void open(Lift lift) {
            System.out.println("failure: 無法重復開門");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 開門狀態下不會運行,自然也不需要停止");
        }
    
        @Override
        public void run(Lift lift) {
            System.out.println("failure: 電梯門沒關,不能運行");
        }
    
    }
    
  • ClosedState.java

    package com.futureweaver.domain;
    
    public class ClosedState extends LiftState {
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 無法重復關門");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 關門狀態下不會運行,自然也不需要停止");
        }
    }
    
  • StopedState.java

    package com.futureweaver.domain;
    
    public class StopedState extends LiftState {
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 停止后就是關門狀態了");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 無法重復停止");
        }
    }
    
  • RunningState.java

    package com.futureweaver.domain;
    
    public class RunningState extends LiftState {
        @Override
        public void open(Lift lift) {
            System.out.println("failure: 運行狀態下不能開門");
        }
    
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 運行狀態下,就一定是關門狀態了");
        }
    
        @Override
        public void run(Lift lift) {
            System.out.println("failure: 無法重復運行");
        }
    }
    
  • LiftTest.java

    package com.futureweaver.domain;
    
    import org.junit.Test;
    
    public class LiftTest {
        @Test
        public void testLift() {
            Lift lift = new Lift();
    
            lift.close();
            lift.close();
            lift.open();
            lift.run();
            lift.open();
            lift.stop();
        }
    }
    
  • 輸出

    failure: 無法重復關閉
    failure: 無法重復關閉
    success: 開門
    failure: 電梯門沒關,不能運行
    failure: 無法重復開門
    failure: 開門狀態下不會運行,自然也不需要停止
    

結論

這種實現方式,電梯與電梯狀態產生了雙向依賴,屬于一種緊耦合;電梯狀態抽象父類,與電梯狀態具體類又產生了雙向依賴,屬于一種緊耦合。理解起來有點繞,一條一條地說。

  • 電梯與電梯狀態的通信

電梯具備一個電梯狀態對象,當接收到操作請求時,電梯對象本身不做任何的判斷和處理,而是交由狀態對象處理。

當電梯狀態接收到轉換請求時,如果可以轉換,那么我們想要得到的結果是:電梯的狀態發生了變化。所以電梯狀態需要向電梯對象發送"改變狀態"的消息,那么電梯狀態就需要知道,到底是哪一個電梯對象。所以電梯狀態的轉換操作,需要接收一個電梯對象的參數。

  • 抽象狀態與具體狀態的通信

具體狀態需要繼承自抽象狀態。

抽象狀態定義了默認方法,其中的默認方法就是: 所有的操作,都是合法的。既然是合法的,就要向目標狀態切換。因為使用了狀態模式,狀態是一個對象了,所以需要創建具體狀態的對象,再把創建好的狀態對象,發送給電梯。

  • 說得比較復雜,結合看一下類圖和序列圖

    • 類圖
Lift-Design-State-Pattern.png
  • 序列圖
Lift-Sequence.png

3 狀態模式總結

State-Pattern.png
  • 狀態模式

    允許一個對象在其內部狀態改變時改變它的行為。對象看起來似乎修改了它的類。

    通過內聚,提升了靈活性、可維護性和可擴展性。

第二部分 狀態模式與策略模式

在上一篇文章《設計模式在RESTful當中的應用》當中,已經聊過策略模式,乍一看它們的類圖,是很相似的:

  • 狀態模式
State-Pattern.png
  • 策略模式
Strategy-Pattern.png

策略模式與狀態模式都把實際的行為,延遲到了子類,以此完成多態。同時,上下文(Context)面向的都是一個抽象類。(一些編程語言明確地區分了接口與抽象類,比如Java;而一些編程語言并沒有明確地區分,比如C++。OOD本身與語言的關聯是比較弱的,所以在OOP的時候,到底是面向接口還是抽象類,是需要酌情考慮的。)

狀態模式與策略模式的區別

類結構相似,想要找出狀態模式與策略模式的區別,就需要從它們的行為入手了

  • 策略模式

    在程序運行的過程中,策略與策略之間,是相互獨立的,從而耦合度是比較松的。因為本身策略中封裝的是一系列可以相互替換的算法,每一個策略是可以獨立完成自己所要完成的工作的。

    上下文(Context)依賴于策略,而策略不依賴于上下文。因為策略在工作時,并不關心這個信息是誰發送過來的。

  • 狀態模式: 允許一個對象在其內部狀態改變時改變它的行為。對象看起來似乎修改了它的類。

    在程序運行的過程中,狀態與狀態之間。比如從A狀態,過渡到B狀態,A狀態是需要獲取一個B狀態(至于到底怎么獲取,創建也好,使用注冊表也好,使用享元也好,這個就要看具體業務了)的狀態對象的。所以狀態與狀態之間是互相依賴的,耦合度是比較緊的。

    上下文(Context)依賴于狀態,而狀態又依賴于上下文。比如從A狀態,過渡到B狀態,A狀態先獲取一個B狀態,之后要找到上下文,把上下文的狀態給修改掉。所以上下文下狀態之間是互相依賴的,耦合度也是比較緊的。

第三部分 總結

綜上所述,策略模式實現起來比較簡單,是真正利用了面向對象的多態技術,完成了算法的互換使用,并且既遵循了高內聚,又遵循了松耦合的設計原則。

而狀態模式實現起來比較復雜,其亦是利用了面向對象的多態技術,完成狀態與狀態之間的過渡。雖然狀態模式遵循了高內聚的設計原則,但卻破壞了松耦合原則。

兩者都是通過內聚,提升了靈活性可維護性可擴展性。但歸根結底,兩者的區別就在于:策略模式是松耦合、狀態模式是緊耦合

打完收工

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容