本文发表于《程序员》5月号
是一个系列的第一篇,目前想到的其他一些主题是:
SQL注入问题
事件模型
AOP模型
UI Framework的实现
Template机制
PHP沉思录
工作模型
PHP的工作模型非常特殊。从某种程度上说,PHP和ASP、ASP.NET、JSP/Servlet等流行的Web技术,有着本质上的区别。
以Java为例,Java在Web应用领域,有两种技术:Java Servlet和JSP(Java Server
Page)。Java
Servlet是一种特殊类型的Java程序,它通过实现相关接口,处理Web服务器发送过来的请求,完成相应的工作。JSP在形式上是一种类似于PHP
的脚本,但是事实上,它最后也被编译成Servlet。也就是说,在Java解决方案中,JSP和Servlet是作为独立的Java应用程序执行的,它
们在初始化之后就驻留内存,通过特定的接口和Web服务器通信,完成相应工作。除非被显式地重启,否则它们不会终止。因此,可以在JSP和Servlet
中使用各种缓存技术,例如数据库连接池。
ASP.NET的机制与此类似。至于ASP,虽然也是一种解释型语言,但是仍然提供了Application对象来存放应用程序级的全局变量,它依托于ASP解释器在IIS中驻留的进程,在整个应用程序的生命期有效。
PHP却完全不是这样。作为一种纯解释型语言,PHP脚本在每次被解释时进行初始化,在解释完毕后终止运行。这种运行是互相独立的,每一次请
求都会创建一个单独的进程或线程,来解释相应的页面文件。页面创建的变量和其他对象,都只在当前的页面内部可见,无法跨越页面访问。在终止运行后,页面中
申请的、没有被代码显式释放的外部资源,包括内存、数据库连接、文件句柄、Socket连接等,都会被强行释放。
也就是说,PHP无法在语言级别直接访问跨越页面的变量,也无法创建驻留内存的对象。见下例:
class StaticVarTester {
public static $StaticVar = 0;
}
function TestStaticVar() {
StaticVarTester :: $StaticVar += 1;
echo "StaticVarTester :: StaticVar = " . StaticVarTester :: $StaticVar;
}
TestStaticVar();
echo "
";
TestStaticVar();
?>
在这个例子中,定义了一个名为StaticVarTester的类,它仅有一个公共的静态成员$StaticVar,并被初始化为0。然后,在
TestStaticVar()函数中,对StaticVarTester :: $StaticVar进行累加操作,并将它打印输出。
熟悉Java或C++的开发者对这个例子应该并不陌生。$StaticVar作为StaticVarTester类的一个静态成员,只在类被装载时进行初
始化,无论StaticVarTester类被实例化多少次,$StaticVar都只存在一个实例,而且不会被多次初始化。因此,当第一次调用
TestStaticVar()函数时,$StaticVar进行了累加操作,值为1,并被保存。第二次调用TestStaticVar()函数,$
StaticVar的值为2。
打印出来的结果和我们预料的一样:
StaticVarTester :: StaticVar = 1
StaticVarTester :: StaticVar = 2
但是,当浏览器刷新页面,再次执行这段代码时,不同的情况出现了。在Java或C++里面,$StaticVar的值会被保存并一直累加下去,我们将会看到如下的结果:
StaticVarTester :: StaticVar = 3
StaticVarTester :: StaticVar = 4
…
但是在PHP中,由于上文叙及的机制,当前页面每次都解释时,都会执行一次程序初始化和终止的过程。也就是说,每次访问时,StaticVarTester都会被重新装载,而下列这行语句
public static $StaticVar = 0;
也会被重复执行。当页面执行完成后,所有的内存空间都会被回收,$StaticVar这个变量(连同整个StaticVarTester类)也就不复存
在。因此,无论刷新页面多少次,$StaticVar变量都会回到起点:先被初始化为0,然后在TestStaticVar()函数调用中被累加。所以,
我们看到的结果永远是这个:
StaticVarTester :: StaticVar = 1
StaticVarTester :: StaticVar = 2
PHP这种独特的工作模型的优势在于,基本上解决了令人头疼的资源泄漏问题。Web应用的特点是大量的、短时间的并发处理,对各种资源的申请
和释放工作非常频繁,很容易导致泄漏。同时,大量的动态html脚本的存在,使得追踪和调试的工作都非常困难。PHP的运行机制,以一种非常简单的方式避
免了这个问题,同时也避免了将程序员带入到繁琐的缓冲池和同步等问题中去。在实践中,基于PHP的应用往往比基于Java或.NET的应用更加稳定,不会
出现由于某个页面的BUG而导致整个站点崩溃的问题。(相比之下,Java或.NET应用可能因为缓冲池崩溃或其他的非法操作,而导致整个站点崩溃。)后
果是,即使PHP程序员水平不高,也无法写出使整个应用崩溃的代码。PHP脚本可以一次调用极多的资源,从而导致页面执行极为缓慢,但是执行完毕后所有的
资源都会被释放,应用仍然不会崩溃。甚至即使执行了一个死循环,也会在30秒(默认时间)后因为超时而中止。从理论上来说,基于PHP的应用是不太可能崩
溃的,因为它的运行机制决定它不存在常规的崩溃这个问题。在实践中,很多开发者也认为PHP是最稳定的Web应用。
但是,这种机制的缺点也非常明显。最直接的后果是,PHP在语言级别无法实现跨页面的缓冲机制。这种缓冲机制缺失造成的影响,可以分成两个方面:
一是对象的缓冲。如我们所知,很多设计模式都依赖于对象的缓冲机制,对于需要频繁应付大量并发的服务端软件更是如此。因此,对象缓冲的缺失,
理论上会极大地降低速度。但是,由于PHP本身的定位和工作机制等原因,它在实际工作中的速度非常快。就作者自己的经验来看,在小型的Web应用中,
PHP至少不比Java慢。在大型的应用中,为了榨干每一分硬件资源,即使PHP本身足够快,一个优秀的对象缓冲机制仍然是必要的。在这种情况下,可以使
用第三方的内存缓冲软件,如Memcached。由于Memcached本身的优异特性(高性能,支持跨服务器的分布式存储,和PHP的无缝集成等),在
大型的PHP应用中,Memcached几乎已经成为不可或缺的基础设施了。比起使用PHP语言自己实现对象缓冲来,这种第三方解决方案似乎更好一些。
二是数据库连接的缓冲。对MySQL,PHP提供了一种内置的数据库缓冲机制,使用起来非常简单,程序员需要做的只是用
mysql_pconnect()代替mysql_connect()来打开数据库而已。PHP会自动回收被废弃的数据库连接,以供重复使用。具有讽刺意
味的是,在实际应用中,这种持久性数据库连接往往会导致数据库连接的伪泄漏现象:在某个时间,并发的数据库连接过多,超过了MySQL的最大连接数,从而
导致新的进程无法连接数据库。但是过一段时间,当并发数减少时,PHP会释放掉一些连接,网站又会恢复正常。出现这种现象的原因是,当使用
pconnect时,Apache的httpd进程会不释放connect,而当Apache的httpd进程数超过了mysql的最大连接数时,就会出
现无法连接的情况。因此,需要小心地调整Apache和Mysql的配置,以使Apache的httpd进程数不会超出MySQL的最大连接数。在某些情
况下,一些有经验的PHP程序员宁可继续使用mysql_connect(),而不是mysql_pconnect()。
就作者所知,在PHP未来的roadmap中,对于工作模型这一部分,没有根本性的变动。这是PHP的缺点,也是PHP的优势,从本质上
说,这就是PHP的独特之处。因此,我们很难期待PHP在近期内会对这一问题做出重大的改变。但是,在对待这个问题带来的一系列后果时,我们必须谨慎应
对。
数据库访问接口
长期以来,PHP都缺乏一个象ADO或JDBC那样的统一的数据库访问接口。PHP在访问不同的数据库时,使用不同的专门API。例如,使用
mysql_connect函数连接MySQL,使用ora_logon函数连接Oracle。平心而论,这种方式并没有象我们想象的那样麻烦。在真实项
目中,把系统从一种数据库完全迁移到另一种数据库的要求是比较少见的,特别是对于LAMP这样的小型项目而言。而且,只要将访问数据库的代码进行了良好的
封装,迁移的工作量也会较少。另外,使用专门API,在效率上多少会有一些优势。
虽然如此,PHP的开发人员仍然在努力构建PHP的统一的数据库访问接口。从PHP 5.1开始,PHP的发行包内置了PDO(PHP Data Objects,PHP数据对象)。PDO具有如下特性:
统一的数据库访问接口。PDO为访问不同的数据库提供了统一的接口,并且能够通过切换数据库驱动程序,方便地支持各种流行的数据库。
面向对象。PDO完全基于PHP 5的对象机制,因此区别于基于过程的专用API。
高性能。PDO的底层用C编写,比起用纯PHP开发的其他类似解决方案,有更高的性能。
一个典型的PDO应用如下例:
$pdo = new PDO("mysql:host=localhost;dbname=justtest", " mysql_user ", " mysql_password");
$query = "SELECT id, username FROM userinfo ORDER BY ID";
foreach ($pdo->query($query) as $row) {
echo $row['id']." | ".$row['username']."
";
}
但是,PDO还有一个更重要的问题没有解决,那就是对数据集的抽象。
无论是ADO还是JDBC,除了提供统一的数据库访问接口以外,也提供了对数据集的抽象。也就是说,在通过ADO/JDBC取回数据集结果以
后,这些数据集以统一的格式被存放在RecordSet/RowSet对象中,业务逻辑代码只需要与数据集对象进行交互即可。对数据集进行抽象的直接后
果,是彻底地分离了业务逻辑层和数据库访问层的代码,并且也在某种程度上起到了OR Mapping的效果。
自从ADO.NET出现后,数据集的抽象又有了一次不小的进步。和ADO相比,ADO.NET中的DataTable/DataSet类的主要新特性如下:
非连接性。在传统的ADO模型中,数据集需要占用一个数据库连接,直到所有工作完成。一旦连接被关闭,数据集的内容也就失效了。ADO.NET中的数据集
是非连接的,也就是说,当连接被关闭后,数据集中的内容仍然保存。这种非连接性带来的直接后果是,数据库连接可以被最大限度地利用,因为一旦工作完成就可
以将连接返回到数据库连接池中。(ADO也支持非连接的数据集,但是需要程序员自己实现,而ADO.NET的数据集在本质上就是非连接的。)
自描述性。ADO.NET中的数据集是完全自我描述的,而且具有完备的metadata,其内容不但可以从任何特定的数据库生成,而且可以由代码动态生成。DataSet可以跟踪数据的变化,并完成相应的操作。
互操作性。由于非连接性和自描述性,ADO.NET中的数据集可以非常方便地在网络之间进行传输。DataSet可以序列化/反序列化为XML或其他特定
的格式。这样,DataSet不但可以用于同一平台的分布式网络环境,而且可以用于异构网络环境。
Java从J2SE 5.0开始内置了CachedRowSet,其原理和ADO.NET的数据集类似。Borland开发的用于取代BDE的新一代数据库引擎dbExpress,其改进也与此类似。
与之对比,PHP中对数据集的支持显得非常原始。无论是传统的API还是PDO,取回的数据仅仅表现为数组,并且没有任何缓存机制。这意味着,在所有需要访问数据集的地方,都必须频繁地使用直接访问数据库的API。下面是一个使用mysql API的例子:
$link = mysql_connect('localhost', 'mysql_user', 'mysql_password');
if (!$link) {
die('Could not connect: ' . mysql_error());
}
mysql_select_db("justtest");
$result = mysql_query("SELECT id, username FROM userinfo ORDER BY ID");
while ($row = mysql_fetch_array($result)) {
echo $row['id']." | ".$row['username']."
";
}
mysql_close($link);
这样做的后果是业务逻辑和访问数据库的代码无法分离,在规模较大的系统中尤其严重。
就作者所知,PHP官方没有提供支持抽象数据集的计划。但是,自己实现这样一个数据集并不是一件难事。作者参照ADO.NET的架构,使用纯PHP代码编
写了一个规模非常小的数据抽象类库,姑且称之为MyPDO。MyPDO大约有1300行代码,在几个真实项目中工作得很好。由于篇幅所限,本文不列出它的
所有代码,仅仅给出几个最主要的类的描述:
DataAdapter接口:定义了所有与数据库操作相关的方法。
ConceptDataAdapter类:实现了DataAdapter,封装了访问特定数据库的代码。如MySqlDataAdapter。
DataSet类:实现了自描述的抽象数据集,与具体数据库无关。可以自我跟踪数据的变化。
SqlCommandBuilder类:可以跟据DataSet类的内容,自动实现Insert、Update、Delete等操作。
下面是MyPDO的一个典型应用:
$Conn = new MySqlDataConnection(new ConnectionInfo("localhost", "mysql_username", "mysql_password", "justtest", "gbk"));
$Da = $Conn->GetDataAdapter();
$Da->SetSqlString("SELECT id, username FROM userinfo ORDER BY ID");
$Ds = new DataSet();
$Da->Fill($Ds);
$Conn->Disconnect(); // 关闭数据库连接,但$Ds仍然保存数据内容
echo $Ds; // 调用DataSet的__tostring()方法,格式化输出内容
通过替换MySqlDataConnection,可以以最小的成本实现不同数据库之间的切换。
由于上文中讨论过的PHP的工作模型,通过MyPDO实现的缓存在性能上获得的好处有限。但是,在采用Memcached的解决方案中,MyPDO还是能够带来很大的便利。因为只有基于非连接方式的数据集,才可能在Memcached这样的内存池中被缓存。
另外,由于MyPDO中的DataSet是自描述的,内置了WriteToXml和ReadFromXml方式,它无需程序员编码就可以保存为XML或从XML中还原,在网络上甚至异构平台之间进行传输和识别。在电子商务领域中,这个特性是非常有用的。
-------------------------------------------------------------------
本文发表在《程序员》第7期
PHP沉思录之二
左轻侯
PME模型
在大规模的程序设计中,组件(component)已经成为一种非常流行的技术。常见的组件技术都基于PME模型,即属性(Property)、方法
(Method)和事件(Event)。基于PME的组件技术可以方便地实现IoC(Inversion of
Control,控制反转),是从IDE的plugin到应用服务器的“热发布”等许多技术的基础。
PHP从版本5开始,大大完善了对OO的支持,以前不能被应用的许多pattern现在都可以在PHP5中实现。因此,是否能够实现基于PHP的组件技术,也就成了一个值得讨论的问题。
下面对PHP对于PME模型的支持,逐一进行讨论:
属性(Property)
PHP并不支持类似Delphi或者C#的property语法,但这并不是问题。Java也不支持property语法,但是通过getXXX
()和setXXX()的命名约定,同样可以支持属性。PHP也可以通过这一方式来支持属性。但是,PHP提供了另一种也许更好的方法,那就是__set
()和__get()方法。
在PHP中,每一个class都会自动继承__set()和__get()方法。它们的定义如下:
void __set ( string name, mixed value )
mixed __get ( string name )
这两个方法将在下列情况下被触发:当程序访问一个当前类没有显式定义的属性时。在这个时候,被访问的属性名称作为参数被传入相应的方法。任何类都可以重载__set()和__get()方法,以实现自己的功能。
如下例:
class PropertyTester {
public function __get($PropName) {
echo "Getting Property $PropName\n";
}
public function __set($PropName, $Value) {
echo "Setting Property $PropName to '$Value'\n";
}
}
$Prop = new PropertyTester();
$Prop->Name;
$Prop->Name = "some string";
类PropertyTester重载了__set()和__get()方法,为了测试,仅仅将参数打印输出,没有做更多的工作。测试代码创建了
PropertyTester类的实例,并试图读写它并不存在的一个属性Name。此时,__set()和__get()相继被调用,并打印出相关参数。
它的输出结果如下:
Getting Property Name
Setting Property Name to 'some string'
基于这种机制,我们可以将属性的值放在一个private的List中,在读写属性时,通过重载__set()和__get()方法,读写List中的属性值。
但是,__set()和__get()方法的有趣之处远不止及。通过这两个方法,可以实现动态属性,也就是不在程序中显式定义,而是在运行时动态生成的属
性。只要想想这种技术在OR
Mapping中的作用就能够明白它的重要性了。配合__call()方法(用于实现动态方法,在下一节中详述),它能够取代丑陋的代码生成器(code
generator)的大部分功能。
方法(Method)
PHP对方法的支持比较简单,没有太多可以讨论的。值得一提的是,PHP从版本5开始支持类的静态方法(static method),这使得程序员再也不用无谓地增加许多全局函数了。
事件(Event)
事件也许是PHP遇到的最复杂的问题。PHP并没有在语法层面提供对事件的支持,我们只能考虑通过别的途径来实现。因此,我们需要先对事件的概念和其他语言对事件的实现方式进行讨论。
事件模型可以简述如下:充当事件触发者的代码本身并不处理事件,而仅仅是在事件发生时,把程序控制权转交给事件的处理者,在事件处理完成后,再收回控制权。事件触发者本身并不知道事件将会被如何处理,在大多数情况下,事件触发者的代码要先于事件处理者的代码被完成。
在传统的面向过程的语言(例如C或者PASCAL)中,事件可以通过函数指针来实现。具体来说,事件触发者定义一个函数指针,这个函数指针可以在
以后被指向某个处理事件的函数。在事件发生时,调用该函数指针指向的处理函数,并将事件的上下文作为参数传入。处理完成后,控制权再回到事件触发者。
在面向对象的语言中,方法指针(指向某个类的方法的指针)取代了函数指针。以Delphi为例,事件处理的例子如下:
type
TNotifyEvent = procedure(Sender: TObject) of object;
TMainForm = class(TForm)
procedure ButtonClick(Sender: TObject);
…
End;
Var
MainForm: TMainForm;
OnClick: TNotifyEvent;
…
可以看出,TNotifyEvent被定义为所谓的过程类型(Procedural
Type),事实上就是一个方法指针。TMainForm的ButtonClick方法是一个事件处理者,符合TNotifyEvent的签名。
OnClick是一个事件触发者。在实际使用时,通过如下代码:
OnClick := MainForm.ButtonClick;
将MainForm.ButtonClick方法绑定到了OnClick事件。当OnClick事件触发时,MainForm.ButtonClick方法将被调用,并且将Sender(触发事件的组件对象)作为参数传入。
回到PHP,由于PHP不支持指针,因此无法使用函数指针这一技术。但是,PHP支持所谓的“函数变量”,可以把函数赋予某个变量,其作用类似于函数指针。如下例:
function EventHandler($Sender) {
echo "Calling EventHandler(), arv = $Sender\n";
}
$Func = 'EventHandler';
$Func('Sender Name');
由于PHP是一种动态语言,变量可以为任何类型,所以无须先定义函数指针的类型作为事件的签名。直接定义了一个函数EventHandler作为事件处理
者,然后将它赋予变量$Func(注意直接使用了字符串形式的函数名),最后触发该事件,并将一个字符串“Sender
Name”传给它作为参数。输出的结果是:
Calling EventHandler(), arv = Sender Name
同样地,PHP也提供了类似方法指针的机制。如下例:
Class EventHandler {
public function DoEvent($Sender) {
echo "Calling EventHandler.DoEvent(), arg = $Sender\n";
}
}
$EventHanler = new EventHandler();
$HandlerObject = $EventHanler;
$Method = 'DoEvent';
$HandlerObject->$Method('Sender Name');
由于PHP中没有能够直接引用对象方法的变量,因此需要使用两个变量来间接实现:$HandlerObject指向对象,$Method指向对象方法。通过$HandlerObject->$Method方式的调用,可以动态地指向任何对象方法。
为了使代码更加优雅和更适合复用,可以定义一个专门的类NotifyEvent,并使用一段新的调用代码:
final class NotifyEvent {
private $HandlerObject;
private $Method;
public function __construct($HandlerObject, $Method) {
$this->HandlerObject = $HandlerObject;
$this->Method = $Method;
}
public function Call($Sender) {
$Method = $this->Method;
$this->HandlerObject->$Method($Sender);
}
}
$EventHanler = new EventHandler();
$NotifyEvent = new NotifyEvent($EventHanler, 'DoEvent');
$NotifyEvent->Call('Sender Name');
NotifyEvent类定义了两个私有变量$HandlerObject和$Method,分别指向事件处理者对象和处理方法。在构造函数中对这两个变量赋值,再通过Call方法来调用。
熟悉C#的读者可以发现,NotifyEvent类与C#中的Delegate十分类似。Delegate超过NotifyEvent的地方在于支持多播
(Multicast),也就是一个事件可以绑定多个事件处理者。只要事件触发者自己维护一个NotifyEvent对象数组,支持多播也不是一件难事。
至此,PHP对事件的支持已经得到了比较圆满的解决。但是,人的求知欲是无穷无尽的。还有没有可能通过其他的方式来实现事件呢?
除了方法指针,接口(interface)也可以用于实现事件。在Java中,这种技术被广泛应用。其核心思想是,将事件处理者的处理函数定义抽象为一个
接口(相当于函数指针的签名),事件触发者针对这个接口编程,事件处理者则实现这个接口。这种方式的好处在于,不需要语言支持函数指针或方法指针,让代码
显得更加清晰和优雅,缺陷在于,实现同一种功能,要使用更多的代码。如下例:
interface IEventHandler {
public function DoEvent($Sender, $Arg);
}
class EventHanlerAdapter implements IEventHandler {
public function DoEvent($Sender, $Arg) {
echo "Calling EventHanlerAdapter.DoEvent(), Sender = $Sender, arg = $Arg\n";
}
}
class EventRaiser {
private $EventHanlerVar;
public function __construct($EventHanlerAdapter) {
$this->EventHanlerVar = $EventHanlerAdapter;
}
public function RaiseEvent() {
if ($this->EventHanlerVar != null) {
$this->EventHanlerVar->DoEvent($this, 'some string');
}
}
public function __tostring() {
return 'Object EventRaier';
}
}
$EventHanlerAdapter = new EventHanlerAdapter();
$EventRaiser = new EventRaiser($EventHanlerAdapter);
$EventRaiser->RaiseEvent();
首先定义了一个接口IEventHandler,它包含了方法的签名。EventHanlerAdapter类作为事件处理者,实现了这个接口,
并提供了相应的处理方法。EventRaiser类作为事件触发者,针对$EventHanlerVar变量(它应该是IEventHandler接口类
型,但是在PHP中不用显式定义)编码。在实际应用中,将EventHanlerAdapter的实例作为参数赋予传给EventRaiser类的构造函
数,当事件发生时,相应的处理方法将被调用。输出结果如下:
Calling EventHanlerAdapter.DoEvent(), Sender = Object EventRaier, arg = some string
最后,让我们回到现实世界中来。虽然我们用PHP完整地实现了PME模型,但是这到底有什么用呢?毕竟,我们不会用PHP去编写IDE,也不会用
它编写应用服务器。回答是,基于PME模型的组件技术可以实现更加方便和更大规模的代码复用。在基于PHP的应用系统中,虽然插件已经被广泛使用,但是通
过组件技术可以实现功能更强大、更加规范和更容易维护的插件。此外,组件技术在实现一些大的Framework(例如,针对Web
UI的Framework)时,也是不可或缺的。
阅读(1005) | 评论(0) | 转发(0) |