Алексей Гоголев
Value Object Pattern в PHP
Что такое Value Object?
Сначала разберемся, что такое value object. Value object — это объект, который моделирует типы данных, отсутствующие в языке. Примерами value object могут служить такие вещи как числа, даты, деньги. Любой value object, должен содержать метод, обеспечивающий проверку эквивалентности двух экземпляров объекта. То есть, если value object является представлением доллара, то пять долларов (представленные в первом экземпляре объекта) должны быть равны пяти долларам (представленным во втором экземпляре объекта).
Пример Value Object
Построим Value Object для денег. В коде приведен сам Value Object и пример его использования.
<?php
class BadDollar
{
protected $_amount; //на php4 - "$_amount;"
function BadDollar($amount)
{
$this->_amount = (float)$amount;
}
function getAmount()
{
return $this->_amount;
}
function add($dollar)
{
$this->_amount += $dollar->getAmount();
}
function equals ($otherBadDollar)
{
if (get_class($this) != get_class($otherBadDollar) ) return false;
return ($this->getAmount() == $otherBadDollar->getAmount()) ;
}
}
class OneDay
{
protected $_money; // на php4 - "$_money;"
function OneDay()
{
$this->_money = new BadDollar(200);
}
function payDay()
{
return $this->_money;
}
}
class Person
{
var $wallet;
}
function test_BadDollar()
{
$day = new OneDay;
$p1 = new Person; // это первый клиент
$p2 = new Person; // а это второй
$p1->wallet = $day->payDay();
// теперь у 1-го клиента на кошельке 200$
echo "p1 wallet =".$p1->wallet->getAmount()."n";
// вывели баланс 1-го клиента
$p2->wallet = $day->payDay();
// теперь и у 2-го клиента на кошельке 200$
echo "p2 wallet =".$p2->wallet->getAmount()."t";
// вывели баланс 2-го клиента
echo $p1->wallet->equals($p2->wallet);
//вывели, равны ли сбережения $p1 сбережениям $p2
$p1->wallet->add($day->payDay());
// положили 1-му клиенту 200$ на счет
echo "p1 wallet =".$p1->wallet->getAmount()."n";
// вывели баланс 1-го клиента
echo "p2 wallet =".$p2->wallet->getAmount()."n";
// вывели баланс 2-го клиента
echo $p1->wallet->equals($p2->wallet);
//вывели, равны ли сбережения $p1 сбережениям $p2
$payday = $day->payDay();
echo "payDay =".$payday->getAmount()."n";
//вывели сумму, которая начисляется каждый день
// (напомню, она равна 200$ по теории)
}
test_BadDollar();
?>
Где можно сделать ошибку?
Итак, есть три класса с помощью, которых реализуется начисление клиенту 200$.
Если используется php4 — данный пример работает четко. А вот с php5 этот код не будет работать корректно.
Вот что выдал нам php4:
Тут все хорошо. Сумма, которая начисляется каждый день, равна 200$. Изначально мы положили и первому и второму клиенту 200$ на счет. После добавили первому еще 200$. Таким образом: у первого клиента (p1) сейчас 400$, у второго (p2) 200$, и ежедневная сумма пополнения (payDay) не изменилась — 200$.
А вот результат полученный с php5:
Вот тут и стало ясно, почему с php5 этот код не работает корректно. Видь деньги начислялись только первому клиенту, а изменилась и сумма на кошельке второго, и (самое ужасное!) — изменилась и сумма ежедневного пополнения счета. И теперь, если вовремя не исправить ошибку, каждый день всем клиентам будет начисляться не 200$, а 400$ (то-то они обрадуются).
В чем причина? Переменные $day::_money, $p1::wallet, $p2::wallet, поля трех идейно разных объектов, ссылаются на один и тот же экземпляр класса BadDollar. Таким образом, при начислении первому клиенту денег, изменяется поле экземпляра класса BadDollar, а значит меняются и $day::_money и $p2::wallet.
Как не допустить ошибку?
Php4 обращается с value objects как и подобает обращаться с value objects, т.к. в четвертой версии оператор «=» делает копию объекта, если пропустить оператор ссылки «&». Таким образом, в случае если код пишется на php4 необходимо просто не ставить амперсанды («&»), и тогда код будет верным. Так и было сделано в примере выше. Как быть, в случае с php5? Посмотрите внимательно на исправленный код и все сразу станет ясно. Исправлено только две строчки — они отмечены комментариями.
<?php
class Dollar
{
protected $_amount;
function Dollar($amount)
{
$this->_amount = (float)$amount;
}
function getAmount()
{
return $this->_amount;
}
function add($dollar)
{
return new Dollar($this->_amount + $dollar->getAmount());
// раньше тут было: "$p1->wallet->add($day->payDay());"
}
function equals ($otherBadDollar)
{
if (get_class($this) != get_class($otherBadDollar) ) return false;
return ($this->getAmount() == $otherBadDollar->getAmount()) ;
}
}
class OneDay
{
protected $_money;
function OneDay()
{
$this->_money = new Dollar(200);
}
function payDay()
{
return $this->_money;
}
}
class Person
{
var $wallet;
}
function test_Dollar()
{
$day = new OneDay;
$p1 = new Person;
$p2 = new Person;
$p1->wallet = $day->payDay();
echo "p1 wallet =".$p1->wallet->getAmount()."n";
$p2->wallet = $day->payDay();
echo "p2 wallet =".$p2->wallet->getAmount()."t";
echo $p1->wallet->equals($p2->wallet);
//вывели, равны ли сбережения $p1 сбережениям $p2
$p1->wallet = $p1->wallet->add($day->payDay());
// раньше тут было: "$this->_amount += $dollar->getAmount();"
echo "p1 wallet =".$p1->wallet->getAmount()."n";
echo "p2 wallet =".$p2->wallet->getAmount()."n";
echo $p1->wallet->equals($p2->wallet);
//вывели, равны ли сбережения $p1 сбережениям $p2
$payday = $day->payDay();
echo "payDay =".$payday->getAmount()."n";
}
test_Dollar();
?>
Исправленный код выдал правильный результат результат:
Неизменные (immutable) Value Objects
Бывают случаи, когда необходимо, чтобы value-объект оставался неизменным на протяжении всей работы приложения (например, деньги которые начисляются каждый месяц – неизменны, если их случайно поменять, то начнутся проблемы). В таком случае нужно защитить «неизменное» поле как можно надежней. Вот три шага для создания «неизменного» value object:
1. Защитить поля от прямого доступа извне (объявить как protected).
2. Возможность выбрать значение защищенных полей только при инициализации.
3. Не создавать функций типа “setter”, или других функций позволяющих изменить значение защищенных полей.
Заключение
Еще раз повторюсь, что Value Object, используется для создания типов данных (деньги в нашем случае), отсутствующих в языке. Для того, чтоб можно было как-то оперировать экземплярами Value-класса, создается метод ->equals(). С помощью этого метода можно сравнивать экземпляры класса, и таким образом строить логику приложения.
Вот и все. Надеюсь, эта статья поможет вам успешно применять value-объекты в вашей работе.
ссылки по теме:
1. php architect’s Guide to PHP Design Patterns by Jason E. Sweat
2. ValueObject at Portland Pattern Repository's Wiki
Последние комментарии:
Статья переработана | Алексей Гоголев |
---|---|
2 Bolk, mux Вы меня убедили. Статью переработал. |
|
+1 | mux |
Полностью согласен с BOLK. Вообще, свойство immutable не самое главное, можно и без него неплохо обходиться, цель создания ValueObject — в том, чтобы можно было выразить в виде объекта отсутствующие в языке конкретные типы данных. Причём с деньгами хороший пример, но смысл не в наличии метода getAmount, а в наличии метода equals. Можно его хоть вот так написать:
<?php Но он должен быть внутри, чтобы внешние пользователи класса не брали на себя ответственность за разбор его внутренностей. |
|
Обсудить (комментариев: 2)