0%

Single Responsibility Principle(單一職責)

剛學寫程式的時候,很容易就把所有的功能寫在同一個檔案上或者是類別上,我們就這樣創造了 God Class(神之類別),這樣有什麼不妥嗎?會有什麼問題呢?一起用一個小故事來看看這樣潛在的風險與如何避免吧!

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


Solid 的第一個 S 代表的就是 Single Responsibility Principle (SRP) ,翻成中文為「單一職責」。至於 Solid 是什麼意思,網路上應該滿多解釋的,而且我也可能無法在解釋的比他們好,有興趣可以參考底下的連結:https://zh.wikipedia.org/wiki/SOLID\_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1)


為什麼要單一化職責?

我們從一個情境來當起手勢:

傑西的夢想是開一家早餐店,但是一開始的時候把所有的資金都投資在早餐店的設備了,所以只能自己一個人經營這家早餐店,所以早餐店開門營業後需要的工作簡單地分為:

  • 接受客人的點餐(take client order)
  • 烹飪客人的餐點(cook by order)
  • 將餐點給客人,並且收錢(receive payment)

如果用程式來表示的話可能會像這樣子:

1
2
3
4
5
6
7
8
interface IBreakfastWorker
{
public function takeClientOrder($order);

public function cookByOrder();

public function receivePayment($money);
}

一開始第一個月因為新開幕,客人還不太知道這裡新開了一家早餐店,光顧的客人還不多,所以傑西一個人負責上述的三件事情就十分綽綽有餘了。

我們簡單地用圖解釋一下傑西目前的職責:

我們將傑西的工作簡單用程式碼呈現一下:

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
class BreakfastWorker implements IBreakfastWorker
{
private $order = null;
private $totalMoney = 0;
private $foods = null;

public function takeClientOrder($order)
{
// 接受客人的餐點
$this->order = $order;
}


public function cookByOrder()
{
// 針對訂單的每一筆細項餐點烹飪
foreach ($this->order as $orderItem) {
$this->foods[] = $this->cookFor($orderItem);
}
}


public function receivePayment($money)
{
// 收錢
$this->totalMoney += $money;
// 將食物交給客人
return $this->foods;
}
}

如果是客人的話,來早餐店消費會做兩件事情:

客人用程式表現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface IClient {
public function makeOrder();

public function payMoney();
}

// ----------------------------------------

class Client implements IClient{

public function makeOrder()
{
// 我想點漢堡、紅茶
return ['漢堡', '紅茶'];
}

public function payMoney()
{
// 漢堡 80 元,紅茶 20 元
return 100;
}
}

所以傑西與客人的互動用程式表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 我是傑西
$jc = new BreakfastWorker();

// 我是客人
$client = new Client();

// 客人點餐:我要點漢堡、紅茶
$order = $client->makeOrder();

// 傑西接受客人的餐點
$jc->takeClientOrder($order);

// 傑西準備客人的餐點
$jc->cookByOrder();

// 客人付款:總共 100 元
$money = $client->payMoney();

// 傑西收款,並且將餐點交給客人
$food = $jc->receivePayment($money);

這樣子悠閒的日子就這樣子過了一個多月,但是隨著傑西早餐店的品質與價格優異的 CP 值似乎碾壓了附近的早餐店,網路上的介紹推薦文也如雨後春筍班地爆炸性出現,所以傑西早餐店的名聲迅速地傳遞開來。

隨著聲名遠播的效應,越來越多人慕名而來,過不了多久,傑西就覺得他一個人似乎無法撐起這個永無止盡的客人人潮,所以他很快就找了阿白來幫忙,請阿白分攤傑西目前所做的任務,所以阿白的任務是:

當阿白也一起加入這家早餐店的工作後,一開始似乎有解緩一些工作內容,但過沒多久之後發現會出現以下狀況:

  • 兩個人一起都在料理,沒人幫客人點餐
  • 料理好了,但是沒人幫忙給客人並且收款
  • 兩人都在點餐,卻沒人在幫客人準備料理

