首 页   · 站长博客 · 用户注册 · 会员登陆  · 会员排行  ·最新主题  ·最近回复  精华区  版权声明  ·论坛管理
  当前登录身份:游客,请先登录。  笔名: 口令: 验证码:   
楼 主  index »  PHP相关资源下载区 » 《PHP设计模式介绍》第六章 伪对象模式  


  作者:lxshark
  注册时间:2009-08-06
  主题/回复:49/50
  积分:639
  等级:★★★(六级)
  称号:声名鹊起

  lxshark@yeah.net..
  593993745
  hi.baidu.com/mythicsky

 

 发表:2009-08-08 20:45:20 阅读 1973 次 回复 1 次 得分0  |   字号 字色
《PHP设计模式介绍》第六章 伪对象模式
面向对象的编程之所以丰富多彩,部分是由于对象间的相互联系与作用。一个单一的对象就能封装一个复杂的子系统,使那些很复杂的操作能够通过一些方法的调用而简化。(无所不在的数据库连接就是这样的一个对象实例。) 
然而经常有这样的情况,对象间的交互性是如此复杂以至于我们不得不面对类似“先有鸡还是先有蛋”这样伤脑筋的问题:如何创建并测试这样一个对象,他要么依赖于很多已创建的对象,要么依赖于其他一些难以意识到的情况,如整个数据库的创建和测试。

问题

如何分隔并测试一个与其他对象和资源有关的代码段?又如何再创建一个或多个对象、程序来验证你的代码能正常运行?

解决方案

当用situ(或在一个仿真的程序环境中)测试一个对象代价不菲或困难重重时,就可用伪对象来模拟这个行为。伪对象有同真实对象一样的接口,但却能提供预编译响应,能跟踪方法调用,并验证调用次序。 
伪对象是测试的“特别力量”。他们被秘密训练,渗透进目标代码,模拟并监视通信方式,回报结果。伪对象有助于查找和消除程序漏洞并能支持更多正常调试环境下的“防危险”操作。


注:The ServerStub

伪对象模式是另一种测试模式ServerStub的扩展。ServerStub模式替代一个资源并返回其方法所调用的相应值。当其参与指定次序的方法的调用时ServerStub就成了伪对象。

其并非是一个设计模式

本章与其他章不同,因为伪对象是一个测试模式而不是设计模式。这类似于一个附加的章节,但对它的使用 确实很值得你纳入到编码进程中。另一个不同是我们不再关注这个模式如何编码之类的基础问题,而是强调   如何在SimpleTest中使用伪对象。

本章先举一个非常简单的例子来示范SimpleTest下伪对象的基本机制。然后向你演示如何使用伪对象帮助重构已有代码与如何测试新的解决方案。

样本代码

伪对象是对象在测试中的一个替代品,用它测试代码更加简便。例如,替代一个真实的数据连接——这个真实的数据连接由于一些原因而不能实际连接——你就可以创建一个伪对象来模拟。这意味着伪对象需要准确地回应代码中所调用的相同的应用程序接口。

让我们创建一个伪对象来替代一个简单的名为Accumulator的类,这是一个求和的类。如下是最初的Accumulator类:


