Yii 2.0 框架学习笔记 - Component 的变化

Yii 2.x 最底层的抽象是 Object,但是,窃以为,最重要的基础抽象,仍然是 Component。这点与 Yii 1.x 相比,并没有本质的变化,就是,我仍然没有参透,为什么要多出来一个 Object 的抽象,表面上看,这个 Object 比 Component 少了不少东西,如果没有必要的话,显然继承 Object 会更加合算一点,但是这么一点好处的话,简直微不足道。

Yii 1.x 的 CComponent 的回顾

我们先来回顾一下 Yii 1.x 的 CComponent 到底提供了一些什么样的核心抽象呢?

  1. 属性的抽象,支持 getter 和 setter
  2. 事件,以及配套的事件处理器
  3. Behaviour

所有 Yii 框架的对象几乎都会继承 CComponent,也即在 Yii 框架里,几乎所有的对象,潜在都会支持这几个特性。属性,我们在 Object 的文章里,已经谈及了,这个抽象,可以说是面向对象程序设计的基础,其实是对成员变量的一层封装,在成员变量基础上,提供了更灵活的可见性控制。不过,Yii 1.x 的属性实现,是有点奇怪的,这个怪异性我本来是无感知的,看完了 Yii 2.x 的实现后,我才明白它怪在哪里。1.x 的属性,在判定 hasProperty 的时候,只承认有 getter 或者 setter 方法的属性是属性,如果用公有成员变量的话,不被承认为属性。其实,我们从效果上看,公有成员变量,其实看起来就比较像是一个读写都开放的属性。使用 __set 和 __get 的魔术方法,无非是想让一个属性,用起来更像是一个公有成员变量那样,哪怕它并不存在实体,但是,一个公有成员变量真的存在时候,哪怕它用起来像是一个属性,但是竟然不被承认是属性,这确实有点奇怪。

Yii 1.x 的事件机制,我认为实现得并不好,其主要原理是,使用 CComponent 类的一个私有成员变量 _e 来记录事件处理器,所以,就预示着一个特点,就是事件的绑定,必须在对象创建之后。这就带来了极大的局限性,什么局限性呢,限制了事件绑定的位置。例如,我想要捕获一个事件,必须在产生这个事件的对象创建之后,这个事件发生之前去绑定我的处理器。按照我们一般的经验,这个,最好就是在对象创建刚刚完毕的时候,其实也就只能写在 __construct 函数的最后一行。如弱你要不想把事件绑定给写到对象类里面,那就更难用了。这么实现,一个缺点,是会把事件的绑定,散布到各个类的实现里面,另一个,就是当目标对象的类,不能修改的时候,会很痛苦,比如,是某个类库或者别的团队维护的类,那就很难看了。只能继承它,然后覆盖构造函数,但是如果对方的模块没有提供替换目标类的方法,你基本就束手无策了,所以,这种设计,对整个应用全局的代码设计,都提出了非常高的要求,极大的降低了易用性。

此外,还有一个比较难看的点是,事件的附着句柄,是一个以 on 前缀的方法名,也就是你想出发一个叫 afterBuy 的事件的时候,你必须在你订单类里面,定义一个方法,叫 onAfterBuy,而这个方法可能除了触发这个事件外,别无其他业务了,所以,如果事件比较多,就会在你的类实现里面,出现很多的只有一行的方法。至少从视觉上是比较难看的。

第三个东西,就是 Behaviour,这个我特意没有翻译成中文,因为字面意思是“行为”,但是这两个字,是无从表达这个东西的实质的。在 Yii 框架的文档里,会提到这个东西的别名,是 Mixin,字面意思是“混入”,但是,这个也比较难以说明问题。从现象上理解,就是一个类可以调用本不属于自己的成员方法,通过 __call 这样的 Magic Method 来实现的,利用了 PHP 语言动态的特性做到的。但是,这个Behaviour 也好,Mixin 也好,到底带来了什么好处,这也是我在整个应用框架过程中,所不能体会的一个点。这个 Behaviour 的实现,如果我没猜错,我觉得,也是有一定的问题的,实现得并不那么方便,因为,每个具体的对象,通过一个 behaviours() 方法来注册 Behaviour,但是,并不是每个品种的对象,都支持这个特性的。事实上,我搜了一下,在 Yii 1.x 里面,只有 7 个类,缺省激发了 Behaviour 机制。

它们是:CModule,CApplication,CController,CConsoleCommand,CActiveRecord,CFormModel,CApplicationComponent。这个机制,一般,都在 __construct 方法里面被激活,但是,也有例外的,比如 CApplicationComponent 在 init 方法里被激活,确实比较奇怪。除了这 7 个类,以及它们的子类,Yii 1.x 框架里,别的对象,虽然也预置了这个机制,却不是默认被激活的。

