範例:光的三原色(狀態模式)

Pattern: 狀態模式

Class Diagram: 光的三原色


情境:透過開關紅綠藍三種不同類型的光,我們可以顯示出各種不同的顏色

  • 定義光的三原色的 Enum
1
2
3
4
5
6
7
8
namespace DotNetCoreKata.Enums;

public enum RgbColor
{
Red = 1,
Green = 2,
Blue = 3,
}
  • 定義顯示顏色的 Enum
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()
{
// Do nothing
}

public void TurnOffGreenLight()
{
// Do nothing
}

public void TurnOffBlueLight()
{
// Do nothing
}
}

說明一下黑色狀態的實作:

  • 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()
{
// Do nothing
}

public void TurnOnGreenLight()
{
rgbMonitor.ToYellowColorState();
}

public void TurnOnBlueLight()
{
rgbMonitor.ToMagentaColorState();
}

public void TurnOffRedLight()
{
rgbMonitor.ToBlackColorState();
}

public void TurnOffGreenLight()
{
// Do nothing
}

public void TurnOffBlueLight()
{
// Do nothing
}
}

說明一下紅色狀態的實作:

  • 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()
{
// Do nothing
}

public void TurnOnBlueLight()
{
rgbMonitor.ToCyanColorState();
}

public void TurnOffRedLight()
{
// Do nothing
}

public void TurnOffGreenLight()
{
rgbMonitor.ToBlackColorState();
}

public void TurnOffBlueLight()
{
// Do nothing
}
}

  • 實作藍色狀態
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()
{
// Do nothing
}

public void TurnOffRedLight()
{
// Do nothing
}

public void TurnOffGreenLight()
{
// Do nothing
}

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()
{
// Do nothing
}

public void TurnOnGreenLight()
{
// Do nothing
}

public void TurnOnBlueLight()
{
rgbMonitor.ToWhiteColorState();
}

public void TurnOffRedLight()
{
rgbMonitor.ToGreenColorState();
}

public void TurnOffGreenLight()
{
rgbMonitor.ToRedColorState();
}

public void TurnOffBlueLight()
{
// Do nothing
}
}

  • 實作洋紅色狀態
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()
{
// Do nothing
}

public void TurnOnGreenLight()
{
rgbMonitor.ToWhiteColorState();
}

public void TurnOnBlueLight()
{
// Do nothing
}

public void TurnOffRedLight()
{
rgbMonitor.ToBlueColorState();
}

public void TurnOffGreenLight()
{
// Do nothing
}

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()
{
// Do nothing
}

public void TurnOnBlueLight()
{
// Do nothing
}

public void TurnOffRedLight()
{
// Do nothing
}

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()
{
// Do nothing
}

public void TurnOnGreenLight()
{
// Do nothing
}

public void TurnOnBlueLight()
{
// Do nothing
}

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();
}
  • 實作 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
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(): 使用策略模式來呼叫當前狀態對應的方法。
    • TurnOffLight() 同理。
  • ToBlackColorState(): 將當前狀態改為黑色狀態
    • ToRedColorState()ToGreenColorState() 等同理。

透過使用狀態模式、策略模式等方法,我們讓原本的 if/else 邏輯消失,
卻也讓原本的結構變得相對複雜,這就是程式碼複雜度守恆定律。


ʕ •ᴥ•ʔ:重讀一次 Design Pattern,改用 C# 來寫範例。

Share