HIHI~😍 如果你是第一次來的話,『Chan-Chan-Dev』是一個專門用簡單的圖文與故事講解網路程式技術的網站。
若你也喜歡用這種方式學習的話,歡迎加入 Chan-Chan-Dev Facebook 的粉絲團,在發佈的時候就有比較多機會收到通知喔!😍


在寫程式的過程中,一定很常遇到需要很多不同狀態而有不同行爲的功能。這樣子例子在生活中其實滿常見的,例如天氣變冷了,我們受到 Natural call 就會不自覺地想要吃火鍋,而天氣變熱了,我們就會覺得吃涼麵比較清爽,明明都是『吃』的行爲,但是卻因爲『氣溫』的狀態不同,就會讓這個『吃』的行爲而有所不同。

所以如果要達到上述的不同狀態有不同的行爲,那條件語句是邏輯判斷的過程中無可避免的必須,但隨著狀態增長,條件語句也會跟着變多,很快地就會來到難以維護的狀態了。所以在狀態很多的情況下,要如何用更少的 if else 更有結構與更有效率地來完成一樣的功能呢? State 設計模式提供了一套棒的解法,讓我們用個小故事來輕鬆地學習如何將 State 設計模式運用在我們日常的開發當中吧 😍

State 設計模式是什麼?

State 是一種行爲類的設計模式,讓一個物件因爲狀態的改變,進而改變行爲。

如果沒用 State 會怎樣嗎?🧐

讓我們一樣用一個故事來當起手式,這次要講講花心劈腿男大衛的故事,大衛是個花心的劈腿渣男,但是他卻很得意的自喻爲自己是 2020 年的現代韋小寶,他的夢想就是備齊他的十二金釵,過著十二女友同時在線且人人稱羨的幸福快樂的日子。只是他距離這個夢想還有點遙遠,因爲他目前只交到到三個女朋友,分別是:Mary、Babe、Linda。

三個女朋友各有自己興趣、喜歡吃的東西、親密的暱稱,以及他騙那些女朋友他目前自己的職業是什麼,以下簡單地圖解整理一下:

所以每次大衛要跟他們出去約會的時候,就要記得他女朋友的每個資訊細節,而且需要針對不同的女友(狀態)表現出正確的行爲。如果不小心把 Mary 叫成北鼻,或者是跟 Linda 說,我這個禮拜要在營區留守的話,他的劈腿大計就會馬上就會識被了。所以他要針對這些不同女朋友(狀態)而去改變他的稱呼方法(行爲)。

所以在還沒學過 State 設計模式的大衛會怎麼管理他的女友們呢?

David Class

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
public class David {
private String girlFriend;

public David(String girlFriend) {
System.out.println("目前在陪的女朋友:" + girlFriend);
this.girlFriend = girlFriend;
}

public void changeGirlFriend(String girlFriend) {
System.out.println("-----------------------");
System.out.println("目前在陪的女朋友:" + girlFriend);
this.girlFriend = girlFriend;
}

// 問女友想要吃什麼?
public void askWhatToEet() {
if (girlFriend.equals("Mary")) {
System.out.println("要不要去吃韓式烤肉?");
}
if (girlFriend.equals("Babe")) {
System.out.println("要不要去吃漢堡王?");
}
if (girlFriend.equals("Linda")) {
System.out.println("要不要去吃法國菜?");
}
}

// 問女友想要哪裏?
public void askWantToGo() {
if (girlFriend.equals("Mary")) {
System.out.println("我這邊有多一張免費的電影票,要不要去看電影呀?");
}
if (girlFriend.equals("Babe")) {
System.out.println("最近好像有新開一個 Outlet,要不要去看看?");
}
if (girlFriend.equals("Linda")) {
System.out.println("我們要不要去誠品看看有什麼新書嗎?");
}
}

// 跟女友打招呼
public void sayHello() {
if (girlFriend.equals("Mary")) {
System.out.println("豬豬,想不想我?");
}
if (girlFriend.equals("Babe")) {
System.out.println("我好愛你喔,北鼻 😍");
}
if (girlFriend.equals("Linda")) {
System.out.println("親愛的,你今天想我了嗎?");
}
}

// 找藉口不能陪女友
public void findExcuseToEscape() {
if (girlFriend.equals("Mary")) {
System.out.println("豬豬,我這個週末被連長留下來留守,所以不能陪你了,哭哭。");
}
if (girlFriend.equals("Babe")) {
System.out.println("北鼻,這個禮拜老闆剛好派我到地中海去出差,所以可能這週無法陪你,要想我喔。");
}
if (girlFriend.equals("Linda")) {
System.out.println("親愛的,這個禮拜要陪教授開研討會,你要自己堅強喔。");
}
}
}

