Dependency Injection-依赖注入详解

举报
feichaiyu 发表于 2019/12/17 21:34:17 2019/12/17
【摘要】 依赖注入是目前很多优秀框架都在使用的一个设计模式。Java的开发框架如Spring在用,PHP的Laravel/Phalcon/Symfony等也在用。好多不同语言的框架,设计思想大同小异,相互借鉴参考。熟悉了一个语言的开发框架,其它不同的框架甚至不同语言的开发框架,往往也很容易从设计理念和概念上理解。不过,有些语言因为设计特色,一些设计模式反而看似消失不见了。其实是融入了语言里面,不易察觉...

依赖注入是目前很多优秀框架都在使用的一个设计模式。Java的开发框架如Spring在用,PHP的Laravel/Phalcon/Symfony等也在用。好多不同语言的框架,设计思想大同小异,相互借鉴参考。熟悉了一个语言的开发框架,其它不同的框架甚至不同语言的开发框架,往往也很容易从设计理念和概念上理解。不过,有些语言因为设计特色,一些设计模式反而看似消失不见了。其实是融入了语言里面,不易察觉。我看见过这么一句话:“设计模式是编程语言固有缺陷的产物”。有一个讨论在这里:Why is IoC / DI not common in Python?

Dependency Injection 常常简称为:DI。它是实现控制反转(Inversion of Control – IoC)的一个模式。有一本依赖注入详解的书在这里:Dependency Injection 。它的本质目的是解耦,保持软件组件之间的松散耦合,为设计开发带来灵活性。

这里借用一套PHP代码的演化过程,解释依赖注入模式的出现过程。代码来自Phalcon框架文档。个人感觉,从演化出发,最能达成理解的目标,就如同数学推理一样让人信服,自然而然。想当年,我研究Windows时代的COM技术体系,看到有一本书也是这么做的 – Dan Box的《COM本质论》第1-2章,阐述了从Dll到COM组件的设计过程。

假设我们要开发一套组件,这个组件干啥目前并不重要,不过它需要连接数据库。最简单的实现,当然是把数据库的配置信息写在组件里面。

class SomeComponent{    /**
     * The instantiation of the connection is hardcoded inside
     * the component, therefore it's difficult replace it externally
     * or change its behavior
     */
    public function someDbTask()
    {
        $connection = new Connection(
            [                "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(
    [        "host"     => "localhost",        "username" => "root",        "password" => "secret",        "dbname"   => "invo",
    ]
);

好一点。不过如果我们在很多地方都要用这个组件,那么意味着每次用的时候,都要创建这么一个连接配置对象,不仅冗余,而且难以变更和管理。我们把这个连接配置对象单独放在一个地方管理,DRY原则。

class Registry{    /**
     * Returns the connection
     */
    public static function getConnection()
    {        return new Connection(
            [                "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(
            [                "host"     => "localhost",                "username" => "root",                "password" => "secret",                "dbname"   => "invo",
            ]
        );
    } 
    /**
     * Creates a connection only once and returns it
     */
    public static function getSharedConnection()
    {        if (self::$_connection === null) {            self::$_connection = self::_createConnection();
        } 
        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::getNewConnection()
);

这就是“依赖注入”模式了,它解决了组件的依赖项和组件之间的过度耦合问题。不过还有个麻烦:如果这个组件依赖项很多怎么办?每次都要创建并设置一大堆依赖项。

// 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);

每次使用这个组件,都要创建一堆附加的依赖项。如果以后我们修改组件依赖,那么必须挨个改掉。代码的坏味道又来了。再改。

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);
    }
}

估计好多人走到这一步就会停下脚步了。代码用个工厂模式不就行了嘛。可是你对比下开头的代码,组件和它的依赖项的耦合不就又来了么?现在,问题又回到开头了。

一个更好的办法是使用依赖注入容器。它就如同一个全局的注册表,像桥一样获取依赖项,并解耦。

use Phalcon\Di;use Phalcon\DiInterface; 
class SomeComponent{    protected $_di; 
    public function __construct(DiInterface $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 every time
        $connection = $this->_di->getShared("db"); 
        // This method also requires an input filtering service
        
    $filter = $this->_di->get("filter");
    }
}
 
$di = new Di(); 
// Register a "db" service in the container$di->set(    "db",    function () {        return new Connection(
            [                "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->someDbTask();”

问题解决。获取依赖项只要通过DI容器接口操作,不需要的部分甚至都不会创建,节约了资源。

在Java的Spring框架里面,依赖注入和控制反转设计思想是近似的,道理相同但是实现不同。因为编程语言各有各的设计特点可以利用。

Spring框架的依赖注入容器接口是:ApplicationContext.

ApplicationContext context
  = new ClassPathXmlApplicationContext("applicationContext.xml");

但是Spring使用DI,有好几种方法,比如注解式,利用了语言的功能。

具体解释参考这篇文章,不翻译了。

Intro to Inversion of Control and Dependency Injection with Spring

作者博客

转载声明:本文转载自异步社区

原文链接:https://www.epubit.com/articleDetails?id=N6a529cf2-7249-4ea5-bb62-2a84cd5597b8

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。