面向对象编程(oop)可让开发人员通过使用数据抽象、封装、模块化、多态性和继承减少和简化代码 — 在对 oop 有着深刻的理解的前提下。对 oop 特性的了解还让 php 编码者得以利用设计模式 — 一些众所周知的用来解决常见问题的算法。php 自 v3.0 就已经提供了 oop 功能,但直到 v5.3 到来时,php 的 oop 实现内的怪异之处还是会阻止一些常见设计模式的使用。随着 php v5.3 的延后静态绑定(lsb)特性的出现,这些怪异之处均已彻底消失。
本文向您介绍了在 php v5.3 出现之前,存在问题的一些设计模式,解释了这些模式为何不能工作。然后展示了 php v5.3 的 lsb 特性,并给出了单例和活动记录设计模式。
重新回顾 oop
如果您过去曾接触过 php oop,那么很可能会出于以下原因而决定不使用它:
读过诸多宣称 php oop 有问题的博文中的一条。
曾尝试实现一个简单的设计模式,但没有成功。
而对于 php v5.3,有关 oop 的博文都是正面的,并且 php oop 的问题在很大程度上已得到解决。是时候重回 php oop 了。通过本文,您将看到在 v5.3 出现之前曾存在问题的一些设计模式:单例、生成器、工厂方法和活动记录。
单例、生成器和工厂方法设计模式被视为是 创建型 的模式,因它们可协助对象的构建。单例模式可能是最常用的 oop 设计模式之一了 ;它限制了一个类的对象实例数只能为 1。比如数据库连接池就是单例设计模式的一个例子:我们一般不想让应用程序具有连接池类的多个资源密集型实例。
在需要分离复杂对象的构建和表示时,就需要用到生成器设计模式,您可以使用相同的构造过程来创建多个对象。生成器模式的实现可以很复杂,但一旦生成器可用,它就可以简化生成器所创建对象的构造和使用。具有输出 html、xml 或 pdf 能力的转变器就是需要使用生成器的一个例子。
而工厂方法模式,顾名思义,定义的是一个用来大量产出对象的方法的实现。您可以在应用程序需要创建其类型依赖于子类的实现的对象时,使用工厂方法模式。
活动记录模式则可用来在域类内包装关系数据库持久性方法。一个活动记录的每个实例都关系到数据库内的特定行。这个类包含了要插入、删除和更新数据库内的一行或多个行的方法。活动记录设计模式是由 martin fowler 在 patterns of enterprise application architecture 内定义的,并因在 ruby on rails 内的使用而日益流行。
前-lsb 的创建型设计模式实现问题
上述提到的所有这四个设计模式均使用了静态的属性和方法。例如,看一下清单 1 内所示的这个连接池单例。
清单 1. 一个简单的单例
getinstancecount());
$db2 = connpool::getinstance();
assert (1 == $db2->getinstancecount());
?>
请注意这个静态的 $onlyone 变量。该变量被设计用来保存连接池对象的一个实例。$onlyone 之前的静态修饰符将此变量关系到类本身。$onlyone 变量是一个类属性,因为其作用域是这个类。而 $onlyone 属性只有一个实例。当一个属性不具有静态修饰符时,就称其是一个对象属性,因为该属性对类的每个实例都是惟一的。
注意到 connpool 的构造函数方法(called __construct)是空的。在一个生产实现中,可以使用该方法来创建数据库连接池的间隔。
静态 getinstance 方法包含单例的模板代码。只有在静态的 $onlyone 变量为空时,它才会创建一个 $onlyone 实例。请注意它是如何使用 __class__ 变量来获得类的类型并随即创建该类的一个实例的。
使用 getinstancecount 方法只是为了证明只创建了连接池的一个实例。清单 1 底部的四行代码则证明无论请求 connpool 池类的一个实例多少次,它都会返回相同的对象。
所以, 到目前为止,此单例一切正常 — 直到您决定想要以面向对象的继承树的形式对这个连接池进行子类处理来支持多个数据库。清单 2 显示了这个继承树(为了清晰起见,删除了实例计数器和构造函数代码)。
清单 2. 在没有 lsb 时对单例进行的一次失败尝试
为了支持多个类型的单例类,connpool 类添加了一个 $klass 静态变量并假设它会在子类中被覆盖。 connpoolas400 子类扩展了 connpool 类并提供了 $klass 属性自己的版本。 我们的预期是当 connpoolas400 类的实例创建时,$klass 属性会保存 connpoolas400。但是当执行这些代码时,它不会按预期的那样运行。当 php 实用函数 get_class 返回 connpool 而不是 connpoolas400 时,代码底部的声明会失败。 问题是 connpool 类的 getinstance 方法使用的是它自己的 $klass 属性版而非 connpoolas400 的覆盖版。
