深刻浅出依赖注入

本文首发于 深刻浅出依赖注入,转载请注明出处。

本文试图以一种易于理解的行文讲解什么是「依赖注入」这种设计模式。php

或许您已经在项目中已经使用过「依赖注入」,只不过因为某些缘由,导致您对它的印象不是特别深入。html

「依赖注入」多是最简单的设计模式之一,但即使如此我发现要想真正的以一种老小咸宜的方式把它讲解透彻也绝非易事。sql

本文在写做过程当中参考了诸多优秀的与「依赖注入」相关文章,我会从如下几个方面给你们讲解「依赖注入」到底是一种怎样的设计模式:数据库

目录结构

  • 什么是「组件」和「服务」segmentfault

    • 「组件」的定义
    • 「服务」的定义
    • 「组件」与「服务」的异同
  • 什么是控制反转和依赖注入设计模式

    • 一个简单的示例
    • 控制反转
    • 依赖注入
  • 如何实现依赖注入缓存

    • 经过构造函数注入依赖
    • 经过 setter 设值方法注入依赖
  • 什么是依赖注入容器
  • 依赖注入的优缺点cookie

    • 优势
    • 不足
  • 如何选择依赖注入的方式session

    • 选择经过构造函数注入:
    • 选择经过 setter 设值方法注入
  • 参考资料

提示:本文内容较多,会耗费较多的阅读实现,建议抽取空闲时间进行阅读;建议不要错过参考资料部分的学习;另外,因为本人技术水平所限表述不到的地方欢迎指正。异步

若是您以为本文对您有帮助,在收藏的同时请随手点个「赞」,谢谢!

什么是「组件」和「服务」

在讲解什么是依赖注入以前,咱们须要对什么是依赖这个问题进行说明。

所谓的「依赖」就是指在实现某个功能模块时须要使用另一个(或多个)「组件」或「服务」,那么这个所需的「组件」或「服务」将被称为「依赖」。

后续文中统一使用「组件」表示某个模块的「依赖」,「依赖注入」就是指向使用者注入某个「组件」以供其使用。

「组件」的定义

「组件」:它是可能被做者没法控制的其它应用使用,但使用者不能对其源码进行修改的一个功能模块。

「服务」的定义

「服务」指:使用者以同步(或异步)请求远程接口来远程使用的一个功能接口。

「组件」与「服务」的异同

「组件」和「服务」的 共同之处 就是它们都将被其余应用程序或功能模块使用。

它们的不一样之处在于:

  • 「组件」是在本地使用(如 jar 文件、dll 或者源码导入)
  • 「服务」是在远程使用(如 WebService、消息系统、RPC 或者 Socket)

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

「控制反转」和「依赖注入」本质上就是一个从 问题发现实现 的过程。

即在项目中咱们经过使用「依赖注入」这种技术手段实现功能模块对其依赖组件的「控制反转」。

咱们在开发的过程当中时长会遇到这样一个问题:如何才能将不一样的「组件」进行组装才能让它们配合默契的完成某个模块的功能?

「依赖注入」就是为了完成这样的 目标:将 依赖组件 的配置和使用分离开,以下降使用者与依赖之间的耦合度。

在阐述「依赖注入」这个模式具体含义前,仍是先看一个常见的示例,或许对于理解更有帮助。

一个简单的示例

这个示例的灵感来自 What is Dependency Injection? 这篇文章(译文 什么是依赖注入?)。

从事服务端研发工做的同窗,应该有这样的体验。

因为 HTTP 协议是一种无状态的协议,因此咱们就须要使用「Session(会话)」机制对有状态的信息进行存储。一个典型的应用场景就是存储登陆用户的状态到会话中。

<?php
$user = ['uid' => 1, 'uname' => '柳公子'];
$_SESSION['user'] = $user;

上面这段代码将登陆用户 $user 存储「会话」的 user 变量内。以后,同一个用户发起请求就能够直接从「会话」中获取这个登陆用户数据:

<?php
$user = $_SESSION['user'];

接着,咱们将这段面向过程的代码,以面向对象的方法进行封装:

<?php
class SessionStorage
{
    public function __construct($cookieName = 'PHP_SESS_ID')
    {
        session_name($cookieName);
        session_start();
    }

    public function set($key, $value)
    {
        $_SESSION[$key] = $value;
    }

    public function get($key)
    {
        return $_SESSION[$key];
    }

    public function exists($key)
    {
        return isset($this->get($key));
    }
}

而且须要提供一个接口服务类 user:

<?php
class User
{
    protected $storage;

    public function __construct()
    {
        $this->storage = new SessionStorage();
    }

