0%

責任鏈設計模式 (Chain of Responsibility)

Hi 👋 ~ 今天來跟大家聊聊責任鏈設計模式 (Chain of Responsibility Design Pattern)!

責任鏈設計模式是什麼?

  • 責任鏈模式是一種行爲設計模式。
  • 針對一個處理的請求,在一連串由不同節點組成的流程中,每一個節點可以決定是否進行處理、傳遞給下一個節點或判斷不需要往下處理而駁回請求。

沒有使用責任鏈模式會怎麼樣嗎?🤯

我想大家都有去面試過的經驗,不少公司的面試流程很多都需要有 3 - 4 次面試 🥲

我們這次就用面試流程來看看如何使用責任鏈模式吧!

背景設定

JC KeyCode 是一家新創公司,它需要應徵「軟體工程師」與「設計師」。

針對「軟體工程師」的面試流程是:

  1. HR 電話初步面試
  2. 線上技術解題面試
  3. 到公司與 CTO 技術解題面試
  4. 到公司與 CEO 的 1 on 1 面試

針對「設計師」的面試流程是:

  1. HR 電話初步面試
  2. 到公司與設計總監進行設計作品集面試
  3. 到公司與 CEO 的 1 on 1 面試

沒有使用責任鏈設計模式的程式碼

我們會先有應徵者的 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const POSITION_TYPE_ENGINEER = 'enginner';
const POSITION_TYPE_DESIGNER = 'designer';

class Interviewee
{
public string $name;
public string $position;
public int $performance;

public function __construct(string $name, string $position, int $performance)
{
$this->name = $name;
$this->position = $position;
$this->performance = $performance;
}
}

然後就是 JcKeyCode 這家公司的 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class JcKeyCode
{

public function interview(Interviewee $interviewee): bool
{
// Todo:面試流程 ...
}

public function hire(Interviewee $interviewee)
{
$position = $interviewee->position == POSITION_TYPE_ENGINEER ? "工程師" : "設計師";
echo "{$interviewee->name} - {$position} 面試流程開始: ==================== " . PHP_EOL;
$result = $this->interview($interviewee);
if ($result) {
$message = "get the offer ! 🎉";
} else {
$message = "interview failed 😭";
}
echo "{$interviewee->name} - {$position} 面試流程結束: {$message} ==================== " . PHP_EOL;
echo "" . PHP_EOL;
}
}

接下來實作 interview 這個 method 的內容:

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
public function interview(Interviewee $interviewee): bool
{
echo "HR 電話初步面試: Start" . PHP_EOL;
// HR 電話初步面試
if ($interviewee->performance > 5) {
echo "HR 電話初步面試: Pass" . PHP_EOL;
// if Engineer
if ($interviewee->position == POSITION_TYPE_ENGINEER) {
// 線上技術解題面試
echo "線上技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance > 30) {
echo "線上技術解題面試: Pass" . PHP_EOL;
// 到公司與 CTO 技術解題面試
echo "到公司與 CTO 技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance > 70) {
echo "到公司與 CTO 技術解題面試: Pass" . PHP_EOL;
// 到公司與 CEO 的 1 on 1 面試
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance > 90) {
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return true;
} else {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
} else {
echo "到公司與 CTO 技術解題面試: Failed" . PHP_EOL;
return false;
}
} else {
echo "線上技術解題面試: Failed" . PHP_EOL;
return false;
}
}

// if Designer
if ($interviewee->position == POSITION_TYPE_DESIGNER) {
// 到公司與設計總監進行設計作品集面試
echo "到公司與設計總監進行設計作品集面試: Start" . PHP_EOL;
if ($interviewee->performance > 80) {
echo "到公司與設計總監進行設計作品集面試: Pass" . PHP_EOL;
// 到公司與 CEO 的 1 on 1 面試
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance > 90) {
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return true;
} else {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
} else {
echo "到公司與設計總監進行設計作品集面試: Failed" . PHP_EOL;
return false;
}
}
} else {
echo "HR 電話初步面試: Failed" . PHP_EOL;
return false;
}
}

