Home » Code » 再看控制反转(IoC)容器与依赖注入

再看控制反转(IoC)容器与依赖注入

一年前写过关于这个的文章了(点此查看),但感觉不是很到位,如今看了下Laravel框架,就再写一篇吧。即使如此,我依然是感觉不到位,这些理论,估计得用得多了才能有更好的理解。

12年前(2004年)Java界的大师就写过一篇著名的文章详细说介绍过这些东西(见此Inversion of Control Containers and the Dependency Injection pattern),按该大师的说法,一开始人们认为这种能消除应用程序对插件实现的依赖的容器实现了控制反转,因此起名控制反转模式。但大师认为“控制反转”一词太泛,常常让人迷惑,因此他跟一些朋友讨论起了一个更能说明其特点的名字:依赖注入。我也觉得,依赖注入更确切。叫什么其实不是很重要,重要的是理解它到底是什么,是如何产生的,以及如何应用它来解决实际问题。找了不少这方面的文章,其中有一篇被转得还挺多的——《laravel 学习笔记 —— 神奇的服务容器》,但我感觉没有phalcon框架文档中的例子好,因此,下边的内容参来自这篇文章——《理解PHP 依赖注入|Laravel IoC容器》

首先,我们假设,我们要开发一个组件命名为SomeComponent。这个组件中现在将要注入一个数据库连接。

在这个例子中,数据库连接在component中被创建,这种方法是不切实际的,这样做的话,我们将不能改变数据库连接参数及数据库类型等一些参数。

class SomeComponent
{

    /**
     * The instantiation of the connection is hardcoded inside
     * the component so is difficult to replace it externally
     * or change its behavior
     */
    public function someDbTask()
    {
        $connection = new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));

        // ...
    }

}

$some = new SomeComponent();
$some->someDbTask();

为了解决上面所说的问题,我们需要在使用前创建一个外部连接,并注入到容器中。就目前而言,这看起来是一个很好的解决方案:

class SomeComponent
{

    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

}

$some = new SomeComponent();

//Create the connection
$connection = new Connection(array(
    "host" => "localhost",
    "username" => "root",
    "password" => "secret",
    "dbname" => "invo"
));

//Inject the connection in the component
$some->setConnection($connection);

$some->someDbTask();

现在我们来考虑一个问题,我们在应用程序中的不同地方使用此组件,将多次创建数据库连接。使用一种类似全局注册表的方式,从这获得一个数据库连接实例,而不是使用一次就创建一次。

class Registry
{

    /**
     * Returns the connection
     */
    public static function getConnection()
    {
       return new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));
    }

}

class SomeComponent
{

    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection){
        $this->_connection = $connection;
    }

    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

}

$some = new SomeComponent();

//Pass the connection defined in the registry
$some->setConnection(Registry::getConnection());

$some->someDbTask();

现在,让我们来想像一下,我们必须在组件中实现两个方法,首先需要创建一个新的数据库连接,第二个总是获得一个共享连接:

class Registry
{

    protected static $_connection;

    /**
     * Creates a connection
     */
    protected static function _createConnection()
    {
        return new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));
    }

    /**
     * Creates a connection only once and returns it
     */
    public static function getSharedConnection()
    {
        if (self::$_connection===null){
            $connection = self::_createConnection();
            self::$_connection = $connection;
        }
        return self::$_connection;
    }

    /**
     * Always returns a new connection
     */
    public static function getNewConnection()
    {
        return self::_createConnection();
    }

}

class SomeComponent
{

    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection){
        $this->_connection = $connection;
    }

    /**
     * This method always needs the shared connection
     */
    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

    /**
     * This method always needs a new connection
     */
    public function someOtherDbTask($connection)
    {

    }

}

$some = new SomeComponent();

//This injects the shared connection
$some->setConnection(Registry::getSharedConnection());

$some->someDbTask();

//Here, we always pass a new connection as parameter
$some->someOtherDbTask(Registry::getConnection());

到此为止,我们已经看到了如何使用依赖注入解决我们的问题。不是在代码内部创建依赖关系,而是让其作为一个参数传递,这使得我们的程序更容易维护,降低程序代码的耦合度,实现一种松耦合。(除了这里使用的setter方式进行注入,通过构造函数注入也是一种常用做法。这里的注册树,换个名字就是所谓的单例模式。)但是从长远来看,这种形式的依赖注入也有一些缺点。

