当前位置: 首页 > 图文教程 > 网络编程 > PHP > PHP系列教程:设计模式介绍Ⅱ值对象模式

PHP
php 多线程上下文中安全写文件实现代码
PHP类的使用 实例代码讲解
用php实现让页面只能被百度gogole蜘蛛访问的方法
php 学习笔记
PHP编程过程中需要了解的this,self,parent的区别
php 操作excel文件的方法小结
使用PHP获取网络文件的实现代码
PHP 巧用数组降低程序的时间复杂度
php下将XML转换为数组
php 文件上传代码(限制jpg文件)
php 无极分类(递归)实现代码
PHP 采集获取指定网址的内容
PHP 将图片按创建时间进行分类存储的实现代码
PHP 存储文本换行实现方法
PHP 批量更新网页内容实现代码
用PHP查询搜索引擎排名位置的代码
用php实现的获取网页中的图片并保存到本地的代码
php实现首页链接查询 友情链接检查的代码
处理php自动反斜杠的函数代码
php实现的遍历文件夹下所有文件,编辑删除

PHP系列教程:设计模式介绍Ⅱ值对象模式


出处:互联网   整理: 软晨网(RuanChen.com)   发布: 2009-11-03   浏览: 153 ::
收藏到网摘: n/a

《PHP设计模式介绍》第二章 值对象模式

在所有的最简单的程序中,大多数对象都有一个标识,一个重要的商业应用对象,例如一个Customer或者一个SKU,有一个或者更多的属性--- id,name,email地址,这样可以把它从同一个类的其他实例区分开来。此外,对象有一个恒定的标识:它是贯穿于整个应用程序的一个唯一的标识,对于程序员来说,”customer A”在任何地方就是”customer A”,并且只要你的程序在持续运行时"customer A"仍然是"customer A"。 但是一个对象不需要有一个标识。有些对象仅仅是为了描述其他对象的属性。

例如:通常用一个对象描述一个日期、一个数字或者货币。日期、整数或美元的类定义是都是便于使用的、快捷、便于封装的,并且方便进行拷贝,相互比较,甚至是创建。

从表面上看,这些描述简单的对象很容易被执行:它们的语句非常少,在构造类时无论是应用于Customer还是SKU都没有什么不同。这个想法似乎是正确的,但是所谓的"似乎正确"很容易产生一些bug。

请看下面的代码,这是一个关于以美元给员工发放工资的对象的定义和执行操作。多数情况下,它的运行是没有问题的。(这个类被命名为BadDollar,因为它还存在着bug)。考虑一下,看你是否能发现它的bug。

// PHP5
class BadDollar {
protected $amount;
public function __construct($amount=0) {
$this->amount = (float)$amount;
}
public function getAmount() {
return $this->amount;
}
public function add($dollar) {
$this->amount += $dollar->getAmount();
}
}

 

class Work {
protected $salary;public function __construct() {
$this->salary = new BadDollar(200);}
public function payDay() {
return $this->salary;
}
}
class Person {
public $wallet;
}

 

function testBadDollarWorking() {
$job = new Work;
$p1 = new Person;
$p2 = new Person;
$p1->wallet = $job->payDay();
$this->assertEqual(200, $p1->wallet->getAmount());
$p2->wallet = $job->payDay();
$this->assertEqual(200, $p2->wallet->getAmount());
$p1->wallet->add($job->payDay());
$this->assertEqual(400, $p1->wallet->getAmount());
//this is bad — actually 400
$this->assertEqual(200, $p2->wallet->getAmount());
//this is really bad — actually 400
$this->assertEqual(200, $job->payDay()->getAmount());
}

那么, bug是什么呢?如果不能上面的代码例子中直观地发现问题,这里有个提示:雇员对象$p1和对象$p2使用着同一个BadDollar对象实例。

首先,类Work和类Person的实例已经创建。那么,假设每一个雇员最初有一个空的电子钱包,雇员的电子钱包Person:wallet是通过Work::payDay()函数返回的对象资源变量赋值的,所以被设定为一个BadDollar类的对象实例。