因為上述的問題造成傑西早餐店的服務品質大受影響,網路上也開始出現負面的評價,所以他與他的 Partner 阿白針對這件事情大吵一架。

傑西:「你不要我在點餐的時候也跑來跟我一起點餐好不好!總是有人要負責出餐阿!」

阿白:「是我在點餐的時候你也跑來點好不好,客人就這麼多阿,我比較會跟客人 social,不然之後就我都負責點餐,你負責出餐跟收錢阿!」


單一化職責調整

經過這場不歡而散的爭吵與網路上越來越多的負面評價,傑西痛定思痛,在想要怎麼改善這個兩個人都在做一樣的事情的工作流程,讓早餐店恢復以往的光輝人潮,後來他認真思考阿白在爭吵中的提議:

讓阿白只負責點餐

所以傑西在早餐店新增了一個職位叫做服務生,負責以下事情:

  • 接受客人的點餐(take client order)

服務生的任務用程式碼來表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IWaiter
{
public function takeClientOrder($order);
}

class Waiter implements IWaiter {
private $order = null;

public function takeClientOrder($order)
{
// 接受客人的餐點
$this->order = $order;
}
}

既然有專門的服務生負責接受客人點餐的這個任務了,那麼傑西的任務似乎就可以移除掉接受客人餐點的任務囉:

程式碼更新為以下:

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
interface IBreakfastWorker {
public function cookByOrder($order);

public function receivePayment($money);
}

// ----------------------------------------

class BreakfastWorker implements IBreakfastWorker
{
private $totalMoney = 0;
private $foods = null;

public function cookByOrder($order)
{
// 針對訂單的每一筆細項餐點烹飪
foreach ($order as $orderItem) {
$this->foods[] = $this->cookFor($orderItem);
}
}


public function receivePayment($money)
{
// 收錢
$this->totalMoney += $money;
// 將食物交給客人
return $this->foods;
}
}

所以之後的傑西、阿白與客人的互動就會變成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 我是傑西
$jc = new BreakfastWorker();

// 我是阿白
$white = new Waiter();

// 我是客人
$client = new Client();

// 客人點餐:我要點漢堡、紅茶
$order = $client->makeOrder();

// 阿白接受客人的餐點,並且記錄在菜單上
$orderPaper = $white->takeClientOrder($order);

// 傑西準備客人的餐點
$jc->cookByOrder($orderPaper);

// 客人付款:總共 100 元
$money = $client->payMoney();

// 傑西收款,並且將餐點交給客人
$food = $jc->receivePayment($money);

經過上述的任務分配調整,終於成功地解決了兩個人都在點餐卻沒人備餐或者是收錢的問題,阿白就專心地紀錄客人的點單記錄成訂單,並且將訂單交給傑西處理後續的問題。


錯誤原因容易查找

雖然點餐的問題改善了,網路上還是出現其他的負評,而這些負評似乎都是發生在備餐與收款的問題上,常常收完錢之後就忘記剛剛的餐點準備到哪一個了,所以這個環節也造成客戶等待太久或者是漏餐等等的困擾。

也因為先前的職責區分,所以馬上就知道這個問題是發生在傑西的身上,阿白就可以完全地免除了這個問題的責任。

傑西看著每天要負責的任務似乎還太多了,似乎還是需要找一個人來協助自己處理收款的任務,讓自己可以心無旁騖地備餐,於是傑西找了果汁來幫助他收款的任務,所以新增了一個職位:收銀員,負責以下的任務:

  • 將餐點給客人,並且收錢(receive payment)

程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface ICashier {
public function receivePayment($foods, $money);
}

// -------------------------------

class Cashier implements ICashier
{
private $totalMoney = 0;

public function receivePayment($foods, $money)
{
// 收錢
$this->totalMoney += $money;
// 將食物交給客人
return $foods;
}

}

