在實際開發中,只要內容開始複雜,使用到的類別越來越多,就會慢慢發現,為什麼我只是要抽換或更改其中的類別,卻會限制這麼多以及需要大量改動。
這時我才注意到這個主題的重要性,但是在了解依賴注入之前讓我們先談談甚麼是控制反轉(IoC)

控制反轉

控制反轉(Inversion of Control,簡稱 IoC),它指的是將應用程式的控制權轉移到外部元件或框架,以實現更大程度的可配置性和可擴展性。IoC強調的是將依賴管理的責任從應用程式內部移出,通常透過依賴注入來實現。IoC有助於改善軟體架構,降低耦合性,提高可測試性,並使系統更容易配置和調整。

依賴注入

依賴注入(Dependency Injection,簡稱 DI)是一種軟體設計模式,主要用途於解決程式碼中的相依性問題。在傳統的程式設計中,一個類通常負責創建和管理其所需的相依物件。然而,這種方式容易導致高耦合的程式碼,難以測試和維護。


從以上兩點說明可以知道,控制反轉是一種解除依賴降低於耦合性的概念,至於依賴注入則是實現該概念其中一種方法。

範例說明

有一個紀錄Log的服務,以下為具體實現服務的類別FileLogger和DatabaseLogger。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Logger {
public function log($message);
}

class FileLogger implements Logger {
private $filename;

public function __construct($filename) {
$this->filename = $filename;
}

public function log($message) {
file_put_contents($this->filename, $message . PHP_EOL, FILE_APPEND);
}
}

class DatabaseLogger implements Logger {
public function log($message) {
// 將日誌寫入資料庫的具體實現
}
}

假設沒有使用依賴注入,在服務內部直接實例兩種具體物件,高度耦合內部物件,假設要新增新類別,就會需要改動內部邏輯,非常不方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class EventService {
private $fileLogger;
private $databaseLogger;

public function __construct() {
$this->fileLogger = new FileLogger("app.log"); // 直接依賴於FileLogger
$this->databaseLogger = new DatabaseLogger("app.log"); // 直接依賴於FileLogger
}

public function processEvent($eventName) {
// 處理事件的邏輯

// 記錄事件
$this->fileLogger->log("Event processed: " . $eventName);
$this->databaseLogger->log("Event processed: " . $eventName);
}
}

$eventServiceWithFileLogger = new EventService();
$eventServiceWithFileLogger->processEvent("Event 1");

這時加入了依賴注入,把實例的行為移到外部,讓服務不去控制內部類別,而是改成外部去控制,這個概念就是控制反轉,這樣就能自由加入需要的類別,大大增加擴展性與測試性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EventService {
private $logger;

public function __construct(Logger $logger) {
$this->logger = $logger;
}

public function processEvent($eventName) {
// 處理事件的邏輯

// 記錄事件
$this->logger->log("Event processed: " . $eventName);
}
}

$fileLogger = new FileLogger("app.log");
$eventServiceWithFileLogger = new EventService($fileLogger);

$databaseLogger = new DatabaseLogger();
$eventServiceWithDatabaseLogger = new EventService($databaseLogger);

$eventServiceWithFileLogger->processEvent("Event 1");
$eventServiceWithDatabaseLogger->processEvent("Event 2");

優點

優點就如同上面說明與範例可得知,有以下幾點:

  1. 測試性提升: 通過依賴注入,我們可以將虛擬的相依物件注入到類中,從而使單元測試更加輕鬆。這種方式下,我們可以專注於測試類的行為,而不需要實際創建相依物件。

  2. 鬆散耦合: 依賴注入降低了類之間的耦合度,使不同模塊更加獨立。這使得程式碼更加靈活和易於修改。

  3. 可維護性提升: 依賴注入使程式碼的組織更加清晰,類不再負責管理相依物件,使程式碼更易於理解和維護。

缺點

然而,依賴注入也並非沒有缺點,以下是一些可能出現的問題:

  1. 循環依賴:在使用依賴注入時,有時可能會出現循環依賴的情況,即不同的類彼此相互依賴,形成一個閉環。這種情況下,必須謹慎設計類之間的相依關係,以避免循環依賴導致程式碼難以理解和維護。

如同以下簡單範例,A實例需要B,B實例需要A,相互依賴,這樣會導致程式發生無窮迴圈,雖然這個缺點相對比較難發生,但是如果應用程式越來越龐大,以及PHP的自動加載功能,也是有可能發生此問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
// A.php
class A {
public function __construct(B $b) {
// ...
}
}

// B.php
class B {
public function __construct(A $a) {
// ...
}
}
  1. 過度注入: 過度注入指的是在類的建構子中注入過多的相依物件,導致建構子參數過多,影響程式碼的可讀性。過度注入可能在某些情況下使程式碼變得複雜。

如同以下簡單範例,一個類別注入多種類別,導致建構函式變得冗長且難以管理,特別是如果未來需要添加更多依賴時,程式碼將變得更複雜。

1
2
3
4
5
6
7
8
9
10
class Order {
public function __construct(
Database $database,
Logger $logger,
EmailService $emailService,
PaymentGateway $paymentGateway
) {
// ...
}
}

總結

如果想要寫出高擴充、高測試性、好維護的服務,其中依賴注入是一個相當重要的概念,這也是為什麼Laravel以及許多框架這麼愛用的原因。推薦給大家這個好用的設計模式!!