从面对对象的观点看 Drupal 的设计

原文链接 Drupal programming from an object-oriented perspective


目前无论在 Drupal 的源代码还是 API 接口中我们都看不到使用 OOP 的直接证据(例如: 在一些明显应该使用 class 的地方没有看到 class 的定义), 这为 Drupal 招来了很多的批评, 被认为是 Drupal 一个不合潮流的缺点. 的确 Drupal 没有直接使用 PHP 的 OOP 特性, 但是在 Drupal 的设计中还是应用了很多 OO 的设计原则. 本文从面对对象的视角来描述 Drupal 的系统架构, 以便 OOP 程序员能够从 OO 设计原则的角度上对 Drupal 做出正确的评价, 并希望通过本文使读者对 Drupa 有更深入的了解l. 今后随着 PHP 和 Drupal 的逐渐成熟, Drupal 会越来越多的引入 PHP 的 OOP 特性.


当前设计的动机和原因


自从 Drupal 发布 4.6 版本以后, Drupal 就决定不再使用 PHP 的 class , 做出这个决定是源自如下几个理由:

  首先, 最初设计 Drupal 的时候大家都还在使用 PHP 4 , 那时 PHP 4 对 OO 的支持还很不成熟,  直到 PHP 5 发布这种情况才得到明显的改善.

  其次, PHP 在加载大量代码时性能会大大降低, 这在没有 PHP accelerator(Zend) 时表现的尤其明显: 在测试过程中 PHP 编译 Drupal 页面源代码的时间占到整个处理过程的一半以上. 为了解决这个关键性问题 Drupal  使用了"延时加载", 使每一次页面请求都只加载真正需要的那部分代码. 这种方法在 Drupal "模块-函数-HOOK" 模式下工作的很好, 但是 PHP 不支持在 class 中的应用. 这就导致要么忍受"慢(加载大量定义了 class 的 php 代码)", 要么忍受"乱(在 index.php 中编写大量的逻辑代码以减少代码加载量)", 显然这都是不能接受的.

  最后, 有些 OOP 设计原则是在 Drupal 准备要使用的时候发现 PHP 不支持或者是当时没有被支持. 例如, Drupal 在 theme system 中想使用一种类似于 Objective-C's "categories", (Ruby - 'open classes', Javascript - 'modifying object prototypes', C# - 'extension methods'). 这个想法的核心就是 class 能够被多次定义, 你可以在不同的地方定义同一个类. 这样就允许在不修改原始定义的情况下扩展一个类的功能, 非常符合 OOD 中"开-闭"原则即"对修改关闭对扩展开放".


在即将到来的 Drupal 7 中会有一些改变: 首先 Drupal 7 会强制要求使用 PHP 5 这会给引入 OOP 带来很多的便利, 还有就是在一些模块中开始使用 class. 但 Drupal 的核心结构依然不会使用 class.

Drupal 中的 OOP


 

尽管在 Drupal 中没有明确的使用 class, 但是在 Drupal 的设计中还是使用了一些 OO 的特征. 我们可以依照 OO 的规范在 Drupal 的设计中找到 在一个 OO 设计系统中必不可少的特征.

Objects - 对象

Drupal 中有很多元素是符合 OOP 规范中关于"对象"的描述的. 其中能够比较明显的被看成是"对象"的是: 模块(modules), 主题(themes), 节点(nodes)和用户(uses) 这几个组件.

节点在 Drupal 站点中是最基础的信息构件, 在一个典型的信息发布系统中它代表了所有类型的信息数据. 通常系统会通过函数 node_invoke() 调用定义在 node.module 中功能方法. "用户"对象则包含了站点帐号数据, 用户的自定义数据和 Session 信息. 这些对象的数据结构都使用数据表代替 class 定义在数据库中. 由于 Drupal  使用的是关系数据库, 这就允许其他模块采用附加数据的方式扩展这些对象.

模块和主题也象是一个对象, 在很多方面它们都充当着"控制者"的角色. 模块不仅仅代表一个源文件, 它还是一个相关函数的集合并且在 Drupal 定义的 Hook 系统中重要的组成部分.

Abstraction - 抽象

Drupal 的钩子系统(或者叫回调系统)可以看作一种接口实现. "钩子"定义了能在模块上或被模块执行的操作, 如果模块实现了一个钩子, 就相当于缔结了一个执行特定任务或返回特定信息的契约. 调用方并不需要知道模块和钩子实现细节就可以调用钩子.

Encapsulation - 封装

Drupal 在封装特性的实现上没有采用严格的"访问控制", 它采用的是"命名约定". Drupal 的代码是基于函数的, 所有的函数都在一个命名空间下, 系统可以通过函数名字中的前缀划分出子空间. 通过简单的"命名约定"模块可以定义没有冲突的私有函数和变量. 

命名约定同样可以从一个模块内的函数中定义出公共接口. 私有函数的名字前面有一个"_"做前缀, 外部模块是不应该访问另一个模块中这样命名的函数的. 例如: _user_categories() 是一个私有函数, 从外面直接访问它是不合约定并且很不安全的, 它的任何变化是没有预先通告的. user_save() 是 user 模块的公共接口(API), 调用这个函数就可以很安全的把用户对象保存到数据库中.

Polymorphism - 多态

通常情况下"节点"是有多种形态的(常见的有 Page 和 Story), 如果一个模块要在页面上显示一个"节点", 首先要调用这个"节点"上的 node_build(), 然后调用 drupal_render() 得到包含"节点"内容的 HTML代码. 实际的渲染过程(得到 HTML 代码的过程)依赖于传入"节点"的类型, 这一点很像一个类型对象根据接受到的消息来决定自己的行为. Drupal 内部实现了通常在 OOP 语言运行库中完成的任务.

