Laravel Value Object

  • October 20, 2023
  • 756

What is value object?

Value object là đại diện cho một giá trị đơn giản hoặc nhóm giá trị, bảo đảm tính hợp lệ giá trị mà chúng chứa. Giá trị này thường được xác định bởi các thuộc tính của đối tượng và không thay đổi sau khi được khởi tạo. Có chứa business logic, các rule về dữ liệu. Sử dụng Value object giúp chúng ta viết code linh hoạt, dễ đọc hơn và giảm thiểu lỗi.

Tính chất của value object

  • Bất biến (immutable): Value object không thể thay đổi giá trị của các thuộc tính sau khi được khởi tạo. Bất kỳ sự thay đổi nào cần phải tạo ra một đối tượng mới.
  • Value-based comparison: Value objects được so sánh dựa trên giá trị của thuộc tính, không phải dựa trên địa chỉ bộ nhớ hay tham chiếu.
  • No complex behavior: Value objects không chứa các xử lý phực tạp
  • Used to represent fixed values: Value objects thường được sử dụng để biểu diễn các khái niệm có ý nghĩa đặc biệt hoặc giá trị cố định, chẳng hạn như ngày tháng, tiền tệ, địa chỉ...

Why do we need use value object?

Khi không sử dụng value object dữ liệu truyền vào sẽ không được validate chính xác. VD ta tạo một user như sau


class User {
  public function __construct(
    public string $name,
    public string $phone
  ) {}
}

$user = new User('Test', 'test@gmail.com');

Các thuộc tính của đối tượng user có kiểu dữ liệu là string. Khi chúng ta khởi tạo đối tượng user với giá trị phone sai cũng không có thông báo lỗi. Có cách nào để throw ra lỗi không? value object chính là câu trả lời. hehe

Viết một Phone value object để validate giá trị phone truyền vào đúng trả về phone, sai trả về lỗi.


readonly class Phone implements Stringable
{
    public function __construct(private string $phone)
    {
        if (!preg_match('/(84|0[3|5|7|8|9])+([0-9]{8})$/', $phone)) {
            throw new Exception('Phone is invalid');
        }
    }

    public function __toString(): string
    {
        return $this->phone;
    }

    public function getPhone(): string
    {
        return $this->phone;
    }
}

Khởi tạo user


class User {
    public function __construct(
      public string $name,
      public Phone $phone
    ) {
    }
}
$user = new User('a', new Phone('0973537383'));
echo $user->phone; // 0973537381

$user = new User('a', new Phone('ddd'));
echo $user->phone;
// Error sẽ xảy ra

Reuse code

Value object giúp tái sử dụng code. Cùng xem VD tạo 1 Price value object.


<?php

enum Currency : string
{
  case Dollar = 'USD';
  case Euro = 'EUR';
}

// Immutable object, so let's make it readonly!
readonly class Price implements Stringable
{
  public function __construct(
    private float $amount,
    private Currency $currency)
  {
    // Run validation check
    if ($this->amount <= 0) {
      throw new InvalidPriceAmountException();
    }
  }

  public function __toString(): string
  {
    return number_format($this->amount, 2) . ' ' . $this->currency->value;
  }

  public function getAmount(): float
  {
    return $this->amount;
  }

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

$price = new Price(15, ‘$’); // Invalid Argument Exception
$price = new Price(0, Currency::Dollar); // InvalidPriceAmountException

$price = new Price(29.99, Currency::Dollar); // OK
echo $price; // 29.99 USD

Trước khi sử dụng value object thì chúng ta luôn phải kiểm tra giá trị của price và curency mỗi khi muốn sử dụng chúng trong 1 function nhất định.


class UserBalance
{
  ...

  function addToBalance(float $amount, string $currency) : void
  {
    // Validation before we can trust the values we
    // received are "business-usable"
    if (!currencyIsValid($currency)) {
      throw new InvalidCurrencyException;
    }

    if ($amount <= 0) {
      throw new InvalidAmountException;
    }

    $this->balance += $amount;
  }
}

class Cart
{
  function removeFromTotal(float $amount, string $currency) : void
  {
    // Validation needed again
    if (!currencyIsValid($currency)) {
      throw new InvalidCurrencyException;
    }

    if ($amount <= 0) {
      throw new InvalidAmountException;
    }

    $this->total -= $amount;
  }
}

...
$userBalance->addToBalance(59.99, Currency::Dollar);
$cart->removeFromTotal(19.99, Currency::Dollar);

Sau khi sử dụng value object


class UserBalance
{
  ...

  function addToBalance(Price $price) : void
  {
    $this->balance += $price->getAmount();
  }
}

class Cart
{
  function removeFromTotal(Price $price, float $discount) : void
  {
    $this->total -= $price->getAmount();
  }
}

...
$price = new Price(59.99, Currency::Dollar);
$userBalance->addToBalance($price);

$cart->removeFromTotal(new Price(19.99, Currency::Dollar));

Equality check

Kiểm tra xem 2 value object có bằng nhau không bằng các thêm hàm vào vào value object


readonly class Price
{
  ...
  public function isEqualTo(Price $other) : bool
  {
    return $this->getAmount() == $other->getAmount()
      && $this->getCurrency() == $other->getCurrency();
  }
}

$first = new Price(59.99, Currency::Dollar);
$second = new Price(59.99, Currency::Dollar);

$first->isEqualTo($second); // true!

Chúng ta có thể thay đổi business logic để check vaule object giống nhau.

Additional functionality

Thêm function xử lý logic cho value object


readonly class DateRange
{
    public function __construct(
        private DateTime $start,
        private DateTime $end
    ) {}

    ...
    public function contains(DateTime $selected) : bool
    {
      return $selected >= $start && $selected <= $end;
    }
}

$exhibition = new DateRange($startDate, $endDate);
$exhibition->contains($selectedDate);

Nested value objects

Chúng ta cũng có thể nested value object


readonly class Salary
{
  public function __construct(
    private float $amount,
    private string $currency,
    private float $bonus
  ) {
    // Validation...
  }
}

readonly class Employee
{
  public function __construct(
    private string $name,
    private string $email,
    // Using the Salary value object directly
    private Salary $salary)
  ...
}

$employee = new Employee('Paul', 'home@work.com', new Salary(24, 'USD', 0));

Value object and DTO

Làm sao để phân biệt value object và DTO? DTO chỉ làm công việc chuyển đổi dữ liệu giữa các lớp trong ứng dụng, các giá trị truyền đi có thể bị thay đổi và không được chứa business logic còn value object giá trị của đối tượng sẽ không bị thay đổi khi khởi tạo và nó có chứa các business logic. VD về 1 DTO đơn giản, chúng chỉ định nghĩa các dữ liệu cần trao đổi giữa các lớp.


class EmployeeDTO
{
    public function __construct(
      private string $name,
      private string $phone,
      private string $email,
      private string $salaryCurrency,
      private float $salaryAmount,
      ....
    )
}

Sử dụng value object trong DTO giúp cho dữ liệu được truyền đi giữa các layer chính xác và toàn vẹn.


class EmployeeDTO
{
    public function __construct(
      private string $name,
      private Phone $phone,
      private Email $email,
      private Salary $salary,
      ....
    )
}

Tổng kết

Trong bài viết này chúng ta đã tìm hiểu xong các khái niệm cơ bản của value object, ở phần tiếp theo chúng ta sẽ tìm hiểu áp dụng value object vào laravel như thế nào.


Tham khảo thêm:

https://www.conroyp.com/articles/building-resilient-code-harnessing-the-power-of-value-objects
https://martinjoo.dev/value-objects-everywhere