    public function login($user)
    {
        if (!$this->storage->exists('user')) {
            $this->storage->set('user', $user);
        }

        return 'success';
    }

    public function getUser()
    {
        return $this->storage->get('user');
    }
}

以上就是登陆所需的大体功能,使用起来也很是容易:

<?php
$user = new User();
$user->login(['uid' => 1, 'uname' => '柳公子']);
$loginUser = $user->getUser();

这个功能实现很是简单:用户登陆 login() 方法依赖于 $this->storage 存储对象,这个对象完成将登陆用户的信息存储到「会话」的处理。

那么对于这个功能的实现,究竟还有什么值得咱们去担忧呢?

一切彷佛几近完美,直到咱们的业务作大了,会发现经过「会话」机制存储用户的登陆信息已近没法知足需求了,咱们须要使用「共享缓存」来存储用户的登陆信息。这个时候就会发现:

User 对象的 login() 方法依赖于 $this->storage 这个具体实现,即耦合到一块儿了。这个就是咱们须要面对的 核心问题

既然咱们已经发现了问题的症结所在,也就很容易获得 解决方案:让咱们的 User 对象不依赖于具体的存储方式,但不管哪一种存储方式,都须要提供 set 方法执行存储用户数据。

具体实现能够分为如下几个阶段:

  1. 定义 Storage 接口

定义 Storage 接口的做用是: 使 UserSessionStorage 实现类进行解耦,这样咱们的 User 类便再也不依赖于具体的实现了。

编写一个 Storage 接口彷佛不会太复杂:

<?php

interface Storage
{
    public function set($key, $value);

    public function get($key);

    public function exists($key);
}

而后让 SessionStorage 类实现 Storage 接口:

<?php
class SessionStorage implements Storage
{
    public function __construct($cookieName = 'PHP_SESS_ID')
    {
        session_name($cookieName);
        session_start();
    }

    public function set($key, $value)
    {
        $_SESSION[$key] = $value;
    }

    public function get($key)
    {
        return $_SESSION[$key];
    }

    public function exists($key)
    {
        return isset($this->get($key));
    }
}
  1. 定义一个 Storage 接口让 User 类仅依赖 Storage 接口

如今咱们的 User 类看起来既依赖于 Storage 接口又依赖于 SessionStorage 这个具体实现:

<?php

class User
{
    protected $storage;

    public function __construct()
    {
        $this->storage = new SessionStorage();
    }
}

固然这已是一个完美的登陆功能了,直到我将这个功能开放出来给别人使用。然而,若是这个应用一样是经过「会话」机制来存储用户信息,现有的实现不会出现问题。

但若是使用者将「会话」机制更换到下列这些存储方式呢?

  • 将会话存储到 MySQL 数据库
  • 将会话存储到 Memcached 缓存
  • 将会话存储到 Redis 缓存
  • 将会话存储到 MongoDB 数据库
  • ...
<?php
// 想象下下面的全部实现类都有实现 get,set 和 exists 方法
class MysqlStorage {}

class MemcachedStorage {}

class RedisStorage {}

class MongoDBStorage {}

...

此时咱们彷佛没法在不修改 User 类的构造函数的的状况下,完成替换 SessionStorage 类的实例化过程。即咱们的模块与依赖的具体实现类耦合到一块儿了。

有没有这样一种解决方案,让咱们的模块仅依赖于接口类,而后在项目运行阶段动态的插入具体的实现类,而非在编译(或编码)阶段将实现类接入到使用场景中呢?

这种动态接入的能力称为「插件」。

答案是有的:可使用「控制反转」。

控制反转

「控制反转」提供了将「插件」组合进模块的能力。

在实现「控制反转」过程当中咱们「反转」了哪方面的「控制」呢?其实这里的「反转」的意义就是 如何去定位「插件」的具体实现

采用「控制反转」模式时,咱们经过一个组装模块,将「插件」的具体实现「注入」到模块中就能够了。

依赖注入

了解完「控制反转」,咱们再来看看什么是「依赖注入」。「依赖注入」和「控制反转」之间是怎样的一种关系呢?

「控制反转」是目的:它但愿咱们的模块可以在运行时动态获取依赖的「插件」,而后,咱们经过「依赖注入」这种手段去完成「控制反转」的目的。

这边我试着给出一个「依赖注入」的具体的定义:

应用程序对须要使用的依赖「插件」在编译(编码)阶段仅依赖于接口的定义,到运行阶段由一个独立的组装模块(容器)完成对实现类的实例化工做,并将其「注射」到应用程序中称之为「依赖注入」。

如何实现依赖注入

如何实现依赖注入或者说依赖注入有哪些形式?

Inversion of Control Containers and the Dependency Injection pattern 一文中有过相关的阐述:

