2->The Value Object Pattern( forest预定)
时间:2006-03-27
来源:互联网
The Value
Object Pattern
In all but the simplest applications, most objects have an “identity.” An important business object,
such as a Customer or a SKU, will have one or more attributes―an ID, or a name and an email
address, say―that differentiate it from other instances of the same class. Moreover, an object with
an identity “persists”: it’s a singularity that exists across the entire application. To you, the programmer,
“Customer A” is “Customer A” everywhere, and changes to “Customer A” endure for as long as
your application is running.
But an object need not have an identity. Some objects merely describe the characteristics of other
objects.
For example, it’s common to use an object to represent a date, a number, or money. A Date,
Integer, or Dollar class is a handy―and inexpensive―encapsulation, easily copied, compared, or
created when needed.
At first blush, small descriptive objects may seem a cinch to implement: they’re just (tiny or small)
classes, no different in structure than a Customer or SKU. That’s almost right, but “almost right” leads to
bugs.
作者: PHPChina 发布时间: 2006-03-26
作者: Bantu 发布时间: 2006-03-28
作者: forest 发布时间: 2006-03-29
[ 本帖最后由 forest 于 2006-4-9 19:43 编辑 ]
作者: forest 发布时间: 2006-04-09
作者: forest 发布时间: 2006-04-09
作者: forest 发布时间: 2006-04-09
The Value Object Pattern 价值对象模式
Business Logic in ValueObjects价值对象中的商务逻辑
Immutable不可变的
Terminology ― Business Logic术语------商务逻辑
Value Objects价值对象
setter设置器
getters接收器
BankruptException破产异常
copy-by-value
Object Handles对象句柄
rules规则
nearly always近乎常规
clone克隆
Business Logic in ValueObjects价值对象中的商务逻辑
作者: forest 发布时间: 2006-04-12
在所有的最简单的程序中,大多数对象都有一个标识,一个重要的商业运行对象,例如一个Customer或者一
个SKU,有一个或者更多的属性---id,name,email地址,说明区分它要从同一个类的其他实例。此外,对象有一个
恒定的标识:它是贯穿于整个应用程序的一个唯一的标识,对于程序员来说,”customer A”在任何地方就是”
customer A”,并且只要你的程序在持续运行时"customer A"仍然是"customer A"。 但是一个对象不需要有一个
标识。有些对象几乎不描述其他对象的特征。
例如:通常用一个对象描述一个日期,一个数字或者货币。一个日期,整数或美元类是一个便于使用的,廉
价的,封装的类,当需要时易于拷贝,比较,创建。
乍一看,少量的对象描述似乎很容易实现:它们仅仅是小类,在构造类时与Customer或SKU相比较没有什么
不同。那似乎是正确的,但是"似乎正确"将导致bug。
40.The Value Object Pattern:
考虑下列一个美元的执行似乎认为正确的,(这个类被命名为BadDollar,因为它并不是一个理想的工具)。看
你是否能发现它的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());
}
-------------------------------------------------------------------------------------------------
41.The Value Object Pattern:
那么,什么是bug呢?如果测试的例子不能使问题更直观,这有个提示:employees的对象p1和对象p2共享同
一个BadDollar类。首先,类Work和类Person的实例已经创建。那么,假设每一个人最初有一个空的钱包,成员
变量Person:wallet是被Work::payDay()方法返回的设定为BadDollar类的对象。
还记得你的朋友PHP5对象柄么?正是由于它,$job::salary, $p1::wallet和$p2::wallet,三个概念不同的
对象使用不同的“标识”,事实上,它们全部涉及同一个对象.
因此,第二个付费时间,方法$job->payDay()本来仅仅是想增加P1的工资,却不小心的再次给P2付钱了从而
改变了$job(改工作)的基本$salary(工资),因此,最后两个声明错误。
--------------------------------------------------------------------------------------------------
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模式的目标所在。
[ 本帖最后由 forest 于 2006-4-12 11:49 编辑 ]
作者: forest 发布时间: 2006-04-12
正如以上你所看到的,PHP5的我们设法仿效PHP4的通过一个句柄一个范例来引用对象的方法是一个需要解
决的问题。为了解决那个问题并实现一个专有对象Dollar的值,使属性$amount的对象的所有属性的一个值在一
般情况下不可变或不能改变。但是PHP作为语言的工具没有提供不变性,你完全可以结合属性的可见性来获得和
设置方法来充分的模仿它。
相反地,PHP4对待所有的对象就像对待Value Objects对象一样,因为PHP4指定操作相当于当你忽略了引用
操作的时候对对象做了一个拷贝。为了在PHP4中实现Value Objects对象确实应该打破你细心地培养的通过引用
创建、传递、捕获对象的习惯。
术语(Terminology) 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)拒绝通过属性直接被访问。
The Value Object Pattern 43
通常地,当你使用面向对象编程风格,你就创建了一个“setter”函数就像:
public setAmount($amount)
{
$this->amount=$amount;
}
一样,在这种情况下,虽然没有设定函数Dollar::amount(),但在对象的实例化期间,函数Dollar::amount()就
已经被定义了。方法Dollar::getAmount()是一个提供访问Dollar对象的作为一个浮点型的amount变量的辅助方
法。
最有趣的变化是在Dollar::add()方法中。并不是通过改变$this->amount变量的值从而改变已存在的
Dollar实例的状态,而是这个方法创建并返回一个新的Dollar实例。现在,尽管你指定当前对象给多个变量,但
是每一个变量在任何其他变化中是独立的。
对于The Value Object Pattern不变性是关键,任何对于一个Value Object的变量amount的改变,是通过
创建一个新的带有不同预期值的类的实例来完成的。上文中的$this->amount变量从未改变。
简短的总结,PHP5里的The Value Object Pattern的基本特点是:
1.保护Value Object的属性禁止被直接访问。
2.在构造器中设定对象的属性。
3.提供一个没有设定的允许更改属性的函数。
以上三步创建了一个不变的值,这个值一旦被初始化设置之后就不能被改变。当然,你也应该提供一个接
收器或者是访问Value Object的属性的方法,并且提供一些与这个类有密切关系的函数。Value Object 也不必
是一个简单的结构,它也可以控制重要的商务逻辑。让我们看看下一个例子:
背景实例:
让我们浏览一下在语言环境中的一个最大的例子----价值对象模式(The Value Object Pattern),让我们
开始实现一个的基于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 “Go”
* @param Player $player the player to pay
* @return void
*/
public function passGo($player)
{
$player->collect($this->go_amount);
}
}
--------------------------------------------------------------------------------------------------
-
44.The Value Object Pattern
到目前为止,Monopoly是一个很小的类。构造器创建一个Dollar类的实例$go_amount,设定为200,实例
go_amount常常被passtGo()函数调用,它带着一个player参数,并让对象player的方法collect被200初始化.
Player类的声明于下面,Monoplay类调用带一个Dollar参数的Player::collect()方法。然后把Dollar的总
数加到Player的现金结算上,除了这一个方法外,让我们再加上一个Player::getBalance()方法使访问当前
Player和Monopoly类对象运行中的Player对象的保存当前可用的现款生效。
--------------------------------------------------------------------------------------------------
--
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();
}
}
------------------------------------------------------------------------------------------------
作者: forest 发布时间: 2006-04-12
上边已经给出了一个Monopoly和Player类,你现在应该能写出一个满足需求的一个测试实例了:
Monoploy的一个测试实例可以像下面这样写:
假如你运行Monoploy这个测试实例,你可能会遇到新的障碍,现在应该继续添加一些属性。
另一个重要的概念是对象Monopoly中的租金支付。让我们首先写一个测试实例(a la 扩展驱动的测试)为下
列一连串的代码来设置一个目标。
------------------------------------------------------------------------------------------------
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());
}
--------------------------------------------------------------------------------------------------
看看这个测试例子payRent()方法需要加到Monopoly对象中以便一个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()方法执行已经存在变量from和to,方法Player::collect()完成两个player对象之间的交互。
但是这个Player::pay()方法必须被添加,让实例from的pay()方法支付一个Dollar对象的总数到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;
}
}
--------------------------------------------------------------------------------------------------
作者: forest 发布时间: 2006-04-12
Player::pay()方法返回被支付的变量amount使$to->collect($from->pay($rent))中的Monopoly::payRent()方
法完全可以工作了。这样在将来你精制“商务逻辑”时帮助你不会付出比职员:工作更多的工资。(在这种情况
下,将返回与工作者相符合的工资,或许将会产生一个“破产异常”去计算已经修改了的支付金代替完整的总金
额. 变量to将仍然想从变量from工作者取得同样多的工资。)
术语------商务逻辑(Terminology ― Business Logic)
在对一个商务平台建模的时候,如果在上下文提到“商务逻辑”看起来是不完善的 。商务在这里并不是所
涉及到的从事商业的公司,而是一些领域中特殊应用需求的概念。就像“在这儿你的商务什么?”一样,考虑商
务的解释犹如“一个直接的任务或目标”。当然,垄断领域的特定问题与“商务逻辑”可能的涵义正好相同。
PHP4样本代码:
和PHP5不一样的是,PHP4的的copy-by-value复制对象的语法和价值对象设计模式是一样的。然而,PHP4不
支持可见的属性和方法,在PHP4中实现一个价值对象设计模式(The Value Object Pattern)同样有细微的差别。
假如你回想一下这本书序言中的“对象句柄”部分提出的三个“规则”到“接近常规”的应用。当你在
PHP4中使用对象工作时去模仿PHP5中的对象句柄。
1.通过参照($obj=&new class;)来创建对象。
2.参照(function funct(&$obj) param{})来传递对象。
3.参照(function &some_funct() {} $returned_obj =& some_funct())来获取一个对象。
The Value Object Pattern有相当多异常在这些规则中的“近乎常规”部分中,正是忽视了这些规则,正
如得到一个PHP4对象的拷贝(这相当于PHP5中的“克隆”操作,描述在
http://www.php.net/manual/en/language.oop5.cloning.php)
当PHP4生成对象的时候,它复制了原基类。----在语言中它继承了一个行为,不变性可以通过PHP4中创建
价值对象(Value Objects)的惯例来实现,而不是通过引用来创建或获取对象的值,并且所有以“private”为前
缀的属性和方法的使用一个下划线来命名。按照惯例,变量控制对象价值的属性应该使用一个下划线来标识它的
私有性。
下面是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对象中,私有变量的前缀使用一个下划线,来控制直接访问如私有特性和方法。
价值对象中的商务逻辑(Business Logic in ValueObjects):
价值对象(Value Objects)不必限制用最小限度的访问方法去访问简单的数据结构,它同样包括有价值的商
务逻辑,考虑到当你想在许多人中平均分配金钱的情况。如果总钱数确实是可分的,你可以用每一个相同的部分
返回一个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()函数让你测试一个已命名类的实例(或继承)的一个特定的变量。你也可以使用这种声明
去改变象string,number,或array这样的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);
}
---------------------------------------------------------------------------------------------
[ 本帖最后由 forest 于 2006-4-12 12:09 编辑 ]
作者: forest 发布时间: 2006-04-12
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;这样的临
界条件,或者考虑当你没有提供整数分隔器将会发生什么呢?
解决上边这些问题的方法是什么呢?使用测试导向的开发循环模式:增加一个实例,观察可能的错误,编写代码来生成一个新的实例,观
察是否还有问题存在。最后根据需要重复操作。
作者: forest 发布时间: 2006-04-12
热门阅读
-
office 2019专业增强版最新2021版激活秘钥/序列号/激活码推荐 附激活工具
阅读:74
-
如何安装mysql8.0
阅读:31
-
Word快速设置标题样式步骤详解
阅读:28
-
20+道必知必会的Vue面试题(附答案解析)
阅读:37
-
HTML如何制作表单
阅读:22
-
百词斩可以改天数吗?当然可以,4个步骤轻松修改天数!
阅读:31
-
ET文件格式和XLS格式文件之间如何转化?
阅读:24
-
react和vue的区别及优缺点是什么
阅读:121
-
支付宝人脸识别如何关闭?
阅读:21
-
腾讯微云怎么修改照片或视频备份路径?
阅读:28