值对象的价值
你知道为什么值对象不可变很重要吗?也许你对值对象的概念不太熟悉,但别担心,我会用最简单的方式解释一切。让我们赌一把!
什么是值对象?
首先,我们需要一个值对象的定义。从维基百科我们得知 值对象是一个代表简单实体的小型对象,其实体相等性不是基于身份:即两个值对象相等时,它们具有相同的值,不一定相同对象。
我知道这可能有些令人困惑,所以我们来简化一下。通过将其与实体进行比较,理解值对象将更容易。当我们构建新的应用程序时,我们将其分为类。在这些类中,我们可以找到服务、工厂、构建器等等,但也可以找到实体和值对象。你很可能正在使用值对象,但你并不知道这些对象被称为这样。
让我们从实体开始。这些是有身份的对象——例如,对于电子商务应用程序,Product 对象是一个实体,Order 对象是一个实体,Customer 对象也是一个实体。身份是这些类共有的。这很重要,因为我们可以有具有相同名字、姓氏和甚至出生日期的两个相似的用户对象,但具有不同的身份,所以这些对象不相等。我们通过身份比较实体。我们也可以说,我们通过它们的引用来比较实体——它们必须引用相同的对象才能被认为是相等的。
现在我们可以转向值对象了。乍一看,值对象看起来几乎与实体相同。主要区别是值对象没有身份。例如,日期对象——两个日期为 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
看起来不错!
第1轮:两位玩家都赢得了100美元
Lottery::rewardWinners(/* Kate & Paul */, new Dollar(100));
Kate: 210USD
Paul: 210USD
嗯,这很奇怪……但让我们看看第2轮
第2轮:只有凯特赢得了200美元
Lottery::rewardWinners(/* Kate */, new Dollar(200));
Kate: 410USD
Paul: 410USD
这绝对不是我们预期的结果!原因是美元类的可变性。在整个应用程序中,我们都在使用美元类的同一个对象!让我们考虑这一点,再次进行这些计算
- 我们创建了一个价值10美元的美元对象
- 我们增加了两次100美元(一次用于凯特的奖励,一次用于保罗的奖励):10 + 100 + 100 = 210美元
- 然后我们增加了200美元(凯特的奖励):210 + 200 = 410美元!
现在一切都清楚了。我们总是将奖励添加到同一个对象上。当然,我们可以通过克隆美元对象来解决这个问题,但这几乎是不可能的,总是记住这一点。更好的解决方案是使美元类不可变。为了实现这一点,我们需要做两件事
- 移除setter
- 在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来添加一些天到您的日期
- 具有包括(), gap(), abuts()等有用方法的日期范围
我在想什么
- 将所有getter移动到某种DateWrapper类中
- 提供“花哨”的计算——我的意思是添加计算方法:$date->add('3 days')
我打算做什么
- timeSince()方法
- 提供可扩展性(这将通过使用策略模式来完成)
- DateFactory
如果您对新的日期类的外观或您需要的功能有任何建议,请告诉我!
在Joomla社区杂志上发表的一些文章代表作者对特定主题的个人观点或经验,可能不符合Joomla项目的官方立场
通过接受,您将访问https://magazine.joomla.net.cn/外部第三方提供的服务
评论