7分钟阅读时间 (1436 字)

值对象的价值

The Value of Value Objects

你知道为什么值对象不可变很重要吗?也许你对值对象的概念不太熟悉,但别担心,我会用最简单的方式解释一切。让我们赌一把!

什么是值对象?

首先,我们需要一个值对象的定义。从维基百科我们得知 值对象是一个代表简单实体的小型对象,其实体相等性不是基于身份:即两个值对象相等时,它们具有相同的值,不一定相同对象

我知道这可能有些令人困惑,所以我们来简化一下。通过将其与实体进行比较,理解值对象将更容易。当我们构建新的应用程序时,我们将其分为类。在这些类中,我们可以找到服务、工厂、构建器等等,但也可以找到实体和值对象。你很可能正在使用值对象,但你并不知道这些对象被称为这样。

让我们从实体开始。这些是有身份的对象——例如,对于电子商务应用程序,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

这绝对不是我们预期的结果!原因是美元类的可变性。在整个应用程序中,我们都在使用美元类的同一个对象!让我们考虑这一点,再次进行这些计算

  1. 我们创建了一个价值10美元的美元对象
  2. 我们增加了两次100美元(一次用于凯特的奖励,一次用于保罗的奖励):10 + 100 + 100 = 210美元
  3. 然后我们增加了200美元(凯特的奖励):210 + 200 = 410美元

现在一切都清楚了。我们总是将奖励添加到同一个对象上。当然,我们可以通过克隆美元对象来解决这个问题,但这几乎是不可能的,总是记住这一点。更好的解决方案是使美元类不可变。为了实现这一点,我们需要做两件事

  1. 移除setter
  2. 在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项目

我们(我,HermanSøren)已经讨论了很多理论概念。这真的很令人惊讶,因为我以前没有人可以谈论这些概念。这对我的信息量很大。遗憾的是,我在这个项目上有点落后于进度,但我们已经为后续工作打下了良好的基础。

简而言之,我已经完成的事情

  • 新的日期类不会扩展PHP DateTime(但有一个用于PHP DateTime对象的getter)
  • 提供当前Joomla的DateTime的所有功能
  • 简单的计算,例如,您不需要使用DateInterval来添加一些天到您的日期
  • 具有包括(), gap(), abuts()等有用方法的日期范围

我在想什么

  • 将所有getter移动到某种DateWrapper类中
  • 提供“花哨”的计算——我的意思是添加计算方法:$date->add('3 days')

我打算做什么

  • timeSince()方法
  • 提供可扩展性(这将通过使用策略模式来完成)
  • DateFactory

如果您对新的日期类的外观或您需要的功能有任何建议,请告诉我!

在Joomla社区杂志上发表的一些文章代表作者对特定主题的个人观点或经验,可能不符合Joomla项目的官方立场

0
在Joomla!中使用Microdata的实践...
 

评论

已经注册? 登录这里
还没有发表评论。成为第一个发表评论的人

通过接受,您将访问https://magazine.joomla.net.cn/外部第三方提供的服务