什么是依賴注入?
依賴注入(Dependency Injection)是設計模式的一種。名字比較抽象,但是,要解決的問題卻是比較明確。對於給定的應用程序,需要借助一些相對獨立的組件來完成功能。一般來說,使用這些組件的過程就包含在應用程序的邏輯語句之中。問題是,當這些組件想要做成類似插件功能,以達到應用程序的業務邏輯不變就能隨意的更改組件的實現的效果。這種靈活性取決於應用程序如何組裝這些組件。如果說應用程序依賴於這些組件的話,依賴注入就是把這些依賴關系從應用程序的邏輯中剝離,放到插件的實現中去的一種方法。
一個簡單的例子是: 一個用戶類表類,依賴於一個finder類來生成這個列表。而這個finder類呢,依賴於某個db來獲取數據。在不考慮依賴注入的時候,程序可能會這樣寫:
1 $db = new \yii\db\Connection(['dsn' => '...']); 2 $finder = new UserFinder($db); 3 $lister = new UserLister($finder);
可是,當db的實現發生變化的時候,會引起應用程序的變化,finder類的實現發生變化的時候,會引起應用程序的變化。如果把db,finder看做插件,就需要一套機制來告訴應用程序,lister依賴於finder和db。
依賴注入的基本理就是用一個獨立的對象作為組裝器,該組裝器用一個合適的實例提供給應用程序來使用。有三種類型的依賴注入,
- 構造函數注入: 就是在構造函數的參數中提供組件的實現,在構造函數中把這種實現固化到應用程序中。
- Setter 注入: 專門通過setter函數實現以上的注入,
- 接口注入: 通過接口的實現,達到依賴注入。
有關依賴注入的詳細的信息,請參考 Martin Flower 2004年的文章 Inversion of Control Containers and the Dependency Injection pattern
Yii2中的依賴注入
在yii中,我們可以發現第一和第二種類型應用,也就是構造函數注入和setter注入。yii2中的組裝器是容器類(Container)。依賴的聲明和依賴的注入是通過調用容器類的set和get方法來實現的。
整個過程分三步,
- 第一步是類的聲明,在類的構造函數中,聲明依賴關系。
- 第二部是容器參數的載入, 告訴容器, 要生成某個對象時,是用哪些參數,以及類來實現的。這個過程通過調用 set來完成
- 第三部 實例的獲取。調用容器的get函數,獲取新創建的實例。
用容器來實現上面的用戶列表的例子,可能是這樣的。
1 // 創建容器 2 $container = new Container; 3 4 //指定如何生成yii\db\Connection,其中,yii\db\Connection 是類的名字 5 $container->set('yii\db\Connection', [ 6 'dsn' => '...', 7 ]); 8 9 //指定如何生成 app\models\UserFinderInterface, 所用的類是userFinder 10 $container->set('app\models\UserFinderInterface', [ 11 'class' => 'app\models\UserFinder', 12 ]); 13 14 //指定如何生成 UserLister 15 $container->set('userLister', 'app\models\UserLister'); 16 17 //生成lister 18 $lister = $container->get('userLister');
看起來最后生成lister跟前面db 和UserFinderInterface 沒有任何關系。這里面的秘密在於每個類的構造函數隱式地聲明了類之間的依賴關系。
//首先聲明一個接口類 interface UserFinderInterface { function findUser(); } //UserFinder 實現這個接口類 class UserFinder extends Object implements UserFinderInterface { public $db; //注意,第一個參數是Connection $db, 包含類的名字的 public function __construct(Connection $db, $config = []) { $this->db = $db; parent::__construct($config); } public function findUser() { } } // UserLists 類, 構造函數的第一個參數聲明了它依賴於UserFinderInterface. class UserLister extends Object { public $finder; public function __construct(UserFinderInterface $finder, $config = []) { $this->finder = $finder; parent::__construct($config); } }
容器的作用就是
-
- set,指明實例的生成類和參數
- 在get的時候,根據構造函數分析依賴,根據依賴關系,生成相應的對象。
容器類的內部實現:
數據結構:容器類有幾個比較重要的內部數組
/** * @var array singleton objects indexed by their types */ private $_singletons = []; /** * @var array object definitions indexed by their types */ private $_definitions = []; /** * @var array constructor parameters indexed by object types */ private $_params = []; /** * @var array cached ReflectionClass objects indexed by class/interface names */ private $_reflections = []; /** * @var array cached dependencies indexed by class/interface names. Each class name * is associated with a list of constructor parameter types or default values. */ private $_dependencies = [];
容器的重要函數:
public function set($class, $definition = [], array $params = [])
public function get($class, $params = [], $config = [])
set函數的作用是把一個類注冊到容器中,這樣容器就知道生成一個類的實例時,應該如何找依賴信息了。 set函數可以注冊一個類名字,一個接口名字, 一個別名。注冊的時候,還可以指定相應的配置信息 (配置信息的目的是把這些信息賦給生成的類實例)
最基本的就是,第一個參數就是鍵值,可以是類的名字,接口的名字,別名。而第二個參數是定義,可以是字符串,比如一個類的名字,接口的名字或者是別名;可以是數組,表示相應的配置信息;還可以可以是一個PHP的可調用對象,該對象的參數為function ($container, $params, $config)。該對象在get()調用的時候被執行。$params 是構造函數的參數,$config 是對象的配置信息,常常是個數組。 而$container是容器對象。 返回值是生成的對象,被get()返回。 set函數的第三個參數是生成對象的時候提供給構造函數的參數。
從set函數的源代碼可以看出,第一個參數,即class,是作為前文提到的幾個關鍵數組的鍵值的。定義放在_definitions中,參數放在_params中,
public function set($class, $definition = [], array $params = []) { $this->_definitions[$class] = $this->normalizeDefinition($class, $definition); $this->_params[$class] = $params; unset($this->_singletons[$class]); return $this; }
get函數負責根據set設置的依賴關系生成響應的對象實例。其中第一個參數class是用來訪問容器中不同數組的鍵值,第二個參數是$params,跟set中提供的params合並以后,提供給類的構造函數。第三個參數config是配置信息(注 配置就是要給新生成的對象賦一些屬性,而參數是類的構造函數處理要處理的參數)
get函數首先從_definition中取出定義,根據其類型,做不同的處理,比如,如果它是一個函數,則把參數合並以后,調用set提供的函數。
新創建的對象實例是調用build方法來構建的。在這個build函數中, 首先要獲取依賴。那么這個依賴從哪里來呢?
依賴從類的構造函數的反射分析中來。
方法是根據類的名字空間創建反射對象,取得構造函數,逐個分析構造函數的參數。某個參數有缺省的值,則把缺省值記錄到依賴中來。如果一個參數有類,則根據類的名字,生成一個Instance 對象,記錄類的名字,為后面依賴的解析作准備。
生成的反射信息放到_reflection數組中,依賴放到_dependency數組中。依賴需要解析,解析的過程就是生成實例的過程,遞歸調用get的過程。
處理過的依賴數組,作為參數,傳給反射對象的newInstanceArgs,進而生成類的實例。其實質就是把帶有類的指示的參數實例化了,而實例化是根據set函數預先定義的方法。
以上介紹的整個過程就是Yii2中利用container實現依賴注入的過程。可以看到,對依賴的注入是通過分析類的構造函數參數來實現的。
依賴注入的使用:
在yii中,主程序的配置就使用了這種基於容器的依賴注入。
在yii/config/web.php 中config數組中組件 (components) 就是在指定元素的定義。在application的 preInit方法中,用戶指定的這些components會跟系統的核心components融合。
這些組件可以通過 \Yii::$app->componentID 的形式訪問, 比如 \Yii::$app->cache. (這屬於service locator 概念了) 組件在第一次訪問的時候通過Yii::createObject靜態函數實例化,實例化的過程就遵循了上文所說的依賴注入的過程。
當然了,如果你指定要bootstrap某個組件, 比如下面。這樣每一個請求來的時候,都會實例化該log組件。
'bootstrap' => [ 'log', ],),
以下是配置中components中的內容示例。
'components' => [ 'request' => [ // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation 'cookieValidationKey' => '...', ], 'cache' => [ 'class' => 'yii\caching\FileCache', ], 'user' => [ 'identityClass' => 'app\models\User', 'enableAutoLogin' => true, ], 'errorHandler' => [ 'errorAction' => 'site/error', ], 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', // send all mails to a file by default. You have to set // 'useFileTransport' to false and configure a transport // for the mailer to send real emails. 'useFileTransport' => true, ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'class' => 'yii\log\FileTarget', 'levels' => ['error', 'warning','profile'], ], ], ], 'db' => require(__DIR__ . '/db.php'),