標籤:擷取對象 injection config ext ble tde 指定 實踐 class類
理論發現問題
在深入細節之前,需要確保我們理解"IOC控制反轉"和"DI依賴注入"是什麼,能夠解決什麼問題,這些在維基百科中有非常清晰的說明。
- 控制反轉(Inversion of Control,縮寫為IoC):是物件導向編程中的一種設計原則,可以用來減低電腦代碼之間的耦合度。
- 依賴注入(Dependency Injection,簡稱DI):DI是IOC的一種實現,表現為:在類A的執行個體建立過程中即建立了依賴的B對象,通過類型或名稱來判斷將不同的對象注入到不同的屬性中。
- 依賴尋找(Dependency Lookup,簡稱DL):DL是IOC的另外一種實現,在需要的時候通過調用架構提供的方法來擷取對象,擷取時需要提供相關的設定檔路徑、key等資訊來確定擷取對象
依賴注入與依賴尋找是控制反轉的2種實現方式,後者很少見,我們主要研究依賴注入。
如果此前沒有接觸過這些概念,可能還是過於抽象不容易理解,但是下面這個情境你應該是見過的:
因為大多數應用程式都是由兩個或是更多的類通過彼此的合作來實現商務邏輯,這使得每個對象都需要擷取與其合作的對象(也就是它所依賴的對象)的引用。如果這個擷取過程要靠自身實現,那麼這將導致代碼高度耦合并且難以維護和調試。
也就是說:"Class A中用到了Class B的對象b,一般情況下,需要在A的代碼中顯式的new一個B的對象",這就導致如果A想將B替換為一個更優的實現版本B+時,需要修改代碼顯式的new一個B+對象。
解決這個問題的傳統做法一般是為B和B+提取一個InterfaceOfB介面,然後讓class A只依賴InterfaceOfB,最終由A類的調用方決定傳入B還是B+對象,修改調用方代碼和修改類A代碼對我們來說並沒有本質的改變,那是否有更好的方式呢?
解決思路
終於,懶惰的程式員對這種代碼開發方式感到厭煩:因為我們在代碼裡控制了B類對象的產生,從而導致代碼耦合,即便A類依賴InterfaceOfB,還是要在程式某處寫死new B()或者new B+()這樣的代碼,怎麼破解?
答案是:將B類對象的產生交給一個獨立的對象產生器來負責,那麼A類只需要依賴這個對象產生器,而至於到底是產生B還是B+對象,則是對象產生器內部的行為,這樣就將A和B解耦開了,這就是所謂的"控制反轉",即將控制權交給了對象產生器。
這麼簡單的將問題拋給對象產生器可不行,因為對象產生器還要面臨new B還是new B+的寫入程式碼問題,因此必須賦予對象產生器一個超能力:
- 在對象產生器的設定檔中進行這樣的描述:{"InterfaceOfB" : "Class B+"},表示InterfaceOfB介面應該執行個體化B+對象。
- A類建構函式有一個InterfaceOfB的入參,例如:function __construct(InterfaceOfB obj)。
- 調用對象產生器(DI)擷取A類對象,DI->get("class A")。對象產生器會利用反射分析class A的建構函式,發現InterfaceOfB參數後根據此前設定檔描述,new B+()對象傳入到A的建構函式,從而產生A對象。
總結上述流程就是:對象產生器通過反射機制分析A類的建構函式依賴,並根據配置中的關係產生依賴的對象執行個體傳入給建構函式,最終完成A類對象的建立。
上面的過程就是"依賴注入"主要實現方式了,對象產生器我們通常成為"DI Container",也就是"依賴注入容器"。
需要注意的是:B或者B+的建構函式可以會依賴InterfaceOfC,因此整個依賴關係的分析是遞迴的。
實踐
上面在談‘DI依賴注入"的時候,我們非常清楚的瞭解到 DI會根據建構函式進行依賴分析,但是很容易忽視{"InterfaceOfB" : "Class B+"}這個資訊的來源。如果DI不知道這個資訊,那麼在分析建構函式時是不可能知道介面InterfaceOfB應該對應什麼對象的,這個資訊在DI實現中一般是通過set方法主動設定到DI容器的依賴關係中的,當然這個資訊的儲存介質可以是設定檔或者寫入程式碼傳入。
下面拿PHP的Yii2.0架構為例,看看它實現DI時的核心思路是什麼,不會講的太細,但上面提到的思路和概念都會有所體現。
set設定類定義
public function set($class, $definition = [], array $params = []){ $this->_definitions[$class] = $this->normalizeDefinition($class, $definition); $this->_params[$class] = $params; unset($this->_singletons[$class]); return $this;}
這就是上面提到{"InterfaceOfB" : "Class B+"}的設定介面,比如這樣用:
$container->set(‘yii\mail\MailInterface‘, ‘yii\swiftmailer\Mailer‘);
意思就是如果遇到依賴MailInterface的,那麼構造一個Mailer對象給它,params是用於傳給Mailer::__construct的構造參數,之前提過依賴分析是遞迴的,Mailter對象的構造也是DI負責的(不是簡單的new出來),一旦你傳了構造參數給Mailer,那麼DI就不用反射分析Mailter的依賴了,直接傳入params既可new一個Mailer出來。
get產生類對象
public function get($class, $params = [], $config = []) { if (isset($this->_singletons[$class])) { // singleton // 此前已經get過並且設定為單例,那麼返回單例對象既可 return $this->_singletons[$class]; } elseif (!isset($this->_definitions[$class])) { // 非單例需要產生新對象,但是此前沒有set過類定義, // 因此只能直接反射分析建構函式的依賴 return $this->build($class, $params, $config); } // 此前設定過的類定義,對類進行了更具體的定義,協助我們更快的構造出對象 $definition = $this->_definitions[$class]; // 類定義可以是一個函數,用於直接為DI產生對象 if (is_callable($definition, true)) { // 將set設定的構造參數和本次傳入的構造參數merge到一起 // 然後分析這些傳入的構造參數是否為實參(比如:int,string),這是因為yii允許 // params是Instance對象,它代表了另外一個類定義(它內部指向了DI容器中某個definition) // 為了這種構造參數能夠傳入到當前的建構函式,需要遞迴調用di->get將其建立為實參。 $params = $this->resolveDependencies($this->mergeParams($class, $params)); // 這個就是函數式的指派至,前提是構造參數需要確保都是實參 $object = call_user_func($definition, $this, $params, $config); } elseif (is_array($definition)) { // 普通的類定義 $concrete = $definition[‘class‘]; unset($definition[‘class‘]); // 把set設定的config和這次傳入的config合并一下 $config = array_merge($definition, $config); // 把set設定的params構造參數和這次傳入的構造參數合并一下 $params = $this->mergeParams($class, $params); // 這裡: $class代表的就是MailInterface,而$concrete代表的是Mailer if ($concrete === $class) { // 這裡是遞迴出口,產生目標class對象既可,沒有什麼可研究的 $object = $this->build($class, $params, $config); } else { // 顯然,這裡要構造MailInterface是等同於去構造Mailer對象 $object = $this->get($concrete, $params, $config); } } elseif (is_object($definition)) { return $this->_singletons[$class] = $definition; } else { throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition)); } if (array_key_exists($class, $this->_singletons)) { // singleton $this->_singletons[$class] = $object; } return $object; }
實現思路在此前的分析裡都說的很明白了,並不是很難理解。這個函數通過class指定要分配的類,params指定了構造參數,和之前的set原理一樣:如果構造參數齊全是不需要分析依賴的。(最後的config是要注入到對象的額外屬性,屬於yii2特性,不是重點)。
至於build構造對象時,又需要做什麼呢?就是基於反射機制擷取建構函式依賴了哪些類,然後如果params傳入了構造參數那麼直接使用params參數,如果沒有指定則需要遞迴的DI->get()去產生實參,最終通過建構函式產生對象。
protected function build($class, $params, $config) { /* @var $reflection ReflectionClass */ // 利用反射,分析類建構函式的參數, // 其中,傳回值reflection是class的反射對象, // dependencies就是建構函式的所有參數了,有幾種情況: // 1,參數有預設值,直接用 // 2, 沒有預設值,並且不是int這種非class,那麼返回Instance指向對應的class,等待下面的遞迴get list ($reflection, $dependencies) = $this->getDependencies($class); // 傳入的建構函式參數優先順序最高,直接覆蓋前面反射分析的構造參數 foreach ($params as $index => $param) { $dependencies[$index] = $param; } // 完整的檢查一次參數,如果依賴是指向class的Instance,那麼遞迴DI->get擷取執行個體 // 如果是指定int,string這種的Instance,那麼說明調用者並沒有params傳入值,建構函式預設參數也沒有值, // 必須拋異常 // 如果不是Instance,說明是params使用者傳入的實參可以直接用 $dependencies = $this->resolveDependencies($dependencies, $reflection); if (empty($config)) { return $reflection->newInstanceArgs($dependencies); } // 最後通過反射對象,傳入所有構造實參,完成對象建立 if (!empty($dependencies) && $reflection->implementsInterface(‘yii\base\Configurable‘)) { // set $config as the last parameter (existing one will be overwritten) $dependencies[count($dependencies) - 1] = $config; return $reflection->newInstanceArgs($dependencies); } else { $object = $reflection->newInstanceArgs($dependencies); foreach ($config as $name => $value) { $object->$name = $value; } return $object; } }
如果你感興趣可以看看getDependencies和resolveDependencies實現,前者緩衝了每個類的反射資訊(反射很耗費效能),後者體現了Instance的用法:代表尚未執行個體化的class類對象,需要DI->get擷取。
protected function getDependencies($class) { if (isset($this->_reflections[$class])) { return [$this->_reflections[$class], $this->_dependencies[$class]]; } $dependencies = []; $reflection = new ReflectionClass($class); $constructor = $reflection->getConstructor(); if ($constructor !== null) { foreach ($constructor->getParameters() as $param) { if ($param->isDefaultValueAvailable()) { $dependencies[] = $param->getDefaultValue(); } else { $c = $param->getClass(); $dependencies[] = Instance::of($c === null ? null : $c->getName()); } } } $this->_reflections[$class] = $reflection; $this->_dependencies[$class] = $dependencies; return [$reflection, $dependencies]; } protected function resolveDependencies($dependencies, $reflection = null) { foreach ($dependencies as $index => $dependency) { if ($dependency instanceof Instance) { if ($dependency->id !== null) { $dependencies[$index] = $this->get($dependency->id); } elseif ($reflection !== null) { $name = $reflection->getConstructor()->getParameters()[$index]->getName(); $class = $reflection->getName(); throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\"."); } } } return $dependencies; }最後
最後還想簡單說一下yii2的ServiceLoader,它基於DI Container封裝了一層,將組件component單例維護在ServiceLoader內,而component的產生則通過DI Container實現。
不過有意思的是,ServiceLoader這樣的實現並沒能充分的使用DI Container的構造依賴注入能力,僅僅是傳入component的class完成對象建立,最後注入了幾個config指定的屬性而已,並沒有控制params的能力,這個可以看ServiceLoader中的set和get方法,然而這個設計基本要求了component的建構函式參數都應該能獨立構造而不需要外部幹預(幹預是指DI->set進行類定義)。
除去ServiceLoader不談,整個yii2.0架構也沒找到可以通過設定檔自動化調用DI->set進行類定義的能力,寫入程式碼屬於走倒退的路,這基本上導致yii2.0對DI的應用能力停留在ServiceLoader層面,在遞迴解析依賴時也基本只能走無構造參數或者預設參數構造的路子。
正是在這種背景下,yii2.0的"依賴注入"也基本蛻化為ServiceLoader的get嵌套get,也就是類似"依賴尋找"概念:在配置中分別寫好A和B的component配置,並且配置A compoenent依賴B component,然後通過ServiceLoader得到A component,A類內部從配置中取出依賴的component(也就是B),最後通過ServiceLoader得到B component。
談談php裡的IOC控制反轉,DI依賴注入