什麼是依賴注入與Ioc容器

背景
在很多編程語言(例如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的基本概念是:不創建對象,但是描述創建它們的方式。在代碼中不直接與對象和服務連接,但在配置文件中描述哪一個組件需要哪一項服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章