最後使用的程式碼:

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
function main()
{
$jcKeyCode = new JcKeyCode();
// deisgner
$john = new Interviewee("John", POSITION_TYPE_DESIGNER, 3);
$jcKeyCode->hire($john);
$alice = new Interviewee("Alice", POSITION_TYPE_DESIGNER, 15);
$jcKeyCode->hire($alice);
$paul = new Interviewee("Paul", POSITION_TYPE_DESIGNER, 85);
$jcKeyCode->hire($paul);
$patty = new Interviewee("Patty", POSITION_TYPE_DESIGNER, 95);
$jcKeyCode->hire($patty);

// engineer
$mary = new Interviewee("Mary", POSITION_TYPE_ENGINEER, 4);
$jcKeyCode->hire($mary);
$tommy = new Interviewee("Tommy", POSITION_TYPE_ENGINEER, 15);
$jcKeyCode->hire($tommy);
$peter = new Interviewee("Peter", POSITION_TYPE_ENGINEER, 80);
$jcKeyCode->hire($peter);
$david = new Interviewee("David", POSITION_TYPE_ENGINEER, 99);
$jcKeyCode->hire($david);
}

main();

沒有使用責任鏈設計模式的完整程式碼

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?php

const POSITION_TYPE_ENGINEER = 'enginner';
const POSITION_TYPE_DESIGNER = 'designer';

class Interviewee
{
public string $name;
public string $position;
public int $performance;

public function __construct(string $name, string $position, int $performance)
{
$this->name = $name;
$this->position = $position;
$this->performance = $performance;
}
}

class JcKeyCode
{

public function interview(Interviewee $interviewee): bool
{

echo "HR 電話初步面試: Start" . PHP_EOL;
// HR 電話初步面試
if ($interviewee->performance > 5) {
echo "HR 電話初步面試: Pass" . PHP_EOL;
// if Engineer
if ($interviewee->position == POSITION_TYPE_ENGINEER) {
// 線上技術解題面試
echo "線上技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance > 30) {
echo "線上技術解題面試: Pass" . PHP_EOL;
// 到公司與 CTO 技術解題面試
echo "到公司與 CTO 技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance > 70) {
echo "到公司與 CTO 技術解題面試: Pass" . PHP_EOL;
// 到公司與 CEO 的 1 on 1 面試
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance > 90) {
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return true;
} else {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
} else {
echo "到公司與 CTO 技術解題面試: Failed" . PHP_EOL;
return false;
}
} else {
echo "線上技術解題面試: Failed" . PHP_EOL;
return false;
}
}

// if Designer
if ($interviewee->position == POSITION_TYPE_DESIGNER) {
// 到公司與設計總監進行設計作品集面試
echo "到公司與設計總監進行設計作品集面試: Start" . PHP_EOL;
if ($interviewee->performance > 80) {
echo "到公司與設計總監進行設計作品集面試: Pass" . PHP_EOL;
// 到公司與 CEO 的 1 on 1 面試
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance > 90) {
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return true;
} else {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
} else {
echo "到公司與設計總監進行設計作品集面試: Failed" . PHP_EOL;
return false;
}
}
} else {
echo "HR 電話初步面試: Failed" . PHP_EOL;
return false;
}
}

public function hire(Interviewee $interviewee)
{
$position = $interviewee->position == POSITION_TYPE_ENGINEER ? "工程師" : "設計師";
echo "{$interviewee->name} - {$position} 面試流程開始: ==================== " . PHP_EOL;
$result = $this->interview($interviewee);
if ($result) {
$message = "get the offer ! 🎉";
} else {
$message = "interview failed 😭";
}
echo "{$interviewee->name} - {$position} 面試流程結束: {$message} ==================== " . PHP_EOL;
echo "" . PHP_EOL;
}
}

function main()
{
$jcKeyCode = new JcKeyCode();
// deisgner
$john = new Interviewee("John", POSITION_TYPE_DESIGNER, 3);
$jcKeyCode->hire($john);
$alice = new Interviewee("Alice", POSITION_TYPE_DESIGNER, 15);
$jcKeyCode->hire($alice);
$paul = new Interviewee("Paul", POSITION_TYPE_DESIGNER, 85);
$jcKeyCode->hire($paul);
$patty = new Interviewee("Patty", POSITION_TYPE_DESIGNER, 95);
$jcKeyCode->hire($patty);

// engineer
$mary = new Interviewee("Mary", POSITION_TYPE_ENGINEER, 4);
$jcKeyCode->hire($mary);
$tommy = new Interviewee("Tommy", POSITION_TYPE_ENGINEER, 15);
$jcKeyCode->hire($tommy);
$peter = new Interviewee("Peter", POSITION_TYPE_ENGINEER, 80);
$jcKeyCode->hire($peter);
$david = new Interviewee("David", POSITION_TYPE_ENGINEER, 99);
$jcKeyCode->hire($david);
}

