C++ 設計模式 - 狀態模式 State Pattern

當你在使用某些應用程式時,是否曾經發現它們的行為會隨著某些條件或狀態的改變而改變?例如音樂播放器,當它處於「播放」狀態時去按下「播放」按鈕是沒有反應的,但當處於「暫停」狀態時,按下相同按鈕就會繼續播放音樂。這樣的設計就是狀態模式的核心概念。

什麼是狀態模式?

狀態模式是一種行為型設計模式,用於當物件內部狀態發生改變時,自動改變其行為。這個模式的關鍵在於將行為與狀態解耦,讓物件在不同的狀態下有不同的表現方式。換句話說,物件的行為是由當前所處的狀態決定的,而不是依賴於外部的控制邏輯。

透過狀態模式,我們可以避免大量的 if-elseswitch 判斷,讓程式碼更加清晰並易於擴展。

狀態模式在音樂播放器中的應用

以音樂播放器為例,我們可以用狀態模式來處理不同的播放狀態,如「播放」、「暫停」和「停止」。每一個狀態都代表著播放器不同的行為,而狀態的切換可以由使用者操作或其他事件來驅動。

讓我們來看看如何一步步定義這個音樂播放器的狀態系統。

我們首先需要定義一個狀態的介面,讓不同的具體狀態類別去實現。例如,我們定義了 play()pause()stop() 這三個動作,每個狀態可以根據自己的邏輯來實現這些動作,

1
2
3
4
5
6
7
8
9
10
// 前向宣告,避免迴圈引用
class MusicPlayer;

// 狀態基底類別
class State {
public:
virtual void play(MusicPlayer* player) = 0;
virtual void pause(MusicPlayer* player) = 0;
virtual void stop(MusicPlayer* player) = 0;
};

接著我們可以定義具體的狀態類別,例如「播放中」和「暫停中」狀態。每一個具體狀態都會根據當前的情況來決定該如何處理使用者的操作,

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
// 具體狀態:「播放中」
class PlayingState : public State {
public:
void play(MusicPlayer* player) override;

void pause(MusicPlayer* player) override;

void stop(MusicPlayer* player) override;
};

// 具體狀態:「暫停中」
class PausedState : public State {
public:
void play(MusicPlayer* player) override;

void pause(MusicPlayer* player) override;

void stop(MusicPlayer* player) override;
};

// 具體狀態:「停止中」
class StoppedState : public State {
public:
void play(MusicPlayer* player) override;

void pause(MusicPlayer* player) override;

void stop(MusicPlayer* player) override;
};

void PlayingState::play(MusicPlayer* player) {
std::cout << "Already playing." << std::endl;
}

void PlayingState::pause(MusicPlayer* player) {
std::cout << "Pausing the music." << std::endl;
player->setState(new PausedState()); // 切換到暫停狀態
}

void PlayingState::stop(MusicPlayer* player) {
std::cout << "Stopping the music." << std::endl;
player->setState(new StoppedState()); // 切換到停止狀態
}

void PausedState::play(MusicPlayer* player) {
std::cout << "Resuming the music." << std::endl;
player->setState(new PlayingState()); // 切換回播放狀態
}

void PausedState::pause(MusicPlayer* player) {
std::cout << "Already paused." << std::endl;
}

void PausedState::stop(MusicPlayer* player) {
std::cout << "Stopping the music." << std::endl;
player->setState(new StoppedState()); // 切換到停止狀態
}

void StoppedState::play(MusicPlayer* player) {
std::cout << "Starting to play music." << std::endl;
player->setState(new PlayingState()); // 切換到播放狀態
}

void StoppedState::pause(MusicPlayer* player) {
std::cout << "Can't pause. The music is stopped." << std::endl;
}

void StoppedState::stop(MusicPlayer* player) {
std::cout << "Already stopped." << std::endl;
}

音樂播放器本身只需要依賴狀態物件來決定它的行為,當狀態發生變化時,播放器自動切換狀態。這樣播放器不需要自己處理具體行為,所有的行為交給不同狀態來處理,

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
// 播放器類別
class MusicPlayer {
private:
State* state; // 當前狀態
public:
MusicPlayer(State* initState) : state(initState) {}

~MusicPlayer() {
delete state; // 釋放當前狀態
}

void setState(State* newState) {
delete state; // 切換狀態前釋放舊狀態
state = newState;
}

void play() {
state->play(this);
}

void pause() {
state->pause(this);
}

void stop() {
state->stop(this);
}
};

現在我們可以在客戶端直接使用這個播放器來進行播放、暫停、停止操作,

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
// 建立播放器並初始化為停止狀態
MusicPlayer musicPlayer(new StoppedState());

// 嘗試播放、暫停、停止操作
musicPlayer.play(); // 開始播放
musicPlayer.pause(); // 暫停播放
musicPlayer.play(); // 恢復播放
musicPlayer.stop(); // 停止播放

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
Starting to play music.
Pausing the music.
Resuming the music.
Stopping the music.

狀態模式的優缺點

狀態模式的優點是它可以讓物件的行為與其狀態緊密相關,當狀態變化時,行為也會隨之變化,這讓系統變得更加靈活且容易擴展。舉例來說,如果未來想新增「快進」狀態,我們只需定義新的 FastForwardState 類別,然後無需改動其他類別的邏輯。

狀態模式的缺點在於會增加類別的數量。每個狀態都必須有一個對應的類別,這在狀態非常多的情況下,可能會讓程式變得繁瑣。另外動態切換狀態的過程中,可能會有一定的記憶體管理問題,需要小心處理。

總結

狀態模式特別適合那些行為會隨著狀態改變而改變的系統。透過將狀態封裝成獨立的類別,系統可以變得更具擴展性,且更加容易維護。雖然可能會引入更多的類別和稍微複雜的結構,但它能有效地減少程式中繁瑣的判斷邏輯,讓程式碼更加清晰易懂。