还记得PHP5的对象赋值处理方式吗?因为PHP5的对象赋值的处理方式,所以$job::salary,、$p1::wallet和$p2::wallet这三个看上去不同的对象实例虽然使用着不同的“标识符”,但是事实上,它们全部都指定到同一个对象实例。

因此,接下来的发放工资的操作(PayDay表示发放工资的日子,这里表示发放工资的动作),使用$job->payDay()本来仅仅是想增加$P1的工资,却出乎意料地次给$P2也发放了。并且,这个动作还改变了工作的基本工资的额度。因此,最后两个值的检测报错。

Value Object PHP5 Unit Test
1) Equal expectation fails because [Integer: 200] differs from [Float: 400] by 200
in testBadDollarWorking
in ValueObjTestCase
2) Equal expectation fails because [Integer: 200] differs from [Float: 400] by 200
in testBadDollarWorking
in ValueObjTestCase
FAILURES!!!

问题

那么,你该如何为Date或Dollar这样一些描述简单的应用定义一个高效的类,并且易于创建呢。

解决方案

高效的对象应该像PHP的整型那样运作:如果你把同一个对象资源赋值给两个不同的变量,然后改变其中的一个变量,另一个变量仍然不受影响。事实上,这就是Value Object模式的目标所在。

执行Value Object时,php4和php5是有区别的。

正如以上你所看到的,PHP5通过new进行对象资源的赋值传递的是对象资源的指针就像我们在PHP4中通过指针传递一样。很明显,这是一个问题。为了解决那个问题并实现一个专有对象Dollar的值,我们必须使属性$amount的对象的所有属性的一个值在一般情况下不可变或不能改变。但是在 PHP语言的没有提供参数不可改变的功能的情况下,你完全可以结合属性的可见性与获得和设置方法来实现。

 相反地,PHP4操作所有的对象都是遵循Value Objects对象规律的,因为PHP4的赋值操作相当于对对象做了一个拷贝。所以为了在PHP4中实现Value Objects设计模式你需要打破你细心地培养的通过指针赋值来创建、传递、提取对象的习惯。

注:术语 不可变的(Immutable):

在词典中Immutable的定义是不允许或不易受影响。在编程中,这个术语表示一个一旦被设置就不能改变的值。

PHP5 样本代码:

既然我们开始用PHP5编写代码,让我们优化一个PHP5的Value Object的实例并创建一个较好的Dollar类定义。命名在面向对象编程中非常重要,选择一个唯一的货币类型作为这个类的名字,说明它不被定义为可以处理多种货币类型的类。

class Dollar {
protected $amount;
public function __construct($amount=0) {
$this->amount = (float)$amount;
}
public function getAmount() {
return $this->amount;
}
public function add($dollar) {
return new Dollar($this->amount + $dollar->getAmount());
}
}

类里面的属性如果加上protected前缀,别的类是访问不了的。protected(和private)拒绝通过属性直接被访问。

通常,当你使用面向对象进行编程的时候,你经常需要创建了一个“setter”函数,就类似于:

public setAmount($amount)
{
$this->amount=$amount;
}

一样,在这种情况下,虽然没有设定函数Dollar::amount(),但在对象的实例化期时,参数Dollar::amount就已经被赋值了。而函数Dollar::getAmount()只是提供一个访问Dollar属性的功能,在这里访问的数据类型为浮点型。

最有趣的变化是在Dollar::add()方法函数中。并不是直接改变$this->amount变量的值从而会改变已存在的Dollar 对象实例,而是创建并返回一个新的Dollar实例。现在,尽管你指定当前对象给多个变量,但是每一个变量的变化都不会影响其它的变量实例。

对于价值设计模式不变性是关键,任何对于一个Value Object的变量amount的改变,是通过创建一个新的带有不同预期值的类的实例来完成的。上文中提高的最初那个$this->amount变量的值从未改变。

简单来说,在PHP5里面使用价值设计模式时,需要注意以下几个方面:

1. 保护值对象的属性,禁止被直接访问。
2. 在构造函数中就对属性进行赋值。
3. 去掉任何一个会改变属性值的方式函数(setter),否则属性值很容易被改变。

