前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >依赖注入和控制反转是什么?

依赖注入和控制反转是什么?

作者头像
Marser
发布2018-06-25 17:07:18
1.9K0
发布2018-06-25 17:07:18
举报
文章被收录于专栏:智能合约智能合约

年前,@绵阳飞在群里发起了一个讨论,依赖注入和控制反转到底是什么?

我之前对依赖注入和控制反转也不甚理解,直至在学习Phalcon框架的过程中,发现在Phalcon文档中有一个篇幅通过代码示例的方式专门描述了依赖注入的原理。本文打算通过此文档中的代码示例来讲解什么是依赖注入(DI)和控制反转(IoC)。通过示例代码,来更加深入的了解这些概念。

接下来的例子有些长,但解释了为什么我们要使用依赖注入。所以绝对都是干货,请耐心读完,必会有所收获。

依赖的产生

首先,假设我们正在开发一个组件,叫SomeComponent,它需要执行的内容现在还不重要。 但是我们的组件需要依赖数据库连接。

代码语言:javascript
复制
<?php

class SomeComponent
{
    /**
     * 数据库连接是被写死在组件的内部
     * 因此,我们很难从外部替换或者改变它的行为
     */
    public function someDbTask()
    {
        $connection = new Connection(
            array(
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo"
            )
        );

        // ...
    }
}

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

从上面这个例子中,可以看到数据库连接是在组件内部建立的。在我们日常开发中,类似这样的依赖关系在项目中非常常见。但是这种方法其实是不太实用的;我们不能改变创建数据库连接的参数或者选择不同的数据库系统,因为数据库连接是在组件被创建时建立的。

依赖注入

为了解决这样的情况,我们通过一个setter函数,在使用前注入独立外部依赖:

代码语言:javascript
复制
<?php

class SomeComponent
{
    protected $_connection;

    /**
     * 设置外部传入的数据库连接
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

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

        // ...
    }
}

$some = new SomeComponent();

// 建立数据库连接
$connection = new Connection(
    array(
        "host"     => "localhost",
        "username" => "root",
        "password" => "secret",
        "dbname"   => "invo"
    )
);

// 向组件注入数据库连接
$some->setConnection($connection);

$some->someDbTask();

目前来看,通过setter函数来注入独立的外部依赖的方式,已经解耦了数据库连接与应用程序的依赖。这里已经使用到了控制反转(IoC)的模式。具体概念稍后再解释,还是以代码示例的方式来增加理解。

我们想像一下,假设这个组件在应用内的好几个地方都需要用到,那在注入数据库连接时,我们还需要建立好几次数据库连接。 是否可以获取到数据库连接,而不用每次都创建新的连接呢?可以使用某种全局注册的方式来解决这样的问题:

代码语言:javascript
复制
<?php

class Registry
{
    /**
     * 返回数据库连接
     */
    public static function getConnection()
    {
        return new Connection(
            array(
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo"
            )
        );
    }
}

class SomeComponent
{
    protected $_connection;

    /**
     * 设置外部传入的数据库连接
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

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

        // ...
    }
}

$some = new SomeComponent();

// 把数据库连接传递给组件
$some->setConnection(Registry::getConnection());

$some->someDbTask();

通过上述方法,我们实现了共享数据库连接,而不用每次都创建新的数据库连接。

那么,让我们再扩展一下,我们可以实现2个方法,第一个方法总是创建新的数据库连接,第二方法总是使用一个共享的数据库连接:

代码语言:javascript
复制
<?php

class Registry
{
    protected static $_connection;

    /**
     * 建立一个新的数据库连接
     */
    protected static function _createConnection()
    {
        return new Connection(
            array(
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo"
            )
        );
    }

    /**
     * 只建立一个数据库连接,后面的请求共享该连接
     */
    public static function getSharedConnection()
    {
        if (self::$_connection===null) {
            $connection = self::_createConnection();
            self::$_connection = $connection;
        }

        return self::$_connection;
    }

    /**
     * 总是返回一个新的数据库连接
     */
    public static function getNewConnection()
    {
        return self::_createConnection();
    }
}

class SomeComponent
{
    protected $_connection;

    /**
     * 设置外部传入的数据库连接
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

    /**
     * 这个方法使用共享的数据库连接
     */
    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

    /**
     * 这个方法总是使用新的数据库连接
     */
    public function someOtherDbTask($connection)
    {

    }
}

$some = new SomeComponent();

// 注入共享的数据库连接
$some->setConnection(Registry::getSharedConnection());

$some->someDbTask();

// 这里总是传递一个新的数据库连接
$some->someOtherDbTask(Registry::getNewConnection());

到目前为止,我们已经看到通过依赖注入怎么解决我们的问题了。把依赖作为参数来传递,而不是在内部建立它们,这使我们的应用更加容易维护和更加解耦。但是这种形式的依赖注入还有一些缺点。

例如,如果这个组件有很多依赖, 我们需要创建多个参数的setter方法来传递依赖关系,或者建立一个多个参数的构造函数来传递它们,另外在使用组件前还需要每次都创建依赖,这让我们的代码像这样不易维护:

代码语言:javascript
复制
<?php

