PHP程序員如何理解依賴注入容器(dependency injection container)

PHP程序員如何理解依賴注入容器(dependency injection container)

背景知識

傳統的思路是應用程序用到一個Foo類,就會建立Foo類並調用Foo類的方法,假如這個方法內須要一個Bar類,就會建立Bar類並調用Bar類的方法,而這個方法內須要一個Bim類,就會建立Bim類,接着作些其它工做。php

// 代碼【1】
    class Bim
    {
        public function doSomething()
        {
            echo __METHOD__, '|';
        }
    }
    
    class Bar
    {
        public function doSomething()
        {
            $bim = new Bim();
            $bim->doSomething();
            echo __METHOD__, '|';
        }
    }
    
    class Foo
    {
        public function doSomething()
        {
            $bar = new Bar();
            $bar->doSomething();
            echo __METHOD__;
        }
    }
    
    $foo = new Foo();
    $foo->doSomething(); //Bim::doSomething|Bar::doSomething|Foo::doSomething

使用依賴注入的思路是應用程序用到Foo類,Foo類須要Bar類,Bar類須要Bim類,那麼先建立Bim類,再建立Bar類並把Bim注入,再建立Foo類,並把Bar類注入,再調用Foo方法,Foo調用Bar方法,接着作些其它工做。java

// 代碼【2】
    class Bim
    {
        public function doSomething()
        {
            echo __METHOD__, '|';
        }
    }
    
    class Bar
    {
        private $bim;
    
        public function __construct(Bim $bim)
        {
            $this->bim = $bim;
        }
    
        public function doSomething()
        {
            $this->bim->doSomething();
            echo __METHOD__, '|';
        }
    }
    
    class Foo
    {
        private $bar;
    
        public function __construct(Bar $bar)
        {
            $this->bar = $bar;
        }
    
        public function doSomething()
        {
            $this->bar->doSomething();
            echo __METHOD__;
        }
    }
    
    $foo = new Foo(new Bar(new Bim()));
    $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething

這就是控制反轉模式。依賴關係的控制反轉到調用鏈的起點。這樣你能夠徹底控制依賴關係,經過調整不一樣的注入對象,來控制程序的行爲。例如Foo類用到了memcache,能夠在不修改Foo類代碼的狀況下,改用redis。git

使用依賴注入容器後的思路是應用程序須要到Foo類,就從容器內取得Foo類,容器建立Bim類,再建立Bar類並把Bim注入,再建立Foo類,並把Bar注入,應用程序調用Foo方法,Foo調用Bar方法,接着作些其它工做.程序員

總之容器負責實例化,注入依賴,處理依賴關係等工做。github

代碼演示 依賴注入容器 (dependency injection container)

經過一個最簡單的容器類來解釋一下,這段代碼來自 Twitteeredis

class Container
    {
        private $s = array();
    
        function __set($k, $c)
        {
            $this->s[$k] = $c;
        }
    
        function __get($k)
        {
            return $this->s[$k]($this);
        }
    }

這段代碼使用了魔術方法,在給不可訪問屬性賦值時,__set() 會被調用。讀取不可訪問屬性的值時,__get() 會被調用。segmentfault

$c = new Container();
    
    $c->bim = function () {
        return new Bim();
    };
    $c->bar = function ($c) {
        return new Bar($c->bim);
    };
    $c->foo = function ($c) {
        return new Foo($c->bar);
    };
    
    // 從容器中取得Foo
    $foo = $c->foo;
    $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething

這段代碼使用了匿名函數 數組

再來一段簡單的代碼演示一下,容器代碼來自simple di container閉包

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.');
        }
    }
    
    IoC::bind('bim', function () {
        return new Bim();
    });
    IoC::bind('bar', function () {
        return new Bar(IoC::make('bim'));
    });
    IoC::bind('foo', function () {
        return new Foo(IoC::make('bar'));
    });
    
    
    // 從容器中取得Foo
    $foo = IoC::make('foo');
    $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething

這段代碼使用了後期靜態綁定ide

依賴注入容器 (dependency injection container) 高級功能

真實的dependency injection container會提供更多的特性,如

  • 自動綁定(Autowiring)或 自動解析(Automatic Resolution)

  • 註釋解析器(Annotations)

  • 延遲注入(Lazy injection)

下面的代碼在Twittee的基礎上,實現了Autowiring。

class Bim
    {
        public function doSomething()
        {
            echo __METHOD__, '|';
        }
    }
    
    class Bar
    {
        private $bim;
    
        public function __construct(Bim $bim)
        {
            $this->bim = $bim;
        }
    
        public function doSomething()
        {
            $this->bim->doSomething();
            echo __METHOD__, '|';
        }
    }
    
    class Foo
    {
        private $bar;
    
        public function __construct(Bar $bar)
        {
            $this->bar = $bar;
        }
    
        public function doSomething()
        {
            $this->bar->doSomething();
            echo __METHOD__;
        }
    }
    
    class Container
    {
        private $s = array();
    
        public function __set($k, $c)
        {
            $this->s[$k] = $c;
        }
    
        public function __get($k)
        {
            // return $this->s[$k]($this);
            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);
            }
    
            /** @var ReflectionClass $reflector */
            $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.');
        }
    }
    
    // ----
    $c = new Container();
    $c->bar = 'Bar';
    $c->foo = function ($c) {
        return new Foo($c->bar);
    };
    // 從容器中取得Foo
    $foo = $c->foo;
    $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
    
    // ----
    $di = new Container();
    
    $di->foo = 'Foo';
    
    /** @var Foo $foo */
    $foo = $di->foo;
    
    var_dump($foo);
    /*
    Foo#10 (1) {
      private $bar =>
      class Bar#14 (1) {
        private $bim =>
        class Bim#16 (0) {
        }
      }
    }
    */
    
    $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething

以上代碼的原理參考PHP官方文檔:反射,PHP 5 具備完整的反射 API,添加了對類、接口、函數、方法和擴展進行反向工程的能力。 此外,反射 API 提供了方法來取出函數、類和方法中的文檔註釋。

若想進一步提供一個數組訪問接口,如$di->foo能夠寫成$di['foo'],則需用到ArrayAccess(數組式訪問)接口

一些複雜的容器會有許多特性,下面列出一些相關的github項目,歡迎補充。

參考代碼

推薦閱讀

相關文章
相關標籤/搜索