值对象的价值
你知道为什么值对象不可变很重要吗?也许你对值对象的概念不太熟悉,但别担心,我会用最简单的方式解释一切。我们来赌一把吧!
什么是值对象?
首先,我们需要对值对象下一个定义。从维基百科中我们了解到,值对象是一个小的对象,它表示一个简单的实体,其等价性不是基于身份:即两个值对象相等时,它们具有相同的值,而不一定是同一个对象。
是的,我知道这可能会让人困惑,所以让我们简化一下。当我们比较它与实体时,理解值对象是什么会更容易。当我们构建一个新的应用程序时,我们将它分为类。在这些类中,我们可以找到服务、工厂、构建器等等,但也可以找到实体和值对象。你很可能正在使用值对象,但你可能不知道这些对象被称为这样。
让我们从实体开始。这些是具有身份的对象——例如,对于一个电子商务应用程序,Product对象是一个实体,Order对象是一个实体,Customer对象也是一个实体。身份是这些类共有的。这很重要,因为我们可以有两个具有相同的名字、姓氏和出生日期但具有不同身份的用户对象,所以这些对象并不相等。我们通过身份来比较实体。我们也可以说我们通过它们的引用来比较实体——它们必须引用同一个对象才能被认为是相等的。
现在我们可以转向值对象了。从第一眼看来,值对象看起来几乎与实体相同。主要的区别是值对象没有身份。例如,一个Date对象——具有2014-06-12日期的两个对象是相等的,就是这样。我们不关心它们的身份。我们通过值来比较值对象。它们不必是同一个对象。只要它们具有相同的值,我们就认为它们相等。
这些类可以被视为值对象:日期、金钱、电子邮件、地址、范围。当然,我们不能说这些对象始终是值对象——这总是取决于上下文。对此最好的解释是金钱类。对于一个电子商务系统,金钱类是一个值对象,例如,对任何人来说,两张一百美元的纸币都是相同的。
我们可以很容易地交换这些纸币,而没有人会失去或获得任何价值——它们是相等的。但对政府来说,这并不那么明显。对他们来说,我的那张一百美元的纸币和你的是不同的,因为他们关心每一张纸币的身份。在这种情况下,金钱对象不会是一个值对象,而是一个实体。
问题:值对象不应该可变
网上有很多关于值对象的例子。对我来说,最好的例子之一,完美地描述了为什么这些小对象应该是不可变的,是金钱和日期。我将通过金钱来说明一个可变性问题。
我们正在构建一个彩票应用。目前我们有两个赌徒:凯特和保罗。现在我们不想关心彩票系统是如何工作的,因为我们只想关注最好的部分——支付。我们需要一个方法来奖励玩家(为了简单起见,我们的彩票将只使用美元作为货币)。我们还希望使我们的彩票更有吸引力,因此我们将为每位玩家提供免费的10美元!
class Lottery { /** @var Gambler[] */ private $gamblers;
/** @var Dollar */ private $initialBalance;
public function __construct() { // @todo: get value of a intial balance from a database $this->initialBalance = new Dollar(10); $this->gamblers = array(); }
public function enroll(Person $person) { $this->gamblers[$person->getName()] = new Gambler($person, $this->initialBalance); }
private function rewardWinners(array $gamblers, Dollar $amount) { foreach($gamblers as $gambler) { $gambler->addReward($amount); } } }
以及一个赌徒类
class Gambler { /** @var Person */ private $person; /** @var Dollar */ private $balance; public function __construct(Person $person, $initialBalance) { $this->person = $person; $this->balance = $initialBalance; } public function addReward(Dollar $amount) { $this->balance = $this->balance->add($amount); } }
现在我们可以进入最有趣的部分——美元类
class Dollar
{
/** @var int */
private $amount;
/** @var string */
private $currency = "USD";
public function __construct($amount)
{
$this->amount = $amount;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
public function add(Dollar $amount)
{
$this->amount += $amount->getAmount();
}
}
看起来一切都很正常,是吗?那么我们还在等什么呢?让我们开始赌吧!
初始余额
Kate: 10USD
Paul: 10USD
看起来不错!
第一轮:两位玩家都赢了100美元
Lottery::rewardWinners(/* Kate & Paul */, new Dollar(100));
Kate: 210USD
Paul: 210USD
嗯,这很奇怪...但让我们看看第二轮
第二轮:只有凯特赢了200美元
Lottery::rewardWinners(/* Kate */, new Dollar(200));
Kate: 410USD
Paul: 410USD
这绝对不是我们预期的结果!原因是美元类的可变性。在整个应用程序中,我们都在使用同一个美元类的对象!让我们考虑这一点,再次进行这些计算
- 我们创建了一个价值为10美元的美元对象
- 我们加了两次100美元(一次是凯特的奖励,一次是保罗的奖励):10 + 100 + 100 = 210美元
- 然后我们再加200美元(凯特的奖励):210 + 200 = 410美元!
现在一切都很清楚。我们总是将奖励添加到同一个对象上。当然,我们可以通过克隆美元对象来解决这个问题,但几乎不可能总是记住这一点。更好的解决方案是使美元类不可变。为了实现这一点,我们需要做两件事
- 移除设置器
- 在add()方法中返回一个新的美元对象,而不是更改当前对象的价值
经过这些更改后,我们的类应该看起来像这样
class Dollar
{
/** @var int */
private $amount;
/** @var string */
private $currency = "USD";
public function __construct($amount)
{
$this->amount = $amount;
}
public function add(Dollar $amount)
{
return new Dollar($this->amount + $amount->amount);
}
}
说实话,我们并不真的需要一个getAmount()方法,所以我把它也删除了。现在add()方法返回一个新对象,而不是更改一个值。正因为如此,我们需要修改Gambler类中的addReward()方法
public function addReward(Dollar $amount)
{
$this->balance = $this->balance->add($amount);
}
让我们再次开始赌
START
Kate: 10USD
Paul: 10USD
AFTER FIRST ROUND
Kate: 110USD
Paul: 110USD
AFTER SECOND ROUND
Kate: 310USD
Paul: 110USD
所以现在一切都很正确。你可以自己检查——只需从https://github.com/tomaszhanc/lottery克隆此项目——这是一个可变版本,所以你可以练习并将其改为不可变。
我为什么对此如此大惊小怪?因为PHP DateTime类(低于5.5)是可变的。例如下面的代码
$start = new DateTime('2014-06-12');
$end = $start->add(new DateInterval('P3D'));
print $start->format('Y-m-d'); // prints 2014-06-15
print $end->format('Y-m-d'); // prints 2014-06-15
正如你所看到的,这和我们之前在美元类中遇到的问题是一样的。
我希望我已经说服你使用不可变的值对象而不是可变的对象。如果这里有任何东西不清楚,请告诉我——我将尽一切努力使其简单化。
GSoC项目
我们(我、Herman 和 Søren)谈论了很多理论概念。这真的很令人惊讶,因为我以前没有人可以谈论这些概念。这对我的信息量非常大。遗憾的是,我在这个项目中有点落后于进度,但我们已经为后续工作打下了良好的基础。
简而言之,我已经完成的事情
- 新的日期类不会扩展PHP DateTime(但有一个获取PHP DateTime对象的getter)
- 提供了当前Joomla的DateTime的所有功能
- 简单的计算,例如,您不需要使用DateInterval来添加一些天到您的日期
- 具有includes()、gap()、abuts()等有用方法的日期范围
我在想的事情
- 将所有getter移动到某种DateWrapper类中
- 提供“花哨”的计算 - 我的意思是添加用于计算的函数,例如:$date->add('3 days')
我将要做什么
- timeSince()方法
- 提供可扩展性(它将通过使用策略模式来完成)
- DateFactory
如果您对新的日期类的外观或您需要的功能有任何建议,请告诉我!
在Joomla社区杂志上发表的一些文章代表了作者对特定主题的个人观点或经验,可能与Joomla项目的官方立场不一致
通过接受,您将访问https://magazine.joomla.net.cn/之外由第三方提供的服务的服务
评论