例如,如果组件中有较多的依赖关系,我们需要创建多个setter方法传递,或创建构造函数进行传递。另外,每次使用组件时,都需要创建依赖组件,使代码维护不太易,我们编写的代码可能像这样:

//Create the dependencies or retrieve them from the registry
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();

//Pass them as constructor parameters
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);

// ... or using setters

$some->setConnection($connection);
$some->setSession($session);
$some->setFileSystem($fileSystem);
$some->setFilter($filter);
$some->setSelector($selector);

我想,我们不得不在应用程序的许多地方创建这个对象。如果你不需要依赖的组件后,我们又要去代码注入部分移除构造函数中的参数或者是setter方法。为了解决这个问题,我们再次返回去使用一个全局注册表来创建组件。但是,在创建对象之前,它增加了一个新的抽象层:

class SomeComponent
{

    // ...

    /**
     * Define a factory method to create SomeComponent instances injecting its dependencies
     */
    public static function factory()
    {

        $connection = new Connection();
        $session = new Session();
        $fileSystem = new FileSystem();
        $filter = new Filter();
        $selector = new Selector();

        return new self($connection, $session, $fileSystem, $filter, $selector);
    }

}

这一刻,我们好像回到了问题的开始,我们正在创建组件内部的依赖,我们每次都在修改以及找寻一种解决问题的办法,但这都不是很好的做法。这里使用了工厂模式,实际它只是将依赖进行了转移,由对多个具体的外部依赖(Connection,Session,FileSystem等)变成了对单个工厂(factory)的依赖!依赖并未消除!

一种实用和优雅的来解决这些问题,是使用容器的依赖注入,像我们在前面看到的,容器作为全局注册表,使用容器的依赖注入做为一种桥梁来解决依赖可以使我们的代码耦合度更低,很好的降低了组件的复杂性:

class SomeComponent
{

    protected $_di;

    public function __construct($di)
    {
        $this->_di = $di;
    }

    public function someDbTask()
    {

        // Get the connection service
        // Always returns a new connection
        $connection = $this->_di->get('db');

    }

    public function someOtherDbTask()
    {

        // Get a shared connection service,
        // this will return the same connection everytime
        $connection = $this->_di->getShared('db');

        //This method also requires a input filtering service
        $filter = $this->_db->get('filter');

    }

}

$di = new Phalcon\DI();

//Register a "db" service in the container
$di->set('db', function(){
    return new Connection(array(
        "host" => "localhost",
        "username" => "root",
        "password" => "secret",
        "dbname" => "invo"
    ));
});

//Register a "filter" service in the container
$di->set('filter', function(){
    return new Filter();
});

//Register a "session" service in the container
$di->set('session', function(){
    return new Session();
});

//Pass the service container as unique parameter
$some = new SomeComponent($di);

$some->someTask();

现在,该组件只有访问某种service的时候才需要它,如果它不需要,它甚至不初始化,以节约资源。该组件是高度解耦。他们的行为,或者说他们的任何其他方面都不会影响到组件本身。

Phalcon\DI 是一个实现了服务的依赖注入功能的组件,它本身也是一个容器。

由于Phalcon高度解耦,Phalcon\DI 是框架用来集成其他组件的必不可少的部分,开发人员也可以使用这个组件依赖注入和管理应用程序中不同类文件的实例。

基本上,这个组件实现了 Inversion of Control 模式。基于此,对象不再以构造函数接收参数或者使用setter的方式来实现注入,而是直接请求服务的依赖注入。这就大大降低了整体程序的复杂性,因为只有一个方法用以获得所需要的一个组件的依赖关系。

此外,这种模式增强了代码的可测试性,从而使它不容易出错。

框架本身或开发人员都可以注册服务。当一个组件A要求调用组件B(或它的类的一个实例),可以从容器中请求调用组件B,而不是创建组件B的一个实例。

这种工作方式为我们提供了许多优点:

  • 我们可以更换一个组件,从他们本身或者第三方轻松创建。
  • 在组件发布之前,我们可以充分的控制对象的初始化,并对对象进行各种设置。
  • 我们可以使用统一的方式从组件得到一个结构化的全局实例。

