From Apprentice To Artisan - Single Responsibility Principle 單一職責原則

這陣子又把這本書拿出來重新 review 一次,有了新的想法,特別做一份簡單的筆記紀錄。

The Single Responsibility Principle states that a class should have one, and only one, reason to change. In other words, a class’ scope and responsibility should be narrowly focused.
單一職責原則狀態是指一個類必須有一個也只能有一個理由去改變。換句話說就是一個類必須專注集中且狹隘的。

乍聽之下感覺是說類當中僅能做特定的某些事情,但實際上在許多情況下卻很難完全照著方針做,由於會被業務性和流程做影響,但在這樣子的環境下,就是展現自我專業的時刻,以自我要求為出發點,做到最好。

書中舉的例子非常好

class OrderProcessor 
{
public function __construct(BillerInterface $biller)
{
$this->biller = $biller;
}

public function process(Order $order)
{
$recent = $this->getRecentOrderCount($order);

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

$this->biller->bill($order->account->id, $order->amount);

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

protected function getRecentOrderCount(Order $order)
{
$timestamp = Carbon::now()->subMinutes(5);
return DB::table('orders')
->where('account', $order->account->id)
->where('created_at', '>=', $timestamps)
->count();
}
}

看到這段代碼的時候其實會思考,這樣做有什麼不對嗎?但仔細分析後你會發現其實混合著判斷和處理,另外會發現當如果我是要去修改 獲得 10 分鐘前訂單狀況數量 我還是要回到 OrderProcessor 做資料庫查詢條件修改,但要記住這個類是訂單處理器,而不是取得最近訂單數量判斷器。

class OrderProcessor 
{
...

public function process(Order $order)
{
/* 判斷區塊 */
// 獲得 5 分鐘前訂單狀況數量
$recent = $this->getRecentOrderCount($order);

// 如發現數量 > 0 則代表重複訂單,拋出異常
if($recent > 0) {
throw new Exception('Duplicate order likely.');
}
/* 判斷區塊 */

// 訂單處理流程...
$this->biller->bill($order->account->id, $order->amount);

// 歷程紀錄
DB::table('orders')->insert(array(
'account' => $order->account->id,
'amount' => $order->amount,
'created_at'=> Carbon::now()
));
}

// 獲得 5 分鐘前訂單狀況數量
protected function getRecentOrderCount(Order $order)
{
$timestamp = Carbon::now()->subMinutes(5);

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

這邊會發現有兩個地方都有分別在呼叫資料庫並且做 query 的動作,不彷把這樣子的資料庫互動抽出成 OrderRepository 讓其更加專注處理與資料庫互動的部分。

class OrderRepository 
{
public function getRecentOrderCount(Account $account)
{
$timestamp = Carbon::now()->subMinutes(5);

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

public function logOrder(Order $order)
{
DB::table('orders')->insert(array(
'account' => $order->account->id,
'amount' => $order->amount,
'created_at'=> Carbon::now()
));
}
}

最後再將其 OrderRepository 注入到 OrderProcessor,讓 OrderRepository 承載與其資料庫資料作互動查詢的動作。

class OrderProcessor 
{
public function __construct(BillerInterface $biller, OrderRepository $orders)
{
$this->biller = $biller;
$this->orders = $orders;
}

public function process(Order $order)
{
$recent = $this->orders->getRecentOrderCount($order->account);

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

$this->biller->bill($order->account->id, $order->amount);

$this->orders->logOrder($order);
}
}

這麼做的好處是什麼?

讓其更加方便修改

假如原本 獲得 5 分鐘前訂單狀況數量 要修改成 獲得 10 分鐘前訂單狀況數量 我們去 OrderRepository 做修改即可,這邊就會有一個問題了,以上面這個例子來說可能還沒有太大感覺,但想想如果說 getRecentOrderCount 這個函式許多地方會使用到,假設沒有將它獨立提取會導致修改多處,這將違反 DRY(don’t repeat yourself.)

職責清晰可靠

以上面的例子來說,OrderProcessor 專注在 handle 其流程處理,他不需要去理會與資料庫作互動查詢。而 OrderRepository 則負責資料庫互動查詢,他也不需要去理會流程面。在這樣子的限制下可以寫出高內聚(專注集中)的類。

代碼更容易測試

透過高內聚專注類,每個類都可以獨立被測試,舉例來說我們現在要測試 OrderRepository 就僅需要 mock 與資料庫互動查詢有關的類即可,不需要去考慮更上層的流程問題,專注在資料庫互動查詢結果上。而 OrderProcessor 則可以直接 mock OrderRepository ,因為可以放心地確認他是可靠而穩定的類別,專注在流程上。

下一篇會筆記關於 Open Closed Principle 開放封閉原則。