资讯专栏INFORMATION COLUMN

PHP程序员如何理解IoC/DI

DataPipeline / 1670人阅读

摘要:依赖注入容器管理应用程序中的全局对象包括实例化处理依赖关系。为了解决这样的问题,我们再次回到全局注册表创建组件。参考文章程序员如何理解依赖注入容器补充很多代码背后,都是某种哲学思想的体现。

思想

思想是解决问题的根本
思想必须转换成习惯
构建一套完整的思想体系是开发能力成熟的标志
——《简单之美》(前言)

.

“成功的软件项目就是那些提交产物达到或超出客户的预期的项目,而且开发过程符合时间和费用上的要求,结果在面对变化和调整时有弹性。”
——《面向对象分析与设计》(第3版)P.236

术语介绍

——引用《Spring 2.0 技术手册》林信良

非侵入性 No intrusive

框架的目标之一是非侵入性(No intrusive)

组件可以直接拿到另一个应用或框架之中使用

增加组件的可重用性(Reusability)

容器(Container)

管理对象的生成、资源取得、销毁等生命周期

建立对象与对象之间的依赖关系

启动容器后,所有对象直接取用,不用编写任何一行代码来产生对象,或是建立对象之间的依赖关系。

IoC

控制反转 Inversion of Control

依赖关系的转移

依赖抽象而非实践

DI

依赖注入 Dependency Injection

不必自己在代码中维护对象的依赖

容器自动根据配置,将依赖注入指定对象

AOP

Aspect-oriented programming

面向方面编程

无需修改任何一行程序代码,将功能加入至原先的应用程序中,也可以在不修改任何程序的情况下移除。

分层

表现层:提供服务,显示信息。
领域层:逻辑,系统中真正的核心。
数据源层:与数据库、消息系统、事务管理器及其它软件包通信。
——《企业应用架构模式》P.14

代码演示IoC

假设应用程序有储存需求,若直接在高层的应用程序中调用低层模块API,导致应用程序对低层模块产生依赖。

/**
 * 高层
 */
class Business
{
    private $writer;

    public function __construct()
    {
        $this->writer = new FloppyWriter();
    }

    public function save()
    {
        $this->writer->saveToFloppy();
    }
}

/**
 * 低层,软盘存储
 */
class FloppyWriter
{
    public function saveToFloppy()
    {
        echo __METHOD__;
    }
}

$biz = new Business();
$biz->save(); // FloppyWriter::saveToFloppy

假设程序要移植到另一个平台,而该平台使用USB磁盘作为存储介质,则这个程序无法直接重用,必须加以修改才行。本例由于低层变化导致高层也跟着变化,不好的设计。

正如前方提到的

控制反转 Inversion of Control
依赖关系的转移
依赖抽象而非实践

程序不应该依赖于具体的实现,而是要依赖抽像的接口。请看代码演示

/**
 * 接口
 */
interface IDeviceWriter
{
    public function saveToDevice();
}

/**
 * 高层
 */
class Business
{
    /**
     * @var IDeviceWriter
     */
    private $writer;

    /**
     * @param IDeviceWriter $writer
     */
    public function setWriter($writer)
    {
        $this->writer = $writer;
    }

    public function save()
    {
        $this->writer->saveToDevice();
    }
}

/**
 * 低层,软盘存储
 */
class FloppyWriter implements IDeviceWriter
{

    public function saveToDevice()
    {
        echo __METHOD__;
    }
}

/**
 * 低层,USB盘存储
 */
class UsbDiskWriter implements IDeviceWriter
{

    public function saveToDevice()
    {
        echo __METHOD__;
    }
}

$biz = new Business();
$biz->setWriter(new UsbDiskWriter());
$biz->save(); // UsbDiskWriter::saveToDevice

$biz->setWriter(new FloppyWriter());
$biz->save(); // FloppyWriter::saveToDevice

控制权从实际的FloppyWriter转移到了抽象的IDeviceWriter接口上,让Business依赖于IDeviceWriter接口,且FloppyWriter、UsbDiskWriter也依赖于IDeviceWriter接口。

这就是IoC,面对变化,高层不用修改一行代码,不再依赖低层,而是依赖注入,这就引出了DI。

比较实用的注入方式有三种:

Setter injection 使用setter方法

Constructor injection 使用构造函数

Property Injection 直接设置属性

事实上不管有多少种方法,都是IoC思想的实现而已,上面的代码演示的是Setter方式的注入。

依赖注入容器 Dependency Injection Container

管理应用程序中的『全局』对象(包括实例化、处理依赖关系)。

可以延时加载对象(仅用到时才创建对象)。

促进编写可重用、可测试和松耦合的代码。

理解了IoC和DI之后,就引发了另一个问题,引用Phalcon文档描述如下:

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

//创建依赖实例或从注册表中查找
$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。为了解决这样的问题,我们再次回到全局注册表创建组件。不管怎么样,在创建对象之前,它增加了一个新的抽象层:

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

}