我简单查了一下维基百科,这个 Mixin 的思想,本质上,还是提供了一种抽象的颗粒度,比类更加细小,复用程度更高。另一方面,Mixin 的机制,可以部分弥补没有多重继承的烦恼,而用不着引入多重继承的复杂性。其实,PHP 在语言层面提供了支持,就是一个叫 traits 的特性,比框架带的这个 Mixin 强很多,但是,这个在 PHP 很高的版本才出现的,在语言版本没有更广泛的扩散前,还是框架自己实现比较好。

这个东西实现的缺点,和事件机制,有点异曲同工。都是绑定点必须在目标类实现里面去写,才比较合适,一旦不能改目标类实现,就会浑身都蛋疼了。在整个 Yii 1.x 使用经验中,我用到这个特性的机会少之又少,第一,是没太看懂这个东西的本质是什么,另一个是,这个东西也确实用起来没那么方便,假使回到从前我能有现在对这个特性的领悟,说不定我能够用起来吧,无聊的感慨啊~

Yii 2.x 的 Component 实现

在了解历史的基础上,在回到现实,可能就多了很多的便利。我们现在来看看 Yii 2.x 提供的核心抽象有哪些改进。

从整个 Component 提供的公有成员函数列表来看,到了 2.x Yii 框架的核心理念,并没有什么巨大的变化。仍然是三个主流的东西,属性、事件、Behaviour。

第一个不同,是 Component 不再是最底层的抽象了,底下还有 Object,所以 Component 是继承自 Object 的。但是,我们仔细看 Component 的方法实现,就会发现,Component 几乎没有从Object 继承任何东西回来。当然,还是有一丁点东西的,就是对象生命周期的概念,是继承过来了,因为 Component 里面没有构造函数和 init。除此以外,Object 提供的其他特性,Component 都涵盖了,而且完全覆盖了父类的方法,真是一种奇怪的思路(与其这样,再写一遍构造不是好了么,这么玩,仅仅为了 DRY 么?),但是我参悟也不是很深,也不便过多去评价这个。

属性,作为组件的第一个抽象特性,有了一些变化。第一个是上文提到的,公有成员是否被认定为属性的问题。在 2.x 里面得到了统一,公有成员变量,是被认定为属性的。第二个是,上文提到了一个 Mixin 的东西,在 2.x 里面,Mixin 这个特性被进一步向语言提供的 traits 的能力对其了,不光是方法可以被“混入”,现在属性也可以被“混入”了,也就是你可以通过一个 Behaviour,来向一个对象注入属性了。这已经比较逼近 traits 的能力了。而且,“混入”的属性,在进行 hasProperty 判定的时候,也会被判定为是 true。

事件,有了比较大的变化。第一个,是取消了使用方法来做事件句柄的做法,这样,你不必为了出发一个事件,来定义一个 on 前缀的方法,可以在随便一个字符串作为事件的名字去绑定事件,另一方面想要触发事件时候,可以用 trigger 方法随意触发任意事件了。整体来说,更加灵活了。但是,事件的处理器,仍然是用对象的私有成员变量 _events 来存储的,这就使得那个事件绑定点的问题,仍然存在。不过,还是部分被解决了,且看下文。

Behaviour,如果说,Component 里面变化最大的东西,莫过于是 Behaviour 了。从 PHP 5.4.0 开始,官方语言层面加入了对 Traits 的支撑。可见,这个东西在面向对象里面,渐渐成为了一种趋势了,大家虽然憎恨多重继承的复杂性,但是对与比继承更细粒度的代码复用,还是有所渴求的,所以这个轻量级的 Traits,被直接写入了语言。而 Yii 2.x,我觉得也是在深化这种复用形式的功能。一个是引入了除方法外,属性的“混入”,第二,将 Behaviour 的绑定,全面深化到 Component 层面,也即,这个特性,缺省在所有 Yii 2.x 的对象里都打开了,不像 1.x 那样,只有 7 个类是打开的。它被更加深入的当成一种代码复用手段,被全面地鼓励使用了。

通过 Behaviour,解决了我说的那个问题,必须把事件绑定给写到目标对象类的实现里面,这次不用了,你可以写到一个 Behaviour 里面,然后通过配置文件的形式去“混入”到目标对象里面,就可以达到目的,但是,仍然没有解决那个必须找到对象构造点的问题,却用另一些新的东西给解决了,比如DI,比如Service Locator等,且看其他文章的介绍了。