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.
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
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));
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.
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);
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));
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,
....
)
}
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