相信无论是初学PHP的新手, 还是多年经验的PHP老司机, 在学习Laravel这套框架的时候, 总是会被依赖注入和控制反转这两个概念绞的头晕. 但是这两个概念又是这个框架的核心思想, 而手册中对这两个概念的描述要么是蜻蜓点水, 隔靴搔痒的点到为止, 要么是类关系中引用一些暂时没法实践的代码, 让人在纯概念上反复折腾也不得其解. 所以有很多人反复阅读手册中的两个概念, 却总是无法抓住这两个概念的精髓.
这其实不奇怪, 首先, 这些工程化的概念本身不属于PHP或Javascript的, 而是来自于Java和.net这样的强类型和纯面向对象的语言, 实际上, 用PHP和Javascript这样的弱类型语言, 并不能在控制反转所要达到的效果进行严格的约束, 至少说方法的返回值类型上没法限制, 控制反转就不是那么真正彻底的控制反转(暂时看不懂这句话没关系, 看完这篇文章后面部分, 你就能懂我说的了).
其次, 无论是PHP程序员还是Javascript程序员, 在选择这门语言作为饭碗的时候, 很多人的考量是不想去烧脑Java中那些复杂的概念, 入门和选择这些语言, 就是因为他们理念简单, 复杂概念少, 能够能够快速做网站, 只是项目到达一定的代码量后, 在工程层面上任何编程语言的组织是殊途同归的, 因此, 最初选择PHP的逃避理由终究成为一道坎(这也就是有10年经验PHP程序员看不懂手册中核心观念的原因), 那么, 我们今天就来看看这道坎的第一步: 依赖注入, 控制反转, 容器, 这到底是怎么来的.
这两个概念其实所指的是一件事情的两个层面, 只是这种设计模式同时又是很多现代框架的核心, 不仅仅Laravel, PHP另一套框架Phalcon, python的Django, 以及前端的Angularjs和Angular(这是两个不同的框架)都在用, 这就好比观察者模式在rxjs中的位置一样, 当然, 有一种感觉就是, 不懂他们也能用框架, 但是怎么用怎么觉得不是很有底. 不过, 无论是前端后端, 既定的事实是他们已经是核心了, 那么我们就从头开始来研究它吧.
本文会从几个Laravel很基础的概念讲起, 契约式编程, 服务, 服务提供者, 以及看不见摸不着的IOC容器(也叫服务容器), 和依赖注入怎么使用, 控制反转到底控制什么反转什么, 等等...... 一步一步的讲述这几个核心的概念. 如果您明白了这些基础概念所描述的东西是怎么一回事, 那么基本上再理解核心实现就会简单多了.当然, 我并不会按照学究式的方式给一堆概念, 而是直观的形象描述.
废话不多说, 我们开始.............(本文的代码基于Laravel, 其实其他框架在理解上也没差......)
1.契约式编程:
以定义接口方式组织类别的一种编程方式.
就是说, 以前, 我要做一个数据库操作的模块, 怎么办?翻看手册, 把PDO操作相关的函数啪啦啪啦看一遍, 然后就写一个DB class, 把这些函数用一个类给它封装起来, 顺手封装一个单例工厂, 要用的时候, 实例化一个对象, DB对象->方法(), 搞定!
而契约式编程方法只是做了稍微拆分, 同样要做一个数据库操作的模块, 我不着急先写实现类, 先写一个接口(interface)或这抽象类, 把这个模块要实现的功能先定义出来, 然后再写一个数据库操作的实现类去继承抽象类或者实现接口. 其实就这么回事儿.
不同的是, 在要用到数据库操作模块的地方(比如构造方法中或调用的方法中), 直接用接口声明一个实现数据库操作的对象, 而不是在调用中实例化实现类的对象. 这里有些人会犯糊涂了, 接口不能实例化啊, 抽象类也不能实例化啊, 为什么我又能在方法中声明一个实现接口的对象, 不着急, 后面会详细解释, 现在先理解契约式编程这个思路就好了.
契约式编程说白了就是, 我要写一个类, 先写一个接口定义操作的规范, 然后再写一个类去实现这个接口, 这个过程就叫做契约式编程. 在Laravel中vendor/laravel/framework/src/Illuminate/Contracts目录下面规范了一大堆接口, 然后实际类都是实现了这些接口. 也就是说, 契约式编程就是通过定义接口的方式告诉你, 类不要乱写, 要写某个功能的实现类, 得按照接口的规范来写, 至于怎么使用实现类, 我们后面再说.
2.服务(service):
Larave官方没有定义, Angular官方倒是给出一堆定义, 但是看不懂^_^...........其实所谓服务, 就是我能提供什么, 能做什么, 简单说就是功能类啊! 这就是服务.
比如在laravel中要数据库操作, 我把数据库操作的逻辑都写在Model中, 感觉Model太胖了, 那可以单独写一个Repository的类, 这个类去定义数据库进行增删改查的逻辑方法, 然后通过方法把结果返回去. 而这个单独写出来的Repository功能类, 就是一个服务.
前面契约式编程里说到, 类不是不让随便定义吗? 不是要符合接口规范吗?其实这是针对在Laravel核心中, laravel团队开发的所有类都依据Contracts的规范来写, 而我们业务需求上的自定义类嘛, 不一定非得先写一个RepositoryInterface再写一个Repository类来实现, 这里涉及到一个过度设计的问题, 就是说我目前只是要一个操作数据库逻辑的类, 就只有一个作用也不存在什么统一规范的多个逻辑需求类, 那我就简单的直接先写个类就好. 当然, 也可以先写个接口规范然后再实现以各类, 这个看需求.
3.服务提供商(service provider)与依赖注入:
有了服务, 我们怎么提供服务呢? 这里就分为两种情况:
一个是我们只是写了一个功能类(服务), 这个功能类没有实现任何接口, 那么我要用的时候怎么办?非常简单, 比如我在PostController中要用到前面的Repository类, 那在控制器方法或构造方法中直接声明一个对象就好了, 比如 index(Repository $repository), 这样, 我们就能够用这个功能类所提供的public方法了.换一种说法就是: 我们能够利用Repository所提供的服务.
也可以这么理解: 这个功能类(服务)没有实现任何接口, 是这个功能领域中唯一的一个功能类(服务), 没有同类型(实现同一接口)的服务, 因此它自身的这个服务就不需要注册服务提供商了, 我们需要这个服务直接使用依赖注入的方式, 通过注入点(调用的index方法)注入到服务容器中就可以了.
在这里, 只是在方法的参数中声明一个类型的对象, 这个对象就可以在类中使用, 并不需要在需要使用对象的方法中进行实例化, 这个声明的对象会由服务容器自动实例化并提供方法使用, 这就是依赖注入. 这种依赖注入, 虽然服务使用者(也就是上面的控制器)已经不用自己实例化对象, 但是因为依赖的功能类有且只有一个, 需要的服务只是由控制器声明后就没有选择使用哪个对象的余地(只能是Repository类的对象), 但是也就是说控制器依赖于服务类, 或者说被服务类所控制.
生活中的一个案例是: 假如我(上文中的控制器)需要家政服务, 然而这个地方整个家政行业有且只有一家公司提供家政服务(功能类), 那么我要找家政(注入点声明)也就直接打个电话(依赖注入)给这个家政公司就能使用家政服务.(想想看, 只有一家家政公司, 没选择啊, 家政公司修改了业务, 不打扫卫生了, 因为只有一家家政公司嘛, 我依赖它, 那我自己不会打扫, 我只能修改需求把打扫卫生这个服务取消, 这就是我依赖于家政公司, 也可以说, 我要的这个服务被这个家政公司控制了)
4.服务提供商(service provider)和控制反转:
另外一种情况是: 假如我们声明了一个RepositoryInterface接口, 然后写了两个接口实现类:PostRepository类和UserRepository类, 在控制器中, 我们并没有直接用类声明, 而是用接口声明对象: index(RepositoryInterface $repository), 可是这个接口声明的$repository依赖的接口会有不同的类来实现啊(也就是说, $repository可以是多种服务, 到底要用哪个服务?), 这时候, 我们需要通过某种方式来告诉Laravel我们需要实现RepositoryInterface的对象的具体实现类是哪个, 怎么告诉它我们要用哪个类来实现呢?
这就要通过注册服务提供商的方法, 我们可以通过 php artisan make:provider RepositoryServiceProvider的方式来生成一个服务提供商, 并且在app.config中配置这个服务提供商(也就是告诉laravel, 在一个生命周期中, 存在RepositoryServiceProvider这个服务提供商, 在我这里注册了实现RepositoryInterface接口的具体服务是哪一个), 在这个服务提供商register()方法中绑定RepositoryInterface的具体实现类为PostRepository类或是UserRepository类, 这样依赖于这个功能的控制器中就能明白我们要需要的服务是哪个了.
还是上面的生活案例: 现在这个地方的家政公司有很多, 提供的都是同一类的家政服务, 自然这就会有一家中介公司, 那么现在我需要家政服务的时候就学聪明了, 我在需要服务的地方(依赖注入点)声明一个要求规范(接口声明对象RepositoryInterface $repository), 然后我要服务也不去找家政公司了, 我直接找中介公司(服务提供商, RepositoryServiceProvider), 告诉他我要的服务规范(接口), 那么中介公司就去查看自己的花名册(register()方法)中注册了符合这个规范的家政公司(服务, 即功能类), 然后我再找到已经注册的家政公司(再register()方法中绑定的实现功能类)来为我提供服务. 这样, 我需要家政服务的时候, 不再依赖于具体哪家家政公司, 而是服务提供商,同时我已经声明了规范, 要换一家家政公司的时候, 我自己不用做任何事, 中介的花名册修改成另一个符合我声明规范的家政公司, 我就能找到那个家政公司并让它为我服务. 也就是, 原先家政公司只有一家, 我被家政公司绑架了, 是家政公司控制了我, 现在家政公司多了, 我就可以声明标准来选择家政公司, 控制权在于这个标准(接口)里了, 这个就是控制反转!!!
High-level modules should not depend on low-level modules. Both should depend on abstractions.
高级模块不要依赖于低级模块, 应都依赖于抽象.
5.服务容器:
现在我们大体上知道了依赖注入和控制反转, 那么这里有一个核心的问题是, 依赖注入为什么不用实例化就可以使用对象? 接口不是不能实例化对象吗? 控制反转为什么我们用接口声明一个对象就可以直接使用这个对象? 这个服务容器在哪儿? 这就需要了解一下Laravel的服务容器了. 首先说明的是, 本文不会对服务容器怎么实现的具体细节进行描述, 而是在整体层面上来解释.
整体层面弄清楚了, 具体的实现可以参考一下的链接:
https://www.cnblogs.com/lishanlei/p/7627367.html
在这里, 我主要是从框架整体的角度来探讨服务容器, 这个在laravel4.0的时候称之为IOC Container, 在laravel5.0以后称之为Service Container, 不管怎么称呼, 其实就是一个能实现依赖注入和控制反转的对象! 那么这个对象到底在哪儿?这是一个很让人迷惑的问题!
要弄明白这个问题, 我们就要从MVC架构的原理上来说:
按照一个简单的MVC框架的简单原理来说, 一般会有一个index.php的文件作为整个框架的入口, 然后在这个入口文件中载入路由判断的逻辑, 根据浏览器中传入的路由参数, 调用相关的控制器并实例化后调控制器方法, 控制器方法中引用模型的数据, 并将模型数据按逻辑处理后引入模版将变量传给视图后显示. 这是一个最简单的原理流程, 在框架应用的过程中, 入口文件一般我们不会去改动, 也就是控制器的实例化我们基本不管, 我们只做控制器中间的逻辑, 但如果只是这样的过程, 那么我们在控制器逻辑中要用到模型数据, 我们必须去new一个对象来, 需要其他依赖类, 我们也需要去new一个来, 我们除了控制器由入口文件帮我们new好之外, 其他的相关对象都需要自己去new, 这可能看起来没有什么, 不过就是需要对象的时候多一行new而已, 没多少工作量啊^_^.......然而, 假如我们控制器依赖的一个类修改了构造函数, 那么我们在控制器方法内new的那一行也需要修改相应的构造函数, 这也还好........再复杂一点哪?多个控制器同时依赖?那就是每一个依赖的地方都需要修改, 依赖的依赖还需要接连着修改, 这样修改一个依赖的功能类, 导致要连带着修改控制器,而控制器本身又承担着视图和模型的逻辑, 这样改起来就很痛苦了...........
既然在入口文件能帮我们实例化一个控制器, 那么有没有这样一种可能 , 我们在index.php实例化控制器的地方加一些自动化实例化对象的代码, 写一个类也行, 做一个函数也行, 总之就是帮我们把控制器相关依赖的类集中实例化, 这样我们在控制器中碰到依赖的时候就不用一个个调用构造方法实例化了, 只要先写好声明就好, 声明后写正常的使用逻辑代码, 然后最后程序运行的时候, 实例化控制器的时候同时也自动的实例化控制器依赖的其他类. 这就是最初的依赖注入方式的需求, 那么, 要实现这样的过程, 就得在index.php中写这个集中实例化并自动识别类依赖的功能代码, 在程序员这个最高智商的群体的摸索之下, 终于通过反射机制写出了实现这个目标的代码, 而实现这个功能的代码写成一个类, 给他一个命名叫做: 容器(Container).由于这个容器可以自动识别相关的依赖, 并实例化所需要的类, 有了这个神器, 我们之前在index.php中写控制器实例化的代码也不需要了, 把控制器实例化的事情也一并交给容器就好了.
而这个容器类, 再laravel中就是Illuminate\Foundation\Application这个类, 因此,在Laravel的index中, 一开始就啥都不管, 按PSR标准把所有的依赖整理好autoload之后, 直接先实例化一个容器出来赋给$app, 然后给这个容器初始绑定内核, 内核再依赖其他的类, 一层层依赖下去, 实例化内核的时候就会把这一层层依赖的类, 就通过容器的自动实例化机制把内核依赖的类一个个实例化出来, 再递归解决依赖的依赖.(由于laravel比我们前面提的简单框架考虑的更多, 简单框架里的控制器实例化这个动作, 也就安排到内核加载之后的一系列实例化步骤的某一个环节中去了).
所以, 什么是容器? 在Laravel中, 第一个启动实例化的动作, 只有Illuminate\Foundation\Application这个类, 这个类就是容器类, 实例化出来的$app就是一个容器, 这个$app绑定kernel之后, 所有的控制器和模型方法以及相关依赖的类, 都会由$app实例化的过程连锁实例化出来, 也可以这么认为, 整个Laravel生命周期开始到结束依赖的所有类, 都是从$app的实例化之后连锁自动实例化, 也就是许多文章常提到的说法, 这些类都在$app这个容器中.
现在, 我们容易理解一种说法了, 比如, 我自己写了一个类A, 现在我在控制器中依赖注入A的对象$a, 控制器方法中的代码可以写为:public function index(A $a), 既然我们的控制器也是在这个容器实例化之后的连锁实例化, 也就可以形象的说成是控制器在容器中, 这时候我们就可以这么描述这件事了, $a通过依赖注入注入到容器中(理解了这句话, 就理解了容器了). 当然, $app虽然是在最开始的时候实例化, 那我们在容器里面的代码也可以引用这个$app(也就是容器的实例化引用)来在中途动态的绑定一些自定义想要绑定的服务, 比如在服务提供者中调用$app->bind()绑定相应的接口和实现类, 实际上在容器内的任何一个位置都可以调用, 比如在控制器中, 模型中, 依赖类中, 只是最常用的还是在服务提供商中调用而已.
现在明白了什么是容器到底是个什么东西, 容器到底在哪儿了吧?^_^.
其实我前面写的内容, 已经把这写核心概念讲的比较明白了, 如果前面的概念您没看明白, 那么实际跟着我把下面的代码敲一遍, 再回头去看看前面的叙述, 相信您就会对Laravel的依赖注入和控制反转有一个本质的掌握, 至于这两个概念具体实现的源代码细节, 我个人认为看具体能力, 因为看懂这些细节的全部实现逻辑, 确实是一件比较烧脑的事情, 同时也不是有那么强烈的必要.