Yii2中的依賴注入

基本概念

1.依賴倒置(反轉)原則(DIP):一種軟件架構設計的原則(抽象概念,是一種思想)
在面向對象編程領域中,依賴反轉原則(Dependency inversion principle,DIP)是指一種特定的解耦(傳統的依賴關係建立在高層次上,而具體的策略設置則應用在低層次的模塊上)形式,使得高層次的模塊不依賴於低層次的模塊的實現細節,依賴關係被顛倒(反轉),從而使得低層次模塊依賴於高層次模塊的需求抽象。php

該原則規定:mysql

  1. 1.高層次的模塊不該該依賴於低層次的模塊,二者都應該依賴於抽象接口。
  2. 2.抽象接口不該該依賴於具體實現。而具體實現則應該依賴於抽象接口。

圖片描述

在上圖中,高層對象A依賴於底層對象B的實現;圖2中,把高層對象A對底層對象的需求抽象爲一個接口A,底層對象B實現了接口A,這就是依賴反轉。web

該原則顛倒了一部分人對於面向對象設計的認識方式。如高層次和低層次對象都應該依賴於相同的抽象接口。它轉換了依賴,高層模塊不依賴於低層模塊的實現,而低層模塊依賴於高層模塊定義的接口。通俗的講,就是高層模塊定義接口,低層模塊負責實現。spring

2.控制反轉(IoC):一種反轉流、依賴和接口的方式(DIP的具體實現方式,一種設計原則)
控制反轉(Inversion of Control,縮寫爲IoC),是面向對象編程中的一種設計原則,能夠用來減低計算機代碼之間的耦合度。其中最多見的方式叫作依賴注入(Dependency Injection,簡稱DI),還有一種方式叫「依賴查找」(Dependency Lookup)。經過控制反轉,對象在被建立的時候,由一個調控系統內全部對象的外界實體,將其所依賴的對象的引用傳遞給它。也能夠說,依賴被注入到對象中。sql

它把傳統上由程序代碼直接操控的對象的調用權交給容器,經過容器來實現對象組件的裝配和管理。所謂的「控制反轉」概念就是對組件對象控制權的轉移,從程序代碼自己轉移到了外部容器。編程

實現控制反轉主要有兩種方式:
1.依賴注入:
2.依賴查找數組

二者的區別在於,前者是被動的接收對象,在類A的實例建立過程當中即建立了依賴的B對象,經過類型或名稱來判斷將不一樣的對象注入到不一樣的屬性中,然後者是主動索取相應類型的對象,得到依賴對象的時間也能夠在代碼中自由控制。緩存

3.依賴注入(DI):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)
依賴注入有以下實現方式:架構

  • 接口注入(Interface Injection):實現特定接口以供外部容器注入所依賴類型的對象。
  • 設值注入(Setter Injection): 實現特定屬性的public set方法,來讓外部容器調用傳入所依賴類型的對象。
  • 構造器注入(Constructor Injection): 實現特定參數的構造函數,在新建對象時傳入所依賴類型的對象。
  • 基於註解 : 基於Java的註解功能,在私有變量前加「@Autowired」等註解,不須要顯式的定義以上三種代碼,即可以讓外部容器傳入對應的對象。該方案至關於定義了public的set方法,可是由於沒有真正的set方法,從而不會爲了實現依賴注入致使暴露了不應暴露的接口(由於set方法只想讓容器訪問來注入而並不但願其餘依賴此類的對象訪問)。

3.依賴查找(DL):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)
依賴查找更加主動,在須要的時候經過調用框架提供的方法來獲取對象,獲取時須要提供相關的配置文件路徑、key等信息來肯定獲取對象的狀態app

小結
依賴倒置原則(DIP):一種軟件架構設計的原則(抽象概念,一種思想)。
控制反轉(IoC):一種反轉流、依賴和接口的方式(DIP的具體實現方式)。
依賴注入(DI):IoC的一種實現方式,用來反轉依賴(IoC的具體實現方式)。
IoC容器(也稱DI Container):提供了動態地(自動化)建立、注入依賴單元,映射依賴關係等功能,減小了許多代碼量(DI框架)。

須要注意的一些地方
1.控制反轉的層面
在傳統的應用中,程序流程的順序是由開發者主導的,因爲IoC,主導權轉移到了框架的手裏(由於IoC容器)

2.控制反轉須要解決的問題
查找,生成所需的實例,返回給須要者(所以又叫依賴注入)

3.實現依賴注入的目的
儘管一個類A對它所依賴的類B是如何實現的一無所知,類A依然可以與類B通訊(經過定義一些通用接口)。類B在開發中可能會有多種實現,依賴注入(同時也是IoC)解決的問題就是自動地將這些類B的實如今須要的時候傳遞給類A。