服务可以通过以下几种方式注入到容器:

//Create the Dependency Injector Container
$di = new Phalcon\DI();

//By its class name
$di->set("request", 'Phalcon\Http\Request');

//Using an anonymous function, the instance will lazy loaded
$di->set("request", function(){
    return new Phalcon\Http\Request();
});

//Registering directly an instance
$di->set("request", new Phalcon\Http\Request());

//Using an array definition
$di->set("request", array(
    "className" => 'Phalcon\Http\Request'
));

在上面的例子中,当向框架请求访问一个请求数据时,它将首先确定容器中是否存在这个”reqeust”名称的服务。

容器会反回一个请求数据的实例,开发人员最终得到他们想要的组件。

在上面示例中的每一种方法都有优缺点,具体使用哪一种,由开发过程中的特定场景来决定的。

用一个字符串来设定一个服务非常简单,但缺少灵活性。设置服务时,使用数组则提供了更多的灵活性,而且可以使用较复杂的代码。lambda函数是两者之间一个很好的平衡,但也可能导致更多的维护管理成本。

Phalcon\DI 提供服务的延迟加载。除非开发人员在注入服务的时候直接实例化一个对象,然后存存储到容器中。在容器中,通过数组,字符串等方式存储的服务都将被延迟加载,即只有在请求对象的时候才被初始化。

//Register a service "db" with a class name and its parameters
$di->set("db", array(
    "className" => "Phalcon\Db\Adapter\Pdo\Mysql",
    "parameters" => array(
          "parameter" => array(
               "host" => "localhost",
               "username" => "root",
               "password" => "secret",
               "dbname" => "blog"
          )
    )
));

//Using an anonymous function
$di->set("db", function(){
    return new Phalcon\Db\Adapter\Pdo\Mysql(array(
         "host" => "localhost",
         "username" => "root",
         "password" => "secret",
         "dbname" => "blog"
    ));
});

以上这两种服务的注册方式产生相同的结果。然后,通过数组定义的,在后面需要的时候,你可以修改服务参数:

$di->setParameter("db", 0, array(
    "host" => "localhost",
    "username" => "root",
    "password" => "secret"
));

从容器中获得服务的最简单方式就是使用”get”方法,它将从容器中返回一个新的实例:

$request = $di->get("request");

或者通过下面这种魔术方法的形式调用:

$request = $di->getRequest();

Phalcon\DI 同时允许服务重用,为了得到一个已经实例化过的服务,可以使用 getShared() 方法的形式来获得服务。具体的 Phalcon\Http\Request 请求示例:

$request = $di->getShared("request");

参数还可以在请求的时候通过将一个数组参数传递给构造函数的方式:

$component = $di->get("MyComponent", array("some-parameter", "other"));

以上括号内和加粗的部分我加的,其他都是phalcon框架文档中的内容。这里边举的例子很适当的描述了程序中各种服务插件的互相依赖,以及用传统的工厂模式不能很好的解决,从而引入IoC容器的过程。

假如要自己实现一个IoC容器,就实现基本的注入依赖以及取出实例的功能,要如何写呢。看了Laravel的文档,几经琢磨,粗略的自己写了这么一个如下:

namespace Container;

class Container
{
	private $dependencies = array();

	//指定为单例
	public function singleton($key, $parameters)
	{
		$type = $this->getParameterType($parameters);
		if (empty($type)) {
			echo 'error parameters type';
			return false;
		}

		$depend['type'] = $type;
		$depend['is_single'] = true;
		$depend['data'] = $parameters;
		$this->dependencies[$key] = $depend;
	}

	//没有指定为单例
	public function bind($key, $parameters)
	{
		$type = $this->getParameterType($parameters);
		if (empty($type)) {
			echo 'error parameters type';
			return false;
		}
		if ($type === 'object') {
			$depend['is_single'] = true;//绑定的是对象,跟单例无异
		} else {
			$depend['is_single'] = false;
		}
		$depend['type'] = $type;
		$depend['data'] = $parameters;
		$this->dependencies[$key] = $depend;
	}