以上三步创建了一个不变的值,这个值一旦被初始化设置之后就不能被改变。当然,你也应该提供一个查看函数或者是访问Value Object的属性的方法,并且可以添加一些与这个类相关的函数。值对象并不是只能用在一个简单的架构上,它也可以实现重要的商务逻辑应用。让我们看看下一个例子:

详细例子

让我们在一下更加复杂的例子中查看值对象模式的功能。

让我们开始实现一个的基于PHP5中Dollar类中的一个Monopoly游戏。

第一个类Monopoly的框架如下:

class Monopoly {
protected $go_amount;
/**
* game constructor
* @return void
*/
public function __construct() {
$this->go_amount = new Dollar(200);
}
/**
* pay a player for passing 揋o?/span>
* @param Player $player the player to pay
* @return void
*/
public function passGo($player) {
$player->collect($this->go_amount);
}
}

目前,Monopoly的功能比较简单。构造器创建一个Dollar类的实例$go_amount,设定为200,实例go_amount常常被 passtGo()函数调用,它带着一个player参数,并让对象player的函数collect为player机上200美元.

Player类的声明请看下面代码,Monoplay类调用带一个Dollar参数的Player::collect()方法。然后把Dollar 的数值加到Player的现金余额上。另外,通过判断Player::getBalance()方法函数返回来的余额,我们可以知道使访问当前 Player和Monopoly对象实例是否在工作中。

class Player {
protected $name;
protected $savings;
/**
* constructor
* set name and initial balance
* @param string $name the players name
* @return void
*/
public function __construct($name) {
$this->name = $name;
$this->savings = new Dollar(1500);
}
/**
* receive a payment
* @param Dollar $amount the amount received
* @return void
*/
public function collect($amount) {
$this->savings = $this->savings->add($amount);
}
* return player balance
* @return float
*/
public function getBalance() {
return $this->savings->getAmount();
}
}

上边已经给出了一个Monopoly和Player类,你现在可以根据目前声明的几个类定义进行一些测试了。

MonopolyTestCase的一个测试实例可以像下面这样写:

class MonopolyTestCase extends UnitTestCase {
function TestGame() {
$game = new Monopoly;
$player1 = new Player(‘Jason’);
$this->assertEqual(1500, $player1->getBalance());
$game->passGo($player1);
$this->assertEqual(1700, $player1->getBalance());
$game->passGo($player1);
$this->assertEqual(1900, $player1->getBalance());
}
}

如果你运行MonopolyTestCase这个测试代码,代码的运行是没有问题的。现在可以添加一些新的功能。

另一个重要的概念是对象Monopoly中的租金支付。让我们首先写一个测试实例(测试引导开发)。下面的代码希望用来实现既定的目标。

function TestRent() {
$game = new Monopoly;
$player1 = new Player(‘Madeline’);
$player2 = new Player(‘Caleb’);
$this->assertEqual(1500, $player1->getBalance());
$this->assertEqual(1500, $player2->getBalance());
$game->payRent($player1, $player2, new Dollar(26));
$this->assertEqual(1474, $player1->getBalance());
$this->assertEqual(1526, $player2->getBalance());
}

根据这个测试代码,我们需要在Monopoly对象中增加payRent()的方法函数来实现一个Player对象去支付租金给另一个Player对象

Class Monopoly {
// ...
/**
* pay rent from one player to another
* @param Player $from the player paying rent
* @param Player $to the player collecting rent
* @param Dollar $rent the amount of the rent
* @return void
*/
public function payRent($from, $to, $rent) {
$to->collect($from->pay($rent));
}
}

payRent()方法函数实现了两个player对象之间($from和$to)的租金支付。方法函数Player::collect()已经被定义了,但是Player::pay()必须被添加进去,以便实例$from通过pay()方法支付一定的Dollar数额$to对象中。首先我们定义 Player::pay()为:

class Player {
// ...
public function pay($amount) {
$this->savings = $this->savings->add(-1 * $amount);
}
}

但是,我们发现在PHP中你不能用一个数字乘以一个对象(不像其他语言,PHP不允许重载操作符,以便构造函数进行运算)。所以,我们通过添加一个debit()方法函数实现Dollar对象的减的操作。

