孿生兄弟狀態模式與策略模式有什麼區別,究竟該如何選擇
都說狀態模式和策略模式很像,它們的 UML 類圖一樣。這也說明,單純從程式碼角度來講,它們的本質一樣,其實都是多型的應用。但它們實際所代表的的事物特徵是有本質區別的,選擇哪個設計模式,代表了你看待業務場景的角度。從合理角度地對業務程序抽象,選擇恰當的設計模式,才能讓程式碼有更好的結構。 這篇文章重點說說我對狀態模式和策略模式區別的理解,以及如何選擇。
一、策略模式
關於策略模式,我之前寫過一篇筆記,不過是 C# 寫的。策略模式解決了程式碼邏輯分支較多,對不同的分支,採取不同措施的問題。不熟悉策略模式的,也可以上集回顧: 扯一扯 C#委託和事件?策略模式?介面回撥?
策略模式簡介
在策略模式(Strategy Pattern)中,一個類的行為或其演算法可以在執行時更改。我們建立表示各種策略的物件和一個行為隨著策略物件改變而改變的 context 物件。策略物件改變 context 物件的執行演算法。這種型別的設計模式屬於行為型模式。
意圖:定義一系列的演算法,把它們一個個封裝起來, 並且使它們可相互替換。 主要解決:在有多種演算法相似的情況下,使用 if...else 所帶來的複雜和難以維護。 何時使用:一個系統有許多許多類,而區分它們的只是他們直接的行為。 如何解決:將這些演算法封裝成一個一個的類,任意地替換。 關鍵程式碼:實現同一個介面。
策略模式的模型程式碼
- 策略的抽象,定一個策略介面,宣告不同策略方案所需實現的方法:
public interface Stragety {
void function();
}
- 具體的策略類,定義不同的策略類,實現策略抽象介面:
public class StrategyA implements Stragety { @Override public void function() { System.out.println("invoke StrategyA function ..."); } }
public class StrategyB implements Stragety {
@Override
public void function() {
System.out.println("invoke StrategyB function ...");
}
}
- 操作策略的上下文環境,Context 類:
public class Context { private Stragety stragety; public Context() { } public Context(Stragety stragety) { this.stragety = stragety; } public void setStragety(Stragety stragety) { this.stragety = stragety; } public void function() { if (stragety == null) { System.out.println("not set strategy..."); return; } stragety.function(); } }
最後呼叫的測試程式碼如下:
public class Test {
public static void main(String[] args) {
Context context = new Context();
context.function();
context.setStragety(new StrategyA());
context.function();
context.setStragety(new StrategyB());
context.function();
}
}
結果如下:
二、狀態模式
狀態模式中的行為是由狀態來決定的,在狀態模式(State Pattern)中,類的行為是基於它的狀態改變的。我們建立表示各種狀態的物件和一個行為隨著狀態物件改變而改變的 context 物件。這種型別的設計模式也屬於行為型模式。
狀態模式簡介
意圖:允許物件在內部狀態發生改變時改變它的行為,物件看起來好像修改了它的類。
主要解決:物件的行為依賴於它的狀態(屬性),並且可以根據它的狀態改變而改變它的相關行為。
何時使用:程式碼中包含大量與物件狀態有關的條件語句。
如何解決:將各種具體的狀態類抽象出來。
關鍵程式碼:狀態模式的介面中通常有一個或者多個方法。而且,狀態模式的實現類的方法,一般返回值,或者是改變例項變數的值。也就是說,狀態模式一般和物件的狀態有關。實現類的方法有不同的功能,覆蓋介面中的方法。狀態模式和策略模式一樣,也可以用於消除 if...else 等條件選擇語句。
狀態模式的模型程式碼
- 狀態的抽象,定一個狀態介面,宣告不同狀態下所需實現的方法:
public interface State {
void function1();
void function2();
}
- 具體的狀態類,定義不同的狀態類,實現狀態抽象介面:
public class StateA implements State {
@Override
public void function1() {
System.out.println("invoke StateA function1 ...");
}
@Override
public void function2() {
System.out.println("invoke StateA function2 ...");
}
}
public class StateB implements State {
@Override
public void function1() {
System.out.println("invoke StateB function1 ...");
}
@Override
public void function2() {
System.out.println("invoke StateB function2 ...");
}
}
- 維護狀態的上下文環境,Context 類:
public class Context {
private State state;
public Context() {
}
public Context(State originalState) {
this.state = originalState;
}
public void setState(State state) {
this.state = state;
}
public void setStateA() {
setState(new StateA());
}
public void setStateB() {
setState(new StateB());
}
public void function1() {
state.function1();
}
public void function2() {
state.function2();
}
}
最後呼叫的測試程式碼如下:
public class Test {
public static void main(String[] args) {
Context context = new Context();
context.setStateA();
context.function1();
context.function2();
context.setStateB();
context.function1();
context.function2();
}
}
結果如下:
三、狀態模式和策略模式的區別
通過上面模型程式碼的對比,有的同學可能發現了,乍一看程式碼,其實兩者幾乎沒有沒什麼區別,都是在玩多型的語法糖。這讓我想起了經典書籍《重構,改善既有程式碼的設計》第一章中的那個例子,重構篇中有如下一段話——
可以用多型來取代switch語句,但是因為:一部影片可以在生命週期內修改自己的分類,一個物件卻不能在生命週期內修改自己所屬的類。所以這裡不能用策略模式,用多型取代switch,而應該用狀態模式(State)。
這是一個State模式還是一個Strategy模式?答案取決於Price類究竟代表計費方式,還是代表影片的某個狀態。
也就是說對於一個場景的抽象選擇策略模式還是狀態模式,取決於你對這個場景的認知。我的理解是策略,即方法,不同的策略實現類中相同的方法,地位應該是平等的。舉幾個例子,早餐選擇吃麵包,中餐選擇吃米飯,這是策略上的決定;交通工具是選擇乘坐公交車還是乘坐地鐵;在中國選擇說中文,在美國選擇說英語... 而狀態之間,往往伴隨著轉換,即狀態遷移,可能是在時序上的狀態遷移,也可能是在某個狀態上執行某種行為(呼叫某個方法)的時候,轉化為另外一種狀態。它的重點應該是狀態遷移,譬如燒水過程中,水溫可以當做狀態;手機移動資料藍芽WiFi的開關、汽車行駛的速度;在中國的時候說中文,在美國的時候說英語...都可以抽象成狀態。咦,等等!!!在中國選擇說中文,在美國選擇說英語,到底抽象成策略模式還是狀態模式? 其實這就還是要看你是怎麼看待這個場景了,你要把它當做兩中平等的場景,只是在不同的場景中,做一個選擇,則可以抽象成策略模式,如果你把在中國和在美國當做兩種狀態,並且兩種狀態可以發生轉換,比如處於在中國這種狀態下,有一個搭乘飛機飛到了中國的行為,狀態變成了在中國,此時就應該考慮抽象成狀態模式。
- 狀態轉換場景中,最好不要由客戶端來直接改變狀態(也不是絕對不可以),而是客戶端做了某種其它操作引起狀態遷移,也就是說客戶端最好不要直接建立一個具體 State 實現類的例項物件,通過 setState() 方法來設定。
- 狀態轉換也可能是物件的內部行為造成的。
這麼一想,狀態模式和策略模式程式碼實現還是有區別的,下面我結合以上想法將狀態模式模型程式碼做修改。 針對第一點,比如,客戶端執行了 actionB 方法方法,使得狀態改變成 StateB , 針對第二點,假設,在狀態 StateB 中,執行 function2 的時候,會切換狀態為 StateA。此時程式碼應該是這樣的:
- 定一個狀態介面:
public interface State {
void function1();
void function2(Context context);
}
- 具體的狀態類:
public class StateA implements State {
@Override
public void function1() {
System.out.println("invoke StateA function1 ...");
}
@Override
public void function2(Context context) {
System.out.println("invoke StateA function2 ...");
}
}
public class StateB implements State {
@Override
public void function1() {
System.out.println("invoke StateB function1 ...");
}
@Override
public void function2(Context context) {
System.out.println("invoke StateB function2 ...");
context.setStateA();
}
}
- 增加一個可能在其它操作中改變狀態的方法,封裝成介面:
public interface Others {
void actionB();
}
- 維護狀態的上下文環境,Context 類:
public class Context implements Others{
private State state;
public Context() {
}
// 為了方便下文說明,標記為--------MARK1
public Context(State originalState) {
this.state = originalState;
}
// 為了方便下文說明,標記為--------MARK2
public void setState(State state) {
this.state = state;
}
public void setStateA() {
setState(new StateA());
}
public void setStateB() {
setState(new StateB());
}
public void function1() {
state.function1();
}
public void function2() {
state.function2(this);
}
@Override
public void actionB() {
System.out.println("invoke actionB ...");
setStateB();
}
}
最後呼叫的測試程式碼如下:
public class Test {
public static void main(String[] args) {
Context context = new Context();
context.setStateA();
context.function1();
context.actionB();
context.function1();
context.function2();
context.function1();
context.function2();
}
}
此時的執行結果如下:
四、總結
在現實世界中,策略和狀態是兩種完全不同的思想。雖然狀態模式和策略模式擁有相似的結構,雖然它們都基於開閉原則,但是,它們的意圖是完全不同的。當我們對狀態和策略進行建模時,這種差異會導致完全不同的問題。對狀態進行建模時,狀態遷移是一個核心內容,狀態模式幫助物件管理狀態;而在選擇策略時,狀態遷移與此毫無關係。另外,策略模式允許一個客戶選擇或提供一種策略,而這種思想在狀態模式中完全沒有,所以在狀態模式中,如果需要避免客戶端的不安全操作,我們完全可以不提供程式碼中標記為 MARK1
的構造器,並將程式碼中標記為 MARK2
的方法私有化。
從程式碼上理解:是誰促使了行為的改變。狀態模式中,狀態轉移由 Context 或 State 自己管理。如果你在State中管理狀態轉移,那麼它必須持有Context的引用。例如,在上面程式碼中,StateB 的 function2() 方法需要呼叫 setState()方法去改變它的狀態,它就需要傳入一個 Context 型別引數。而策略模式中,Strategy 從不持有Context的引用,是客戶端把所選擇的 Strategy 傳遞給Context。由於狀態模式和策略模式在工作中的使用場景比較多(我自己最近專案就有用到),所以本文重點分析記錄狀態模式和策略模式的異同,來加深我自己對它們的理解。也希望能幫助到有緣看到本文的朋友。