程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> PHP綜合 >> php教程:php設計模式介紹之值對象模式

php教程:php設計模式介紹之值對象模式

編輯: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中限制一個屬性只能被外部更改:

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對象,而不是簡單的返回數量正確的數組。

實現這個仍然只需要一行語句:

最後一段代碼需要解決一個除數不能把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;這樣的臨界條件,或者考慮一下當你的除數不是一個整型數字,你該怎麼做?

解決上邊這些問題的方法是什麼呢?還是使用測試導向的開發循環模式:增加一個需求實例,觀察可能的錯誤,編寫代碼來生成一個新的實例進行運行,還有問題存在時繼續分解。最後重復上述過程。

 

public function divide($divisor) {

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

 

// 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());
}
}

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved