簡介
依賴注入(DI),控制反轉(IoC),容器(Container) 經常都經常會提到,但很長一段時間都是一知半解,現在抽空把自己淺顯理解的內容記錄下來,與大家探討。
引子
Route::get('/{id}','\App\Http\Controllers\IndexController@index');
class IndexController extends Controller
{
public function index(Request $request, $id)①
{
app(UserService::class)②->getUserNameById($id);
③// TypeError: Too few arguments to function App/Services/UserService::__construct(), 0 passed in Psy Shell code on line 1 and exactly 1 expected
(new UserService())->getUserNameById($id);
}
}
class UserService
{
public $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
public function getUserNameById($id)
{
return $this->cache->get('user:id:' . $id);
}
}
很早之前就很好奇:
- 為什麼方法的引數位置①是需要傳入 2 個引數的,一個是
Request
型別的引數,一個是不定型別的id
引數,但路由只有一個id
引數,那$request
引數是哪裡來的? UserService
的__construct
方法明確例項化需要一個Cache
型別的引數,但②中並沒有傳入,為什麼能使用呢?③為什麼使用new
如果不傳引數就會報錯呢?
不知道大家開發時有沒有好奇過這兩個問題呢?後來聽說這叫 依賴注入
,也不知道是個啥,那就抱著這兩個疑問開始尋找答案。
貫穿全文
接下來會圍繞這 3 個點來講
依賴控制
- 依賴:誰依賴誰
- 注入:注入什麼
控制反轉
- 控制:誰控制誰
- 反轉:反轉什麼
什麼是容器
常規程式碼
Controller1
class Index1Controller
{
public $userService;
public function __construct() {
/**
* 因為我需要(依賴) UserService() 給我提供資料, 所以建立了一個 UserService() 物件
*
* 控制:我 (IndexController) 控制了 UserService() 物件的建立
* 反轉:我 (IndexController) 絕對控制 UserService() 物件的權利,建立物件的控制權沒有發生轉移,所以沒有反轉,一切都是親力親為。
*/
$this->userService = new UserService();
}
public function index() {
// 我 (index) 控制了 UserService() 物件的建立
$userService = new UserService();
$userName = $userService->getUserName();
$userName2 = $this->userService->getUserName();
return [$userName, $userName2];
}
}
(new IndexController())->index();
Index2.php
<?php
(new Index1Controller())->index();
生活比喻:
依賴:我要吃麵包,麵包需要(依賴)麵粉才能製作
注入:買麵粉 -> 注入水 -> 製作麵包 -> 吃
控制:我控制了麵包的製作
反轉:無
依賴注入和控制反轉
Controller2
class Index2Controller
{
public $userService;
/**
* 因為我需要(依賴) UserService() 給我提供資料, 所以我需要接收一個 UserService 型別的引數
* 把依賴從外部傳入進來,把需要的依賴傳入進來了,就是依賴注入
*
* 控制:呼叫者控制了 UserService() 物件的建立
* 反轉:我 (IndexController) 控制 UserService 建立的權利已經沒有了(轉移了),那轉移給誰了?這裡的控制權轉移給呼叫者了。
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function index()
{
/**
* 在方法中建立物件
* 我 (index) 控制了 UserService() 物件的建立
*/
$userService = new UserService();
$userName = $userService->getUserName();
$userName2 = $this->userService->getUserName();
return [$userName, $userName2];
}
}
// __construct() 中建立 new UserService() 轉移到了這裡
$userService = new UserService();
// 將 $userService 傳入(注入) controller 中
(new Index2Controller($userService))->index();
Index2.php
<?php
// __construct() 中建立 new UserService() 轉移到了這裡
$userService = new UserService();
// 將 $userService 傳入(注入) controller 的建構函式中
$rs = (new Index2Controller($userService))->index();
var_dump($rs);
生活比喻:
依賴:我要吃麵包,依賴麵包店
注入:告訴麵包店老闆要吃什麼 -> 老闆給你(注入) -> 吃
控制:麵包店老闆控制麵包的製作
反轉:原來我控制麵包的製作的權利沒有了,轉移給了麵包店的老闆
IoC 容器自動注入
上面的 依賴注入和控制反轉
並沒有解決開頭引出的兩個問題的答案,依賴還是需要手動建立,然後手動注入,如何實現依賴的自動注入呢?這個時候就需要一個 IoC 容器了
如何注入
使用
PHP
提供的 反射(Reflection) 功能我們需要注入哪裡的引數
依賴注入是以建構函式引數的形式傳入,所以我們需要自動注入建構函式指定的引數
我們需要注入哪些引數
我們只注入類例項,其他引數原樣傳入
Container
IoC 容器其實就是一個普通的 class
類,實現了某些功能而已,不必想的太複雜。
class Container
{
// 在 laravel 中這個方法是 `make()`, 這裡為了方便和常用的 new xxx() 理解,所以命名成了「自動注入的new」
public static function autoInjectNew($className, $params = [])
{
$reflect = new \ReflectionClass($className);
// 獲取建構函式
$construct = $reflect->getConstructor();
// 儲存例項化需要的引數
$args = [];
if ($construct) {
/**
* 獲取建構函式的引數
* array(2) {
* [0] => object(ReflectionParameter)#3 (1) {["name"]=> string(11) "userService"}
* [1] => object(ReflectionParameter)#4 (1) {["name"]=> string(3) "uid"}
* }
*/
$consParams = $construct->getParameters();
foreach ($consParams as $param) {
$class = $param->getClass();
if ($class) {
// $args[] = new $class->name();
// 如果這樣處理依賴的的 UserService() 還有依賴的話則無法兼顧,所以需要遞迴處理
// demo 中這裡相當於就是 new Study\Di\Services\UserService()
$args[] = self::autoInjectNew($class->name);
}
}
}
// 合併引數
$args = array_merge($args, $params);
/**
* IoC 控制反轉:
* 控制:容器控制了物件的建立
* 反轉:建立物件的權利已經轉移到了容器中來了,不再是 IndexController() 中的 __construct() 了。
* DI 依賴注入:
* 依賴:$args 儲存了儲存了需要那些依賴
* 注入:把 $args 中的依賴作為引數傳入(注入),返回例項
*/
// 相當於:$instance = new Index3Controller(new UserService)
$instance = $reflect->newInstanceArgs($args);
return $instance;
}
}
驗證一下
Controller3
class Index3Controller
{
protected $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function index()
{
$userName = $this->userService->getUserName();
return $userName;
}
}
index3.php
<?php
$index3Instance = Container::autoInjectNew(Index3Controller::class);
$rs = $index3Instance->index();
var_dump($rs);
現在再看看是不是沒有主動傳入 new UserService()
引數也可以成功呼叫啦
回顧問題
路由中的
Request $request
引數是哪裡來的答:請求進入框架之後,框架解析
url
找到相對應的控制器類,呼叫容器寫好的自動注入方法(案例中是autoInjectNew()
),進行注入引數,這樣就可以愉快又方便的使用啦。使用
app()
和new
有什麼不同答:其實
laravel
中app()
就是使用Container
例項化的一個助手函式,我們可以來寫一個助手函式先看看
laravel
中的助手函式function app($abstract = null, array $parameters = []) { if (is_null($abstract)) { return Container::getInstance(); } // 這裡的 make 就相當於當前專案中的 autoInjectNew() return Container::getInstance()->make($abstract, $parameters); }
實現助手函式 app()
index3.php
<?php $index3Instance = Container::autoInjectNew(Index3Controller::class); $rs = $index3Instance->index(); var_dump("indexRs: ", $rs); // 使用助手函式 $appRs = app(Index3Controller::class)->index(); var_dump("appRs: ", $appRs); // 助手函式 function app($class, $params = []) { return Container::autoInjectNew($class, $params); }
總結
剛開始的時候在網上找了很多相關的文章,但看下來說的似乎都大同小異,但還是不理解,很是苦惱。經常看到「服務容器是 Laravel 的核心」這樣的說法,所以就去從 laravel
的 index.php
開始一步一步過,但 laravel
的原始碼看的確實也有點頭大,所以我轉了個彎,把 ThinkPHP
的的框架 clone
下來看了看,確實看的輕鬆許多,再回頭看 laravel
的原始碼,還是很複雜,但理解起來相對直接看 laravel
就簡單多了。
文章很多都是作者自己的理解,文章提供的大多也只是很少一部分的程式碼,要弄清楚還是得閱讀原始碼。
這個案例的 Container
中似乎沒有太體現出 容器
這個詞,因為還沒有實現例項化物件的儲存,具體可以看看相關的原始碼。
案例demo
參考
github.com/top-think/framework/blo...
github.com/laravel/framework/blob/...
segmentfault.com/a/119000001894890...
blog.csdn.net/bestone0213/article/...
www.cnblogs.com/DebugLZQ/archive/2...
本作品採用《CC 協議》,轉載必須註明作者和本文連結