Main Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
David david = new David("Mary");// 目前在陪的女朋友:Mary
david.sayHello(); // 豬豬,想不想我?
david.askWantToGo(); // 我這邊有多一張免費的電影票,要不要去看電影呀?
david.askWhatToEet(); // 要不要去吃韓式烤肉?
david.findExcuseToEscape(); // 豬豬,我這個週末被連長留下來留守,所以不能陪你了,哭哭。

david.changeGirlFriend("Babe"); // 目前在陪的女朋友:Babe
david.sayHello(); // 我好愛你喔,北鼻 😍
david.askWantToGo(); // 最近好像有新開一個 Outlet,要不要去看看?
david.askWhatToEet(); // 要不要去吃漢堡王?
david.findExcuseToEscape(); // 北鼻,這個禮拜老闆剛好派我到地中海去出差,所以可能這週無法陪你,要想我喔。

david.changeGirlFriend("Linda"); // 目前在陪的女朋友:Linda
david.sayHello(); // 親愛的,你今天想我了嗎?
david.askWantToGo(); // 我們要不要去誠品看看有什麼新書嗎?
david.askWhatToEet(); // 要不要去吃法國菜?
david.findExcuseToEscape(); // 親愛的,這個禮拜要陪教授開研討會,你要自己堅強喔。
}

}

而最近 David 又與他的夢想拉近一點距離了,因爲他在酒吧成功地把到一位新的混血女友 June,而她的資訊如下:

所以就會需要在每一個不同的行爲內在新增新的女友 June 的判斷條件:

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
// 問女友想要吃什麼?
public void askWhatToEet() {
// 同上省略 ....
// ....
// ....
if (girlFriend.equals("June")) {
System.out.println("要不要去逛饒河夜市");
}
}

// 問女友想要哪裏?
public void askWantToGo() {
// 同上省略 ....
// ....
// ....
if (girlFriend.equals("June")) {
System.out.println("我們今天要來看 Netflix 上有什麼好看的嗎?");
}
}

// 跟女友打招呼
public void sayHello() {
// 同上省略 ....
// ....
// ....
if (girlFriend.equals("June")) {
System.out.println("哈尼,我不能沒有你!");
}
}

// 找藉口不能陪女友
public void findExcuseToEscape() {
// 同上省略 ....
// ....
// ....
if (girlFriend.equals("June")) {
System.out.println("哈尼,醫院要值班留守,所以可能無法陪你喔");
}
}

問題:判斷式會因爲不同的狀態增加 😭

由 David class 可以看出所有的要行爲都需要判斷目前的女友是哪一位,然後進行在這個女友的狀態下的合理行爲,所以隨著 David 的女朋友變多,每一個行為的判斷式就會越多,就像是新增 June 一樣需要在每一個行爲新增判斷式。所以可想而知的是:

有 12 個女友可能就會需要 12 個判斷式來符合把每個女朋友安撫地服服貼貼的需求。在這種架構之下,很難讓新增女友這件事情可以被規模化! 😆

所以 David 去上了女友管理學線上課程,學習到了 State 的設計模式,以下是他學習後的筆記內容。

1. 定義在不同狀態下執行的行爲界面

1
2
3
4
5
6
7
8
9
10
11
12
public interface IGirlFriend {
// 問女友想要吃什麼?
public void askWhatToEet();
// 問女友想要哪裏?
public void askWantToGo() ;
// 跟女友打招呼
public void sayHello();
// 找藉口不能陪女友
public void findExcuseToEscape();
// 顯示女友名字
public String showName();
}