class Dollar {
protected $amount;
public function __construct($amount=0) {
$this->amount = (float)$amount;
}
public function getAmount() {
return $this->amount;
}
public function add($dollar) {
return new Dollar($this->amount + $dollar->getAmount());
}
public function debit($dollar) {
return new Dollar($this->amount - $dollar->getAmount());
}
}

引入Dollar::debit()后,Player::pay()函数的操作依然是很简单的。

class Player {
// ...
/**
* make a payment
* @param Dollar $amount the amount to pay
* @return Dollar the amount payed
*/
public function pay($amount) {
$this->savings = $this->savings->debit($amount);
return $amount;
}
}

Player::pay()方法返回支付金额的$amount对象,所以Monopoly::payRent()中的语句$to-> collect($from->pay($rent))的用法是没有问题的。这样做的话,如果将来你添加新的“商业逻辑”用来限制一个player 不能支付比他现有的余额还多得金额。(在这种情况下,将返回与player的账户余额相同的数值。同时,也可以调用一个“破产异常处理”来计算不足的金额,并进行相关处理。对象$to仍然从对象$from中取得$from能够给予的金额。)

注:术语------商业逻辑

在一个游戏平台的例子上提及的“商业逻辑”似乎无法理解。这里的商业的意思并不是指正常公司的商业运作,而是指因为特殊应用领域需要的概念。请把它认知为 “一个直接的任务或目标”,而不是“这里面存在的商业操作”。

所以,既然目前我们讨论的是一个Monopoly,那么这里的 “商业逻辑”蕴含的意思就是针对一个游戏的规则而说的。

PHP4样本代码

和PHP5不一样的是,PHP4赋值对象资源的时候是拷贝该对象,这个语法的特点本质上和值对象设计模式要求正好吻合。

然而,PHP4不能控制的属性和方法函数在对象之外的可见性,所以实现一个值对象设计模式相对PHP5也有细微的差别。

假如你回想一下这本书序言中的“对象句柄”部分,它提出了三个 “规则”,当你在PHP4中使用对象去模仿PHP5中的对象句柄时,这三个规则总是适用的:

  1. 通过指针($obj=&new class;)来创建对象。
  2. 用指针(function funct(&$obj) param{})来传递对象。
  3. 用指针(function &some_funct() {} $returned_obj =& some_funct())来获取一个对象。

然后,值对象设计模式却不能使用上述三个“总是适用”的规则。只有忽视了这些规则,才能总是得到一个PHP4对象的拷贝(这相当于PHP5中的“克隆”操作,描述在http://www.php.net/manual/en/language.oop5.cloning.php)

因为PHP4可以轻松地赋值一个对象—这在PHP语言中是一个固有的行为,所以实现变量的不可更改就需要通过值对象通用协定来实现。在PHP4中,如果要使用值对象,请不要通过指针来创建或获取一个对象,并且给所有需要保护以免外界修改的属性或者方法函数命名时,都在属性和方法函数的名字加上下划线(_)做前缀。按照协定,变量如果具有值对象的属性,应该使用一个下划线来标识它的私有性。

下面是PHP4中的Dollar类

// PHP4
class Dollar {
var $_amount;
function Dollar($amount=0) {
$this->_amount = (float)$amount;
}
function getAmount() {
return $this->_amount;
}
function add($dollar) {
return new Dollar($this->_amount + $dollar->getAmount());
}
function debit($dollar) {
return new Dollar($this->_amount - $dollar->getAmount());
}
}

下面这个实例可以说明,你不能在PHP4中限制一个属性只能被外部更改:

function TestChangeAmount() {
$d = new Dollar(5);
$this->assertEqual(5, $d->getAmount());
//only possible in php4 by not respecting the _private convention
$d->_amount = 10;
$this->assertEqual(10, $d->getAmount());
}

再重复一次,在所有PHP4对象中,私有变量的前缀使用一个下划线,但是你还是可以从外部来直接访问私有属性和方法函数。

值对象中的商业逻辑

值对象(Value Objects)不仅仅用于最小限度的访问方法这样的简单的数据结构,它同样还可以包括有价值的商业逻辑。考虑以下你如果实现许多人中平均分配金钱。

如果总钱数确实是可以分成整数,你可以生成一组Dollar对象,而且每一个Dollar对象都拥有相同的部分。但是当总数可以整数的美元或者美分的时候,我们该怎么处理呢?

让我们开始用一个简单的代码来测试一下:

// PHP5
function testDollarDivideReturnsArrayOfDivisorSize() {
$full_amount = new Dollar(8);
$parts = 4;
$this->assertIsA(
$result = $full_amount->divide($parts)
,’array’);
$this->assertEqual($parts, count($result));
}

注释 assertIsA:

assertIsA()的作用是让你测试:一个特定的变量是否属于一个实例化的类。当然你也可以用它来验证变量是否属于一些php类型:字符串、数字、数组等。

为了实现上述测试, Dollar::divide()方法函数的编码如下…

public function divide($divisor) {
return array_fill(0,$divisor,null);
}

最好加上更多的细节。

function testDollarDrivesEquallyForExactMultiple() {
$test_amount = 1.25;
$parts = 4;
$dollar = new Dollar($test_amount*$parts);
foreach($dollar->divide($parts) as $part) {
$this->assertIsA($part, ‘Dollar’);
$this->assertEqual($test_amount, $part->getAmount());
}
}

现在,应当返回存有正确数据的Dollar对象,而不是简单的返回数量正确的数组。

实现这个仍然只需要一行语句:

public function divide($divisor) {

return array_fill(0,$divisor,new Dollar($this->amount / $divisor));

最后一段代码需要解决一个除数不能把Dollar的总数均匀的除开的问题。

这是一个棘手的问题:如果存在不能均匀除开的情况,是第一部分还是最后一部分能得到一个额外的金额(便士)?怎样独立测试这部分的代码?

一个方法是:明确指定代码最后需要实现目标:这个数组的元素数量应该是与除数表示的数量相等的,数组的元素之间的差异不能大于0.01,并且所有部分的总数应该与被除之前的总数的值是相等的。

上面的描述通过正如下面的代码实现:

function testDollarDivideImmuneToRoundingErrors() {
$test_amount = 7;
$parts = 3;
$this->assertNotEqual( round($test_amount/$parts,2),
$test_amount/$parts,
’Make sure we are testing a non-trivial case %s’);
$total = new Dollar($test_amount);
$last_amount = false;
$sum = new Dollar(0);
foreach($total->divide($parts) as $part) {
if ($last_amount) {
$difference = abs($last_amount-$part->getAmount());
$this->assertTrue($difference <= 0.01);
}
$last_amount = $part->getAmount();
$sum = $sum->add($part);
}
$this->assertEqual($sum->getAmount(), $test_amount);
}

注释 assertNotEqual:

当你要确保两个变量的值是不相同时,你可以用它来进行检验。这里面的值相同是PHP的”==”运算符进行判断的。任何情况下当你需要确保两个变量的值是不相同的时候,你就可以使用它。

现在根据上述代码,如果来构造Dollar::divide()方法函数呢?

class Dollar {
protected $amount;
public function __construct($amount=0) {
$this->amount = (float)$amount;
}
public function getAmount() {
return $this->amount;
}
public function add($dollar) {
return new Dollar($this->amount + $dollar->getAmount());
}
public function debit($dollar) {
return new Dollar($this->amount - $dollar->getAmount());
}
public function divide($divisor) {
$ret = array();
$alloc = round($this->amount / $divisor,2);
$cumm_alloc = 0.0;
foreach(range(1,$divisor-1) as $i) {
$ret[] = new Dollar($alloc);
$cumm_alloc += $alloc;
}
$ret[] = new Dollar(round($this->amount - $cumm_alloc,2));
return $ret;
}
}

这段代码可以正常运行,但是仍然有一些问题,考虑一下如果在testDollarDivide()的开始处改变$test_amount 为 0.02; $num_parts 为 5;这样的临界条件,或者考虑一下当你的除数不是一个整型数字,你该怎么做?

解决上边这些问题的方法是什么呢?还是使用测试导向的开发循环模式:增加一个需求实例,观察可能的错误,编写代码来生成一个新的实例进行运行,还有问题存在时继续分解。最后重复上述过程。