main();

John - 設計師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Failed
John - 設計師 面試流程結束: interview failed 😭 ====================

Alice - 設計師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
到公司與設計總監進行設計作品集面試: Start
到公司與設計總監進行設計作品集面試: Failed
Alice - 設計師 面試流程結束: interview failed 😭 ====================

Paul - 設計師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
到公司與設計總監進行設計作品集面試: Start
到公司與設計總監進行設計作品集面試: Pass
到公司與 CEO 的 1 on 1 面試: Start
到公司與 CEO 的 1 on 1 面試: Failed
Paul - 設計師 面試流程結束: interview failed 😭 ====================

Patty - 設計師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
到公司與設計總監進行設計作品集面試: Start
到公司與設計總監進行設計作品集面試: Pass
到公司與 CEO 的 1 on 1 面試: Start
到公司與 CEO 的 1 on 1 面試: Pass
Patty - 設計師 面試流程結束: get the offer ! 🎉 ====================

Mary - 工程師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Failed
Mary - 工程師 面試流程結束: interview failed 😭 ====================

Tommy - 工程師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
線上技術解題面試: Start
線上技術解題面試: Failed
Tommy - 工程師 面試流程結束: interview failed 😭 ====================

Peter - 工程師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
線上技術解題面試: Start
線上技術解題面試: Pass
到公司與 CTO 技術解題面試: Start
到公司與 CTO 技術解題面試: Pass
到公司與 CEO 的 1 on 1 面試: Start
到公司與 CEO 的 1 on 1 面試: Failed
Peter - 工程師 面試流程結束: interview failed 😭 ====================

David - 工程師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
線上技術解題面試: Start
線上技術解題面試: Pass
到公司與 CTO 技術解題面試: Start
到公司與 CTO 技術解題面試: Pass
到公司與 CEO 的 1 on 1 面試: Start
到公司與 CEO 的 1 on 1 面試: Pass
David - 工程師 面試流程結束: get the offer ! 🎉 ====================

🤯 上述程式碼的問題:

  1. interveiw() 裏面 ifelse 的複雜的判斷邏輯混雜在一起
  2. interveiw() 裏面的判斷式無法共用,例如 engineerdesigner 最後都需要經過 CEO 的面試,但卻要因爲不同的狀況要分別寫在兩處
  3. interveiw() 裏面已經這麼混亂的情況下,如果要再新增一個 HR 的職位面試判斷,想必是一場災難。。。 🤯

例如: HR 只需要給 CEO 面試過關即可。

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 function interview(Interviewee $interviewee): bool
{

echo "HR 電話初步面試: Start" . PHP_EOL;
// HR 電話初步面試
if ($interviewee->performance > 5) {
echo "HR 電話初步面試: Pass" . PHP_EOL;
// if Engineer
if ($interviewee->position == POSITION_TYPE_ENGINEER) {
// 工程師的面試流程 ... 略
}
// if Designer
if ($interviewee->position == POSITION_TYPE_DESIGNER) {
// 設計師的面試流程 ... 略
}
// if HR
if ($interviewee->position == POSITION_TYPE_HR) {
// 到公司與 CEO 的 1 on 1 面試
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance > 90) {
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return true;
} else {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
}
} else {
echo "HR 電話初步面試: Failed" . PHP_EOL;
return false;
}
}

相信你看到上述的實作應該覺得有點不妙。。。 🥲




那些下來我們就來看看如何使用責任鏈設計模式吧!

責任鏈設計模式組成

成員 用途
Handler (interface) * setNext(h Handler)
* handle(r Request)
Base Handler
* setNext(h Handler)
* handle(r Request)
Concrete Handler * setNext(h Handler)
* handle(r Request)

UML

使用責任鏈設計模式的程式碼調整

我們把 Interview 設定爲 Handler interface

1
2
3
4
5
6
// Handler Interface =====================
interface Interview
{
public function handle(Interviewee $interviewee): bool;
public function setNext(Interview $interview);
}

建立 Base Handler 把每一個 Handler 都需要有 setNext()passToNext(),剩下 handle() 給繼承的類別自己實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Base Handler Implemation ======================
abstract class BaseInterview implements Interview
{
protected Interview $next;
public function setNext(Interview $interview)
{
$this->next = $interview;
}

abstract public function handle(Interviewee $interviewee): bool;
protected function passToNext(Interviewee $interviewee): bool
{
if (isset($this->next)) {
return $this->next->handle($interviewee);
}
return true;
}
}