2. 將每個狀態(女友)抽成一個獨立的 class ,並且實作上述不同狀態下執行的行爲界面

所以我們會得到以下的女友(狀態) class

Mary class

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
public class Mary implements IGirlFriend {
// 問女友想要吃什麼?
@Override
public void askWhatToEet() {
System.out.println("要不要去吃韓式烤肉?");
}

// 問女友想要哪裏?
@Override
public void askWantToGo() {
System.out.println("我這邊有多一張免費的電影票,要不要去看電影呀?");
}

// 跟女友打招呼
@Override
public void sayHello() {
System.out.println("豬豬,想不想我?");
}

// 找藉口不能陪女友
@Override
public void findExcuseToEscape() {
System.out.println("豬豬,我這個週末被連長留下來留守,所以不能陪你了,哭哭。");
}

// 顯示女友名字
@Override
public String showName() {
return "Mary";
}
}

Babe class

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
public class Babe implements IGirlFriend {
// 問女友想要吃什麼?
@Override
public void askWhatToEet() {
System.out.println("要不要去吃漢堡王?");
}

// 問女友想要哪裏?
@Override
public void askWantToGo() {
System.out.println("最近好像有新開一個 Outlet,要不要去看看?");
}

// 跟女友打招呼
@Override
public void sayHello() {
System.out.println("我好愛你喔,北鼻 😍");
}

// 找藉口不能陪女友
@Override
public void findExcuseToEscape() {
System.out.println("北鼻,這個禮拜老闆剛好派我到地中海去出差,所以可能這週無法陪你,要想我喔。");
}

// 顯示女友名字
@Override
public String showName() {
return "Babe";
}
}

Linda class

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
public class Linda implements IGirlFriend {
// 問女友想要吃什麼?
@Override
public void askWhatToEet() {
System.out.println("要不要去吃法國菜?");
}

// 問女友想要哪裏?
@Override
public void askWantToGo() {
System.out.println("我們要不要去誠品看看有什麼新書嗎?");
}

// 跟女友打招呼
@Override
public void sayHello() {
System.out.println("親愛的,你今天想我了嗎?");
}

// 找藉口不能陪女友
@Override
public void findExcuseToEscape() {
System.out.println("親愛的,這個禮拜要陪教授開研討會,你要自己堅強喔。");
}

// 顯示女友名字
@Override
public String showName() {
return "Linda";
}
}

經過以上調整後的 class,可以更一眼看到在這個女友(狀態)之下,會需要做的行爲分別是什麼。以單一職責的角度來說,這個 class 也專門負責這個女友(狀態)的所有行爲,而不需要去管其他女友(其他狀態)發生什麼事情。如果對於單一職責的概念不太熟悉的朋友,也可以參考這篇 Single Responsibility Principle(單一職責)
快速地簡單瞭解一下喔 😊。

3. 在 David 中將 girlFriend 屬性變成一種女友(狀態)物件參考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class David {
private IGirlFriend girlFriend;

public David(IGirlFriend girlFriend) {
System.out.println("目前在陪的女朋友:" + girlFriend.showName());
this.girlFriend = girlFriend;
}

public void changeGirlFriend(IGirlFriend girlFriend) {
System.out.println("-----------------------");
System.out.println("目前在陪的女朋友:" + girlFriend.showName());
this.girlFriend = girlFriend;
}

// 以下省略 ....
// 以下省略 ....
// 以下省略 ....
}

4. 在 David 的所有行爲,都直接呼叫女友(狀態)物件定義的方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 問女友想要吃什麼?
public void askWhatToEet() {
girlFriend.askWhatToEet();
}

// 問女友想要哪裏?
public void askWantToGo() {
girlFriend.askWantToGo();
}

// 跟女友打招呼
public void sayHello() {
girlFriend.sayHello();
}

// 找藉口不能陪女友
public void findExcuseToEscape() {
girlFriend.findExcuseToEscape();
}

