SOLID 原則 - Single Responsibility
SOLID 代表著物件導向中,五種不同的開發原則,分別是:
- Single Responsibility Principle (單一職責原則)
- Open Closed Principle (開放封閉原則)
- Liskov Substitution Principle (里氏替換原則)
- Interface Segregation Principle (介面隔離原則)
- Dependency Inversion Principle (依賴反轉原則)
對我來說,在開發過程中嚴守這五種原則,比起去運用某些Design Pattern 來得重要許多,但並不是說Design Pattern 不重要。
而是Design Pattern 的實踐,有很大的部分就是讓你不違反 SOLID 原則。
Single Responsibility
A class should have one, and only one, reason to change.
一個class 應該只做一件事
降低類別對於其他事物的依賴程度,便是Single Responsibility 的核心準則。
一個未遵守Single Responsibility 開發出來的Class,像一個多功能的瑞士刀。
一個擁有各式各樣功能的Class,雖然酷炫華麗,但是有太多的要素會影響到該Class 功能,每一次的修改,便需重寫一隻測試程式來確保功能的正確性。
要做的事情很簡單:
Do one thing and do it well.
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
class SalesReporter { public function between($startDate, $endDate) { $sales = $this->queryDBForSalesBetween($startDate, $endDate);
return $this->format($sales); }
protected function queryDBForSalesBetween($startDate, $endDate) { return DB::table('sales') ->whereBetween('created_at', [$startDate, $endDate]) ->sum('charge') / 100; }
protected function format($sales) { return "<h1>Sales: " . $sales . "</h1>"; } }
|
還記得上方提到的嗎?
A class should have one, and only one, reason to change.
這個範例已經違反了Single Responsibility 原則
- 如果要改變資料的取得,必須改變這個Class
- 如果要改變輸出的方式,必須改變這個Class
我們有兩種consumers 會影響到Class 的生成結構。
Solution
改變資料的取得
1 2 3 4 5 6
| protected function queryDBForSalesBetween($startDate, $endDate) { return DB::table('sales') ->whereBetween('created_at', [$startDate, $endDate]) ->sum('charge') / 100; }
|
我們可以使用Repository 來取得資料
1 2 3 4 5 6 7 8 9 10 11
| <?php
class SalesRepository { public function between($startDate, $endDate) { return DB::table('sales') ->whereBetween('created_at', [$startDate, $endDate]) ->sum('charge') / 100; } }
|
因此,SalesReporter
則修改成如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
class SalesReporter { private $repo;
public function __construct(SalesRepository $repo) { $this->repo = $repo; }
public function between($startDate, $endDate) { $sales = $this->repo->between($startDate, $endDate);
return $this->format($sales); }
protected function format($sales) { return "<h1>Sales: " . $sales . "</h1>"; } }
|
有發現到嗎?我們把取得資料的相依關係透過SalesRepository
來處理
如此一來,當我們要更改取得資料這段邏輯時,便不需要更改 SalesReporter
了。
改變輸出的方式
1 2 3 4
| protected function format($sales) { return "<h1>Sales: " . $sales . "</h1>"; }
|
為什麼這段會有問題呢?
當我們不想透過HTML 輸出,而是要改成CSV 或者Json 格式來輸出,要怎麼處理
是不是只能繼續擴充 method,例如:
1 2 3 4 5 6 7 8 9
| protected function formatHtml($sales) { return "<h1>Sales: " . $sales . "</h1>"; }
protected function formatJson($sales) { return json_encode($sales); }
|
我們可以預期有很多不同的輸出方式,因此可以將輸出的方式抽象出來:
1 2 3 4
| interface SalesOutputInterface { public function output($sales); }
|
而當需要輸出成HTML 格式時,只要擴充Class 即可:
1 2 3 4 5 6 7
| class HtmlOutput implements SalesOutputInterface { public function output($sales) { return "<h1>Sales: " . $sales . "</h1>"; } }
|
或者是你想輸出成Json 格式時:
1 2 3 4 5 6 7
| class JsonOutput implements SalesOutputInterface { public function output($sales) { return json_encode($sales); } }
|
因此,SalesReporter
便能修改成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php
class SalesReporter { private $repo;
public function __construct(SalesRepository $repo) { $this->repo = $repo; }
public function between($startDate, $endDate, SalesOutputInterface $formatter) { $sales = $this->repo->between($startDate, $endDate);
return $formatter->output($sales); } }
|
將輸出的方法獨立出來,降低 SalesReporter
對於輸出的依賴關係
這樣一來 SalesReporter
的consumers 能降低,對於擴充的彈性也能變大。
最後新版的SalesReporter 使用方法如下:
1 2 3 4 5 6 7 8 9
| function foo() { $report = new SalesReporter(new SalesRepository); $begin = Carbon::now()->subDays(10); $end = Carbon::now(); return $report->between($begin, $end, new HtmlOutput); }
|