依赖注入的形式主要有三种,我分别将它们叫作构造注入( Constructor Injection)、设值
方法注入( Setter Injection)和接口注入( Interface Injection)

本文将结合上面的示例稍微讲下:

  1. 经过构造函数注入依赖
  2. 经过 setter 设值方法注入依赖

这两种注入方式。

经过构造函数注入依赖

经过前面的文章咱们知道 User 类的构造函数既依赖于 Storage 接口,又依赖于 SessionStorage 这个具体的实现。

如今咱们经过重写 User 类的构造函数,使其仅依赖于 Storage 接口:

<?php

class User
{
    protected $storage;

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

咱们知道 User 类中的 login 和 getUser 方法内依赖的是 $this->storage 实例,也就无需修改这部分的代码了。

以后咱们就能够经过「依赖注入」完成将 SessionStorage 实例注入到 User 类中,实现高内聚低耦合的目标:

<?php
$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);

经过 setter 设值方法注入依赖

设值注入也很简单:

<?php

class User
{
    protected $storage;

    public function setStorage(Storage $storage)
    {
        $this->storage = $storage;
    }
}

使用也几乎和构造方法注入同样:

<?php
$storage = new SessionStorage('SESSION_ID');
$user = new User();
$user->setStorage($storage);

什么是依赖注入容器

上面实现依赖注入的过程仅仅能够当作一个演示,真实的项目中确定没有这样使用的。那么咱们在项目中该如何去实现依赖注入呢?

嗯,这是个好问题,因此如今咱们须要了解另一个与「依赖注入」相关的内容「依赖注入容器」。

依赖注入容器咱们在给「依赖注入」下定义的时候有提到 由一个独立的组装模块(容器)完成对实现类的实例化工做,那么这个组装模块就是「依赖注入容器」。

「依赖注入容器」是一个知道如何去实例化和配置依赖组件的对象。

尽管,咱们已经可以将 User 类与实现分离,可是还须要进一步,才能称之为完美。

定义一个简单的服务容器:

<?php
class Container
{
    public function getStorage()
    {
        return new SessionStorage();
    }

    public function getUser()
    {
        $user = new User($this->getStorage());
        return $user;
    }
}

使用也很简单:

<?php
$container = new Container();
$user = $container->getUser();

咱们看到,若是咱们须要使用 User 对象仅须要经过 Container 容器的 getUser 方法便可获取这个实例,而无需关心它是如何被建立建立出来的。

这样,咱们就了解了「依赖注入」几乎所有的细节了,可是现实老是会比理想更加骨感。由于,咱们现有的依赖注入容器还至关的脆弱,由于它一样依赖于 SessionStorage,一旦咱们须要替换这个实现,仍是不得不去修改里面的源代码,而没法实如今运行时配置。

作了这么多工做,仍是这样的结果,真是晴天霹雳啊!

为何不考虑将实现类相关数据写入到配置文件中,在容器中实例化是从配置文件中读取呢?

有关使用依赖注入容器的更加详细的使用能够阅读我翻译的 依赖注入 系列文章,文章还部分篇章没有翻译,因此你也能够直接阅读 原文

依赖注入的优缺点

优势

  • 提供系统解耦的能力
  • 能够明确的了解到组件之间的依赖关系
  • 简化测试工做

前两个比较好理解,稍微说下依赖注入是如何简化测试的。

若是咱们在实现 User 类时,尚未实现具体的 SessionStorage 类,而仅定义了 Storage 接口。

那么在测试时,能够编写一个 NopStorage 先用于测试,以后等实现了 SessionStorage 在进行替换便可。

不足

组件与注入器之间不会有依赖关系,所以组件没法从注入器那里得到更多的服务,只能得到配置信息中所提供的那些。

如何选择依赖注入的方式

如何选择依赖注入方式在 Inversion of Control Containers and the Dependency Injection pattern 一文中有给出相关论述。

选择经过构造函数注入:

  • 可以在构造阶段就建立完整、合法的对象;
  • 带有参数的构造子能够明确地告诉你如何建立一个合法的对象;
  • 能够隐藏任何不可变的字段。

选择经过 setter 设值方法注入

  • 若是依赖的「插件」太多时,选择设值注入更优

说完了什么是「控制反转」和「依赖注入」,相信你们已经对这两个概念有了相对比较清晰的了解。我想说的是任何事物的了解程度都不是一蹴而就的,因此即使有号称能一句话讲明白什么是「依赖注入」的文章,其实仍是须要咱们有了相对深刻的了解后才能感悟其中的真意,所谓「读书百遍,其义自见」就是这个道理。

参考资料

相关文章
相关标签/搜索