PHP 中的服务定位和依赖注入

尝试了很多PHP框架,有轻量级的,也有企业级的,发现解耦都做得很不好,自由度都很差。Zend framework2.0 做得还不错,就是封装太多,不够轻量。

一、创建对象

从最原始的new操作符,到abstract factory或builder等经典创建模式,都不能满足人民群众日益高涨的解耦需求。后来,MF创造了控制反转(IoC)/依赖注入(DI)理论,提供了一个完美的解耦方案。听说一个叫"春"的JAVA框架实现了完美的IoC容器,不过我也没接触过,不太了解。

归根结底,DI的本质其实就是把创建对象的工作交给第三方容器,这个容器不但创建了所需要的对象,还把这个对象依赖的所有接口也一起创建并装配好了。而对象或接口之间的依关系可以通过配置文件很方便地做出调整。在代码实现上,一个对象对外部功能的全部需求都以接口的方式来体现;在对象内部,不需要实现主动创建或获取外部接口的代码,一切外部接口都由IoC容器来提供,对象内部代码只要直接使用即可。

另外还有种方法,叫服务定位(Service Locator)。他和DI的区别在于:DI的依赖接口完全由IoC容器装配提供,而服务定位模式需要由对象内部代码显式调用。换句话说,服务定位是由对象主动去获取自己所依赖的接口。在实现上,当然是DI解耦得更彻底,但是在使用上也更加复杂,而服务定位更加简单直观。

二、实现解耦

对于传统的PHP框架,我们很难把这个框架里某个需要的组件提取出来单独使用。因为这个组件可能会用到Logger对象、Config对象等等其他别的什么对象,而且这些外部依赖的代码是写死在源代码里的。有时候想单独使用框架内部的某个功能,不得不写大量的移植代码。要实现一个高度解耦的PHP框架,需要参考一下服务定位和依赖注入两种模式。在Zend framework2.0里,底层实现了DI,上层又按照SL封装了一个ServiceManager。

有人说Service Locator是一种反模式,因为在代码中使用Service Locator也算是一种隐含的外部依赖关系。其实这是矫情,难道在代码中使用IoC容器就不是外部依赖么?问题在于在恰当的场合使用恰当的模式。首先要搞清楚代码究竟是属于“调用者”还是属于“被调用者”。作为“被调用者”,比如某个模块,可以预见到代码会被使用在不同场合,当然是对外部的耦合越小越好,对于自己所需要的外部接口,完全可以依赖外部“调用者”来被动注入;但对于的“调用者”代码来说,作为最终的应用层代码,需要做到统筹全局,当然是不可避免地要直接与各个组件产生耦合了。

至于什么时候用依赖注入,什么时候用服务定位,我个人的看法着这样的:编写组件时,最好使用依赖注入模式,特别是当这个组件可能被用于不同的项目工程中时;编写应用层代码、或者项目平台相关性强的组件时,可以使用服务定位模式。另外,依赖注入的外部接口对于组件来说应该是强依赖的,组件缺少这些外部接口是无法独立运行的;服务定位取得的外部接口应该是弱依赖的,在缺少接口的情况下,组件也能勉强运行。举个例子:用户模型组件在完成用户注册的过程中,会用到两个外部接口,一个是数据访问层接口,用于将用户信息保存到数据库或别的永久储存介质里,另一个是邮件发送接口,用于向用户邮箱发送一封注册确认信。其中数据访问层接口对于用户模型组件是强依赖关系,后者缺了前者将无法正常运行;而邮件发送接口对于用户模型组件是弱依赖关系,没有这个接口也能完成用户注册过程,只不过会产生一些警告信息。

三、PHP中的实现

首先要考虑的是如何配置组件的依赖关系。一般高级语言的IoC容器都使用配置文件或其他DSL来实现配置,ZF2的Di组件使用原生数组来进行配置。IoC容器通过配置信息,并使用语言那只的反射功能,动态创建对象,不过这种方法并不适合PHP。PHP本身就是个低效的语言,其动态创建对象的性能更是非常差劲,和原生的new操作符相比,至少相差一个数量级,但是如果直接用PHP本地代码来创建依赖关系又显得不够灵活。我认为比较好的解决方案是在第一次读取配置文件的时候将配置文件中所表述的依赖关系直接编译成PHP本地代码。看上去挺麻烦的,其实很简单,就是生成一个PHP文件,里面写入一个保存着匿名函数的数组而已。

关于依赖注入的方式,基本上就这几种:构造函数注入、setter方法注入(setXXX成员方法)、属性赋值。考虑到PHP的特性和偷懒的因素,我个人建议采取属性赋值的方式,一方面可以减少代码量、另一方面性能也更好。为了便于自动注入,托管对象可以实现一个依赖关系接口,这个接口用于取得该对象依赖的外部接口,实现自动注入。