從原始資料型別到值物件

xuding發表於2021-09-13

本文轉載自【何以解耦】:codedecoupled.com/value-object.htm...

生活中,我們使用數量詞來描述事物的長度,重量,金額等等。在建模過程中,缺乏經驗的開發者習慣使用原始資料型別(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。我們甚至可以寫得更加深入,給量詞單位建立獨立的列舉型別。

完整的抽象

數量詞的初始化條件,相加行為都完整的封裝在了值物件中,作為使用者,可以放心地使用操作這些數量詞,不需要去關心它們的內部細節。

總結

使用值物件進行建模可大大提高程式碼可讀性,提升協同工作效率。同時為值物件編寫單元測試也非常簡單。

本文轉載自【何以解耦】: codedecoupled.com/value-object.htm... ,如果你也對 TDD,DDD 以及簡潔程式碼感興趣,歡迎關注公眾號【何以解耦】,一起探索軟體開發之道。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Know how, know why meanwhile.

相關文章