	//获取参数类型
	private function getParameterType($parameters)
	{
		if ($parameters instanceof Closure) {
			return 'closure';
		} elseif (is_object($parameters)) {
			$reflectionClass = new \ReflectionClass($parameters);
			if ($reflectionClass->name === 'Closure') {
				return 'closure';
			} else {
				return 'object';
			}
		} elseif (!empty($parameters) && is_string($parameters)) {
			return 'class';
		} else {
			return false;
		}
	}

	public function make($key) 
	{
		if (!isset($this->dependencies[$key])) {
			echo 'no inject the key:' . $key;
			return null;
		}
		$depend = $this->dependencies[$key];
		if ($depend['is_single'] && isset($depend['result'])) {
			return $depend['result'];
		}
		switch ($depend['type']) {
			case 'closure':
				$result = $this->executeClosure($depend['data']);
				break;
			case 'class':
				$result = $this->createObject($depend['data']);
				break;
			case 'object':
				$result = $depend['data'];
				break;
			default:
				$result = null;
				break;
		}
		if ($depend['is_single']) {
			$this->dependencies[$key]['result'] = $result;
		}
		return $result;
	}

	private function createObject($class_name) 
	{
		$reflection = new \ReflectionClass($class_name);
		$constructor = $reflection->getConstructor();//反射方法对象
		if (empty($constructor)) {
			//没有构造函数
			$object = $reflection->newInstance();
		} else {
			$arguments = $this->getConstructorArgs($constructor);
			if (empty($arguments)) {
				$object = $reflection->newInstance();
			} else {
				$object = $reflection->newInstanceArgs($arguments);
			}
		}
		
		return $object;
	}

	private function executeClosure(\Closure $closure)
	{
		$reflection = new \ReflectionFunction($closure);
		$arguments = $this->getConstructorArgs($reflection);
		if (empty($arguments)) {
			$result = $reflection->invoke();
		} else {
			$result = $reflection->invokeArgs($arguments);
		}
		return $result;
	}

	//获取参数
	private function getConstructorArgs(\Reflector $constructor)
	{
		$parameters = $constructor->getParameters();
		if (empty($parameters)) {
			return null;
		}
		$arguments = array();
		foreach ($parameters as $param) {
			if (($param_class = $param->getClass()) !== null) {//ReflectionClass
				//参数为另一个类的对象
				$depend_class_name = $param_class->getName();
				$arguments[] = $this->createObject($depend_class_name);//递归创建对象
			} elseif ($param->isDefaultValueAvailable()) {
				//参数有默认值
				$arguments[] = $param->getDefaultValue();//参数为默认值
			} else {
				//其他类型参数无法处理
			}
		}
		return $arguments;
	}

}

测试如下:

require 'Container.php';

class S
{
	public $name = 'ssbb';
}

class B
{
	public $s;

	public function __construct($s)
	{
		$this->s = $s;
	}
}

//获得容器对象
$c = new Container\Container();

//注入相关依赖

//s是S的一个实例,全局只需要一份
$c->singleton('s', function() {
	return new S;
});

//s2每次取回一个新的S的实例
$c->bind('s2', 'S');

//b每次都取回一个新的B的实例
$c->bind('b', function(S $s) {
	return new B($s);//B依赖于S的实例(全新的)
});

var_dump($c->make('s'));//object(S)#5 (1) { ["name"]=> string(4) "ssbb" }
var_dump($c->make('s'));//object(S)#5 (1) { ["name"]=> string(4) "ssbb" },二者一致

var_dump($c->make('s2'));//object(S)#6 (1) { ["name"]=> string(4) "ssbb" }
var_dump($c->make('s2'));//object(S)#4 (1) { ["name"]=> string(4) "ssbb" },不一致

var_dump($c->make('b'));//object(B)#7 (1) { ["s"]=> object(S)#9 (1) { ["name"]=> string(4) "ssbb" } }
var_dump($c->make('b'));//object(B)#4 (1) { ["s"]=> object(S)#8 (1) { ["name"]=> string(4) "ssbb" } }
//B的实例是全新的,每次依赖的S的实例也是全新的

下一篇,以这个简陋容器,简易模拟一下Laravel里边的容器、服务提供者(Service Provider)、门面(Facade)之类的东西。

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Time limit is exhausted. Please reload CAPTCHA.