說到狀態模式,顧名思義,應該就是跟狀態相關的設計模式了,不過,我們還是跟前面一樣,先不管狀態模式是個什麼東西,先從一個小小的例子出發,看看狀態模式能為我們解決什麼問題。
示例
現在需要實現一個交通燈排程程式,交通燈的顏色需要在紅燈->綠燈->黃燈->紅燈之間迴圈轉換,但是不允許綠燈->紅燈或黃燈->綠燈等情況。這屬於交通規則的常識,現在我們用程式實現它,先看看我們最傳統的思考和實現方式。
首先,我們會很容易想到需要定義一個交通燈顏色的列舉:
public enum LightColor
{
Green,
Red,
Yellow
}
然後,定義一個交通燈的類,在交通燈類中處理顏色轉換及相應的業務邏輯,程式碼如下:
public class TrafficLight
{
private LightColor _lightColor;
public TrafficLight()
{
_lightColor = LightColor.Red;
}
public void Turn()
{
if (_lightColor == LightColor.Red)
{
Console.WriteLine("紅燈停");
_lightColor = LightColor.Green;
}
else if (_lightColor == LightColor.Green)
{
Console.WriteLine("綠燈行");
_lightColor = LightColor.Yellow;
}
else if (_lightColor == LightColor.Yellow)
{
Console.WriteLine("黃燈亮了等一等");
_lightColor = LightColor.Red;
}
}
}
最後,再不妨呼叫執行一下:
static void Main(string[] args)
{
TrafficLight light = new TrafficLight();
light.Turn();
light.Turn();
light.Turn();
light.Turn();
}
顯而易見,這段程式碼是完全滿足需求的,並且邏輯嚴謹,呼叫方式也極其簡單,如果需求不變,這或許就是最好的實現方式了。但是經過前面設計原則的薰陶,我們知道,需求不變是不可能的。因此,我們很容易就會發現這段程式碼存在的問題,充斥著if-else
的條件分支,這就意味著擴充套件困難。這裡例子簡單,可能並不明顯,但是真實專案中必然會有更多的條件分支和更多類似Turn()
的方法,這會導致整個專案擴充套件維護起來極其困難,因為,它嚴重違背了開閉原則。
其實,對於解決if-else
或switch-case
帶來的問題,我們已經相當有經驗了,在簡單工廠模式中,我們採用工廠方法模式抽象出生產具體類的工廠類解決了switch-case
的問題,在上一篇的策略模式中,我們通過將方法抽象成策略類的方式,同樣解決了switch-case
的問題。這裡也不例外,我們也一定需要抽象點什麼才行。但是具體抽象什麼呢?燈的顏色?Turn()
方法?還是別的什麼?思路好像並不是那麼清晰。不過呢,我們發現其實這段程式碼結構跟策略模式改造前的例子極其相似,我們不妨用策略模式改造一下,看看能否滿足需求,如果不滿足,看看還缺點什麼,然後再進一步改造,因為我們知道,策略模式至少能解決if-else
或switch-case
的問題。
我們看看策略模式改造後的程式碼,先將Turn()
方法抽象成策略類:
public interface ITurnStrategy
{
void Turn();
}
public class GreenLightTurnStrategy : ITurnStrategy
{
public void Turn()
{
Console.WriteLine("綠燈行");
}
}
public class RedLightTurnStrategy : ITurnStrategy
{
public void Turn()
{
Console.WriteLine("紅燈停");
}
}
public class YellowLightTurnStrategy : ITurnStrategy
{
public void Turn()
{
Console.WriteLine("黃燈亮了等一等");
}
}
再看看改造後的TrafficLight
類:
public class TrafficLight
{
private ITurnStrategy _turnStrategy;
public TrafficLight(ITurnStrategy turnStrategy)
{
_turnStrategy = turnStrategy;
}
public void Turn()
{
if (_turnStrategy != null)
{
_turnStrategy.Turn();
}
}
public void ChangeTurnStrategy(ITurnStrategy turnStrategy)
{
_turnStrategy = turnStrategy;
}
}
一切看起來似乎都很完美,天衣無縫。再來看看如何使用:
static void Main(string[] args)
{
TrafficLight light = new TrafficLight(new RedLightTurnStrategy());
light.Turn();
light.ChangeTurnStrategy(new GreenLightTurnStrategy());
light.Turn();
light.ChangeTurnStrategy(new YellowLightTurnStrategy());
light.Turn();
light.Turn();
}
一用就發現了問題,呼叫變複雜了。其實,為了能讓系統更容易擴充套件,呼叫時複雜一點也沒什麼,但是,另一個致命的問題卻不能忽視,我們希望燈顏色切換是由內部一套固定機制控制,而不是呼叫方來決定,如果使用者想換什麼顏色就換什麼顏色,交通規則豈不亂套了?顯然,策略模式是不滿足需求的,我們其實希望light.ChangeTurnStrategy()
這個動作,由系統自己內部完成。
既然不滿足需求,那麼問題到底出在哪呢?回過頭來再梳理一下,我們發現或許我們的思路一開始就出現了偏差,交通燈能換顏色嗎?顯然是不能的,因為每個燈的顏色是固定的,我們所謂的換顏色,實際上換的是燈,難道要用工廠方法模式來創造不同顏色的燈?顯然也不合適,三個燈一開始就在那裡,只是迴圈切換而已,不存在建立的過程。實際上,我們或許應該換一種思路,這裡明顯體現的是交通燈的三種狀態,每一種狀態下對應一種需要處理的行為動作,同時,也只有狀態才有切換的過程。
換一種思路後,我們看問題的角度就不一樣了,看看改變思路後的程式碼:
public abstract class TrafficLightState
{
public abstract void Handle(TrafficLight light);
}
public class GreenState : TrafficLightState
{
public override void Handle(TrafficLight light)
{
Console.WriteLine("綠燈行");
light.SetState(new YellowState());
}
}
public class RedState : TrafficLightState
{
public override void Handle(TrafficLight light)
{
Console.WriteLine("紅燈停");
light.SetState(new GreenState());
}
}
public class YellowState : TrafficLightState
{
public override void Handle(TrafficLight light)
{
Console.WriteLine("黃燈亮了等一等");
light.SetState(new RedState());
}
}
public class TrafficLight
{
private TrafficLightState _currentState;
public TrafficLight()
{
_currentState = new RedState();
}
public void Turn()
{
if (_currentState != null)
{
_currentState.Handle(this);
}
}
public void SetState(TrafficLightState state)
{
_currentState = state;
}
}
其實,可以發現,除了類名和方法名變了,程式碼跟策略模式幾乎一模一樣(具體演化過程,文字難以表達清楚,可以看一下我在B站或者公眾號上的視訊),但含義卻是天差地遠,這裡不是直接將方法抽象成策略物件,而是抽象不同的狀態,因此用了抽象類,而非介面(也可以用介面,但是我們通常會將方法抽象成介面,而將物件或屬性抽象成類);併為每個狀態提供處理該狀態下對應行為的介面方法,而不是直接提供具體行為的介面方法。
另外,引數也有所不同,TrafficLightState
中需要持有對TrafficLight
的引用,因為需要在具體的狀態類中處理TrafficLight
的狀態轉移。改造後的程式碼再次完美滿足需求,呼叫方又變得簡單了,狀態的轉移再次迴歸了主權:
static void Main(string[] args)
{
TrafficLight light = new TrafficLight();
light.Turn();
light.Turn();
light.Turn();
light.Turn();
}
這就狀態模式了,下面是交通燈示例最終的類圖:
有限狀態機
從上面的例子中,我們可能會很容易聯想到狀態機,我們也經常聽到或看到有限狀態機或無限狀態機這樣的字眼,那麼有限狀態機跟狀態模式有什麼關係呢?我們先看看有限狀態機的工作原理。有限狀態機的工作原理是,發生 事件(event) 後,根據 當前狀態(cur_state) ,決定執行的 動作(action) ,並設定 下一個狀態(nxt_state)。從交通燈例子可以看到,事件(event) 就是TrafficLight
中的Turn()
方法,由客戶端觸發,觸發後,系統會判斷當前處於哪種燈的狀態,然後執行相應的動作,完成之後再設定下一種燈狀態,和有限狀態機的工作原理完美對應上了。那麼,二者是否等價呢?其實不然,狀態模式只是實現有限狀態機的一種手段而已,因為if-else
版本的實現,也是有限狀態機。
這裡算是一個小插曲,下面我們迴歸到狀態模式。
定義
狀態模式允許一個物件在其內部狀態改變時改變它的行為,從而使物件看起來似乎修改了它的類。
UML類圖
我們將交通燈示例的類圖抽象一下,就可以得到如下狀態模式的類圖:
- Context:上下文環境,定義客戶程式需要的介面,並維護一個具體狀態角色的例項,將與狀態相關的操作委託給當前的
ConcreteState
物件來處理; - State:抽象狀態,定義特定狀態對應行為的介面;
- ConcreteState:具體狀態,實現抽象狀態定義的介面。
優缺點
優點
- 解決
switch-case
、if-else
帶來的難以維護的問題,這個很明顯,沒什麼好說的; - 結構清晰,提高了擴充套件性,不難發現,Context類簡潔清晰了,擴充套件時,幾乎不用改變,而且每個狀態子類也簡潔清晰了,擴充套件時也只需要極少的改變。
- 通過單例或享元可使狀態在多個上下文間共享。
這個問題需要單獨說,我們不難發現,狀態模式雖然解決了很多問題,但是每次狀態的切換都需要建立一個新的狀態類,而原本它僅僅是一個小小的列舉值而已,這樣一對比,物件重複的建立資源開銷是否過於巨大?其實,要解決物件重複建立的問題,我們知道,單例模式和享元模式都是不錯的選擇,具體選用哪一個,就要看狀態類的數量和個人的喜好了。
下面是採用享元模式改進的程式碼,首先是熟悉的享元工廠,程式碼很簡單:
public class LightStateFactory
{
private static readonly IDictionary<Type, TrafficLightState> _lightStates
= new Dictionary<Type, TrafficLightState>();
private static readonly object _locker = new object();
public static TrafficLightState GetLightState<TLightState>() where TLightState : TrafficLightState
{
Type type = typeof(TLightState);
if (!_lightStates.ContainsKey(type))
{
lock (_locker)
{
if (!_lightStates.ContainsKey(type))
{
TrafficLightState typeface = Activator.CreateInstance(typeof(TLightState)) as TrafficLightState;
_lightStates.Add(type, typeface);
}
}
}
return _lightStates[type];
}
}
使用就更簡單了,將建立狀態物件的地方換成享元工廠建立就可以了,程式碼片段如下:
public override void Handle(TrafficLight light)
{
Console.WriteLine("紅燈停");
light.SetState(LightStateFactory.GetLightState<GreenState>());
}
這裡需要特別提一下,由於狀態是單例的,可以在多個上下文間共享,而任何時候,涉及到全域性共享就不得不考慮併發的問題。因此,除非明確需要共享,否則狀態類中不應持有其它的資源,不然可能產生併發問題。同樣的原因,狀態類也不要通過屬性或欄位的方式持有對Context的引用,這也是我採用區域性變數對TrafficLight
進行傳參的原因。
缺點
- 隨著狀態的擴充套件,狀態類數量會增多,這個老生常談了,幾乎所有解決類似問題的設計模式都存在這個缺點;
- 增加了系統複雜度,使用不當將會導致邏輯的混亂,因為,狀態類畢竟增多了嘛,而且還涉及到狀態的轉移,思維可能就更亂了;
- 不完全滿足開閉原則,因為擴充套件時,除了新增或刪除對應的狀態子類外,還需要修改涉及到的相應狀態轉移的其它狀態類,不過相對於原來的實現,這裡已經改善很多了。
與策略模式區別
策略模式
- 強調可以互換的演算法;
- 使用者直接與具體演算法互動,決定演算法的替換,需要了解演算法本身;
- 策略類不需要持有Context的引用。
狀態模式
- 強調改變物件內部的狀態來幫助控制自己的行為;
- 狀態是物件內部流轉,使用者不會直接跟狀態互動,不需要了解狀態本身;
- 狀態類需要持有Context的引用,用來實現狀態轉移。
總結
其實,從類圖和實現方式上可以看出,狀態模式和策略模式真的很像,但是由於策略模式更具有一般性,因此更容易想到。而且,我們也知道狀態模式和策略模式都能解決if-else
帶來的問題,關鍵就在於策略和狀態的識別,就如上述交通燈例子,剛開始識別成策略也很難發現有什麼不對。再舉一個更通俗的例子,老師會根據同學的考試成績對同學給出不同的獎懲方案,如成績低於60分的同學罰款,成績高於90分的同學獎錢,但是怎麼獎怎麼罰,都是老師決定的(不然全考90分以上,老師得哭)。這裡是普通的條件分支,沒有列舉,但是我們依然可以看出,這裡體現的是根據不同的分數段採取不同的策略,可以採用策略模式。再例如,同樣是考試成績,父母對你設定一個指標,考了60以下,罰錢,考了90分以上,獎錢。這時是策略還是狀態呢?感覺好像都可以,但實際上,仔細思考會發現,或許視為狀態會更好,即在不同的狀態會有一個對應的動作,但狀態的有哪些呢?分數段?獎罰?狀態又是怎麼轉移的呢?還得仔細斟酌,這裡例子簡單,或許能想清楚(其實不一定),但實際專案中,估計就沒這麼容易了。
不過呢,一旦我們識別出了狀態,然後識別出了會根據一定的觸發條件發生狀態轉移,那麼十有八九就可以使用狀態模式了。