過程有問題可到 Laravel 台灣臉書社團 發問

依賴反轉原則 (DIP - Dependency Inversion Principle)

  • 節錄自 Laravel: From Apprentice To Artisan - Advanced Architecture With Laravel 4 書中:

Dependency Inversion principle, it states that high-level code should not depend on low-level code. Instead, high-level code should depend on an abstraction layer that serves as a “middle-man” between the high and low-level code.

  • 依賴反轉原則,「高階的」程式碼不應該依賴於「低階的」程式碼,「高階的」程式碼應當依賴於一個「抽象層」做為「高階」程式碼和「低階」程式碼溝通的「中間人」。

Abstractions should not depend upon details, but rather details should depend upon abstractions.

  • 另一種說法是,「抽象」不應當依賴於「實作細節」,反之,「實作細節」應該依賴於「抽象」






反例1:「抽象」依賴於「實作細節」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public function drive(SportsCar $sportCar) {
$sportCar->run();
}
}

class SportsCar {
public function run() {
var_dump('Sport car is running.');
}
}

$someone = new Person();
$someSportCar = new SportsCar();
$someone->drive($someSportCar);

違反了 DIP 原則,所以程式失去彈性, 只能夠開 跑車,不能開其他種車。

那魯宅和肥蛇怎麼辦?逃~~

  • 人 – 「高階」、「抽象」
  • 爸爸 – 「低階」、「實作」
  • 跑車 – 「低階」、「實作」
  • 車 – 「高階」、「抽象」

高、低階是相對比較來的


不太明白?沒關係,再舉個更常見實戰的例子








反例2:「抽象」依賴於「實作細節」之購物車範例

  • 購物車結帳,用信用卡付費
  • 購物車 – 「抽象」、「高階」
  • 信用卡 – 「實作」、「低階」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ShoppingCart {
protected $items;

public function checkout(CreditCard $card) {
$amount = $this->calculateAmount($this->items);

$card->pay($amount);
}
}

class CreditCard {
public function pay($amount) {
// ... 執行信用卡付款流程
}
}
  • 這個購物車類別,只能使用信用卡結帳。
  • 購物車類別「依賴於」信用卡類別,高階的購物車依賴於低階的信用卡類別。






如何「反轉」依賴關係?

  • 製作一個「抽象層」,讓「高階」、「低階」都依賴於「抽象層」,舉例:
1
2
3
4
// Payment.php 抽象層
interface Payment {
public function pay($amount);
}


  • 改寫,讓高階程式「購物車類別ShoppingCart」依賴於「抽象層」Payment 類別
1
2
3
4
5
6
7
8
9
10
// ShoppintCart.php
class ShoppingCart {
protected $items;

public function checkout(Payment $payment) { // <---- 這裡改寫
$amount = $this->calculateAmount($this->items);

$payment->pay($amount);
}
}


  • 改寫,讓低階實作「信用卡類別CreditCard」也依賴於「抽象層」Payment 類別
1
2
3
4
5
6
// CreditCard.php
class CreditCard implements Payment { // <---- 這裡改寫
public function pay($amount) {
// ... 執行信用卡付款流程
}
}


依賴反轉完成!

  • 好處:可以抽換「抽象層」的實作類別,系統彈性 Up! Up!
  • 遵循 DIP 原則,通常同時也會符合「OCP 開放封閉原則」,增加一個付款方式,ShoppingCart 中一行程式碼都不用改,生產力倍增。
  • 舉例:
1
2
3
4
5
6
// ATMPayment.php
class ATMPayment implements Payment {
public function pay($amount) {
// ... 執行 ATM 付款流程
}
}
  • 搭配「簡單工廠」模式來負責產生 Payment 的低階實作物件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// PaymentFactory.php
class PaymentFactory {
static public function choose($paymentType) {
switch ($paymentType) {
case 'ATM':
return new ATMPayment();
case 'Card':
return new CreditCard();
default:
return null;
}
}
}

// checkout.php
$cart = new ShoppingCart();
$payment = PaymentFactory::choose($_POST['paymentType']);
$cart->checkout($payment);
  • 好處:如果再增加第三種付款方式,ShoppingCart 類別完全不需要修改!只要在工廠中增加一種 case 就可以了。
  • 壞處:如果 interface Payment 定的不好,或是考慮的不夠通用、沒有延展性,之後要修改介面會很麻煩。






現在的依賴關係變成…

  • ShoppingCart類別 依賴於 Payment
  • CreditCard類別 依賴於 Payment
  • ATMPayment類別 也依賴於 Payment
  • Payment 做為金流付款的抽象層,定義共同的 API,要結帳就要實作 pay 這隻 API。

註: 單一職責原則,不要把「建立物件」的責任交給 ShoppingCart,會讓你好寫非常多!










來把反例1 也反轉過來,換成複雜一點的情況

  • 並不是家裡每個人都會開車,只有爸爸、媽媽會開,弟弟不會開車…
  • 假設家中有兩台車,一台 Toyota,另一台 Honda,開的方式都一樣

開始反轉吧!

1
2
3
4
5
class Person {
public function eat($food) {
// ... 不重要,就是身為一個人會做的事
}
}
  • 怎麼用程式表達「爸爸」、「媽媽」會開車,「弟弟」不會呢?
  • 只有需要實作 CanDrivePerson 才會依賴於 Car
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Car {
public function boot($key);
public function run($speed);
public function turn($direction);
public function park();
}

interface CanDrive {
public function drive(Car $car);
}

class Parent extends Person implements CanDrive {
public function drive(Car $car) {
$car->boot($this->carKey);
$car->run(100);
$car->run(20);
$car->turn('left');
$car->park();
}
}

