天天看点

控制反转IOC、依赖注入DI的详细说明与举例

文章目录

    • 引入
    • IOC介绍
    • IOC的实现
      • 通过构造函数注入依赖
      • 通过 setter 设值方法注入依赖
      • 依赖注入容器
    • IOC优缺点
      • 优点
      • 缺点

阅读时忽略语言差异,参考了很多其他博主内容,参考博文在最后给出,侵删

引入

由于 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 方法执行存储用户数据。

具体实现可以分为以下几个阶段:

定义 Storage 接口

定义 Storage 接口的作用是: 使 User 与 SessionStorage 实现类进行解耦,这样我们的 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));
    }
}
           

定义一个 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 类的实例化过程。即我们的模块与依赖的具体实现类耦合到一起了。

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

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

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

IOC介绍

面向对象设计的软件系统中,它的底层都是由N个对象构成的,各个对象之间通过相互合作(就像下面的齿轮一样),最终实现系统地业务逻辑。

控制反转IOC、依赖注入DI的详细说明与举例

伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。

软件工程中的耦合是指各个模块依赖程度,为了便于维护,自然希望耦合越低越好。

耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson 1996年提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中。

IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。

1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。

IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。如下图:

控制反转IOC、依赖注入DI的详细说明与举例

大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统:

控制反转IOC、依赖注入DI的详细说明与举例

我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!

我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:

软件系统在没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

软件系统在引入IOC容器之后,这种情形就完全改变了,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

IOC的实现

两种实现方式:依赖查找(DL)、依赖注入(DI)

控制反转IOC、依赖注入DI的详细说明与举例

DL 已经被抛弃,因为他需要用户自己去是使用 API 进行查找资源和组装对象,即有侵入性。

我们着重看看DI:

2004年,Martin Fowler探讨了同一个问题,既然IOC是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现IOC的方法:注入。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

所以,依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。

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

通过构造函数注入依赖

通过前面的文章我们知道 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);
           

接口注入就是实现相关接口,通过接口定义调用其中的inject方法完成注入过程。

依赖注入容器

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

嗯,这是个好问题,所以现在我们需要了解另外一个与「依赖注入」相关的内容「依赖注入容器」。

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

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

尽管,我们已经能够将 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 方法即可获取这个实例,而无需关心它是如何被创建创建出来的。

IOC优缺点

优点

1、灵活性

对于广泛使用的接口,更改其实现类变得更简单(例如,用生产实例替换模拟web服务)

更改给定类的检索策略更简单(例如,将服务从类路径移动到JNDI树)

添加拦截器很容易,而且在一个地方就可以完成(例如,将缓存拦截器添加到基于JDBC的DAO中)

2、可读性

该项目有一个统一一致的组件模型,代码更简洁,而且没有依赖项查找代码(例如调用JNDI InitialContext)

3、可测试性

当依赖项通过构造函数或setter公开时,可以很容易地替换

更容易的测试可以带来更多的测试,更多的测试会带来更好的代码质量、更低的耦合、更高的内聚性

缺点

第一、软件系统中由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。

第二、由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。

第三、具体到IOC框架产品(比如:Spring)来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。

第四、IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。

我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用IOC框架产品。另外,如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,像WEB2.0网站就是这种情况。

参考:

https://www.cnblogs.com/DebugLZQ/archive/2013/06/05/3107957.html

https://www.jianshu.com/p/17b66e6390fd

https://segmentfault.com/a/1190000014803412

https://segmentfault.com/a/1190000014719665

继续阅读