0

0

PHP 依赖注入,自此不再考虑加载顺序

php中文网

php中文网

发布时间:2016-06-13 12:28:25

|

842人浏览过

|

来源于php中文网

原创

PHP 依赖注入,从此不再考虑加载顺序

说这个话题之前先讲一个比较高端的思想--'依赖倒置原则'

"依赖倒置是一种软件设计思想,在传统软件中,上层代码依赖于下层代码,当下层代码有所改动时,上层代码也要相应进行改动,因此维护成本较高。而依赖倒置原则的思想是,上层不应该依赖下层,应依赖接口。意为上层代码定义接口,下层代码实现该接口,从而使得下层依赖于上层接口,降低耦合度,提高系统弹性"

 

笔灵AI论文写作
笔灵AI论文写作

免费生成毕业论文、课题论文、千字大纲,几万字专业初稿!

下载

上面的解释有点虚,下面我们以实际代码来解释这个理论

比如有这么条需求,用户注册完成后要发送一封邮件,然后你有如下代码:

立即学习PHP免费学习笔记(深入)”;

先有邮件类'Email.class.php'

class Mail{    public function send()    {        /*这里是如何发送邮件的代码*/    }}

然后又注册的类'Register.class.php'

class Register{    private $_emailObj;    public function doRegister()    {        /*这里是如何注册*/        $this->_emailObj = new Mail();        $this->_emailObj->send();//发送邮件    }}

然后开始注册

include 'Mail.class.php';include 'Register.class.php';$reg = new Register();$reg->doRegister();

看起来事情很简单,你很快把这个功能上线了,看起来相安无事... xxx天过后,产品人员说发送邮件的不好,要使用发送短信的,然后你说这简单我把'Mail'类改下...

又过了几天,产品人员说发送短信费用太高,还是改用邮件的好...  此时心中一万个草泥马奔腾而过...

这种事情,常常在产品狗身上发生,无可奈何花落去...

 

以上场景的问题在于,你每次不得不对'Mail'类进行修改,代码复用性很低,高层过度依赖于底层。那么我们就考虑'依赖倒置原则',让底层继承高层制定的接口,高层依赖于接口。

interface Mail{    public function send();}
class Email implements Mail(){    public function send()    {        //发送Email    }}
class SmsMail implements Mail(){    public function send()    {        //发送短信    }}
class Register{    private $_mailObj;    public function __construct(Mail $mailObj)    {        $this->_mailObj = $mailObj;    }    public function doRegister()    {        /*这里是如何注册*/        $this->_mailObj->send();//发送信息    }}