接下來把需要的面試實作出來,分別有:

  • HR 電話初步面試
  • 線上技術解題面試
  • 到公司與 CTO 技術解題面試
  • 到公司與設計總監進行設計作品集面試
  • 到公司與 CEO 的 1 on 1 面試

HR 電話初步面試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Handler Implemation 1: HR 電話初步面試 ====================
class HRPhoneCallInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "HR 電話初步面試: Start" . PHP_EOL;
if ($interviewee->performance <= 5) {
echo "HR 電話初步面試: Failed" . PHP_EOL;
return false;
}
echo "HR 電話初步面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

線上技術解題面試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Handler Implemation 2: 線上技術解題面試 ====================
class TechicalOnLineInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "線上技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance <= 30) {
echo "線上技術解題面試: Failed" . PHP_EOL;
return false;
}
echo "線上技術解題面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

到公司與 CTO 技術解題面試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Handler Implemation 3: 到公司與 CTO 技術解題面試 ====================
class OnSiteTechicalWithCTOInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "到公司與 CTO 技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance <= 70) {
echo "到公司與 CTO 技術解題面試: Failed" . PHP_EOL;
return false;
}
echo "到公司與 CTO 技術解題面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

到公司與設計總監進行設計作品集面試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Handler Implemation 4:  到公司與設計總監進行設計作品集面試 ====================
class OnSiteDesignSupervisorInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "到公司與設計總監進行設計作品集面試: Start" . PHP_EOL;
if ($interviewee->performance <= 80) {
echo "到公司與設計總監進行設計作品集面試: Failed" . PHP_EOL;
return false;
}
echo "到公司與設計總監進行設計作品集面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

到公司與 CEO 的 1 on 1 面試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Handler Implemation 5:  到公司與 CEO 的 1 on 1 面試 ====================
class OnSiteFinalCEOInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance <= 90) {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

呼~喘口氣~ 😀


接下來我們就在 JcKeyCode 類別裡新增「設定工程師面試流程」的 method: prepareForEngineerInterview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private function prepareForEngineerInterview(): Interview
{
// HR 電話初步面試
$hrPhoneCallInterview = new HRPhoneCallInterview();
// 線上技術解題面試
$techicalOnLineInterview = new OnSiteDesignSupervisorInterview();
// 到公司與 CTO 技術解題面試
$onSiteTechicalWithCTOInterview = new OnSiteTechicalWithCTOInterview();
// 到公司與 CEO 的 1 on 1 面試
$onSiteFinalCEOInterview = new OnSiteFinalCEOInterview();

// 設定面試的執行順序 ==============================
// HR 電話初步面試 -> 線上技術解題面試
$hrPhoneCallInterview->setNext($techicalOnLineInterview);
// 線上技術解題面試 -> 到公司與 CTO 技術解題面試
$techicalOnLineInterview->setNext($onSiteTechicalWithCTOInterview);
// 到公司與 CTO 技術解題面試 -> 到公司與 CEO 的 1 on 1 面試
$onSiteTechicalWithCTOInterview->setNext($onSiteFinalCEOInterview);
return $hrPhoneCallInterview;
}

JcKeyCode 類別裡新增「設定設計師面試流程」的 method: prepareForDesignerInterview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private function prepareForDesignerInterview(): Interview
{
// HR 電話初步面試
$hrPhoneCallInterview = new HRPhoneCallInterview();
// 到公司與設計總監進行設計作品集面試
$onSiteDesignSupervisorInterview = new OnSiteDesignSupervisorInterview();
// 到公司與 CEO 的 1 on 1 面試
$onSiteFinalCEOInterview = new OnSiteFinalCEOInterview();

// 設定面試的執行順序 ==============================
// HR 電話初步面試 -> 到公司與設計總監進行設計作品集面試
$hrPhoneCallInterview->setNext($onSiteDesignSupervisorInterview);
// 到公司與 CTO 技術解題面試 -> 到公司與 CEO 的 1 on 1 面試
$onSiteDesignSupervisorInterview->setNext($onSiteFinalCEOInterview);
return $hrPhoneCallInterview;
}

接著調整 JcKeyCodeinterview() 爲以下,就大功告成囉!

1
2
3
4
5
6
7
8
9
10
11
public function interview(Interviewee $interviewee): bool
{
switch ($interviewee->position){
case POSITION_TYPE_ENGINEER:
return $this->prepareForEngineerInterview()->handle($interviewee);
case POSITION_TYPE_DESIGNER:
return $this->prepareForDesignerInterview()->handle($interviewee);
default:
throw new \Exception("Position Not Available!");
}
}

使用責任鏈設計模式的完整程式碼

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<?php

const POSITION_TYPE_ENGINEER = 'enginner';
const POSITION_TYPE_DESIGNER = 'designer';

class Interviewee
{
public string $name;
public string $position;
public int $performance;

public function __construct(string $name, string $position, int $performance)
{
$this->name = $name;
$this->position = $position;
$this->performance = $performance;
}
}

// Handler Interface =====================
interface Interview
{
public function handle(Interviewee $interviewee): bool;
}

// Base Handler Implemation ======================
abstract class BaseInterview implements Interview
{
protected Interview $next;
public function setNext(Interview $interview)
{
$this->next = $interview;
}

abstract public function handle(Interviewee $interviewee): bool;
protected function passToNext(Interviewee $interviewee): bool
{
if (isset($this->next)) {
return $this->next->handle($interviewee);
}
return true;
}
}

// Handler Implemation 1: HR 電話初步面試 ====================
class HRPhoneCallInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "HR 電話初步面試: Start" . PHP_EOL;
if ($interviewee->performance <= 5) {
echo "HR 電話初步面試: Failed" . PHP_EOL;
return false;
}
echo "HR 電話初步面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

// Handler Implemation 2: 線上技術解題面試 ====================
class TechicalOnLineInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "線上技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance <= 30) {
echo "線上技術解題面試: Failed" . PHP_EOL;
return false;
}
echo "線上技術解題面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

