今天要聊的主題是依賴注入 Dependency Injection (DI)
,聊這個有點為難,一來是一不小心就會解釋太遠,二來是自認為只是略懂。所以今天打算規劃如下,先說明依賴注入的基本精神,再來說明之後會怎麼用到,至此就有能力繼續之後文章的閱讀,剩餘的篇章會聊聊對於依賴注入的理解,以及 Laravel Service Provider 的運用,因為不確定能否解釋清楚,所以若有幸大神路過,還麻煩指證錯誤(給予鞭打?!) XD 。
依賴注入聽起來就不是很好吃的東西 (欸) ,倒過來說會比較好理解 注入依賴
(後續簡稱 DI)。DI 的最主要精神在於解耦,意思就是說程式之間降低彼此的耦合性,讓彈性增加。
例如,兩津是標準的錢鬼,要他做任何事都建立在錢的基礎上,因此「錢」就是兩津的依賴,而所謂的 DI 就是別人提供阿兩金錢,而不是要阿兩自己生錢出來,同時阿兩任何同等金錢概念的物品都接受,不論是日圓、美金或是小判都好,這樣阿兩對特定的貨幣的耦合性就很低。
好拉不說垃圾話,直接上 code。假設我們有一個功能是「當文章被訂閱的時候,要推送訊息給作者知道」,我們常見的寫法會是:
// ...
class PostService
{
// ...
public function subscribed()
{
$email = new EmailSender();
$email->setTarget($author);
$email->send();
// ...
所謂的 DI 就是將要用到的類別實例,改為由外部帶參數進來:
public function subscribed(EmailSender $email)
{
$email->setTarget($author);
$email->send();
從上面的舉例就比較能呼應 注入依賴
一詞,其中 「依賴」 代表function 內所要使用的一些資料或是實例;而 「注入」 代表由外部提供。
至此即為 DI 基本的寫法,但為因應更多複雜的使用情況以及降低對依賴的耦合,因此可以再進一步改寫:
interface MessageSender
{
public function setTarget($author);
public function send();
}
class EmailSender implements MessageSender
{
public function setTarget($author)
{
$this->target = $author['email'];
}
public function send()
{
// ...
}
}
class LineSender implements MessageSender
{
public function setTarget($author)
{
$this->target = $author['id'];
}
public function send()
{
// ...
}
}
當我們有各種不同的發送訊息工具,文章訂閱功能就可以修改成下面的形式,對於 subscribed
而言,他只要知道要設定傳訊息對象以及傳送訊息兩件事即可,至於是使用哪種工具傳送就不是他要關注的事項了。
public function subscribed(MessageSender $sender)
{
$sender->setTarget($author);
$sender->send();
簡單的 DI 重點整理:
在 Laravel 當中具有自動依賴的部分,而且是屬於巢狀依賴,意思是依賴本身若有其他依賴,依賴本身也會自動注入。 例如,由於 EmailSender 在建構時期有依賴需要注入,因此當程式執行 subscribed 的時候,會自動注入 EmailSender 同時也會為了 EmailSender 再注入 SMTPConnection:
class EmailSender implements MessageSender
{
private $smtp;
public function __construct(SMTPConnection $smtp)
{
$this->smtp = $smtp;
// ...
public function subscribed(EmailSender $email)
{
$email->setTarget($author);
$email->send();
Laravel 之所以能夠實現自動注入,是因為具有 Service Container、Service Provider以及PHP的Reflection機制,由於這部分再講下去會有點遠,因此如果單純運用跟閱讀上這裡先不需要更深入的探討,只要知道一點: 所有 Controller
(存放所有 api 網址對應方法的類別,之後會提到) 的 __construct
與 functions
都會實現自動注入。
Laravel 的 DI 重點整理:
還記得上面的 MessageSender
嗎? 因為他是 interface,所以如果沒有告訴 Laravel 實際上到底要注入甚麼具體類別會出錯。因此 Service Provider 就是提供我們註冊各種日後需要注入依賴的地方。
php artisan make:provider MessageServiceProvider
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class MessageServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
// 在這裡進行註冊
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
// 當註冊完服務之後會呼叫、執行的邏輯
}
}
bind
的方法完成註冊的邏輯 public function register()
{
// 告訴 Laravel 當每次呼叫 MessageSender 自動注入時,
// 都會以執行後面 function (閉包),取得一個全新且正確具體類別實例
$this->app->bind(MessageSender::class, function($app) {
// 各種必要的判斷邏輯
$usingLineSender = request()->has('line');
if ($usingLineSender) {
// 回傳正確的具體類別
return new LineSender();
}
// 回傳正確的具體類別
return new EmailSender();
});
}
configs/app.php
在 providers
中加入下列程式 'providers' => [
// ...
/*
* Application Service Providers...
*/
// ...
App\Providers\MessageServiceProvider::class,
根據不同需求,我們可以利用 Laravel Service Container 綁定各種不同的服務,例如應該許多專案都會有細碎 helper functions 我們就可以集中起來,同時因為 function 大多相互獨立或是不會因為使用情境而有狀態改變的需求,因此我們就可以設計為,當每次呼叫 Helper 類別進行注入,我們都只使用第一次也是唯一的實例,例如下面的範例:
class Helper
{
// 各種獨立沒有關聯性的 function
public function isValidatedRequest($request, &$response) {
// ...
}
public function twTimestamp(string $date, string $format)
{
//...
}
public function register()
{
// 每次呼叫 Helper 進行注入的時候,
// 先檢查系統是否已經存在 Helper 的實例,
// 如果已經存在就回傳該實例,反之,建立、回傳一個新的實例
$this->app->singleton(Helper::class, function($app) {
return new Helper();
});
}
這裡已經開始走鐘了,由於在上面提到 singleton 跟 Helper 這個範例,一定會有人說何必這麼麻煩,寫成 static class 就好了。確實從程式執行的角度來看並沒有甚麼差別,而且製作 singleton 還比較麻煩,這裡沒有要戰的意思,只是在拜讀 Stack Overflow 和網路上各位大神的文章,為 singleton 整理出下列優點:
由於小弟個人初次在撰寫 Laravel 的時候,實在不知道為甚麼各種地方都會將參數自動帶入 __construct
與 functions
,而且還沒看到是怎麼給的,因此利用輕鬆愉快的小周末,和各位大大聊一下 DI,即便是只有要快速開發 api,至少也可以比較不會這麼困惑 (還是只有我太笨 XD),而後半段的 service provider、service container 以及 Singleton pattern 如果有說錯或是不清楚的地方還希望各位大大指證或交換意見! 明天我們就要來寫 Repository 啦~