// 创建依赖对象或从注册表中查找
$connection = new Connection();
$session    = new Session();
$fileSystem = new FileSystem();
$filter     = new Filter();
$selector   = new Selector();

// 把依赖对象作为参数传递给构造函数
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);

// ... 或者使用setter

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

我们假设在应用内不同的地方使用和创建了这些对象。如果当我们永远不需要其中某个依赖对象时,那我们需要去删掉构造函数中的参数,或者去删掉注入的setter函数。为了解决这样的问题,我们再次回到全局注册的方式创建组件。在创建对象之前,给它增加了一个新的抽象层:

代码语言:javascript
复制
<?php

class SomeComponent
{
    // ...

    /**
     * 定义一个工厂方法来创建SomeComponent组件所需的依赖对象
     */
    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);
    }
}

瞬间,我们又绕回到刚刚开始的问题了:我们再次在组件内部创建依赖的对象!一个实用又优雅的解决方法,是为依赖对象提供一个容器。

容器

所谓容器,从字面上来理解,就是可以装东西的东西。变量、对象属性等都可以算是容器。

我们可以通过这个容器获取依赖的各个对象,这样能够降低我们这个组件的复杂性,及对依赖对象的耦合性(即解耦):

代码语言:javascript
复制
<?php

//加载容器
use Phalcon\Di;

class SomeComponent
{
    protected $_di;

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

    public function someDbTask()
    {
        // 获取数据库连接
        // 总是返回一个新的连接
        $connection = $this->_di->get('db');
    }

    public function someOtherDbTask()
    {
        // 获取共享的数据库连接
        // 每次请求都返回相同的数据库连接
        $connection = $this->_di->getShared('db');

        // 这个方法也需要一个过滤的依赖服务
        $filter = $this->_di->get('filter');
    }
}

$di = new Di();

// 在容器中注册一个db服务
$di->set('db', function () {
    return new Connection(
        array(
            "host"     => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname"   => "invo"
        )
    );
});

// 在容器中注册一个filter服务
$di->set('filter', function () {
    return new Filter();
});

// 在容器中注册一个session服务
$di->set('session', function () {
    return new Session();
});

// 把传递服务的容器作为唯一参数传递给组件
$some = new SomeComponent($di);

$some->someDbTask();

上面这段代码并没有介绍DI容器的内部是如何运作的,下面我们自己来实现一个简单的容器:

代码语言:javascript
复制
class Container{

    protected $binds;

    /**
     * 注入服务
     * @param $name  服务名称
     * @param $callback  回调函数
     */
    public function set($name, $callback){
        $this -> binds[$name] = $callback;
    }

    /**
     * 加载服务
     * @param $name  服务名称
     * @param array $param  参数
     */
    public function make($name, $param=[]){
        if(isset($this -> binds[$name])){
            return call_user_func_array($this -> binds[$name], $param);
        }
    }
}

这时候,一个粗糙的容器就诞生了。我们来看看这个容器如何使用:

代码语言:javascript
复制
//创建一个容器
$container = new Container();

//注入db服务
$container -> set('db', function(){
    return new Connection(
        array(
            "host"     => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname"   => "invo"
        )
    );
});

//注入filter服务
$container -> set('filter', function(){
    return new Fileter();
});

如此,SomeComponent这个组件现在可以很简单的获取到它所需要的各个服务,服务采用延迟加载的方式,只有在需要使用的时候才初始化,这也节省了服务器资源。这个组件现在是高度解耦。例如,我们可以替换掉创建数据库连接的方式,它们的行为或它们的任何其他方面,也不会影响该组件。

通过上述代码示例,大家应该初步了解了依赖注入和控制反转其中的原理。这里总结一下概念。

依赖注入(DI)

依赖注入的英文全称为:Dependency Injection. 其基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由容器负责,“查找资源”的逻辑应该从应用组件的代码中抽取出来,交给容器负责。不必自己在代码中维护对象的依赖。

控制反转(IoC)

控制反转的英文全称为:Inversion of Control. 从上述代码示例里可以看出,把传统上由程序代码直接操控的对象的调用权转交给容器,通过容器来实现对象组件的装配与管理。也就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。这就是控制反转。主要就是依赖关系的转移。

容器

  • 管理对象的生成、资源获取、销毁等生命周期
  • 建立对象与对象之间的依赖关系
  • 启动容器后,所有对象直接取用,不用编写任何一行代码来产生对象,或是建立对象之间的依赖关系

参考资料:

https://segmentfault.com/a/1190000002424023

https://segmentfault.com/a/1190000002411255

https://github.com/laracasts/simple-di-container

http://laravelacademy.org/post/769.html

https://docs.phalconphp.com/zh/latest/reference/di.html

  • 本站文章除注明转载外,均为本站原创
  • 欢迎任何形式的转载,但请务必注明出处,尊重他人劳动
  • 转载请注明:文章转载自:Marser https://www.marser.cn
  • 本文标题:依赖注入和控制反转是什么?
  • 本文固定链接: https://www.marser.cnarticle/124.html
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:201602-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 依赖的产生
  • 依赖注入
  • 容器
  • 依赖注入(DI)
  • 控制反转(IoC)
  • 容器
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档