4.如何實現依賴注入
最基本的思路是構造一個獨立的類,它的功能就是統一爲其餘全部類的依賴生成所需的實例(assembler,相似容器),而後構造並返回這個類

依賴注入的具體實現方式(主要介紹設值注入和構造器注入)

備註:對類A,類B,類C的定義以下
類A (須要經過容器獲取的)
類B (類A的依賴,廣義上的接口)
類C (類B的具體實現)

1.構造器注入

  • a)在類A的構造器參數列表中定義了該類全部須要被依賴注入的東西(類B)
  • b)在容器中須要先定義好某個接口(廣義上的interface,即類B)關聯的某個具體實現類(有時還須要配置一些具體參數,即類C),這些容器配置在不一樣的開發中極可能是不同的。一般這些配置會是一個獨立的文件
  • c)在須要某個類A的時候經過容器來生成而不是直接new
class MovieLister...     (MovieLister至關於類A,MovieFinder至關於類B)
public MovieLister(MovieFinder finder) {
    this.finder = finder;       
}

class ColonMovieFinder...   (這個至關於類c)
public ColonMovieFinder(String filename) {
    this.filename = filename;
}

(這裏返回的pico就是IoC容器)
private MutablePicoContainer configureContainer() {
    MutablePicoContainer pico = new DefaultPicoContainer();
    Parameter[] finderParams =  {new ConstantParameter("movies1.txt")};
    //在使用容器前須要先配置,下面的代碼就是對容器的配置
    pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
    pico.registerComponentImplementation(MovieLister.class);
    return pico;
}

下面是經過容器來得到類A的過程
public void testWithPico() {
    MutablePicoContainer pico = configureContainer();//得到一個配置好的容器
    MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);//經過容器來得到類A
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

2.setter注入

  • a)在類A中爲全部須要注入的依賴類(類B)建立setter方法
  • b)在獨立的文件配置類A中的依賴的具體實現(即配置類B的具體實現類C)
  • c)經過容器生成生成類A
class MovieLister...  (一樣的MovieLister爲類A,MovieFinder爲類B) 
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
  this.finder = finder;
}

class ColonMovieFinder...  (這個一樣的至關於類C)
public void setFilename(String filename) {
   this.filename = filename;
}


//下面是容器的配置
<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>


public void testWithSpring() throws Exception {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//得到配置好的容器
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");  //經過容器獲取類A
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

Service Locator

與DI相似,Service Locator也是用來打破依賴的

基本思想
提供一個獨立的類(即Service Locator),它可以爲整個應用提供所需的全部service(也能夠理解爲component)。

具體實現
1.在類A中,依賴的全部類都是經過Service Locator獲取的

MovieFinder finder = ServiceLocator.movieFinder();

2.經過配置能夠定製在Service Locator中實現如何返回一個特定實例,這個與DI相似

小結
1.實際上能夠將Service Locator和DI結合使用,在類A中經過Service Locator獲取依賴,而在Service Locator中則能夠經過DI來實現獲取具體的實例(或者將Service Locator與DI互換也能夠?)

2.動態的Service Locator:使用一張映射表,經過查表實現(或直接獲取)具體的實例

3.Service Locator與DI 的區別:使用Service Locator時是顯式地調用Locator,而Di並無顯式地調用

Yii2中的依賴注入

相關的類:

  • yii\di\Container 容器
  • yii\di\instance 容器或Service Locator中的東西: 本質上是對於某一個類實例的引用
  • yii\di\ServiceLocator

1.yii\di\instance
主要用在兩個地方:

  • 1.在配置DI容器的時候,使用Instance來引用一個類名接口名或者是別名(即Instance的id屬性)。所以後續DI容器能夠將這個引用解析成相應的對象
  • 2.用在那些使用service locator獲取依賴對象的類中

對於 yii\di\Instance:
1.表示的是容器中的內容,表明的是對於實際對象的引用。
2.DI容器能夠經過他獲取所引用的實際對象。
3.Instance類僅有的一個屬性id通常表示的是實例的類型(即component ID, class name, interface name or alias name)。

2.yii\di\Container
注意:下面所說的「對象類型」的具體定義爲「類名,接口名,別名」
對於yiidiContainer

  • a) 5個私有屬性(都是數組):$_singletons,$_definitions,$_params,$_reflections,$_dependencies
  • b) $_singletons // 用於保存單例Singleton對象,以對象類型爲鍵
  • c) $_definitions // 用於保存依賴的定義,以對象類型爲鍵
  • d) $_params // 用於保存構造函數的參數,以對象類型爲鍵
  • e) $_reflections // 用於緩存ReflectionClass對象,以對象類型爲鍵
  • f) $_dependencies // 用於緩存依賴信息,以對象類型爲鍵

DI容器的5個數組

