从面向对象的观点看Drupal编程
从面向对象的观点看Drupal编程 作者:Jonathan Chaffer 译者:Zealy, Blogme.cn(转载请保留,谢谢)
[按] Jonathan Chaffer, 长期作为Drupal核心开发者,写作了一个“从面向对象的观点看Drupal编程”的长篇。本文技术性很强但却完美的展示了Drupal编程的核心概念。
[正文]
版本
1.2 (由 JonBob提交于2005年4月1日)
描述
Drupal经常被那些坚信面向对象是最佳软件设计架构的新来者所批评,因为他们在Drupal代码中看不到“class”的字样,那么它肯定属于 一种不怎么样的解决方案。实际上,Drupal确实没有使用许多PHP的OOP(“面向对象编程”的缩写,下同)特性,但认为使用类就是面向对象设计的同 义词,那就错了。本文将从面向对象的观点覆盖Drupal的一些特性,这样那些已经习惯于这种观点的程序员就能感到对Drupal的基础代码象家一样熟悉 了,也使他们有可能为自己要做的事选择正确的工具。
当前设计的动机
直到版本4.6(译者注:现在可以说直到4.7了),Drupal都没有使用PHP的class结构。这个决定是基于以下几个原因。
首先,在Drupal设计的那个时候PHP对面向对象结构的支持还是很不成熟的。Drupal是在PHP 4之上建立的,而在PHP 5中的绝大多数改进都是关于它的面向对象特性。
其次,Drupal代码已经高度划分到模块中了,每一个模块都定义了他自己的一组函数。包含了这些模块文件就是包含了其中的那些函数;如果每次页面 调用都包括所有的代码PHP的性能将大受其害,所以Drupal尝试每个页面请求只载入尽可能少的的代码。这是一个关键的考虑,特别是在缺少PHP加速器的 时候;此时编译代码的动作就占Drupal处理页面请求的一半。所以从运行时(runtime)的角度来看,Drupal的函数是被定义在其他函数之中 的。这是完美而合理的。然而,PHP不允许这种嵌套的类声明。这就意味着定义这些类的文件的包含必须是“顶层的(top-level)”,而且不能包含在 任何函数里;这就意味着结果要么是更慢的代码(因为总是要包含定义类的所有文件),要么是在index.php中产生庞大的逻辑结构。
最后,使用类实现Drupal自己业已使用的一些先进的面向对象设计模式是很困难的。虽然这里听起来是自相矛盾的,但在下面讨论之后应该就比较清楚了。PHP缺少一些OOP结构例如Objective-C的“categories”,这就意味着要实现一些Drupal机制(例如主题系统),使用类将比使用函数更加繁复。
Drupal中的面向对象原理
除了缺少明确定义的类之外,在Drupal的设计中仍然包含了许多面向对象的范例。要定义一个系统是面向对象的,对其所需的必不可少的“本质特性”有一大堆的说法;我们选择相当流行的定义之一来检查一下Drupal是否具有这些特性。
对象
在Drupal中有许多结构符合“对象”的描述。Drupal组成部分中可认为是对象的、最突出的一些有模块、主题、节点(Nodes)和用户(Users)。
节点是一个Drupal站点的基本内容构件,即一些数据捆绑在一起构成一个典型站点的“页面”。操作这一对象的方法定义在node.module, 通常由node_invoke()调用。表现用户的user对象同样将数据包装在一起,包括用户的账户信息、个人信息、和会话跟踪。在这两种情形下,数据 结构都被定义在数据库表中而不是在类中。Drupal使用它支持的数据库的关系特性,使其他模块可以扩展对象,如增加额外的数据域值。
模块和主题同样也具有“对象”特性,在很多情况下充当“controller”的角色。每个模块都有一个源文件,不但捆绑了一系列相关的函数,同时也遵从已定义的Drupal Hook(调用-钩子)的模式。
抽象
Drupal的Hook系统是它的界面抽象层的基础。Hook定义那些调用模块或由模块调用的操作。如果一个模块实现一个Hook,就是当hook被调用时按约定执行一定的功能。调用代码不用了解模块的任何信息或者被调用时hook是怎样被实现的。
封装
象绝大多数面向对象系统一样,Drupal除了依赖规范以外并没有一个严格限制存取对象内部工作的方法。Drupal的代码是基于函数的,这些函数 共享同一个命名空间(namespace)的。这一命名空间我们约定用前缀进行细分,通过这一简单的约定,每个模块可以定义它自己的函数和变量而不用担心 和其他模块相冲突。
另一个约定是用来将一个类的公共API与它的内部实现分开。内部函数的名字前都加上一个下划线,表示它们不应当被外部模块调用。例如_user_categories()就是一个私有函数,并且可能不加提示的被改动;而user_save()就是user对象的公共接口之一,用于将user对象保存到数据库中(至于是怎么完成这件事的,那就是私有的了)。
多态
节点是传统意义上的多态。如果一个模块需要显示一个节点,例如,它可以对节点调用node_view()得到节点的html表示。然而,实际的html生成过程完全取决于传递给函数的节点类型;这个和有一个类按照接收到的消息类型决定自己的行为是直接类似的。Drupal自身负责处理这种通常由OOP语言运行库处理的内部任务。
此外,这个例子中节点的html生成也受当前活动的主题的影响。主题也象node的方式一样是多态的;主题接收到“表现这个节点”的消息,然后按当前活动的主题定义的方式响应,虽然接口是固定的,但各个主题间的“表现”方式可能完全不同。
继承
模块和主题可以任意定义它们需要的功能。然而,它们都会考虑从基本的抽象类中继承它们的行为。在主题中,这个基类是由theme.inc文件中的函数定义的;如果一个主题没有重载其中的函数,即使主题提供了替代的“表现”函数,显示时也会使用缺省的“表现”函数。模块的情况也是完全类似的,Drupal有很多hook可供选择,模块可以选择一些来实现。
Drupal中的设计模式
Drupal内部结构中的大多数都比简单的继承和消息传递来得复杂,然而,系统中最让人感兴趣的特性还是使用已有的软件设计模式获得的。例如,许多在经典的四人帮《设计模式》中描述的模式都可以在Drupal中看到。
Singleton
如果我们将模块和主题视为对象,那么它们符合Singleton模式。通常这些对象都不包括数据;将一个模块与另一个模块区分开的是它包括的一组功能,因此它应当被认为是一个Singleton的类。
Decorator
Drupal广泛的使用了decorator模式。前面我们已经讨论过节点对象的多态,这只是节点系统的能力的冰山一角。最有意思的是hook_nodeapi()的使用,它允许任意的模块扩展所有节点的所有行为。
这一特性允许为节点加入完全可变的行为而无需子类化(subclassing)。例如,一个基本的story节点只有几个相关数据:标题,作者,正 文,节选和少量元数据。一个很普通的需求是上传并附加文件到一个节点,那么你可以设计一个新的节点类型,它包括story的全部节点特性加附加文件的能 力。Drupal的upload模块就以一种使用NodeApi的标准形式满足了这一需求,它能为需要它的能力的任意节点附加文件。
这种方式可以通过decorator的使用加以模仿:将decorator包裹在每个节点对象上。更简单的,那些支持categories的语言, 如Objective-C,能够增大所有对象的基类来增加新的行为。Drupal的实现方式就是一个简单的hook系统的分支加一个 node_invoke_all()。
Observer
上述的情况与面向对象系统中的Observer的使用是类似的。Observer模式遍及整个Drupal。当Drupal的分类系统中术语表发生 变化时,所有模块中的关于分类的对应动作的hook都将被调用。通过实现hook,这些模块就相当于注册了一个关于术语表对象的observer;任何有 关的改变都会引起适当的动作。
Bridge
Drupal的数据抽象层就是Bridge设计模式的标准近似。模块需要一种与所用的数据库无关的通用读写方法,这一抽象层就做到了这一点。新的数据库层可以按照bridge定义的API添加,这样无需修改模块代码就可以增加对其他的数据库系统的支持了。
Chain of Responsibility
Drupal的菜单系统遵 循Chain of Responsibility模式。对每个页面请求,菜单系统都会确定是否有一个模块负责处理这个请求、用户是否有权存取请求的资源、哪一个函数将被调 用。为做到这一点,一个消息被传递给负责请求路径的菜单项。如果那个菜单项不能响应请求,它会将控制继续传递到下一链,直到一个模块响应了请求、或者一个 模块拒绝用户的访问、或者响应链路已经传递到末尾了。
Command
Drupal的许多hook都使用Command模式来减少必须实现的函数数目,只需将“操作”作为参数传递。实际上,hook系统自己也使用这个模式,因此模块不需要定义每个hook,只需实现那些模块关心的就可以了。
为什么不使用类
上面可能已经比较清楚地描述了Drupal体现多种多样的OOP原理的方法。那么,为什么,不将Drupal迁移到使用类的方向上以便在未来解决这 些问题呢?有一些是历史性的原因,这个前面我们已经提到了。现在,通过前面我们浏览Drupal中的设计模式的应用,另一些原因可能也比较清楚了。
一个很好的例子是主题系统的可扩展性。主题为每一个它希望以特殊方式显示的页面元素定义了函数。就像前面提到的,这使主题看上去就像是从定义了缺省显示方式的基类继承的子类。
但是,当增加了新的页面元素的模块被添加进来时会发生什么呢?主题应当能够重载显示这个元素的缺省过程,但是如果我们定义的是基类的话,新的模块是 无法添加新的方法到基类中的。复杂的模式应该能够解决这个问题,但是Drupal的主题架构通过自己的函数分派(dispatch)系统很优美的处理了这 种情形。在这个例子以及其他诸如此类的情况下,表面上看似简化了系统的类最终将导致系统越来越臃肿和难于扩展。
改进的余地
虽然Drupal反映了许多面向对象的经验,仍然有一些OOP的方面可以以更强有力方式对项目施加影响。
封装,虽然理论上是足够了,并没有足够始终如一的贯穿整个基础代码。模块应当更加严厉的定义哪些函数是共有的、哪些是私有的;目前的趋势是在公共命 名空间里发布大多数函数,哪怕接口还是不稳定的。由于Drupal的策略是必要时放弃向下的兼容性来换取更干净的API,使得这个问题已经恶化了。这一策 略已经让我们获得一些非常好的代码,但我们需要更少的执行这个策略除非更好的执行了封装的规范。
继承在系统中也是比较弱的。虽然像前面提到的一样,所有的模块共享一个行为的公共集合,要将它扩展到新的模块中仍然是困难的。你可以轻松创建较之原 有的模块增加了功能的新模块,但没有办法只是重载原有模块中的一些功能。如果将大的模块划分为“菜单式”的更小的功能组合,这一问题带来的冲击可以被忽 视,这样一个模块中那些不受欢迎的部分可能更容易从系统中剥离。
其他模型
Drupal表面上是一个过程化系统,因为他是建立在一个过程化语言(不用类的PHP)之上的。但一个软件后面的模型并非完全依赖于它的代码表现形 式,同时,Drupal方便的时候它从不害怕从许多完全不同的编程模型中借鉴原理。一个Drupal的强大功能的力量来源于它所基于的关系型数据库,和与 之完全相对应的关系编程技术。和任何web应用程序一样,Drupal的运转实际上充满着许多对离散和快速的页面请求的响应,这些由事件驱动编程的建筑物 所发生的共振构成了系统的行为。对于那些有面向方面编程经验的程序员,在任意模块中发生的hook魔术看起来和切入点(Pointcut)极为类似。而 且,现在应该已经很清楚了,Drupal对面向对象的概念一点也不陌生。