class YoungBrother extends Person {
// 略,沒有 drive 的能力
}






  • 現在來看怎麼讓車子的實作依賴於「抽象」Car 介面
1
2
3
4
5
6
7
8
9
10
11
12
13
class Toyota implements Car {
public function boot($key) { /* 略… */ }
public function run($speed) { /* 略… */ }
public function turn($direction) { /* 略… */ }
public function park() { /* 略… */ }
}

class Honda implements Car {
public function boot($key) { /* 略… */ }
public function run($speed) { /* 略… */ }
public function turn($direction) { /* 略… */ }
public function park() { /* 略… */ }
}








  • 但如果爸爸是回到未來裡的博士,發明的車子可以做時光旅行…
1
2
3
4
5
6
7
8
9
class TimeTravelCar implements Car {
public function boot($key) { /* 略... */ }
public function run($speed) { /* 略... */ }
public function turn($direction) { /* 略... */ }
public function park() { /* 略... */ }

// 除了基本功能,還可以做時光旅行
public function timeTravel($year, $month, $day, $time) { /* 略... */ }
}








見下方程式碼,這樣寫可不可以?

1
2
3
4
5
6
7
8
9
class Father extends Parent {
public function timeTravel(Car $timeTravelCar, $dateObj) {
$timeTravelCar->timeTravel($dateObj->year, $dateObj->month, $dateObj->day, $dateObj->time);
}
}

$specialCar = new TimeTravelCar();
$father = new Father();
$father->timeTravel($specialCar, new MarriageDate("2013-09-20 12:00:00"));









- 以程式語法上來說是會通過的,但這是因為 PHP 是「弱型別」語言
- 試想,如果是「強型別」的語言的話…










這連 Compile 都不會過,因為 Car 介面中,並沒有一個叫 timeTravel() 的方法!










  • 那怎麼辦?老爸自己辛苦製造的時光車就不能開了嗎?
  • 解:
1
2
3
4
5
6
7
8
9
interface CanTimeTravel {
public function timeTravel(TimeTravelCar $specialCar);
}

class Father extends Parent implements CanTimeTravel {
public function timeTravel(TimeTravelCar $specialCar, $dateObj) {
$specialCar->timeTravel($dateObj->year, $dateObj->month, $dateObj->day, $dateObj->time);
}
}
  • 沒錯,這邊「違反了依賴反轉原則」,但我是有個好理由去違反的,TimeTravelCar 這種車是獨一無二的,它在平時和一般車無異,但在進行時光旅行timeTravel 的時候,我百分百肯定這是無法抽換成別的一般車,所以這邊閃亮亮覺得可以不需要再提取 interface。
  • 當然,如果你還是想按照教科書教的方式,給 TimeTravelCar 一個自己的「抽象層」也不是不行,但閃亮亮會覺得這樣就有點「過度設計(over-design)」了

書中範例解說

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Authenticator {
public function __construct(DatabaseConnection $db)
{

$this->db = $db;
}

public function findUser($id)
{

return $this->db->exec('select * from users where id = ?', array($id));
}

public function authenticate($credentials)
{

// Authenticate the user...
}
}
  • 誰是「高階」程式碼? 誰是「低階」程式碼?
  • 這個範例有符合 DIP 原則嗎?

怎麼解? 當回家作業練習看看吧~~~

  • 真的拿書來看啊~ 解答就在上面
  • 才兩、三頁而已,練練英文閱讀吧,用字都不難。










閃亮亮小結

到了本書的最後一個章節,總算是把 SOLID 全部介紹完了,如果這次範例的程式碼對你太過「抽象」,沒關係,把 SOLID 的前面 4 式練好,你自然就會練成第 5 式「依賴反轉原則」,詳見星爺電影<<武狀元蘇乞兒>>。閃亮亮覺得遵循 DIP 依賴反轉原則,是最容易寫出具備高度彈性高可測試性的程式碼,話說回來,也比較麻煩一些。我是指,如果你是新手,對編輯器的操作還不熟,光是開 interface、每個職責建一個 class,在不同檔案間切換的操作等等都是用滑鼠來點擊的話。那我想 DIP 離你還很遠,主要是因為搞這些太累了。先找一個好用的工具,如 PhpStormSublime Text 練熟操作之後再說。

閃亮亮覺得 DIP 原則,是嚴格遵循 OCP 開放封閉原則 會得到的必然結果,平時也沒特別在留意自己的 Code 有沒有符合,但會不斷的檢視自己的 Code 是否能夠在應付變化之下,只要修改一個小小的部份就能支援可能新的需求,也要避免 over design。套一句鐵哥(大澤木小鐵)說的:「為明天的需求設計,但不要為明年的設計」,共勉之~

文章目錄
  1. 1. 過程有問題可到 Laravel 台灣臉書社團 發問
  • 依賴反轉原則 (DIP - Dependency Inversion Principle)
    1. 反例1:「抽象」依賴於「實作細節」
      1. 1. 違反了 DIP 原則,所以程式失去彈性,人 只能夠開 跑車,不能開其他種車。
      2. 2. 高、低階是相對比較來的
    2. 反例2:「抽象」依賴於「實作細節」之購物車範例
  • 如何「反轉」依賴關係?
    1. 1. 依賴反轉完成!
  • 現在的依賴關係變成…
  • 來把反例1 也反轉過來,換成複雜一點的情況
    1. 見下方程式碼,這樣寫可不可以?
      1. 1. 這連 Compile 都不會過,因為 Car 介面中,並沒有一個叫 timeTravel() 的方法!
  • 書中範例解說
    1. 怎麼解? 當回家作業練習看看吧~~~
  • 閃亮亮小結