关于实体间依赖的考虑

    IoC(Inversion of Control,控制反转)和DI(Dependency Injection,依赖注入)
是框架开发中非常常用的依赖处理技术,很多人把这两种技术混淆在一起了,事实上两者
并不是一回事。IoC的名称其实非常形象,它反转了控制权,把控制权交给框架,开发人
员只要编写“插件”,把它插入框架的适当位置即可让程序工作,而传统的程序则是开发
人员编写控制代码来调用类库和工具箱。IoC在目前的很多框架中都采用到,大部分是用
在Action上,我的框架也维持这个思路。很多书用“好莱坞法则”来形容这种控制方式,
不过用这个例子用多了,我个人就不喜欢了,呵呵。

    本质上,IoC是一种面向对象设计原则的体现:依赖倒置原则(DIP,Dependency
Inversion Principle,详见Robert C.Martin《敏捷软件开发 - 原则、模式与实践》)。
依赖倒置原则的精髓在于:接口是属于客户的,而实现不是。这样,上层对象需要下层对
象提供服务时,它就必须抽象出一个它需要的接口,并依赖于该接口。该接口属于上层,
而不属于实现该接口的下层。在开发过程中,这种思想跟测试驱动开发思想非常吻合,实
际上也经常一起使用。在抽象出接口后,我一般会写个最简陋的实现类直接return出客户
需要的结果来测试接口的正确性,呵呵。

    证明接口正确后,自然是重构的过程。把直接的return逐步去掉,把细节实现出来,
一个单元就完成了。当然这其中会涉及大量的重构细节,我就不赘述了,重构的细节可以
参考Martin Fowler的《重构》和Joshua的《重构与模式》,都是比较琐碎的细节。另外
还有PHPUnit,可以从http://www.phpunit.de下载,网站上带有文档。

    客户是依赖于接口的。但对一些有多个派生类的接口(如GoF里的策略模式),如何
在运行期间决定使用哪个具体实现?这个决策由谁做出?这就是DI出手的地方了。

    所谓DI,似乎是从Java领域过来的。DI让你修改配置后,就可以在运行时自动把依赖
的实体注入到依赖该具体实体的客户里面去,这样客户就无需主动去要求获取它所依赖的
具体实体。一般有三种依赖注入方式:接口注入,构造函数注入,和设值方法注入。接口
注入实际上是让客户去实现某个接口,容器在运行时根据该接口给实现了这个接口的实体
塞具体对象(听起来很像观察者模式?呵呵);构造函数注入是在实例化客户时,把具体
实体当成该构造函数的参数来实现具体对象的注入;设值方法注入就是客户声明一个
setter函数,让容器通过这个setter函数给它塞具体对象。这些过程都是由某个容器来处
理的,由于我写的不是Java框架,所有对这些细节就不深究了,只是借鉴一下它的思想。
实际上在PHP中也可以实现依赖注入,目前我的粗略想法是在初始解释时读取配置文件,
然后把需要处理依赖关系的对象装进某个注册表(Registry模式,参见Martin Fowler的
PoEAA《企业应用架构模式》一书),之后的运行由该注册表控制。关于这个,我的想法
还不成熟,所以就此打住。

    实际应用中,实际上常用的还有另一种技术——服务定位器(Service Locator,参
见《Core J2EE Patterns》,J2EE核心模式)。对于这个东西,我一直把它看成工厂。
实际上跟工厂没多少差异,传统的解决方式就是使用工厂,换句话说就是让客户除了依赖
于那个接口外,还要依赖于该工厂,向该工厂请求具体实体。这样,就把依赖决策推给了
工厂。在框架开发中,工厂应该处于一个什么样的位置?从工厂的代码来看,它必须获知
一个具体类名。这个具体类名的获知,又要进一步依赖于某个配置。因此只要工厂获得配
置,它就能实例化出具体对象,如通过反射。否则,就只能通过一串的if/else语句来判
断具体是哪个类了。

    实际上,与其无端地加入一个工厂来解除依赖,何不直接依赖接口或抽象类本身?当
然在这里接口里面是不能有代码的,所以我换成抽象类,实际上用抽象类更符合设计,因
为接口实际上定义了一个角色,而实现这个角色的人是谁,它不管,所有的方法定义的契
约是否100%是接口的,它也不管。而抽象类的意义不一样,它的子类就应该100%是为抽象
类的契约服务的。因此,我使用抽象类,并在抽象类里面声明一个静态工厂方法,由该工
厂方法来解除依赖。

    到这里你可能会说,“把依赖放进抽象类?那抽象类岂不是直接跟实现类耦合在一起
了?”实际上我并不是把依赖推给抽象类,而是由它再外推。推给谁?推给配置文件。看
下面的例子:

class Dispatcher {
    public function dispatch() {
        $router = Router::factory();
        $request = $router->route();
        ...
    }
}
abstract class Router {
    final public static function factory() {
        $className = Core::get('router.className');
        return new $className();
    }
}

    呵呵,就这么简单。在这里,Dispatcher是客户,它需要一个Router对象来完成某项
任务。Router是抽象类,它下面有两个具体的子类RewriteRouter和TraditionalRouter,
分别代表URL重写方式的路由和传统路由。关于什么是路由,这里暂时不解释。从Router
类的factory方法里可以看出,它从Core类获取router实体的类名称,然后new出一个该类
的对象。Core类里有一个数组,保存了各个实体的依赖关系,这个数组是在初始化时从配
置文件里导入的。让Router类依赖Core类,符合框架的一致性。实际上,在我的框架里,
凡是涉及一个抽象类多个实现的选取决策,我全部使用这种方法处理,用一个简单的词来
形容,是“依赖外推”。其实想想这种思路简单得很,不过我愣是写了这么多东东。。

    以上,只是从多种具体策略中选取一个具体策略时的依赖解除方法。其它情况下不一
定要遵循这种方式,尤其是所依赖的类是稳定时。稳定的类是可以直接依赖的。初涉面向
对象时经常会被众多的依赖、关联、泛化、实现、聚合等关系搞得一团糟,实际上根本没
那么复杂。面向对象是一种越学越简单的技术,只是从复杂到简单,有一个或长或短的过
程。

    文章最后,再扯一下配置文件。我是打算直接采用PHP内置的数组(实际上是一种散
列表)来保存配置文件的。当然,我知道有些人使用ini文件,有些人使用xml文件,有些
人使用yaml文件。不过,我选择php文件,个人喜好,目前不打算支持其它格式的文件,
以后可能会添加。配置文件数量,只有一个。我不喜欢维护一大堆配置文件,主要是从性
能考虑,加载多个配置文件意味着多次open系统调用。当然如果说要缓存配置文件,那就
是另一个话题了。
 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值