Main Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
public static void main(String[] args) {
David david = new David(new Mary()); // 目前在陪的女朋友:Mary
david.sayHello(); // 豬豬,想不想我?
david.askWantToGo(); // 我這邊有多一張免費的電影票,要不要去看電影呀?
david.askWhatToEet(); // 要不要去吃韓式烤肉?
david.findExcuseToEscape(); // 豬豬,我這個週末被連長留下來留守,所以不能陪你了,哭哭。

david.changeGirlFriend(new Babe()); // 目前在陪的女朋友:Babe
david.sayHello(); // 我好愛你喔,北鼻 😍
david.askWantToGo(); // 最近好像有新開一個 Outlet,要不要去看看?
david.askWhatToEet(); // 要不要去吃漢堡王?
david.findExcuseToEscape(); // 北鼻,這個禮拜老闆剛好派我到地中海去出差,所以可能這週無法陪你,要想我喔。

david.changeGirlFriend(new Linda()); // 目前在陪的女朋友:Linda
david.sayHello(); // 親愛的,你今天想我了嗎?
david.askWantToGo(); // 我們要不要去誠品看看有什麼新書嗎?
david.askWhatToEet(); // 要不要去吃法國菜?
david.findExcuseToEscape(); // 親愛的,這個禮拜要陪教授開研討會,你要自己堅強喔。
}
}

David class 調整過後,Main Class 調整的幅度不大,只是將這些狀態從字串改爲物件的方式使用,但是可以從調整過後的 David class 看到,藉由 State 的方式已經成功地將 David class 內需要針對每個女友(狀態)所設定的條件判斷全部移除,而只是單純地呼叫女友(狀態)物件內所定義的方法而已。

所以如果我們今天一樣多了一個新的混血女友 June 的話,我們會怎麼增加呢?我們只需要新增一個 June class,而 David class 根本不需要做任何的調整,我們就可以在 Main class 開心的使用 June 的女友(狀態)物件囉!

June class

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

public class June implements IGirlFriend {
// 問女友想要吃什麼?
@Override
public void askWhatToEet() {
System.out.println("要不要去逛饒河夜市");
}

// 問女友想要哪裏?
@Override
public void askWantToGo() {
System.out.println("我們今天要來看 Netflix 上有什麼好看得嗎?");
}

// 跟女友打招呼
@Override
public void sayHello() {
System.out.println("哈尼,我不能沒有你!");
}

// 找藉口不能陪女友
@Override
public void findExcuseToEscape() {
System.out.println("哈尼,醫院要值班留守,所以可能無法陪你喔");
}

// 顯示女友名字
@Override
public String showName() {
return "June";
}
}

Main class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {

// 以上省略 ....
// 以上省略 ....
// 以上省略 ....

david.changeGirlFriend(new June()); // 目前在陪的女朋友:June
david.sayHello(); // 哈尼,我不能沒有你!
david.askWantToGo(); // 我們今天要來看 Netflix 上有什麼好看得嗎?
david.askWhatToEet(); // 要不要去逛饒河夜市
david.findExcuseToEscape(); // 哈尼,醫院要值班留守,所以可能無法陪你喔
}
}

最後來看看調整之後的架構狀態 😊

這樣做是不是要擴充其他女友(狀態)更爲方便了呢? 自從 David 學會了 State 管理大法之後,他也更容易少發生不小心叫錯名字,或者是搞錯女朋友興趣的出包狀況,每個女朋友對他也更滿意了,也介紹了更多身邊的好閨蜜給 David 當女朋友,讓 David 很快就達到他的夢想了,甚至過沒多久就超越他的夢想了,一切都是 State 的功勞呀!(這到底是什麼神展開 🤣)

所以男孩們!也夢想像人生勝利組 David 一樣管理好自己的女友們(狀態們)嗎?那 State 設計模式將會是你人生的必修課囉!

如果有任何問題都歡迎在底下留言,或者是哪裡有不小心講錯的地方也歡迎一起討論 ,最後感謝你收看到這裏囉 😍