文章來自:www.jb51.net/article/163391.htm
微信學習群:Laravel技術交流群
好記性不如爛筆頭,學習php開發也不能懶,作筆記是一種學習的好習慣!
關注以下公眾號,可獲取兩套視訊教程:【laravel7.x 從入門到核心架構講解】 與 【Laravel高階實戰教程42講】 ,助你提升你的php學習技能。最後祝你學習愉快!
背景
在很多程式語言(例如java)開發中,程式設計師在某個類中需要依賴其它類的方法,則通常是new一個依賴類再呼叫類例項的方法,這種開發存在的問題是new的類例項不好統一管理,一旦有修改,牽扯的類會很多。
最早在java的spring提出了依賴注入的思想,即依賴類不由程式設計師例項化,而是通過spring容器幫我們new指定例項並且將例項注入到需要該物件的類中。目前許多主流PHP框架也使用了依賴注入容器,如ThinkPHP、Laravel等。
一、概念
1、容器:字面上理解就是裝東西的東西。常見的變數、物件屬性等都可以算是容器。一個容器能夠裝什麼,全部取決於你對該容器的定義。當然,現在我們討論的是這樣一種容器,它存放的不是文字、數值,而是物件、物件的描述(類、介面)或者是提供物件的回撥(閉包),通過這種容器,我們得以實現許多高階的功能,其中最常提到的,就是 “解耦”、“依賴注入”。
2、IoC - Inversion of Control 控制反轉
控制反轉是從容器的角度在描述,即:容器控制應用程式,由容器反向的嚮應用程式注入應用程式所需要的外部資源。
3、DI - Dependency Injection 依賴注入
依賴注入是從應用程式的角度在描述,可以把依賴注入,即:應用程式依賴容器建立並注入它所需要的外部資源。
備註:依賴注入和控制反轉說的是同一個東西,是一種設計模式,這種設計模式用來減少程式間的耦合,從某個方面講,就是它們描述的角度不同。
二、依賴注入的原理
一般情況下,當存在類與類之間的依賴關係的時候,我們都是通過直接例項化的方式進行呼叫。一旦出現多層依賴,這種方式的耦合程度就很高,在需要修改其中一個類的時候,會牽扯很多依賴它的類的修改,因此對程式碼的改動會比較大。
下面簡單舉一個A->B->C三層依賴的關係解釋怎麼運用依賴注入來解耦,提高開發效率。
而依賴注入方式如下:
解析:
常規寫法裡面,一旦C類需要作出改變,或者B類的呼叫需要改變成D類的時候,還需要考慮到依賴自己的B類,即還需要對B類作出修改。
依賴注入的思想就是即用即例項,反轉類與類之間的控制關係,實現由呼叫類A類控制後續的依賴關係,這樣可以讓B類隨意的更改所需依賴和例項化的類(C類或D類),達到解耦的目的。
三、常用的依賴注入方式:
1、構造方法注入;2、set屬性注入;3、靜態工廠方法注入;
上述的例子使用的就是構造方法注入的方式,將物件作為引數傳遞到構造方法中;同樣的set屬性注入也是相類似的方法,不同的僅僅是在set一個類的成員的屬性時傳遞這個物件引數,在此就不一一舉例了。
除此之外,還有靜態工廠方法注入的方式,這種方法與靜態工廠方法類似。
我們知道靜態工廠方法就是通過一個類來管理需要例項化的多個相似的類,該類會定義一個方法用於獲取需要例項化的物件,而具體要例項化哪個物件就依賴於傳遞進來的物件名引數了。
對於靜態工廠方式的注入,與一般的靜態工廠方法不同之處在於這個傳進來的引數是一個已經例項化過的物件。
<?php
class IoC
{
protected static $registry = [];
public static function bind($name, Callable $resolver) //傳入類名和類物件例項
{
static::$registry[$name] = $resolver;
}
public static function make($name) //靜態工廠方法
{
if (isset(static::$registry[$name])) {
$resolver = static::$registry[$name];
return $resolver(); //例項化
}
throw new Exception('Alias does not exist in the IoC registry.');
}
}
總而言之,三種方式傳遞的都是例項化物件,只是不同之處在於傳遞的位置分別為構造方法、set屬性、靜態工廠方法而已。
四、依賴注入容器(Ioc容器)
大多數時侯,在使用依賴注入方式解耦元件時,並不需要用到容器。
當一段程式需要例項化的類太多或者依賴太多的時候,重複依賴注入的程式碼是比較繁瑣的事情,例如以下情況:
當產生以上關係的時候,依賴注入的程式碼會比較混亂,而且存在重複,更有可能在呼叫一個一般方法時new一個不需要的類,產生冗餘。
此時需要使用容器,使用依賴注入容器後的思路是應用程式需要到A類,就從容器內取得A類。具體是容器建立C類,再建立B類並把C注入,再建立A類,並把B類注入,應用程式呼叫A類方法, A類呼叫B類方法,接著做些其它工作.總之容器負責例項化,注入依賴,處理依賴關係等工作。
對於實際開發中複雜多變的程式碼環境,我們並不能完全知道現在的類在未來會擴充套件成什麼情況,因此我們需要在有新的依賴類加入的時候,通過容器去實現例項化該類的方法。因此,在例項化未知類的時候,最能探索一個類的內部結構和例項化的方法就是利用反射,由此可知,反射是容器管理各個依賴類的核心。我們可以通過例項來了解容器的內部實現:
三個存在依賴關係的類:檔案testClass.php
<?php //依賴關係:Company->Department->Group
class Group
{
public function doSomething()
{
echo __CLASS__.":".'hello', '|';
}
}
class Department
{
private $group;
public function __construct(Group $group)
{
$this->group = $group;
}
public function doSomething()
{
$this->group->doSomething();
echo __CLASS__.":".'hello', '|';
}
}
class Company
{
private $department;
public function __construct(Department $department)
{
$this->department = $department;
}
public function doSomething()
{
$this->department->doSomething();
echo __CLASS__.":".'hello', '|';
}
}
Ioc容器的內部實現:
<?php
class Container
{
private $s = array();
public function __set($k, $c)
{
$this->s[$k] = $c;
}
public function __get($k)
{
return $this->build($this->s[$k]);
}
/**
* 自動繫結(Autowiring)自動解析(Automatic Resolution)
*
* @param string $className
* @return object
* @throws Exception
*/
public function build($className)
{
// 如果是匿名函式(Anonymous functions),也叫閉包函式(closures)
if ($className instanceof Closure) {
// 執行閉包函式,並將結果
return $className($this);
}
/*通過反射獲取類的內部結構,例項化類*/
$reflector = new ReflectionClass($className);
// 檢查類是否可例項化, 排除抽象類abstract和物件介面interface
if (!$reflector->isInstantiable()) {
throw new Exception("Can't instantiate this.");
}
/** @var ReflectionMethod $constructor 獲取類的建構函式 */
$constructor = $reflector->getConstructor();
// 若無建構函式,直接例項化並返回
if (is_null($constructor)) {
return new $className;
}
// 取建構函式引數,通過 ReflectionParameter 陣列返回引數列表
$parameters = $constructor->getParameters();
// 遞迴解析建構函式的引數
$dependencies = $this->getDependencies($parameters);
// 建立一個類的新例項,給出的引數將傳遞到類的建構函式。
return $reflector->newInstanceArgs($dependencies);
}
/**
* @param array $parameters
* @return array
* @throws Exception
*/
public function getDependencies($parameters)
{
$dependencies = [];
/** @var ReflectionParameter $parameter */
foreach ($parameters as $parameter) {
/** @var ReflectionClass $dependency */
$dependency = $parameter->getClass();
if (is_null($dependency)) {
// 是變數,有預設值則設定預設值
$dependencies[] = $this->resolveNonClass($parameter);
} else {
// 是一個類,遞迴解析
$dependencies[] = $this->build($dependency->name);
}
}
return $dependencies;
}
/**
* @param ReflectionParameter $parameter
* @return mixed
* @throws Exception
*/
public function resolveNonClass($parameter)
{
// 有預設值則返回預設值
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new Exception('I have no idea what to do here.');
}
}
require_once "./testclass.php"; //開始測試,先測試已知依賴關係的情況
$c = new Container();
$c->department = 'Department';
$c->company = function ($c) {
return new Company($c->department);
};
// 從容器中取得company
$company = $c->company;
$company->doSomething(); //輸出: Group:hello|Department:hello|Company:hello|
// 測試未知依賴關係,直接使用的方法
$di = new Container();
$di->company = 'Company';
$company = $di->company;
$company->doSomething();//輸出: Group:hello|Department:hello|Company:hello|
我們可以通過一張圖解釋Ioc容器的內部邏輯:
五、總結
IOC的基本概念是:不建立物件,但是描述建立它們的方式。在程式碼中不直接與物件和服務連線,但在配置檔案中描述哪一個元件需要哪一項服務。
本作品採用《CC 協議》,轉載必須註明作者和本文連結