孿生兄弟狀態模式與策略模式有什麽區別,究竟該如何選擇
都說狀態模式和策略模式很像,它們的 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。由於狀態模式和策略模式在工作中的使用場景比較多(我自己最近項目就有用到),所以本文重點分析記錄狀態模式和策略模式的異同,來加深我自己對它們的理解。也希望能幫助到有緣看到本文的朋友。
孿生兄弟狀態模式與策略模式有什麽區別,究竟該如何選擇