// Handler Implemation 3: 到公司與 CTO 技術解題面試 ====================
class OnSiteTechicalWithCTOInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "到公司與 CTO 技術解題面試: Start" . PHP_EOL;
if ($interviewee->performance <= 70) {
echo "到公司與 CTO 技術解題面試: Failed" . PHP_EOL;
return false;
}
echo "到公司與 CTO 技術解題面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

// Handler Implemation 4: 到公司與設計總監進行設計作品集面試 ====================
class OnSiteDesignSupervisorInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "到公司與設計總監進行設計作品集面試: Start" . PHP_EOL;
if ($interviewee->performance <= 80) {
echo "到公司與設計總監進行設計作品集面試: Failed" . PHP_EOL;
return false;
}
echo "到公司與設計總監進行設計作品集面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

// Handler Implemation 5: 到公司與 CEO 的 1 on 1 面試 ====================
class OnSiteFinalCEOInterview extends BaseInterview
{
public function handle(Interviewee $interviewee): bool
{
echo "到公司與 CEO 的 1 on 1 面試: Start" . PHP_EOL;
if ($interviewee->performance <= 90) {
echo "到公司與 CEO 的 1 on 1 面試: Failed" . PHP_EOL;
return false;
}
echo "到公司與 CEO 的 1 on 1 面試: Pass" . PHP_EOL;
return $this->passToNext($interviewee);
}
}