注意
1.在DI容器中,依賴關係的定義是惟一的。 後定義的同名依賴,會覆蓋前面定義好的依賴。
2.上面的鍵具體就是:帶命名空間的類名,接口名,或者是一個別名
3.對於 $_definitions 數組中的元素,它要麼是一個包含了」class」 元素的數組,要麼是一個PHP callable, 再要麼就是一個具體對象。這就是規範化後的最終結果
4.對於$_singletons數組中的元素,要不就是null(表示還未實例化),要不就是一個具體的實例
5.對於$_params數組中的元素,就是一個數組,包含構造函數的全部參數
6.對於$_reflections數組中的元素,就是一個ReflectionClass對象
7.setter注入能夠在實例化後

yiidiContainer使用的具體過程
一個簡單的例子

namespace app\models;

use yii\base\Object;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends Object implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

$container = new Container;
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');

$lister = $container->get('userLister');

// which is equivalent to:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

1.在類A的構造器參數列表中定義了該類全部須要被依賴注入的東西(類B)

2.註冊依賴:
a)yii\di\Container::set()
b)yii\di\Container::setSinglton()
使用到了$_definitions ,$_params, $_singletons

3.對象的實例化
a)解析依賴信息
yii\di\Container::getDependencies() (會被後續的build()調用)
getDependencies():操做$_reflections與$_dependencies
1.會向$_reflections 和 $_dependencies寫入信息
2.使用PHP的反射機制來獲取類的有關信息,主要就是爲了從構造器中獲取依賴信息,會將反射獲得的信息寫入$_reflections
3.將從構造器中獲取的依賴信息(即構造函數的參數列表)寫入$_dependencies
4.返回值: 數組[$reflection, $dependencies]

yii\di\Container::resolveDependencies() (一樣的會被後續的build()調用)
resolveDependencies()利用getDependencies()得到的信息進一步具體處理(遞歸調用)。處理依賴信息, 將依賴信息中保存的Instance實例所引用的類或接口進行實例化。

b)建立實例
yii\di\Container::build()
由getDependencies()得到第一層依賴
由resolveDependencies()遞歸分析依賴,最終生成全部依賴的實例
$reflection->newInstanceArgs($dependencies);//生成全部依賴後生成這個實例

注意:DI容器只支持 yiibaseObject 類,也就是說若是你想你的類能夠放在DI容器裏,那麼必須繼承自 yiibaseObject 類。

4.獲取依賴實例化對象

yii\di\Container::get()
a)若是是已經實例化的單例,直接返回($_singletons)

b)若是是還沒有定義(不存在於$_definition),則說明其實例化沒有依賴,調用build()

c)存在$_definition
    i.$definition爲callable,直接調用
    ii.$definition爲數組,根據$definition數組中的‘class’,遞歸調用get(),遞歸終止的條件是(當具體實現類就是當前的依賴類時),遞歸結束時調用build()進行實例化
    iii.$definition爲對象,直接返回該對象,並將該對象設置爲單例
setSinglton()相似
public function set($class, $definition = [], array $params = [])
{
    //normalizeDefinition()處理後,返回值要麼是一個包含了」class」 元素的數組,要麼是一個PHP callable, 再要麼就是一個具體對象
    $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
    $this->_params[$class] = $params;
    unset($this->_singletons[$class]);
    return $this;
}

public function get($class, $params = [], $config = [])
{
    if (isset($this->_singletons[$class])) {//是單例,且已經實例化(不爲null)
        // singleton
        return $this->_singletons[$class];
    } elseif (!isset($this->_definitions[$class])) {//尚未定義過,須要build
        return $this->build($class, $params, $config);
    }

    $definition = $this->_definitions[$class];

    if (is_callable($definition, true)) {
        $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']);

        $config = array_merge($definition, $config);
        $params = $this->mergeParams($class, $params);

        if ($concrete === $class) {//$concrete至關於以前提到的具體實現類C,而$class則至關於接口類B
            $object = $this->build($class, $params, $config);
        } else {
            $object = $this->get($concrete, $params, $config);//遞歸,直到找到具體的實現類C
        }
    } 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;
}


