PHP项目中的值对象与实体
基本概念
实体(Entity) 和 值对象(Value Object) 是领域驱动设计(DDD)中的核心概念:

实体:
- 有唯一标识(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;
}
}
选择指南
什么时候使用实体:
- 对象需要唯一标识(如用户、订单、产品)
- 对象的身份会随时间改变属性而变化
- 需要跟踪对象的生命周期
什么时候使用值对象:
- 描述对象的属性或特征(如地址、金额、颜色)
- 没有独立生命周期的概念
- 经常被共享和重用
- 通常通过构造或工厂方法创建
最佳实践
- 值对象必须不可变 - 所有修改操作都应返回新实例
- 实体可以有可变状态,但修改应通过命令方法进行
- 值对象应包含自我验证逻辑
- 值对象可以包含业务逻辑,如金额计算
- 实体使用ID比较,值对象使用属性比较
- 优先使用值对象而非基元类型(如用Email类代替string)
这种设计模式使代码更加清晰、可维护,并减少bug。