class JcKeyCode
{
private function prepareForEngineerInterview(): Interview
{
// HR 電話初步面試
$hrPhoneCallInterview = new HRPhoneCallInterview();
// 線上技術解題面試
$techicalOnLineInterview = new OnSiteDesignSupervisorInterview();
// 到公司與 CTO 技術解題面試
$onSiteTechicalWithCTOInterview = new OnSiteTechicalWithCTOInterview();
// 到公司與 CEO 的 1 on 1 面試
$onSiteFinalCEOInterview = new OnSiteFinalCEOInterview();

// 設定面試的執行順序 ==============================
// HR 電話初步面試 -> 線上技術解題面試
$hrPhoneCallInterview->setNext($techicalOnLineInterview);
// 線上技術解題面試 -> 到公司與 CTO 技術解題面試
$techicalOnLineInterview->setNext($onSiteTechicalWithCTOInterview);
// 到公司與 CTO 技術解題面試 -> 到公司與 CEO 的 1 on 1 面試
$onSiteTechicalWithCTOInterview->setNext($onSiteFinalCEOInterview);
return $hrPhoneCallInterview;
}

private function prepareForDesignerInterview(): Interview
{
// HR 電話初步面試
$hrPhoneCallInterview = new HRPhoneCallInterview();
// 到公司與設計總監進行設計作品集面試
$onSiteDesignSupervisorInterview = new OnSiteDesignSupervisorInterview();
// 到公司與 CEO 的 1 on 1 面試
$onSiteFinalCEOInterview = new OnSiteFinalCEOInterview();

// 設定面試的執行順序 ==============================
// HR 電話初步面試 -> 到公司與設計總監進行設計作品集面試
$hrPhoneCallInterview->setNext($onSiteDesignSupervisorInterview);
// 到公司與 CTO 技術解題面試 -> 到公司與 CEO 的 1 on 1 面試
$onSiteDesignSupervisorInterview->setNext($onSiteFinalCEOInterview);
return $hrPhoneCallInterview;
}

public function interview(Interviewee $interviewee): bool
{
switch ($interviewee->position){
case POSITION_TYPE_ENGINEER:
return $this->prepareForEngineerInterview()->handle($interviewee);
case POSITION_TYPE_DESIGNER:
return $this->prepareForDesignerInterview()->handle($interviewee);
default:
throw new \Exception("Position Not Avaiable!");
}
}

public function hire(Interviewee $interviewee)
{
$position = $interviewee->position == POSITION_TYPE_ENGINEER ? "工程師" : "設計師";
echo "{$interviewee->name} - {$position} 面試流程開始: ==================== " . PHP_EOL;
$result = $this->interview($interviewee);
if ($result) {
$message = "get the offer ! 🎉";
} else {
$message = "interview failed 😭";
}
echo "{$interviewee->name} - {$position} 面試流程結束: {$message} ==================== " . PHP_EOL;
echo "" . PHP_EOL;
}
}

function main()
{
$jcKeyCode = new JcKeyCode();
// deisgner
$john = new Interviewee("John", POSITION_TYPE_DESIGNER, 3);
$jcKeyCode->hire($john);
$alice = new Interviewee("Alice", POSITION_TYPE_DESIGNER, 15);
$jcKeyCode->hire($alice);
$paul = new Interviewee("Paul", POSITION_TYPE_DESIGNER, 85);
$jcKeyCode->hire($paul);
$patty = new Interviewee("Patty", POSITION_TYPE_DESIGNER, 95);
$jcKeyCode->hire($patty);

// engineer
$mary = new Interviewee("Mary", POSITION_TYPE_ENGINEER, 4);
$jcKeyCode->hire($mary);
$tommy = new Interviewee("Tommy", POSITION_TYPE_ENGINEER, 15);
$jcKeyCode->hire($tommy);
$peter = new Interviewee("Peter", POSITION_TYPE_ENGINEER, 80);
$jcKeyCode->hire($peter);
$david = new Interviewee("David", POSITION_TYPE_ENGINEER, 99);
$jcKeyCode->hire($david);
}

main();

好的!終於調整完了~ 我想最大的改變就是 interview method 吧?

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
# Before
public function interview(Interviewee $interviewee): bool
{
echo "HR 電話初步面試: Start" . PHP_EOL;
if ($interviewee->performance > 5) {
echo "HR 電話初步面試: Pass" . PHP_EOL;
// if Engineer
if ($interviewee->position == POSITION_TYPE_ENGINEER) {
// 不忍直視 ... 略
}

// if Designer
if ($interviewee->position == POSITION_TYPE_DESIGNER) {
// 不忍直視 ... 略
}
} else {
echo "HR 電話初步面試: Failed" . PHP_EOL;
return false;
}
}

# After
public function interview(Interviewee $interviewee): bool
{
switch ($interviewee->position){
case POSITION_TYPE_ENGINEER:
return $this->prepareForEngineerInterview()->handle($interviewee);
case POSITION_TYPE_DESIGNER:
return $this->prepareForDesignerInterview()->handle($interviewee);
default:
throw new \Exception("Position Not Available!");
}
}

不過調整這樣有什麼好處呢? 😅

新增需求的調整容易

除了把雜亂的 if-else 基於 單一職責原則 放在個別的 Handler 去處理以外,對於上述新增 HR 面試流程的需求,也可以很輕易地調整。我們只需要在 JcKeyCode 類別裡新增「設定 HR 面試流程」的 method: prepareForHRInterview

