[next]
为了实现以观测模式为基础的错误记录句柄,首先我们注意到作为观测者的FileErrorLogger类和 EmailErrorLogger类什么也不能做。那么,FileErrorLogger类是如何向一个文件写出错信息, EmailErrorLogger类又如何发送电子邮件的?接下来,让我来看看用来实现观测模式的技术细节,然后,再集中精力来看看该模式的主体――ErrorHandler的细节。最后,再写一些错误处理函数来调用这个ErrorHandler类。
最后用下面的这一段代码来表示:
// PHP4
$eh =& getErrorHandlerInstance();
$eh-attach(new EmailErrorLogger(‘jsweat_php@yahoo.com’));
$eh-attach(new FileErrorLogger(fopen(‘error.log’,’w’)));
set_error_handler(‘observer_error_handler’);
// ... later
trigger_error(‘this is an error’);
ErrorHandler类是一种单件模式(参考第4章:The Singleton Pattern)。它可以通过函数Attach()来注册各种错误信息观测者,而set_error_handler()函数就是一个指向 ErrorHandler类的函数。最后,当一个错误信息被触发后,所有的观测者都会得到通知。
为了使这次观测的操作生效,你的测试必须能证明所有的这些操作(将错误信息写入日志,利用电子邮件发送错误信息)都能得到执行,并且能正常工作。简而言之,让我们来看看一系列简单的测试。(和这个实例有关的其他更多实例,可以在本书附带的源代码中找到)
这里有FileErrorLogger类联合测试的一部分代码:它用来测试当FileErrorlogger类被某个对象实例化时,是否具有向一个文件写日志的能力。
class FileErrorLoggerTestCase extends UnitTestCase {
var $_fh;
var $_test_file = ‘test.log’;
function setup() {
@unlink($this-_test_file);
$this-_fh = fopen($this-_test_file, ‘w’);
}
function TestRequiresFileHandleToInstantiate() { /* ... */ }
function TestWrite() {
$content = ‘test’.rand(10,100);
$log =& new FileErrorLogger($this-_fh);
$log-write($content);
$file_contents = file_get_contents($this-_test_file);
$this-assertWantedPattern(‘/’.$content.’$/’, $file_contents);
}
function TestWriteIsTimeStamped() { /* ... */ }
} 在这个测试中, setup()函数创建了一个文件指针,指向一个名为test.log的新文件。并且,将该指针保存在变量$_fh中,这个可写的文件指针将作为一个变量传递给FileErrorlogger对象的实例,进行测试。变量$content的值将传递给函数write(),并且,在存储结束后,还将用来被检查$content的值是否确实被正确写入test.log文件中。
(这个测试要求PHP必须具有向那个新建的test.log中写数据的权限。)
下面的一些代码也许可以帮助FileErrorLogger类通过测试。
class FileErrorLogger {
var $_fh;
function FileErrorLogger($file_handle) {
$this-_fh = $file_handle;
}
function write($msg) {
fwrite($this-_fh, date(‘Y-m-d H:i:s: ‘).$msg);
}
} 一个类似的测试代码可以使EmailErrorLogger类生效。
class EmailErrorLoggerTestCase extends UnitTestCase {
function TestEmailAddressFirstConstructorParameter() {
$log =& new EmailErrorLogger;
$this-assertErrorPattern(‘/missing.*1/i’);
}
function TestMail() {
$log =& new EmailErrorLogger(‘jsweat_php@yahoo.com’);
$log-mail(‘test message’);
}
} 接下来,通过这个测试的EmailErrorLogger类的代码如下:
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/php/) class EmailErrorLogger {
var $_addr;
var $_subject;
function EmailErrorLogger($addr,
$subject=’Application Error Message’) {
$this-_addr = $addr;
$this-_subject = $subject;
}
function mail($msg) {
mail($this-_addr
,$this-_subject
,date(‘Y-m-d H:i:s: ‘).$msg);
}
} 你是怎样确定EmailErrorLogger类能真正发送电子邮件的呢?是的,你可以打开你的收件箱,看看其中是否有新邮件,就知道了。但是,那就不是一个全自动的测试了。或者说,这个测试就只是伪模式的一个不错的替代方案。(至于如何创建一个控制邮件的类,将作为一个练习留给读者的。详细信息,请参考第6章The MockObject Pattern 或参考FakeMail项目http://sf.net/projects/fakemail/.)
[next]
有了合适而正确的观测者,我们就可以在观测模式下,从函数attach()开始继续测试ErrorHandler类。
class Observer {
function update() {
die(‘abstract method’);
}
}
Mock::Generate(‘Observer’);
class ErrorHandlerTestCase extends UnitTestCase {
function TestAttach() {
$eh =& new ErrorHandler;
$observer =& new MockObserver($this);
$observer-expectOnce(
‘update’
,array(‘*’)); // array(&$eh)
$eh-attach($observer);
$eh-notify();
$observer-tally();
}
function TestDetach() { /* ... */ }
} 在这次测试中,一个简单的观测类被创建出来,作为所有观测者的接口。为了测试函数attach(),一个基于这个观测类的伪模式被创建出来,并且和ErrorHandler测试实例关联在一起。然后,当公共函数notify()被调用时,伪模式将证实update()函数曾经被调用过。
请注意刚才提及的的在模拟观测中所创建的函数array(&$eh)中的参数。在理想状态中,那个测试应该可以通过的。然而,由于PHP语言的限制,这将产生一个致命错误:Nesting Level Too Deep――循环依赖?。为了避免出现那样的问题,代码中必须使用简单测试下Wild Card功能,以便允许所有参数都能像预期的那样传递。
? Nesting Level Too Deep
因为ErrorHandler在数组$_observer中包含涉及到模拟观测的参数,本来预期是要将它传递给模拟观测的。所以,PHP产生一个Nesting Level Too Deep错误。而循环依赖就像一个初级的PHP问题,甚至可以在一个简单的PHP环境中发现它。(请参考http://bugs.php.net/bug.php?id=31449.)
ErrorHandler开始应该像下面这样构造:
class ErrorHandler {
var $_observers=array();
function attach(&$observer) {
$this-_observers[] =& $observer;
}
function notify() {
foreach(array_keys($this-_observers) as $key) {
$observer =& $this-_observers[$key];
$observer-update($this);
}
} 根据上面的代码,你必须在每一个具体的观测者中添加一个update()函数。在每个实例中,update()函数需要知道如何从被观测者ErrorHandler类中获取信息,进而执行自身的相应功能。这里是添加的代码。
class FileErrorLogger {
var $_fh;
function FileErrorLogger($file_handle) {
$this-_fh = $file_handle;
}
function write($msg) {
fwrite($this-_fh, date(‘Y-m-d H:i:s: ‘).$msg);
}
function update(&$error_handler) {
$error = $error_handler-getState();
$this-write($error[‘msg’]);
}
}
class EmailErrorLogger {
var $_addr;
var $_subject;
function EmailErrorLogger($addr,
$subject=’Application Error Message’) {
$this-_addr = $addr;
$this-_subject = $subject;
}
function mail($msg) {
mail($this-_addr
,$this-_subject
,date(‘Y-m-d H:i:s: ‘).$msg);
}
function update(&$error_handler) {
$error = $error_handler-getState();
$this-mail($error[‘msg’]);
}
} 上面两个update()函数中的每一个,都需要将ErrorHandler作为参数,以便从中获得错误信息,然后调用一个内部函数,来处理这个错误。每个update()函数通过ErrorHandler中的getState()函数来获取信息。那个函数以getState()命名是为了在GoF模式中,保持模式的整体和谐性。但是,如果将这个函数命名为getError()或者getErrorInfo()就更加合适,因为这两个名字更加贴近这个函数的功能。
[next]
另外,如果你不喜欢该模式下对象与对象之间的连接方式,你可以更改update()函数让它来发送一个信息(类似于本例中的错误信息数组或者几个信息对象)来避免引用自己。
这里是一个全新的ErrorHandler,不仅做了最新的修改并且还包含detach()函数:
class ErrorHandler {
var $_observers=array();
var $_error_info;
function attach(&$observer) {
$this-_observers[] =& $observer;
}
function detach(&$observer) {
foreach(array_keys($this-_observers) as $key) {
if ($this-_observers[$key] === $observer) {
unset($this-_observers[$key]);
return;
}
}
}
function notify() {
foreach(array_keys($this-_observers) as $key) {
$observer =& $this-_observers[$key];
$observer-update($this);
}
}
function getState() {
return $this-_error_info;
}
function setState($info) {
$this-_error_info = $info;
$this-notify();
}
} 你现在已经拥有了观测模式下的一个完整工具。
现在,回到本章的原始目标中,让我们看看如何在一个真正的PHP脚本中应用ErrorHandler。为了在一个PHP 应用中包含观测者,你必须实例化ErrorHandler类,并确认函数set_error_handler()使用完全相同的参数。这听起来就像最近的一个问题:单件模式。
让我们作一个Factory()函数,它是一个简单的PHP函数,可以返回ErrorHandler的单态实例。
function &getErrorHandlerInstance() {
static $instance = array();
if (!$instance) $instance[0] =& new ErrorHandler();
return $instance[0];
} 现在,让我们写一个错误记录句柄功能来获取单态ErrorHandler,改变它的状态来反映错误,并且通知观测者。
function observer_error_handler(
$errno, $errstr, $errfile, $errline, $errcontext) {
$eh =& getErrorHandlerInstance();
$eh-setState(array(
‘number’ = $errno
,’msg’ = $errstr
,’file’ = $errfile
,’line’ = $errline
,’context’ = $errcontext
));
} 也许你会注意到这里并没有ErrorHandler::notify()函数。为什么呢?因为ErrorHandler不论何时,只要状态一改变就会自动发出通知。
class ErrorHandler {
// ...
function setState($info) {
$this-_error_info = $info;
$this-notify();
}
} 这种默认通知的方法,有利有弊。但先进之处在于客户端代码不需要包含通知的触发代码。
当然,如果主体对象的状态有好几处变化,所有的变动都对应不同的函数,你就可以选择让客体代码强制调用notify()函数。
自从你能正确使用这些辅助工具后,你给ErrorHandler添加的另一种类型的记录方式就会变得相当的容易?你现在只需要拥有向系统中写日志的权限。稍微查一下PHP手册(http://www.php.net/syslog),你就可以找到一些非常有用的函数来建立日志系统。这些可以很容易的被封装到一个新的类里,以便和ErrorHandler联合使用。
class SyslogErrorLogger {
function SyslogErrorLogger($msg) { define_syslog_variables(); openlog($msg, LOG_ODELAY, LOG_USER);
}
function log($msg) {
syslog(LOG_WARNING, $msg);
}
function update(&$error_handler) {
$error = $error_handler-getState();
$this-log($error[‘msg’]);
}
} 注:错误日志的用处
日志是非常有用的――如果有人使用它们的话。但是,如果没有人使用日志,那么记录日志的代码就是一堆无用的代码
如果想知道更详细的评价,请查看
http://www.lastcraft.com/blog/index.php?p=4
结论
观测模式是非常有用的。这里的例子是完全静态的--观测者可以在脚本的初始化阶段被配置且被生成。要想展示观测模式的灵活性,最好是在一个更加动态的应用中--你需要根据脚本中的其他事情来添加或删除观测者。以常见的生存时间或者说该PHP脚本的允许执行时间打个比方,当同一个脚本在不同的情况下执行时,就可以根据不同的观测者分别配置,而不需要动态改变一个脚本的流程。这就和通过延长脚本执行时间的PHP-GTK 库有很大不同。
下文:《PHP设计模式介绍》第十章 规范模式