Pattern: 狀態模式
Class Diagram: 光的三原色
情境:透過開關紅綠藍三種不同類型的光,我們可以顯示出各種不同的顏色
1 2 3 4 5 6 7 8 namespace DotNetCoreKata.Enums ;public enum RgbColor{ Red = 1 , Green = 2 , Blue = 3 , }
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace DotNetCoreKata.Enums ;public enum Color{ Black = 0 , Red = 1 , Green = 2 , Blue = 3 , Yellow = 4 , Magenta = 5 , Cyan = 6 , White = 7 , }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using DotNetCoreKata.Enums;namespace DotNetCoreKata.Services.Rgb ;public class RgbMonitor { private readonly HashSet<RgbColor> _lights = []; public Color Display () { if (_lights.Count == 3 ) { return Color.White; } if (_lights.Count == 2 ) { if (_lights.Contains(RgbColor.Red) && _lights.Contains(RgbColor.Green)) { return Color.Yellow; } if (_lights.Contains(RgbColor.Red) && _lights.Contains(RgbColor.Blue)) { return Color.Magenta; } return Color.Cyan; } if (_lights.Count == 1 ) { return _lights.First() switch { RgbColor.Red => Color.Red, RgbColor.Green => Color.Green, RgbColor.Blue => Color.Blue, _ => throw new ArgumentOutOfRangeException() }; } return Color.Black; } public void TurnOnLight (RgbColor rgbColor ) { _lights.Add(rgbColor); } public void TurnOffLight (RgbColor rgbColor ) { _lights.Remove(rgbColor); } }
可以發現上述的程式碼有很多 if/else 等的邏輯, 讓我們用狀態模式改造它!
Finite-State Machine
首先讓我們畫出有限狀態機 (FSM)
State
定義狀態類別 (State)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 using DotNetCoreKata.Enums;namespace DotNetCoreKata.DomainModels.Rgb ;public interface IColorState { public Color Display () ; public void TurnOnRedLight () ; public void TurnOnGreenLight () ; public void TurnOnBlueLight () ; public void TurnOffRedLight () ; public void TurnOffGreenLight () ; public void TurnOffBlueLight () ; }
我們將原本 RGB Monitor 開關不同類型的光線, 職責切分的更明確,來簡化各個狀態的實作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class BlackColorState (IRgbMonitor rgbMonitor ) : IColorState{ public Color Display () { return Color.Black; } public void TurnOnRedLight () { rgbMonitor.ToRedColorState(); } public void TurnOnGreenLight () { rgbMonitor.ToGreenColorState(); } public void TurnOnBlueLight () { rgbMonitor.ToBlueColorState(); } public void TurnOffRedLight () { } public void TurnOffGreenLight () { } public void TurnOffBlueLight () { } }
說明一下黑色狀態的實作:
Display() : 顯示黑色。
TurnOnRedLight() : 會將當前狀態轉換成紅色狀態。
常見的實作有兩種:
第一種是透過 情境類別 轉換。
第二種是由 狀態類別 直接設置情境類別的當前狀態。 差異在是否要讓狀態類別們彼此耦合,這裡選擇第一種。
TurnOnGreenLight()、 TurnOnBlueLight() 同理。
TurnOffRedLight() : 因為不會改變當前狀態,所以就不做事。
TurnOffGreenLight()、 TurnOffBlueLight() 同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class RedColorState (IRgbMonitor rgbMonitor ) : IColorState{ public Color Display () { return Color.Red; } public void TurnOnRedLight () { } public void TurnOnGreenLight () { rgbMonitor.ToYellowColorState(); } public void TurnOnBlueLight () { rgbMonitor.ToMagentaColorState(); } public void TurnOffRedLight () { rgbMonitor.ToBlackColorState(); } public void TurnOffGreenLight () { } public void TurnOffBlueLight () { } }
說明一下紅色狀態的實作:
Display() : 顯示紅色。
TurnOnRedLight() : 因為不會改變當前狀態,所以就不做事。
TurnOffGreenLight()、 TurnOffBlueLight() 同理。
TurnOnGreenLight() : 這邊很有趣的因為紅光加綠光會顯示黃色,所以要轉換成黃色狀態。
TurnOnBlueLight() : 同理紅光加藍光會顯示洋紅色,所以要轉換成洋紅色狀態。
TurnOffRedLight() : 會將當前狀態轉換成黑色狀態。
接下來就依樣畫葫蘆,將所有的顏色狀態實作出來吧! 這邊也可以解釋當初為什麼要將開關不同類型的光線切分出來, 狀態的實作會變得相對單純。
下面會依序實作各個狀態類別, 如果我們都懂了,可以跳到定義情境類別 (Context) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;internal class GreenColorState (IRgbMonitor rgbMonitor ) : IColorState{ public Color Display () { return Color.Green; } public void TurnOnRedLight () { rgbMonitor.ToYellowColorState(); } public void TurnOnGreenLight () { } public void TurnOnBlueLight () { rgbMonitor.ToCyanColorState(); } public void TurnOffRedLight () { } public void TurnOffGreenLight () { rgbMonitor.ToBlackColorState(); } public void TurnOffBlueLight () { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class BlueColorState (IRgbMonitor rgbMonitor ): IColorState{ public Color Display () { return Color.Blue; } public void TurnOnRedLight () { rgbMonitor.ToMagentaColorState(); } public void TurnOnGreenLight () { rgbMonitor.ToCyanColorState(); } public void TurnOnBlueLight () { } public void TurnOffRedLight () { } public void TurnOffGreenLight () { } public void TurnOffBlueLight () { rgbMonitor.ToBlackColorState(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class YellowColorState (IRgbMonitor rgbMonitor ): IColorState{ public Color Display () { return Color.Yellow; } public void TurnOnRedLight () { } public void TurnOnGreenLight () { } public void TurnOnBlueLight () { rgbMonitor.ToWhiteColorState(); } public void TurnOffRedLight () { rgbMonitor.ToGreenColorState(); } public void TurnOffGreenLight () { rgbMonitor.ToRedColorState(); } public void TurnOffBlueLight () { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class MagentaColorState (IRgbMonitor rgbMonitor ): IColorState{ public Color Display () { return Color.Magenta; } public void TurnOnRedLight () { } public void TurnOnGreenLight () { rgbMonitor.ToWhiteColorState(); } public void TurnOnBlueLight () { } public void TurnOffRedLight () { rgbMonitor.ToBlueColorState(); } public void TurnOffGreenLight () { } public void TurnOffBlueLight () { rgbMonitor.ToRedColorState(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class CyanColorState (IRgbMonitor rgbMonitor ): IColorState{ public Color Display () { return Color.Cyan; } public void TurnOnRedLight () { rgbMonitor.ToWhiteColorState(); } public void TurnOnGreenLight () { } public void TurnOnBlueLight () { } public void TurnOffRedLight () { } public void TurnOffGreenLight () { rgbMonitor.ToBlueColorState(); } public void TurnOffBlueLight () { rgbMonitor.ToGreenColorState(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using DotNetCoreKata.Enums;using DotNetCoreKata.Services.Rgb;namespace DotNetCoreKata.DomainModels.Rgb ;public class WhiteColorState (IRgbMonitor rgbMonitor ): IColorState{ public Color Display () { return Color.White; } public void TurnOnRedLight () { } public void TurnOnGreenLight () { } public void TurnOnBlueLight () { } public void TurnOffRedLight () { rgbMonitor.ToCyanColorState(); } public void TurnOffGreenLight () { rgbMonitor.ToMagentaColorState(); } public void TurnOffBlueLight () { rgbMonitor.ToYellowColorState(); } }
Context
定義情境類別 (Context)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using DotNetCoreKata.Enums;namespace DotNetCoreKata.Services.Rgb ;public interface IRgbMonitor { Color Display () ; void TurnOnLight (RgbColor rgbColor ) ; void TurnOffLight (RgbColor rgbColor ) ; void ToBlackColorState () ; void ToRedColorState () ; void ToGreenColorState () ; void ToBlueColorState () ; void ToYellowColorState () ; void ToMagentaColorState () ; void ToCyanColorState () ; void ToWhiteColorState () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 using DotNetCoreKata.DomainModels;using DotNetCoreKata.DomainModels.Rgb;using DotNetCoreKata.Enums;namespace DotNetCoreKata.Services.Rgb ;public class RgbMonitor { private IColorState _state; private readonly BlackColorState _blackColorState; private readonly RedColorState _redColorState; private readonly GreenColorState _greenColorState; private readonly BlueColorState _blueColorState; private readonly YellowColorState _yellowColorState; private readonly MagentaColorState _magentaColorState; private readonly CyanColorState _cyanColorState; private readonly WhiteColorState _whiteColorState; public RgbMonitor () { _blackColorState = new BlackColorState(this ); _redColorState = new RedColorState(this ); _greenColorState = new GreenColorState(this ); _blueColorState = new BlueColorState(this ); _yellowColorState = new YellowColorState(this ); _magentaColorState = new MagentaColorState(this ); _cyanColorState = new CyanColorState(this ); _whiteColorState = new WhiteColorState(this ); _state = _blackColorState; } public Color Display () { return _state.Display(); } public void TurnOnLight (RgbColor rgbColor ) { var turnOnColorLightMap = new Dictionary<RgbColor, Action> { {RgbColor.Red, () => _state.TurnOnRedLight()}, {RgbColor.Green, () => _state.TurnOnGreenLight()}, {RgbColor.Blue, () => _state.TurnOnBlueLight()} }; turnOnColorLightMap[rgbColor](); } public void TurnOffLight (RgbColor rgbColor ) { var turnOffColorLightMap = new Dictionary<RgbColor, Action> { {RgbColor.Red, () => _state.TurnOffRedLight()}, {RgbColor.Green, () => _state.TurnOffGreenLight()}, {RgbColor.Blue, () => _state.TurnOffBlueLight()} }; turnOffColorLightMap[rgbColor](); } public void ToBlackColorState () { _state = _blackColorState; } public void ToRedColorState () { _state = _redColorState; } public void ToGreenColorState () { _state = _greenColorState; } public void ToBlueColorState () { _state = _blueColorState; } public void ToYellowColorState () { _state = _yellowColorState; } public void ToMagentaColorState () { _state = _magentaColorState; } public void ToCyanColorState () { _state = _cyanColorState; } public void ToWhiteColorState () { _state = _whiteColorState; } }
說明一下 RGB Monitor 的實作:
RgbMonitor() :
透過將自身傳入顏色狀態的建構式 來初始化各個顏色狀態, 這樣之後顏色狀態才能改變到 RGB Monitor 的當前狀態。
設置當前狀態為黑色狀態。
Display() : 透過當前狀態 來顯示對應的顏色。
TurnOnLight() : 使用策略模式 來呼叫當前狀態對應的方法。
ToBlackColorState() : 將當前狀態改為黑色狀態
ToRedColorState()、 ToGreenColorState() 等同理。
透過使用狀態模式、策略模式等方法,我們讓原本的 if/else 邏輯消失, 卻也讓原本的結構變得相對複雜,這就是程式碼複雜度守恆定律。
ʕ •ᴥ•ʔ:重讀一次 Design Pattern,改用 C# 來寫範例。