既然有專門的收銀員負責收錢的這個任務了,那麼傑西的任務更新如下:

程式碼更新如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface IBreakfastWorker {
public function cookByOrder($order);
}

// -----------------------------------------

class BreakfastWorker implements IBreakfastWorker {
private $foods = null;

public function cookByOrder($order)
{
// 針對訂單的每一筆細項餐點烹飪
foreach ($order as $orderItem) {
$this->foods[] = $this->cookFor($orderItem);
}
return $this->foods;
}
}

後來傑西發現他的工作任務根本是個主廚的角色,所以他就將自己的職位定義的更為清楚改為:主廚,所以會做以下的更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface IChef {
public function cookByOrder($order);
}

// ------------------------------------
class Chef implements IChef {
private $foods = null;

public function cookByOrder($order)
{
// 針對訂單的每一筆細項餐點烹飪
foreach ($order as $orderItem) {
$this->foods[] = $this->cookFor($orderItem);
}
return $this->foods;
}

}

在經歷上述的一連串的職責調整後,與客人的互動程式碼如下:

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
// 我是傑西
$jc = new Chef();

// 我是阿白
$white = new Waiter();

// 我是果汁
$juice = new Cashier();

// 我是客人
$client = new Client();

// 客人點餐:我要點漢堡、紅茶
$order = $client->makeOrder();

// 阿白接受客人的餐點,並且記錄在菜單上
$orderPaper = $white->takeClientOrder($order);

// 傑西準備客人的餐點
$foods = $jc->cookByOrder($orderPaper);

// 客人付款:總共 100 元
$money = $client->payMoney();

// 果汁從傑西那邊收到餐點,裝入塑膠袋,收取客人的付款,並且將餐點交給客人
$food = $juice->receivePayment($foods, $money);

結論

經過一連串的人事異動調整後,每個人各司其職後,傑西早餐店的流程也變得更流暢了,客人等待的時間也大幅減少,吃到嘴裡的餐點也都是剛出爐熱騰騰的美味,很快地如海嘯般湧來的正面評價馬上就刷洗掉了先前的負評。

因為每個人都只負責單一的某一個任務,而不會包攬了很多不同的職責搞的分身乏術,而且一有發生問題也因為職責的單一所以更容易找出問題的發生點,如同上述發生問題後很快地就發現這個問題是出在傑西無法同時處理備餐與收款的狀況,也可以更快地修補與改善這個問題。

在現實生活中主管依據不同的職責角色分派工作,如果每個職員都有他負責的一項任務,因此對於自己的任務的定義也十分清楚明瞭,那在執行上就更快速明確,發生問題也無法推責而確認問題點進而改善。

其實寫程式某種程度跟主管在分派任務一樣,只是分派的任務是由一個個的程式在執行任務。如果我們在寫程式的時候可以用更明確的任務來區分不同的角色,如同上述的大廚、服務生、收銀員等等的不同職位來區分,除了當發生問題的時候也是更容易找到問題的發生點的好處以外,也因為職責的區分明確也更容易擴大規模化,變成一個廚師部門、服務部門、收銀部門等等的規模。

所以下次在撰寫程式碼的時候可以多想一下:

這個 method 是否為這個 class 的職責呢?


雖然標題叫做「單一職責」,但是也不是說一個 class 只能有一個 method。

我自己的理解是:

同一個角色該負責的任務放在同一個 class 內

舉個例子:

如果是上述的大廚角色,也許還會有泡咖啡、煎火腿、製作漢堡、製作甜點等等的工作,但是很直覺就會將這些工作內容放在大廚的角色上,當然也許未來隨著規模更大了,有專門的職位負責飲料相關的角色或者是專門負責製作甜點的角色,那也許就更適合放在那些職位身上了,但是如果今天只有一位大廚的角色的話,那應該還是放在大廚身上是比較適合的。

故事到這邊差不多到了尾聲囉,最後感謝你的收看啦  😃