// PHP4 
class Accumulator { 
var $total=0; 
function add($item) { 
$this->total += $item; 

function total() { 
return $this->total; 

}
 
这个类中add()函数先累加值到$total变量中,再交由total()函数返回 。 一个简单的累加也可以如下面这样(下面的代码被编写为一个函数,但它也可以写成一个类)。 


function calc_total($items, &$sum) { 
foreach($items as $item) { 
$sum->add($item); 


function calc_tax(&$amount, $rate=0.07) { 
return round($amount->total() * $rate,2); 
}  


第一个函数calc_total()用一个累加的动作求一系列值的和。下面是简单的测试:


class MockObjectTestCase extends UnitTestCase { 
function testCalcTotal() { 
$sum =& new Accumulator; 
calc_total(array(1,2,3), $sum); 
$this->assertEqual(6, $sum->total()); 





让我们关注第二个例子。假设实现一个真实的累加动作的代价很大。那么用一个简单的对象来替代它并回应相关代码就是很好的做法了。使用SimpleTest,你可以用如下代码创建一个伪累加动作:


Mock::generate(‘Accumulator’); 
class MockObjectTestCase extends UnitTestCase { 
// ... 
function testCalcTax() { 
$amount =& new MockAccumulator($this); 
$amount->setReturnValue(‘total’,200); 
$this->assertEqual( 
14, calc_tax($amount)); 




为了使用伪对象,具有代表性的做法是你亲自写一个新类(并不要求马上做)。幸运的是,SimpleTest有一种容易的手段来实现 Mock::generate() 方法。

在上面的例子中,这种手段创建了一个名为MockAccumulator的类来响应所有Accumulator类的方法。另外,伪累加的动作还有其他手段来操作伪对象自身的实例。例如 setReturnValue()。给出一个方法名和一个值,

setReturnValue()就可以改变伪对象而给出对应方法所调用的值。因此,这条语句$amount->setReturnValue(‘total’, 200)返回200而不论何时调用了total()方法。

一旦进行完初始化工作后,你可以传递MockAccumulator类到calc_tax()函数来演示一个在真实的Accumulator对象空间中的动作。

如果你止步于此——即用一个对象来返回所调用函数的“封装”响应——你只是使用了ServerStub模式。 用伪对象来验证方法的调用不限于此,因为它可以不限次序与次数。

下面是一个通过对象来验证“数据流”的例子:


class MockObjectTestCase extends UnitTestCase { 
// ... 
function testCalcTax() { 
$amount =& new MockAccumulator($this); 
$amount->setReturnValue(‘total’,200); 
$amount->expectOnce(‘total’); 
$this->assertEqual( 
14, calc_tax($amount)); 
$amount->tally(); 




这里expectOnce()方法使用了一个字符串,它包含你想调用的方法名 。而tally()实际上用来检查你的想法是否实现。这里,如果MockAccumulator::total()只调用一次或不调用,测试将失败。

在很多情况下你可以使用伪对象的”跟踪”特性。例如,如果你传递一个具有三个值的数组到calc_total(),Accumulator::add()是否也如你所想的调用了三次呢?


class MockObjectTestCase extends UnitTestCase { 
// ... 
function testCalcTotalAgain() { 
$sum =& new MockAccumulator($this); 
$sum->expectOnce(‘add’); 
calc_total(array(1,2,3), $sum); 
$sum->tally(); 




那,这里发生了什么?传递调用的测试失败。SimpleTest的错误消息如下所示:


MockObject PHP4 Unit Test 
1) Expected call count for [add] was [1] got [3] at line [51] 
in testcalctotalagain in mockobjecttestcase 
FAILURES!!! 
Test cases run: 1/1, Passes: 2, Failures: 1, Exceptions: 0 


错误消息指出了尽管add() 方法被调用三次,但expectOnce()却一次也没用到。取代expectOnce()的可行方法是使用expectCallCount()。


class MockObjectTestCase extends UnitTestCase { 
// ... 
function testCalcTotalAgain() { 
$sum =& new MockAccumulator($this); 
$sum->expectCallCount(‘add’, 3); 
calc_total(array(1,2,3), $sum); 
$sum->tally(); 




伪对象扮演了一个演员的角色——这个角色由SeverStub提供合理的测试数据来响应方法的调用——并且作为一个评判的角色,验证所调用的方法是否符合预想。

重构已有程序

下面让我们用伪对象来帮助重构一个已有程序。考虑一个简单的脚本,它可以模拟你在无数的PHP程序中所期望的行为:例如一个当检查到你未登录时要求登录的页面;与此类似的还有表单处理页面;它能在成功登录后显示不同内容并提供登出的功能。 让我们写一个这样的页面。首先,对还未登录的用户显示一个登录表单。


<html> 
<body> 
<form method=”post”> 
Name:<input type=”text” name=”name”> Password:<input type=”password” name=”passwd”> 
<input type=”submit” value=”Login”> 
</form> 
</body> 
</html> 


接着,显示登录成功后的内容:


<html> 
<body>Welcome <?php echo $_SESSION[‘name’]; ?> 
<br>Super secret member only content here. 
<a href=”<?php echo SELF; ?>?clear”>Logout</a> 
</body> 
</html> 


加入表单处理的功能,session(会话)开始,还有登出的功能,整体看起来应该类似这样:


session_start(); 
define(‘SELF’, 
‘http://’.$_SERVER[‘SERVER_NAME’].$_SERVER[‘PHP_SELF’]); 
if (array_key_exists(‘name’, $_REQUEST) 
&& array_key_exists(‘passwd’, $_REQUEST) 
&& ‘admin’ == $_REQUEST[‘name’] 
&& ‘secret’ == $_REQUEST[‘passwd’]) { 
$_SESSION[‘name’] = ‘admin’; 
header(‘Location: ‘.SELF); 

if (array_key_exists(‘clear’, $_REQUEST)) { 
unset($_SESSION[‘name’]); 

if (array_key_exists(‘name’, $_SESSION) 
&& $_SESSION[‘name’]) { ?> 
<html> 
<body>Welcome <?=$_SESSION[‘name’]?> 
<br>Super secret member only content here. 
<a href=”<?php echo SELF; ?>?clear”>Logout</a> 
</body> 
</html> <?php 
} else { ?> 
<html> 
<body> 
<form method=”post”> 
Name:<input type=”text” name=”name”> Password:<input type=”password” name=”passwd”> 
<input type=”submit” value=”Login”> 
</form> 
</body> 
</html> <?php 
}
 
重构这个程序的一个目的应该是使其成为一个“易于测试”的程序。基于这个目的,如果你还选择一些PHP中的方便特性——如超级全局变量——你将失去测试上的简洁性。 

例如,如果你直接就用了$_SESSION,即意味着只有一种途径可以测试这个代码,就是改变$_SESSION。如果你忘了将$_SESSION改回先前已知的状态,各种测试间就会互相干扰。

一个好的解决方法是封装$_SESSION到另一个类中,传递所封装类的实例到任何想要访问$_SESSION的对象。如果你创建了一个已封装对象的伪对象用于测试,你能够完全控制对象对所调用方法的响应(就像ServerStub那样)并且你能核实它是如何调用的(那正是创建伪对象的目的)。

具备了这个思想,让我们看看如何封装$_SESSION之类的全局变量。 


class Session { 
function Session() { 
$this->init(); 

function init() { 
if (!isset($_SESSION)) { 
if (headers_sent()) { 
trigger_error( 
‘Session not started before creating session object’); 
} else { 
session_start(); 



function isValid($key) { 
return array_key_exists($key, $_SESSION); 

function get($key) { 
return (array_key_exists($key, $_SESSION)) 
? $_SESSION[$key] 
: null; 

function set($key, $value) { 
$_SESSION[$key] = $value; 

function clear($key) { 
nset($_SESSION[$key]); 

}
 
类Session封装了全局变量$_SESSION。对类SESSION的测试非常类似于对前期的已注册的类的改良测试(参见第5章),但是却无任何通过参数获得或设置相应值的意图。


你也许注意到了构造函数调用了Session::init()方法。为什么这个方法不是构造函数的一部分呢?这样分开的好处是你能静态调用它并确保session已经开始。下面是一个如何使用该类的例子。


Session::init(); 
$page =& new PageDirector(new Session); 



大部分测试方面的文献很推崇伪对象并建议你亲自写一个。如果你打算那样做,开始测试时你就只需要充实那些你需要的方法就可以了。譬如,一个用于处理代码的ServerStub的Session类很可能是这样的:


class MyMockSessionUser1 { 
function isValid($key) { 
return (‘user_id’ == $key) ? true : false; 

function get($key) { 
if (‘user_id’ == $key) { 
return 1; 






幸运的是,你可以用SimpleTest来避免那些易范的错误。Mock::generate()方法允许你创建一个类来实例化或动态地配置你想要的结果。

注:伪对象技术

SimpleTest所使用的方法仅是伪对象的多种用法之一。伪对象的代码传递是另一种。随着PHP5的到来,你也许能看到伪对象以对象中的__call()方法来执行。

以下是如何用SimpleTest生成的伪对象来测试并重构MyMockSessionUser1类(如上例中)。 


Mock::Generate(‘Session’); 
class PageDirectorTestCase extends UnitTestCase { 
function testSomethingWhichUsesSession() { 
$session =& new MockSession($this); 
$session->setReturnValue(‘isValid’, true); 
$session->setReturnValue(‘get’, 1); 
// ... 





更进一步说,你能随心所欲的设置何种方法被调用以及调用多少次。你甚至可以验证那些根本不该被调用的方法。 
下面是一个扩展型的测试,它用来建立和验证那些复杂的设计。


class PageDirectorTestCase extends UnitTestCase { 
function testSomethingWhichUsesSession() { 
$session =& new MockSession($this); 
$session->setReturnValue(‘isValid’, true); 
$session->setReturnValue(‘get’, 1); 
$session->expectOnce(‘isValid’, array(‘user_id’)); 
$session->expectOnce(‘get’, array(‘user_id’)); 
$session->expectNever(‘set’); 
// the actual code which uses $session 
$session->tally(); 





使用伪对象的原因很多,方法也多样化。但在我们继续前,让我们把另外的一些类加入进来,使其来龙去脉更加清楚。 
接下来的一部分是重构已有脚本,创建一个用于检查用户是否有相应权限的名为UserLogin的类。


class UserLogin { 
var $_valid=true; 
var $_id; 
var $_name; 
function UserLogin($name) { switch (strtolower($name)) { case ‘admin’: 
$this->_id = 1; 
$this->_name = ‘admin’; 
break; 
default: 
trigger_error(“Bad user name ‘$name’”); 
$this->_valid=false; 


function name() { 
if ($this->_valid) return $this->_name; 

function Validate($user_name, $password) { 
if (‘admin’ == strtolower($user_name) 
&& ‘secret’ == $password) { 
return true; 

return false; 


(在一个实际的程序中,你应当按照如上所示的逻辑来查询相应的数据表,这种小而且编写起来费神的类体现了你将如何运用ServerStub来组织代码———ServerStub是一个小型的表达你想法的类,但它只是在一些限制环境下可用。)

 
 1#楼  
 
  回复人:lxshark
  注册时间:2009-08-06
  主题/回复:49/50
  积分:639
  等级:★★★(六级)
  称号:声名鹊起

   
 1#楼 发表于2009-08-08 21:36:11  评分:× 

回复给楼主(lxshark)
希望能对你们更好的学习PHP有所帮助,也期望大家有时间多光顾我的
点此打开链接:http://hi.baidu.com/mythicsky
  页数1/1首页 « 1 » 末页
  发表回复:您还没有登陆,无法发表回复。请先[登陆]

一起PHP技术联盟 主办:一起PHP 联系方式:站长QQ:4304410 QQ群:8423742 20159565 站长博客 E-mail: nqp@nqp.me 执行时间:0.021sec
SimsmaBBS 2008 (v6.0) Developed by 17php.com,Copyright(C)2003-2010 All rights reserved. 副本授权:一起PHP官方专用版