1
2
3
4
5
6
7
8
9
10
11
private function prepareForHRInterview(): Interview
{
// HR 電話初步面試
$hrPhoneCallInterview = new HRPhoneCallInterview();
$onSiteFinalCEOInterview = new OnSiteFinalCEOInterview();

// 設定面試的執行順序 ==============================
// HR 電話初步面試 -> 到公司與 CEO 的 1 on 1 面試
$hrPhoneCallInterview->setNext($onSiteFinalCEOInterview);
return $hrPhoneCallInterview;
}

然後只需要在 interview() 上加上 HR 的判斷即可囉!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function interview(Interviewee $interviewee): bool
{
switch ($interviewee->position){
case POSITION_TYPE_ENGINEER:
return $this->prepareForEngineerInterview()->handle($interviewee);
case POSITION_TYPE_DESIGNER:
return $this->prepareForDesignerInterview()->handle($interviewee);
case POSITION_TYPE_HR:
return $this->prepareForHRInterview()->handle($interviewee);
default:
throw new \Exception("Position Not Available!");
}
}
Amy - 人資 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
到公司與 CEO 的 1 on 1 面試: Start
到公司與 CEO 的 1 on 1 面試: Pass
Amy - 人資 面試流程結束: get the offer ! 🎉 ====================

調整組合容易

如果今天想要應徵的工程師也要會點設計的技能,所以在工程師的面試流程中,需要在「到公司與 CTO 技術解題面試」與「到公司與 CEO 的 1 on 1 面試」之間,再多一關「到公司與設計總監進行設計作品集面試」的面試,我們只需要調整 prepareForEngineerInterview 即可~

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
private function prepareForEngineerInterview(): Interview
{
// HR 電話初步面試
$hrPhoneCallInterview = new HRPhoneCallInterview();
// 線上技術解題面試
$techicalOnLineInterview = new OnSiteDesignSupervisorInterview();
// 到公司與 CTO 技術解題面試
$onSiteTechicalWithCTOInterview = new OnSiteTechicalWithCTOInterview();
// 到公司與設計總監進行設計作品集面試 ⬅️ modified
$onSiteDesignSupervisorInterview = new OnSiteDesignSupervisorInterview(); ⬅️ modified
// 到公司與 CEO 的 1 on 1 面試
$onSiteFinalCEOInterview = new OnSiteFinalCEOInterview();

// 設定面試的執行順序 ==============================
// HR 電話初步面試 -> 線上技術解題面試
$hrPhoneCallInterview->setNext($techicalOnLineInterview);
// 線上技術解題面試 -> 到公司與 CTO 技術解題面試
$techicalOnLineInterview->setNext($onSiteTechicalWithCTOInterview);
// 到公司與 CTO 技術解題面試 -> 到公司與設計總監進行設計作品集面試 ⬅️ modified
$onSiteTechicalWithCTOInterview->setNext($onSiteDesignSupervisorInterview); ⬅️ modified
// 到公司與設計總監進行設計作品集面試 -> 到公司與 CEO 的 1 on 1 面試 ⬅️ modified
$onSiteDesignSupervisorInterview->setNext($onSiteFinalCEOInterview); ⬅️ modified
return $hrPhoneCallInterview;
}

David - 工程師 面試流程開始: ====================
HR 電話初步面試: Start
HR 電話初步面試: Pass
到公司與設計總監進行設計作品集面試: Start
到公司與設計總監進行設計作品集面試: Pass
到公司與 CTO 技術解題面試: Start
到公司與 CTO 技術解題面試: Pass
到公司與設計總監進行設計作品集面試: Start
到公司與設計總監進行設計作品集面試: Pass
到公司與 CEO 的 1 on 1 面試: Start
到公司與 CEO 的 1 on 1 面試: Pass
David - 工程師 面試流程結束: get the offer ! 🎉 ====================

是不是方便許多呢? 😀

責任鏈設計模式優缺點

優點

  • 🎉 單一職責原則把不同的驗證放在各自的類別裏面
  • 🎉 驗證的類別被拆開了,因此更方便調整組合順序與新增組合
  • 🎉 若需要增加額外的驗證,只需最小幅度調整即可,例如上述的 HR 面試流程

缺點

  • 😬 檔案數量變得很多 XD

責任鏈設計模式使用時機

如果一個需要經過好幾段檢驗流程的行爲,那也許就會是責任鏈設計模式適合的場景。

總結

以上就是責任鏈設計模式的介紹,感謝你的收看~!

參考