PHP项目值对象与实体

wen PHP项目 1

PHP项目中的值对象与实体

基本概念

实体(Entity)值对象(Value Object) 是领域驱动设计(DDD)中的核心概念:

PHP项目值对象与实体

实体:

  • 有唯一标识(ID),即使所有属性相同,不同ID就是不同对象
  • 可变(Mutable),可以修改内部状态
  • 通过ID判断相等性

值对象:

  • 没有唯一标识,由所有属性值决定其身份
  • 不可变(Immutable),一旦创建不可修改
  • 通过所有属性值判断相等性

代码示例

实体示例

<?php
class User {
    private UserId $id;
    private string $name;
    private Email $email;
    public function __construct(UserId $id, string $name, Email $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }
    public function getId(): UserId {
        return $this->id;
    }
    public function getName(): string {
        return $this->name;
    }
    // 允许修改名称(可变性)
    public function changeName(string $name): void {
        $this->name = $name;
    }
    // 通过ID判断相等
    public function equals(User $other): bool {
        return $this->id->equals($other->id);
    }
}
class UserId {
    private string $id;
    public function __construct(string $id) {
        $this->id = $id;
    }
    public function equals(UserId $other): bool {
        return $this->id === $other->id;
    }
}

值对象示例

<?php
class Email {
    private string $value;
    public function __construct(string $value) {
        // 验证
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('无效的邮箱地址');
        }
        $this->value = $value;
    }
    // 不可变,只能获取值
    public function getValue(): string {
        return $this->value;
    }
    // 通过所有属性判断相等
    public function equals(Email $other): bool {
        return $this->value === $other->value;
    }
    // 转换为字符串
    public function __toString(): string {
        return $this->value;
    }
}
class Address {
    private string $street;
    private string $city;
    private string $zipCode;
    public function __construct(string $street, string $city, string $zipCode) {
        $this->street = $street;
        $this->city = $city;
        $this->zipCode = $zipCode;
    }
    // 不可变,只提供getter方法
    public function getStreet(): string { return $this->street; }
    public function getCity(): string { return $this->city; }
    public function getZipCode(): string { return $this->zipCode; }
    public function equals(Address $other): bool {
        return $this->street === $other->street 
            && $this->city === $other->city 
            && $this->zipCode === $other->zipCode;
    }
}
class Money {
    private float $amount;
    private string $currency;
    public function __construct(float $amount, string $currency) {
        if ($amount < 0) {
            throw new InvalidArgumentException('金额不能为负数');
        }
        $this->amount = $amount;
        $this->currency = $currency;
    }
    // 返回新的值对象,而不是修改原有对象
    public function add(Money $other): Money {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('货币类型不同');
        }
        return new Money($this->amount + $other->amount, $this->currency);
    }
    public function equals(Money $other): bool {
        return $this->amount === $other->amount 
            && $this->currency === $other->currency;
    }
}

实际应用场景

订单系统中的示例

<?php
// 实体:订单
class Order {
    private OrderId $id;
    private CustomerId $customerId;
    private OrderStatus $status;
    private array $lineItems = [];
    private Money $totalAmount;
    public function __construct(OrderId $id, CustomerId $customerId) {
        $this->id = $id;
        $this->customerId = $customerId;
        $this->status = new OrderStatus('pending');
        $this->totalAmount = new Money(0, 'CNY');
    }
    public function addLineItem(LineItem $item): void {
        $this->lineItems[] = $item;
        $this->recalculateTotal();
    }
    public function cancel(): void {
        $this->status = new OrderStatus('cancelled'); // 状态是值对象,不可变
    }
    public function getId(): OrderId {
        return $this->id;
    }
    private function recalculateTotal(): void {
        $total = new Money(0, 'CNY');
        foreach ($this->lineItems as $item) {
            $total = $total->add($item->getSubtotal());
        }
        $this->totalAmount = $total;
    }
    public function equals(Order $other): bool {
        return $this->id->equals($other->id); // 实体通过ID比较
    }
}
// 值对象:订单行项目
class LineItem {
    private ProductId $productId;
    private int $quantity;
    private Money $unitPrice;
    public function __construct(ProductId $productId, int $quantity, Money $unitPrice) {
        $this->productId = $productId;
        $this->quantity = $quantity;
        $this->unitPrice = $unitPrice;
    }
    public function getSubtotal(): Money {
        $amount = $this->quantity * $this->unitPrice->getAmount();
        return new Money($amount, $this->unitPrice->getCurrency());
    }
    public function equals(LineItem $other): bool {
        return $this->productId === $other->productId 
            && $this->quantity === $other->quantity 
            && $this->unitPrice->equals($other->unitPrice);
    }
}
// 值对象:订单状态
class OrderStatus {
    private string $value;
    private const VALID_STATUSES = ['pending', 'paid', 'shipped', 'cancelled'];
    public function __construct(string $value) {
        if (!in_array($value, self::VALID_STATUSES)) {
            throw new InvalidArgumentException('无效的订单状态');
        }
        $this->value = $value;
    }
    public function getValue(): string {
        return $this->value;
    }
    public function equals(OrderStatus $other): bool {
        return $this->value === $other->value;
    }
}

选择指南

什么时候使用实体:

  • 对象需要唯一标识(如用户、订单、产品)
  • 对象的身份会随时间改变属性而变化
  • 需要跟踪对象的生命周期

什么时候使用值对象:

  • 描述对象的属性或特征(如地址、金额、颜色)
  • 没有独立生命周期的概念
  • 经常被共享和重用
  • 通常通过构造或工厂方法创建

最佳实践

  1. 值对象必须不可变 - 所有修改操作都应返回新实例
  2. 实体可以有可变状态,但修改应通过命令方法进行
  3. 值对象应包含自我验证逻辑
  4. 值对象可以包含业务逻辑,如金额计算
  5. 实体使用ID比较,值对象使用属性比较
  6. 优先使用值对象而非基元类型(如用Email类代替string)

这种设计模式使代码更加清晰、可维护,并减少bug。

抱歉,评论功能暂时关闭!