 下面开始发送信息

/* 此处省略若干行 */$reg = new Register();$emailObj = new Email();$smsObj = new SmsMail();$reg->doRegister($emailObj);//使用email发送$reg->doRegister($smsObj);//使用短信发送/* 你甚至可以发完邮件再发短信 */

上面的代码解决了'Register'对信息发送类的依赖,使用构造函数注入的方法,使得它只依赖于发送短信的接口,只要实现其接口中的'send'方法,不管你怎么发送都可以。上例就使用了"注入"这个思想,就像注射器一样将一个类的实例注入到另一个类的实例中去,需要用什么就注入什么。当然"依赖倒置原则"也始终贯彻在里面。"注入"不仅可以通过构造函数注入,也可以通过属性注入,上面你可以可以通过一个"setter"来动态为"mailObj"这个属性赋值。

 

上面看了很多,但是有心的读者可能会发现标题中"从此不再考虑加载顺序"这个字眼,你上面的不还是要考虑加载顺序吗? 不还是先得引入信息发送类,然后在引入注册类,然后再实例化吗? 如果类一多,不照样晕!

确实如此,现实中有许多这样的案例,一开始类就那么多,慢慢的功能越来越多,人员越来越多,编写了很多类,要使用这个类必须先引入那个类,而且一定要确保顺序正确。有这么个例子, "a 依赖于b, b 依赖于c, c 依赖于 d, d 依赖于e", 要获取'a'的实例,你必须依次引入 'e,d,c,b'然后依次进行实例化,老的员工知道这个坑,跳过去了。某天来了个新人,他想实例化'a' 可是一直报错,他都不造咋回事,此时只能看看看'a'的业务逻辑,然后知道要先获取'b'的实例,然后在看'b'的业务逻辑,然后... 一天过去了,他还是没有获取到'a'的实例,然后领导来了...

 

那这个事情到底是新人的技术低下,还是当时架构人员的水平低下了?

 

现在切入话题,来实现如何不考虑加载顺序,在实现前就要明白要是不考虑加载顺序就意味着让程序自动进行加载自动进行实例化。类要实例化,只要保证完整的传递给'__construct'函数所必须的参数就OK了,在类中如果要引用其他类,也必须在构造函数中注入,否则调用时仍然会发生错误。那么我们需要一个类,来保存类实例化所需要的参数,依赖的其他类或者对象以及各个类实例化后的引用

该类命名为盒子 'Container.class.php', 其内容如下:

/***    依赖注入类*/class Container{    /**    [email protected] array 存储各个类的定义  以类的名称为键    */    private $_definitions = array();    /**    [email protected] array 存储各个类实例化需要的参数 以类的名称为键    */    private $_params = array();    /**    [email protected] array 存储各个类实例化的引用    */    private $_reflections = array();    /**    * @var array 各个类依赖的类    */    private $_dependencies = array();    /**    * 设置依赖    * @param string $class 类、方法 名称    * @param mixed $defination 类、方法的定义    * @param array $params 类、方法初始化需要的参数    */    public function set($class, $defination = array(), $params = array())    {        $this->_params[$class] = $params;        $this->_definitions[$class] = $this->initDefinition($class, $defination);    }    /**    * 获取实例    * @param string $class 类、方法 名称    * @param array $params 实例化需要的参数    * @param array $properties 为实例配置的属性    * @return mixed    */    public function get($class, $params = array(), $properties = array())    {        if(!isset($this->_definitions[$class]))        {//如果重来没有声明过 则直接创建            return $this->bulid($class, $params, $properties);        }        $defination = $this->_definitions[$class];        if(is_callable($defination, true))        {//如果声明是函数            $params = $this->parseDependencies($this->mergeParams($class, $params));            $obj = call_user_func($defination, $this, $params, $properties);        }        elseif(is_array($defination))        {            $originalClass = $defination['class'];            unset($definition['class']);            //difinition中除了'class'元素外 其他的都当做实例的属性处理            $properties = array_merge((array)$definition, $properties);            //合并该类、函数声明时的参数            $params = $this->mergeParams($class, $params);            if($originalClass === $class)            {//如果声明中的class的名称和关键字的名称相同 则直接生成对象                $obj = $this->bulid($class, $params, $properties);            }            else            {//如果不同则有可能为别名 则从容器中获取                $obj = $this->get($originalClass, $params, $properties);            }        }        elseif(is_object($defination))        {//如果是个对象 直接返回            return $defination;        }        else        {            throw new Exception($class . ' 声明错误!');        }        return $obj;    }    /**    * 合并参数    * @param string $class 类、函数 名称    * @param array $params 参数    * @return array    */    protected function mergeParams($class, $params = array())    {        if(empty($this->_params[$class]))        {            return $params;        }        if(empty($params))        {            return $this->_params;        }        $result = $this->_params[$class];        foreach($params as $key => $value)         {            $result[$key] = $value;        }        return $result;    }    /**    * 初始化声明    * @param string $class 类、函数 名称    * @param array $defination 类、函数的定义    * @return mixed    */    protected function initDefinition($class, $defination)    {        if(empty($defination))        {            return array('class' => $class);        }        if(is_string($defination))        {            return array('class' => $defination);        }        if(is_callable($defination) || is_object($defination))        {            return $defination;        }        if(is_array($defination))        {            if(!isset($defination['class']))            {                $definition['class'] = $class;            }            return $defination;        }        throw new Exception($class. ' 声明错误');    }    /**    * 创建类实例、函数    * @param string $class 类、函数 名称    * @param array $params 初始化时的参数    * @param array $properties 属性    * @return mixed    */    protected function bulid($class, $params, $properties)    {        list($reflection, $dependencies) = $this->getDependencies($class);        foreach ((array)$params as $index => $param)         {//依赖不仅有对象的依赖 还有普通参数的依赖            $dependencies[$index] = $param;        }        $dependencies = $this->parseDependencies($dependencies, $reflection);        $obj = $reflection->newInstanceArgs($dependencies);        if(empty($properties))        {            return $obj;        }        foreach ((array)$properties as $name => $value)         {            $obj->$name = $value;        }        return $obj;    }    /**    * 获取依赖    * @param string $class 类、函数 名称    * @return array    */    protected function getDependencies($class)    {        if(isset($this->_reflections[$class]))        {//如果已经实例化过 直接从缓存中获取            return array($this->_reflections[$class], $this->_dependencies[$class]);        }        $dependencies = array();        $ref = new ReflectionClass($class);//获取对象的实例        $constructor = $ref->getConstructor();//获取对象的构造方法        if($constructor !== null)        {//如果构造方法有参数            foreach($constructor->getParameters() as $param)             {//获取构造方法的参数                if($param->isDefaultValueAvailable())                {//如果是默认 直接取默认值                    $dependencies[] = $param->getDefaultValue();                }                else                {//将构造函数中的参数实例化                    $temp = $param->getClass();                    $temp = ($temp === null ? null : $temp->getName());                    $temp = Instance::getInstance($temp);//这里使用Instance 类标示需要实例化 并且存储类的名字                    $dependencies[] = $temp;                }            }        }        $this->_reflections[$class] = $ref;        $this->_dependencies[$class] = $dependencies;        return array($ref, $dependencies);    }    /**    * 解析依赖    * @param array $dependencies 依赖数组    * @param array $reflection 实例    * @return array $dependencies    */    protected function parseDependencies($dependencies, $reflection = null)    {        foreach ((array)$dependencies as $index => $dependency)         {            if($dependency instanceof Instance)            {                if ($dependency->id !== null)                 {                    $dependencies[$index] = $this->get($dependency->id);                }                 elseif($reflection !== null)                 {                    $parameters = $reflection->getConstructor()->getParameters();                    $name = $parameters[$index]->getName();                    $class = $reflection->getName();                    throw new Exception('实例化类 ' . $class . ' 时缺少必要参数:' . $name);                }               }        }        return $dependencies;    }}

 

下面是'Instance'类的内容,该类主要用于记录类的名称,标示是否需要获取实例

class Instance{    /**     * @var 类唯一标示     */    public $id;    /**     * 构造函数     * @param string $id 类唯一ID     * @return void     */    public function __construct($id)    {        $this->id = $id;    }    /**     * 获取类的实例     * @param string $id 类唯一ID     * @return Object Instance     */    public static function getInstance($id)    {        return new self($id);    }}

然后我们在'Container.class.php'中还是实现了为类的实例动态添加属性的功能,若要动态添加属性,需使用魔术方法'__set'来实现,因此所有使用依赖加载的类需要实现该方法,那么我们先定义一个基础类 'Base.class.php',内容如下

class Base{    /**    * 魔术方法    * @param string $name    * @param string $value    * @return void    */    public function __set($name, $value)    {        $this->{$name} = $value;    }}

然后我们来实现'A,B,C'类,A类的实例 依赖于 B类的实例,B类的实例依赖于C类的实例

'A.class.php'

class A extends Base{    private $instanceB;    public function __construct(B $instanceB)    {        $this->instanceB = $instanceB;    }    public function test()    {        $this->instanceB->test();    }}

'B.class.php'

class B  extends Base{    private $instanceC;    public function __construct(C $instanceC)    {        $this->instanceC = $instanceC;    }    public function test()    {        return $this->instanceC->test();    }}

'C.class.php'

class C  extends Base{    public function test()    {        echo 'this is C!';    }}de

然后我们在'index.php'中获取'A'的实例,要实现自动加载,需要使用SPL类库的'spl_autoload_register'方法,代码如下

function autoload($className){    include_once $className . '.class.php';}spl_autoload_register('autoload', true, true);$container = new Container;$a = $container->get('A');$a->test();//输出 'this is C!'

上面的例子看起来是不是很爽,根本都不需要考虑'B','C' (当然,这里B,C 除了要使用相应类的实例外,没有其他参数,如果有其他参数,必须显要调用'$container->set(xx)'方法进行注册,为其制定实例化必要的参数)。有细心同学可能会思考,比如我在先获取了'A'的实例,我在另外一个地方也要获取'A'的实例,但是这个地方'A'的实例需要其中某个属性不一样,我怎么做到?

你可以看到'Container' 类的 'get' 方法有其他两个参数,'$params' 和 '$properties' , 这个'$properties' 即可实现刚刚的需求,这都依赖'__set'魔术方法,当然这里你不仅可以注册类,也可以注册方法或者对象,只是注册方法时要使用回调函数,例如

$container->set('foo', function($container, $params, $config){    print_r($params);    print_r($config);});$container->get('foo', array('name' => 'foo'), array('key' => 'test'));

还可以注册一个对象的实例,例如

class Test{    public function mytest()    {        echo 'this is a test';    }}$container->set('testObj', new Test());$test = $container->get('testObj');$test->mytest();

 

 以上自动加载,依赖控制的大体思想就是将类所要引用的实例通过构造函数注入到其内部,在获取类的实例的时候通过PHP内建的反射解析构造函数的参数对所需要的类进行加载,然后进行实例化,并进行缓存以便在下次获取时直接从内存取得

 

以上代码仅仅用于学习和实验,未经严格测试,请不要用于生产环境,以免产生未知bug

 

鄙人才疏学浅,有不足之处,欢迎补足!

4楼玻璃鱼儿
PHP还是适合过程化程序编写,语言特征决定的。
Re: 八面碰壁居士
@玻璃鱼儿,恩,面向过程看起来比较简单,有不少优势。不过当项目大时,就会越来越乱。通常将面向过程和对象结合起来比较好!
3楼freephp
好复杂,特别是后面实现自动加载的。能不能简化一下代码?
Re: 八面碰壁居士
@freephp,恩,好的,我将抽时间对上面每步进行拆解并进行解释
2楼batsing
后面的 spl_autoload_register 不是直接用 __autoload 更简化吗
1楼卓酷
我怎么觉得还是改动一下mail类更方便呢?后面又是接口又是依赖注入的,绕来绕去都懵了。,感觉对于PHP这样的纯粹动态脚本语言来加讲,不要把它当作静态语言来用,否则有很多的特性不但发挥不出来。
Re: 八面碰壁居士
@卓酷,当然这里只是阐述这个思想,如果仅仅改动mail类,在需求不断增加和改动的情况下会使该类十分臃肿,并且产生很多if语句,不利于维护。至于性能问题,我觉得在当前实际情况下瓶颈不在于php,而更多的在于网络和存储方面。性能论不是绝对的,要和开发维护成本进行比较,当前例子极大的降低了维护成本,而带来的性能问题微乎其微甚至在某些情况下还有提升,此处缓冲了类的实例,减少了大部分开销

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
微信文件过期恢复教程
微信文件过期恢复教程

本专题整合了微信文件过期恢复方法、技巧教程,阅读专题下面的文章了解更多详细内容。

0

2026.02.04

抖音网页版入口与视频观看指南 抖音官网视频在线访问
抖音网页版入口与视频观看指南 抖音官网视频在线访问

本专题汇总了抖音网页版的入口链接、官方登录页面以及视频观看入口,帮助用户快速访问抖音网页版,提供免登录访问方式和直接进入视频播放页面的方法,确保顺利浏览和观看抖音视频。

63

2026.02.04

学习通网页版入口与在线学习指南 学习通官网登录与使用方法
学习通网页版入口与在线学习指南 学习通官网登录与使用方法

本专题详细汇总了学习通网页版入口与登录方法,提供学习通官方网页端入口、学生登录平台、网页版使用指南等内容,帮助用户快速稳定地登录学习通官网,顺利进入学习平台,提升学习效率和体验。

9

2026.02.04

Python Web 框架 Django 深度开发
Python Web 框架 Django 深度开发

本专题系统讲解 Python Django 框架的核心功能与进阶开发技巧,包括 Django 项目结构、数据库模型与迁移、视图与模板渲染、表单与认证管理、RESTful API 开发、Django 中间件与缓存优化、部署与性能调优。通过实战案例,帮助学习者掌握 使用 Django 快速构建功能全面的 Web 应用与全栈开发能力。

9

2026.02.04

Java 流式处理与 Apache Kafka 实战
Java 流式处理与 Apache Kafka 实战

本专题专注讲解 Java 在流式数据处理与消息队列系统中的应用,系统讲解 Apache Kafka 的基础概念、生产者与消费者模型、Kafka Streams 与 KSQL 流式处理框架、实时数据分析与监控,结合实际业务场景,帮助开发者构建 高吞吐量、低延迟的实时数据流管道,实现高效的数据流转与处理。

3

2026.02.04

Golang 容器化与 Docker 实战
Golang 容器化与 Docker 实战

本专题深入讲解 Golang 应用的容器化与 Docker 部署,涵盖 Docker 基础概念、容器构建与镜像管理、Go 应用的 Dockerfile 编写、跨平台容器部署与优化、Docker Compose 和 Kubernetes 部署工具。通过实际案例,帮助学习者掌握 如何将 Golang 应用容器化并实现高效部署与管理,提升系统的可扩展性与运维效率。

3

2026.02.04

全国统一发票查询平台入口合集
全国统一发票查询平台入口合集

本专题整合了全国统一发票查询入口地址合集,阅读专题下面的文章了解更多详细入口。

59

2026.02.03

短剧入口地址汇总
短剧入口地址汇总

本专题整合了短剧app推荐平台,阅读专题下面的文章了解更多详细入口。

110

2026.02.03

植物大战僵尸版本入口地址汇总
植物大战僵尸版本入口地址汇总

本专题整合了植物大战僵尸版本入口地址汇总,前往文章中寻找想要的答案。

56

2026.02.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
前端系列快速入门课程
前端系列快速入门课程

共4课时 | 0.4万人学习

react hooks实战移动端企业级项目
react hooks实战移动端企业级项目

共59课时 | 6.5万人学习

PHP函数之array数组函数视频讲解
PHP函数之array数组函数视频讲解

共76课时 | 26.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号