protected function build($class, $params, $config)
{
    /* @var $reflection ReflectionClass */
    list ($reflection, $dependencies) = $this->getDependencies($class); //獲取第一層依賴關係

    foreach ($params as $index => $param) {
        $dependencies[$index] = $param; //額外提供的構造函數參數,添加到依賴中
    }

    $dependencies = $this->resolveDependencies($dependencies, $reflection);//遞歸解析依賴,並會在此返回依賴的實例
    if (!$reflection->isInstantiable()) {
        throw new NotInstantiableException($reflection->name);
    }
    if (empty($config)) {
        return $reflection->newInstanceArgs($dependencies);//經過反射實例生成對象
    }
    
    //config中的對象做爲該類的property使用
    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;
    }
}


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();//這裏要能獲取到類名須要在構造函數中用類型限制參數,不然獲取到null,並且注意對於php的基本類型,獲取到的也是null
                $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) { //這裏的dependency是Instance的實例
                $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中調用組件

先看一下各個類的繼承關係
類的繼承關係

下面以Yii::$app->db爲例
1.配置組件
配置的內容:

'components' => [
    'db' => [
        'class' => 'yii\db\Connection',
        'dsn' => 'mysql:host=localhost;dbname=wechat',
        'username' => 'root',
        'password' => 'michael',
        'charset' => 'utf8',
    ],

2.在框架的啓動過程當中加載組件的定義
Yii2的啓動
入口腳本:

(new yii\web\Application($config))->run();
1.new yii\web\Application($config)
2.run()

yii\web\Application的構造函數

public function __construct($config = [])
{
    Yii::$app = $this;
    static::setInstance($this);   //將當前module存到Yii::$app->loadedModules[]
    $this->state = self::STATE_BEGIN;

    //1.經過$config配置別名,基本參數
    //2.配置核心組件(僅僅是配置,將Yii框架寫好的配置與本身的配置合併)
    $this->preInit($config); 

    $this->registerErrorHandler($config);

    //下面這一行是重點,Component是當前類的祖先
    //在下面的構造函數中執行了Yii::configure($this, $config),將$config中的配置做爲屬性添加到$app中
    Component::__construct($config);
}

Component::__construct($config)

//實際上下面這個構造函數的定義在yii\base\Object中
public function __construct($config = [])
    {
        if (!empty($config)) {
            Yii::configure($this, $config);
        }
        $this->init();
    }

Yii::configure($this, $config)

public static function configure($object, $properties)
    {
        foreach ($properties as $name => $value) {
            //下面這行代碼會觸發魔術方法 ($object->components = $value)
            //實際執行的代碼是ServiceLocator::setComponents($components)
            $object->$name = $value;
        }

        return $object;
    }

ServiceLocator::setComponents($components)

public function setComponents($components)
{
    foreach ($components as $id => $component) {
        $this->set($id, $component);
    }
}

最終加載組件配置的代碼

//從下面的代碼中能夠看到,最終組件的配置被存儲在$app->_definitions數組中
public function set($id, $definition)
{
    if ($definition === null) {
        unset($this->_components[$id], $this->_definitions[$id]);
        return;
    }

    unset($this->_components[$id]);

    if (is_object($definition) || is_callable($definition, true)) {
        // an object, a class name, or a PHP callable
        $this->_definitions[$id] = $definition;
    } elseif (is_array($definition)) {
        // a configuration array
        if (isset($definition['class'])) {
            $this->_definitions[$id] = $definition;
        } else {
            throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
        }
    } else {
        throw new InvalidConfigException("Unexpected configuration type for the \"$id\" component: " . gettype($definition));
    }
}

3.獲取組件
Yii::$app->db會觸發魔術方法,調用ServiceLocator::__get()

public function __get($name)
{
    if ($this->has($name)) {
        //已經定義過組件
        return $this->get($name);
    } else {
        //沒有定義過組件
        return parent::__get($name);
    }
}

public function has($id, $checkInstance = false)
{
    //這裏由於在框架啓動過程當中將組件的配置加載到$_definitions中了,因此會返回true
    return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}


//經過下面的代碼能夠看到,若是組件已經實例化過存儲在$_components中了,就直接返回
//不然經過Yii::createObject($definition)來生成組件實例,並存儲到$_components中
public function get($id, $throwException = true)
{
    if (isset($this->_components[$id])) {
        return $this->_components[$id];
    }

    if (isset($this->_definitions[$id])) {
        $definition = $this->_definitions[$id];
        if (is_object($definition) && !$definition instanceof Closure) {
            return $this->_components[$id] = $definition;
        } else {
            return $this->_components[$id] = Yii::createObject($definition);
        }
    } elseif ($throwException) {
        throw new InvalidConfigException("Unknown component ID: $id");
    } else {
        return null;
    }
}

Yii::createObject($definition)

//在Yii2框架中要使用DI來生成對象的話,能夠經過調用Yii::createObject($definition)實現
public static function createObject($type, array $params = [])
{
    if (is_string($type)) {
        //使用容器
        return static::$container->get($type, $params);
    } elseif (is_array($type) && isset($type['class'])) {
        $class = $type['class'];
        unset($type['class']);
        return static::$container->get($class, $params, $type);
    } elseif (is_callable($type, true)) {
        return static::$container->invoke($type, $params);
    } elseif (is_array($type)) {
        throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
    }

    throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
相關文章
相關標籤/搜索