案例 1 前置作業

按此查看 Git 源碼

  1. 建立專案 laravel new ch8_SRP_demo
  2. 進入專案目錄 cd ch8_SRP_demo
  3. 安裝 Laravel 相依套件 composer install
  4. 編輯 .env 設定資料庫帳密,還有記得 php artisan key:generate
  5. 利用 migrate 測試資料庫設定 php artisan migrate:install
  6. 建立 Account model 類別和對應的 table migration php artisan make:model MyCart/Account -m
  7. 同上,建立 Order model 類別和 table php artisan make:model MyCart/Order -m
  8. 同上,建立 Product model 類別和 table php artisan make:model MyCart/Product -m
  9. 建立測試用 Controller 透過 php artisan make:controller CheckoutController
  10. (optional) 補一下 ide_helperphpsotrm meta 協助 PhpStorm 語法顯示
  11. MyCart 目錄下建立 MyCart 類別,還有模擬加入購物車並結帳的行為,建立 routes 和 checkout 頁面
  12. 此時進入 checkout 頁報錯誤是正常,這時才開始一個個補齊相關的 production code 和 DB 欄位等等,讓頁面顯示
  13. 補上要處理訂單的 handler,並設定 routes 接 form post 資料
  14. 建立訂單(Order),以及訂單處理流程(OrderProcessor),還有金流處理介面(BillerInterface),報錯正常,還未實作
  15. 完成案例 1,實作處理訂單流程

單一職責原則 (Single Responsibility Principle)

所謂單一職責原則(SRP),按照英文直接翻譯「應該且僅有一個原因引起類別的變更」想必大家還是不了解是什麼意思,如果用閃亮亮的話來說,就是

讓一個 class/method 只做一件事



也可以說

特定需求改變的時候,只有「一個」相關的 class/method 需要做修改。



讓我們直接來看一個例子:

案例 1:

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
class OrderProcessor
{

public function process(Order $order)
{

echo "<h4>訂單處理中...</h4>";

$recent = $this->getRecentOrderCount($order);

if ($recent > 0) {
throw new Exception('Duplicate order likely.');
}

$this->biller->bill($order->getAccount()->id, $order->getAmount());

$id = DB::table('orders')->insertGetId(array(
'account' => $order->getAccount()->id,
'amount' => $order->getAmount(),
'created_at' => Carbon::now()
));

return $id;
}

private function getRecentOrderCount(Order $order)
{

$timestamps = Carbon::now()->subMinutes(3);

return DB::table('orders')
->where('account', $order->getAccount()->id)
->where('created_at', '>=', $timestamps)
->count();
}
}

案例研討:

  1. 這段例子,如果按照「單一職責原則」來看有什麼問題?會造成什麼影響? (拜託別回答:因為沒有「一個 class/method 做一件事」)
  2. 那要怎麼樣才叫「一件事」?
  3. 如何把這「一件事」從 OrderProcessorprocess 的職責中抽離,卻不影響原本已寫好的 Code?







========= 我是分隔線 ========== 我是分隔線 ========== 我是分隔線 ==========


職責 1:顯示 “訂單處理中 …”

“顯示” 這件事很明顯不是 process 該做的吧,問題是要放到哪呢?

職責 2:取得最新訂單筆數

要使用 account id 來去資料庫撈資料決定最新訂單筆數,明顯不是已經在 process 中該做的。

職責 3:利用訂單筆數,偵測是不是重複的訂單

這個閃亮亮覺得有爭議,小系統說真的,是可以放在 OrderProcessor 的類別裡的,但應該抽出一個 method 專門處理 isDuplicateOrder() 這個判斷。至於抽出來的 isDuplicateOrder() 又該放哪呢?

職責 4:透過 biller 進行結帳

已經交由 biller 處理了,本來就是處理訂單的流程之一。

職責 5:留下訂單記錄

這個蠻多人應該很容易混淆吧,感覺訂單記錄本來就是處理訂單要做的啊。那閃亮亮換個問法,會不會有不需要 biller 結帳,也不用檢查有沒有重複訂單,但卻要留下訂單記錄的情況?例如:買贈品或免費的商品、例如對已存在的訂單做取消的記錄。









========= 我是分隔線 ========== 我是分隔線 ========== 我是分隔線 ==========