此外, 在上面的例子中"节点"的渲染还会受到当前主题的影响. "主题"的外观效果是多种多样的但是接口是不便的, "主题"通过处理"渲染节点"消息给"节点"附加上特定的外观效果, 而具体的视觉效果取决于当前主题的实现.

Inheritance - 继承

模块和主题可以被看成从"抽象类"中继承的类型. 就"主题类"来说"抽象类"的方法定义在 theme.inc 中, 默认实现是由系统中激活的模块提供的, 自定义的主题可以重写任意一个界面组件的显示. "模块"也是一样, "模块"的 HOOK API 在实现时都是可选.


Drupal 中的设计模式


Drupal 的内部结构大多很复杂, 使用继承和多态这样的简单设计已经不能够很好的处理它们了. 那些令人兴奋的系统特性都是应用了成熟的设计模式的结果. 很多在<设计模式>("Gang of Four"的书)中详细描述的设计模式都在 Drupal 中得到了应用, 例如:

Singleton - 单例

如果我们把"模块"和"主题"都看成对象的话, 那么这些对象都应用了单例模式. 一般情况下这些"对象"都不包含数据, 区分它们的方法是看它们包含的函数, 这就可以把它们看成只有一个实例的类.

Decorator - 装饰

装饰模式在 Drupal 中应用非常广泛. 在我们前面讨论中我们提到过 node 对象具有多种形式, 但这只是 node 系统的一小部分功能. 更加引人注意的是任何模块都可以通过实现 node hook (例如: hook_node_load(), hook_node_view()) 来扩展所有 node 对象的功能.

利用这个特性可以在不进行"子类化"的情况下为 node 扩展出多种功能. 例如, Drupal 内置的新闻报道节点(story node)只包含标题, 作者, 内容摘要,正文等很少几种属性. 现在需要为它添加一个常用的功能-附加文件, 一种方式是设计一个新的节点类型,新类型将包含新闻报道的全部功能并增加附加文件功能. Drupal 的上传模块则使用一种更加模块化, 更简洁也更强大的方式实现了这一需求, 它通过使用 node API 实现了可以为任何一种节点类型添加附加文件功能.

Drupal 上传模块的实现很类似于使用 Decorator 模式包裹每个节点对象. 在支持 categories 特性的 Objective-C 语言中可以很简单的通过扩展 node 基类的功能, 来为所有的 node 对象添加功能. Drupal 通过 hook 系统, 调用 node_invoke() 函数实现了简化的效果.

Observer - 观察者

上面关于 node 对象功能扩展的演示也很类似于在 OO 系统中使用观察者模式. 观察者模式可以说在 Drupal 中无处不在, hook 系统从本质上讲就是允许模块注册成为 Drupal 对象的观察者. 例如: 当在 Drupal 的分类系统中修改词汇表时, hook 系统就会去调用所有实现了hook_taxonomy_vocabulary_update() 的模块. 通过实现分类系统的 hook 模块注册成为词汇表的观察者, 有关词汇表的任何修改都会在恰当的时候通知观察者. 

Bridge - 桥接

在 Drupal 的数据库抽象层的实现方式上多少有些桥接模式印记. 编写模块时要使用数据库抽象层访问数据, 避免使用特定数据库的特有功能.  这样变更数据库时就是不需要修改现有模块的代码.

Chain of Responsibility - 责任链

Drupal 的菜单系统在设计时遵循了责任链模式. 对于每个页面请求, 菜单系统都要判断是否有模块处理这个请求, 当前用户是否有权限访问被请求的资源, 处理这个请求需要调用哪个函数. 为了做到这些系统会把请求路径发送给菜单项, 如果当前的菜单项不能处理这个请求, 它就被发往责任链的下一环, 直到有模块处理了这个请求或者是有模块拒绝了当前用户的访问或者是责任链没有后续环节, 这时请求的处理才会结束.

Command - 命令

许多 Drupal 的 hook 使用命令模式减少必须实现的函数数量(除了传递正常的参数还把要求的操作作为参数传入). 事实上, hook 系统本身也是用命令模式来支持模块只为它们关注的操作定义 hook.


为什么不使用 Classes.


希望在这里能够表述清楚 Drupal 中实现 OOP 概念的方式. 那么, 为什么 Drupal 不倾向于在今后使用 classes 来代替现有设计呢? 一些原因是历史遗留问题, 更重要的是既然我们有办法在 Drupal 应用 OO 设计模式, 那为什么一定要使用 classes.

一个很好的例子是关于主题系统的扩展性, 如果想实现一个新的界面主题, 那么就 OOP 来说很自然的想法是从默认主题类里继承一个新类来实现. 当新主题需要支持一个基类不支持的界面元素时,就没有简单的办法可以使用了. Drupal 的主题系统使用函数分发系统简洁优雅的解决了这个问题. 在类似这样的情况下, classes 从表面上看使系统变的简洁清晰, 但这也让系统变得僵硬难以扩展.

需要改进的方面

虽然 Drupal 设计中做了很多面多对象的实践, 但是在一些方面还需要加强.

  封装, 虽然在设计上做的很充分, 但是在代码编写的时候并没有得到很好的贯彻执行. 本应该严格设计模块的公共函数和私有函数, 但在执行过程中更倾向于公开大部分函数, 即便有些函数不得不经常变动. 而且 Drupal 更新时的向后兼容策略使这个问题日益严重. 这个向后兼容的策略需要对封装做更好的执行才能给系统带来更好的收益.

  继承, 在系统中的应用并不多, 就如上面说的所有的模块共享公共接口, 它很难扩展到新模块中. 用一个新模块扩展一个存在的模块的功能很容易, 但是没有办法重写某个模块的方法. 我们可以把大的模块分解成小的功能函数库来减少模块给系统带来的问题.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值