第九章 观测模式
一些面向对象的编程方式,提供了一种构建对象间复杂网络互连的能力。当对象们连接在一起时,它们就可以相互提供服务和信息。
通常来说,当某个对象的状态发生改变时,你仍然需要对象之间能互相通信。但是出于各种原因,你也许并不愿意因为代码环境的改变而对代码做大的修改。也许,你只想根据你的具体应用环境而改进通信代码。或者,你只想简单的重新构造通信代码来避免类和类之间的相互依赖与相互从属。
问题
当一个对象的状态发生改变时,你如何通知其他对象?是否需要一个动态方案――一个就像允许脚本的执行一样,允许自由连接的方案?
解决方案
观测模式允许一个对象关注其他对象的状态,并且,观测模式还为被观测者提供了一种观测结构,或者说是一个主体和一个客体。主体,也就是被观测者,可以用来联系所有的观测它的观测者。客体,也就是观测者,用来接受主体状态的改变
观测就是一个可被观测的类(也就是主题)与一个或多个观测它的类(也就是客体)的协作。不论什么时候,当被观测对象的状态变化时,所有注册过的观测者都会得到通知。
观测模式将被观测者(主体)从观测者(客体)种分离出来。这样,每个观测者都可以根据主体的变化分别采取各自的操作。(观测模式和Publish/Subscribe模式一样,也是一种有效描述对象间相互作用的模式。)
观测模式灵活而且功能强大。对于被观测者来说,那些查询哪些类需要自己的状态信息和每次使用那些状态信息的额外资源开销已经不存在了。另外,一个观测者可以在任何合适的时候进行注册和取消注册。你也可以定义多个具体的观测类,以便在实际应用中执行不同的操作。
实例代码
举例来说,你可以使用观测模式为你的PHP脚本来创建一个更灵活的记录错误的句柄。因为,默认的错误记录句柄也许只会在屏幕上显示一些出错信息,但是增强后的句柄还可以将出错信息写进一个日志文件中,或将出错信息写进系统日志之中,或将出错信息通过电子邮件发送出去,或利用声音报告出错信息。你甚至还可以构造一种有级别的报错方案,只允许向那些已经为具体的出错信息注册过的观测者报告。从一般的警告信息到像数据库失灵之类的严重出错信息都可以报告。
下面,我们用观测模式来为PHP创建一系列的类来实现刚才所说的那些功能。新建一个名为ErrorHandler的类, 它就是观测模式的主体,也就是被观测者。再建另外两个名为FileErrorLogger和 EmailErrorLogger的类, 它们是观测客体(即观测者)。FileErrorLogger类将出错信息写入日志文件,EmailErrorLogger类利用电子邮件发送出错信息。在UML中,可以表示如下:
// 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类的代码如下:
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/.)
有了合适而正确的观测者,我们就可以在观测模式下,从函数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()就更加合适,因为这两个名字更加贴近这个函数的功能。
另外,如果你不喜欢该模式下对象与对象之间的连接方式,你可以更改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库有很大不同。