阅读时忽略语言差异,参考了很多其他博主内容,参考博文在最后给出,侵删
引入
由于 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个对象构成的,各个对象之间通过相互合作(就像下面的齿轮一样),最终实现系统地业务逻辑。
伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。
软件工程中的耦合是指各个模块依赖程度,为了便于维护,自然希望耦合越低越好。
耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson 1996年提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中。
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。
1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。如下图:
大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统:
我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,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)
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