头图

从原始数据类型到值对象

XuDing

本文转载自【何以解耦】:https://codedecoupled.com/val...

生活中,我们使用数量词来描述事物的长度,重量,金额等等。在建模过程中,缺乏经验的开发者习惯使用原始数据类型(Primitive data type)给数量词造型。

比如行走的距离(使用 int):

class Walker
{
    private int $distance;

    public function __construct(int $distance)
    {
        $this->distance = $distance;
    }

}

比如产品的重量(使用 float 或者 int):

class Goods
{
    private int $weight;

    public function __construct(int $weight)
    {
        $this->weight = $weight;
    }

}

比如一个商品的价格(使用 float 或者 int):

class Product
{
    private int $price;

    public function __construct(int $price)
    {
        $this->price = $price;
    }

}

以上代码有几个问题:

量词单位被隐藏

距离重量价格 作为数量词,量词部分的单位在代码中被隐藏了:

  • 距离($distance)是米,千米还是英里?
  • 重量($weight)是克,千克还是英镑?
  • 价格($price)是人民币,美元还是欧币?
有漏洞的抽象(leaky abstraction)

当需要赋予以上数量词行为的时候,我们会发现使用原始数据类型建立的模型过于单薄,形成了一种几乎不具备内部细节的抽象。

比如我们需要赋予数量词相加行为:

  • 距离($distance), 重量($weight),价格($price)相加之前,我们必须保证它们不能为负数。
  • 距离($distance), 重量($weight),价格($price)相加之前,我们必须保证它们的单位统一。

这种有漏洞的抽象暴露了原始数据类型建模的局限性。

值对象

值对象是 DDD 战术设计中的一种建模工具。它是领域中用来描述,量化或者测量实体的模型。它是一种很简答的模式,不光是在 DDD 项目中,在其他项目中,值对象都是一种很强大的建模方式。

值对象的特点是具有不变性(Immutability),一旦创建以后,一个值对象的属性就定型了,不可更改。

我们可以使用值对象为上文中的数量词建模:

  • $distance 变为:
class Distance
{
    private int $number;

    private string $unit;

    public function __construct(int $number, string $unit)
    {
        if ($number < 0) {
            throw new DomainException('Invalid number');
        }
        $this->number = $number;
        $this->unit = $unit;
    }

    public function add(Distance $distance)
    {
        if ($distance->unit != $this->unit) {
            throw new DomainException('Invalid unit');
        }

        return new Distance(
            $this->number + $distance->getNumber(),
            $this->unit
        );
    }

    public function getUnit(): string
    {
        return $this->unit;
    }

    public function getNumber(): int
    {
        return $this->number;
    }

}
  • $weight 变为:
<?php

class Weight
{
    private int $number;

    private string $unit;

    public function __construct(int $number, string $unit)
    {
        if ($number < 0) {
            throw new DomainException('Invalid number');
        }
        $this->number = $number;
        $this->unit = $unit;
    }

    public function add(Weight $weight)
    {
        if ($weight->unit != $this->unit) {
            throw new DomainException('Invalid unit');
        }

        return new Weight(
            $this->number + $weight->getNumber(),
            $this->unit
        );
    }

    public function getUnit(): string
    {
        return $this->unit;
    }

    public function getNumber(): int
    {
        return $this->number;
    }

}

  • $price 变为:
<?php

class Price
{
    private int $number;

    private string $currency;

    public function __construct(int $number, string $currency)
    {
        if ($number < 0) {
            throw new DomainException('Invalid number');
        }
        $this->number = $number;
        $this->currency = $currency;
    }

    public function add(Price $weight)
    {
        if ($weight->currency != $this->currency) {
            throw new DomainException('Invalid currency');
        }

        return new Price(
            $this->number + $weight->getNumber(),
            $this->currency
        );
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    
    public function getNumber(): int
    {
        return $this->number;
    }

}

仔细看一下以上代码,分析一下它是如何解决原始数据类型所带来的问题:

量词单位明确表达

量词的单位清晰地写入了代码中,类 Distance,Weight,Price 都具备专门的属性来表达量词的单位:$unit$currency。我们甚至可以写得更加深入,给量词单位建立独立的枚举类型。

完整的抽象

数量词的初始化条件,相加行为都完整的封装在了值对象中,作为使用者,可以放心地使用操作这些数量词,不需要去关心它们的内部细节。

总结

使用值对象进行建模可大大提高代码可读性,提升协同工作效率。同时为值对象编写单元测试也非常简单。

本文转载自【何以解耦】:https://codedecoupled.com/val...,如果你也对 TDD,DDD以及简洁代码感兴趣,欢迎关注公众号【何以解耦】,一起探索软件开发之道。

阅读 88

全职编程,兼职跑步,临时健身。

7 声望
0 粉丝
0 条评论

全职编程,兼职跑步,临时健身。

7 声望
0 粉丝
文章目录
宣传栏