還記得「一個 class 只做一件事」嗎?剛剛案例研討埋了一個雷

剛剛的案例研討裡埋了一個雷,講了半天沒有討論到:

process 方法的職責是什麼啊?

怎麼職責全部分光光了?

OrderProcessorprocess 的職責在於處理「流程」
流程包含:(1) 取得最新訂單筆數、(2) 偵測重複訂單、(3) 進行結帳、(4) 留下訂單記錄,
但 (1) 到 (4) 的實作上怎麼進行,則應交給其他的 class/method 來完成。

重構時間

  1. 顯示 “訂單處理中 …” 移到 Controller 或其他地方
  2. 將 “取得最新訂單筆數” 的實作,移到新建的 OrderRepository 類別,並且 inject 到 OrderProcessor 裡。
  3. 將 “偵測重複訂單” 的邏輯新建一個 isDuplicateOrder() 方法在 OrderProcessor
  4. 把 “留下訂單記錄” 的職責移到 OrderRepositorylogOrder 方法中

過程請看 Git Repo Here,或到 Laradiner 讀書會(見置頂文) 來看 Live Coding。

重構後的好處,舉例

  1. 「抓取最新訂單筆數」的邏輯改變成抓 2 分鐘以內,不需動 process 方法
  2. 「判斷重複訂單」的邏輯變成不是按照 account id 來判斷,改成依照所有商品品項如果都一樣的話表示重複。
  3. 「留下訂單記錄」

重構完成如下

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
// app/MyCart/Processors/OrderProcessor.php
class OrderProcessor
{

protected $orders;

public function __construct(BillerInterface $biller, OrderRepository $orders)
{

$this->biller = $biller;
$this->orders = $orders;
}

public function process(Order $order)
{

if ($this->isDuplicateOrder($order)) {
throw new Exception('Duplicate order likely.');
}

$this->biller->bill($order->getAccount()->id, $order->getAmount());

$id = $this->orders->logOrder($order);

return $id;
}

private function isDuplicateOrder($order)
{

$recent = $this->orders->getRecentOrderCount($order);

return $recent > 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
// app/MyCart/Repository/OrderRepository.php
class OrderRepository
{

public function getRecentOrderCount(Order $order)
{

$timestamps = Carbon::now()->subMinutes(2);

return DB::table('orders')
->where('account', $order->getAccount()->id)
->where('created_at', '>=', $timestamps)
->count();
}

public function logOrder(Order $order)
{

$id = DB::table('orders')->insertGetId(array(
'account' => $order->getAccount()->id,
'amount' => $order->getAmount(),
'created_at' => Carbon::now()
));

return $id;
}
}

閃亮亮小結

其實如果真的有做好遵循 SRP 的原則的話,你的程式碼的彈性會相對高,Debug 速度也會超快,每個class/method責任很清楚(考試都考100分 噗),修 bug 不容易動到已有程式。不過,如何去切分 class 的職責其實是一門「藝術」,往往花最多時間的不是寫 Code 而是在取一個適當的類別名字,給予適當的職責,哈哈。只要你時時想著 SRP,不知不覺中很容易就符合下回將介紹的 「Open Close Principle (開放封閉原則)」,敬請期待!

文章目錄
  1. 1. 案例 1 前置作業
    1. 1.0.1. 按此查看 Git 源碼
  • 2. 單一職責原則 (Single Responsibility Principle)
    1. 2.0.1. 案例 1:
    2. 2.0.2. 案例研討:
    3. 2.0.3. 職責 1:顯示 “訂單處理中 …”
    4. 2.0.4. 職責 2:取得最新訂單筆數
    5. 2.0.5. 職責 3:利用訂單筆數,偵測是不是重複的訂單
    6. 2.0.6. 職責 4:透過 biller 進行結帳
    7. 2.0.7. 職責 5:留下訂單記錄
  • 3. 還記得「一個 class 只做一件事」嗎?剛剛案例研討埋了一個雷
    1. 3.0.1. 那 process 方法的職責是什麼啊?
  • 4. 重構時間
    1. 4.0.1. 過程請看 Git Repo Here,或到 Laradiner 讀書會(見置頂文) 來看 Live Coding。
    2. 4.0.2. 重構後的好處,舉例
    3. 4.0.3. 重構完成如下
  • 5. 閃亮亮小結