瞬间,我们又回到刚刚开始的问题了,我们再次创建依赖实例在组件内部!我们可以继续前进,找出一个每次能奏效的方法去解决这个问题。但似乎一次又一次,我们又回到了不实用的例子中。

一个实用和优雅的解决方法,是为依赖实例提供一个容器。这个容器担任全局的注册表,就像我们刚才看到的那样。使用依赖实例的容器作为一个桥梁来获取依赖实例,使我们能够降低我们的组件的复杂性:

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 PhalconDI();

//在容器中注册一个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->someTask();

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

参考文章

PHP程序员如何理解依赖注入容器(dependency injection container)

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

What is Dependency Injection? Fabien Potencier

Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler

补充

很多代码背后,都是某种哲学思想的体现。

以下引用《面向模式的软件架构》卷1模式系统第六章模式与软件架构

软件架构支持技术(开发软件时要遵循的基本原则)

抽象

封装

信息隐藏

分离关注点

耦合与内聚

充分、完整、简单

策略与实现分离

策略组件负责上下文相关决策,解读信息的语义和含义,将众多不同结果合并或选择参数值

实现组件负责执行定义完整的算法,不需要作出与上下文相关的决策。上下文和解释是外部的,通常由传递给组件的参数提供。

接口与实现分离

接口部分定义了组件提供的功能以及如何使用该组件。组件的客户端可以访问该接口。

实现部分包含实现组件提供的功能的实际代码,还可能包含仅供组件内部使用的函数和数据结构。组件的客户端不能访问其实现部分。

单个引用点

软件系统中的任何元素都应只声明和定义一次,避免不一致性问题。
10. 分而治之

软件架构的非功能特性

可修改性

可维护性

可扩展性

重组

可移植性

互操作性

与其它系统或环境交互

效率

可靠性

容错:发生错误时确保行为正确并自行修复

健壮性:对应用程序进行保护,抵御错误的使用方式和无效输入,确保发生意外错误时处于指定状态。

可测试性

可重用性

通过重用开发软件

开发软件时考虑重用

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/20888.html

相关文章

  • PHP序员如何理解依赖注入容器(dependency injection container)

    摘要:代码这就是控制反转模式。是变量有默认值则设置默认值是一个类,递归解析有默认值则返回默认值从容器中取得以上代码的原理参考官方文档反射,具有完整的反射,添加了对类接口函数方法和扩展进行反向工程的能力。 PHP程序员如何理解依赖注入容器(dependency injection container) 背景知识 传统的思路是应用程序用到一个Foo类,就会创建Foo类并调用Foo类的方法,假如这...

    Coding01 评论0 收藏0
  • 聊一聊PHP的依赖注入(DI) 和 控制反转(IoC)

    摘要:前言最近在使用框架,看了下他的源码,发现有很多地方也用到了依赖注入控制反转,觉得有必要和大家简单聊一聊什么是依赖注入以及怎么使用它。概念依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。 前言 最近在使用ThinkPHP5框架,看了下他的源码,发现有很多地方也用到了依赖注入(控制反转),觉得有必要和大家简单聊一聊什么是依赖注入以及怎么使用它。 简介 I...

    sixgo 评论0 收藏0
  • PHP IOC/DI 容器 - 依赖自动注入/依赖单例注入/依赖契约注入/参数关联传值

    摘要:标量参数关联传值依赖是自动解析注入的,剩余的标量参数则可以通过关联传值,这样比较灵活,没必要把默认值的参数放在函数参数最尾部。 更新:github(给个小星星呀) -- 2018-4-11:优化服务绑定方法 ::bind 的类型检查模式 借助 PHP 反射机制实现的一套 依赖自动解析注入 的 IOC/DI 容器,可以作为 Web MVC 框架 的应用容器 1、依赖的自动注入:你只需要...

    Paul_King 评论0 收藏0
  • Spring 冬天来了,春天还远吗

    摘要:当你需要用到某个对象时,就可以使用如下代码从容器中获取获取容器获取容器中的对象管理方式的所创建与存储的对象,我们称之为属性注入方式管理注解方式属性注入注解方式 好处 IoC / DI,方便解耦 AOP 面向切面编程 声明式事务 方便程序测试 方便集成其他优秀框架 IoC 与 DI IoC 与 DI 的关系 IoC控制反转:主要是指【创建对象】这件事交给Spring进行处理,无需开发...

    ddongjian0000 评论0 收藏0
  • OOD、DIP、IOCDI、依赖注入容器(即 控制反转容器,IOC Container)

    摘要:服务本省作为一个高层类,对外提供访问,却受制于提供具体服务的服务提供者定义的实现,高层模块依赖底层模块实现,违背了依赖倒置原则。遵循依赖倒置原则的例子场景同介绍中场景。 1. 名词介绍 OOD,面向对象设计 DIP,依赖倒置(软件设计原则) IOC,控制反转(软件设计模式) DI,依赖注入 IOC Container,控制反转容器,也是依赖注入容器 2. 组成部分 服务清单(功能...

    renweihub 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<