← 返回首页

在 PHP 项目里认真使用 Value Object,能减少一半以上的边界错误

2025-08-10 · PHP 架构 / 领域建模

很多 PHP 项目里,边界错误不是出在复杂算法,而是出在“这个字符串到底是不是手机号”“这个整数是金额分还是元”“这个时间是本地时区还是 UTC”这种低级但高频的问题。Value Object 的意义,就是把这些看似琐碎的约束从注释和约定里搬到类型里。

DTO 解决传输,VO 解决约束

不少团队把 DTO 和 VO 混用,最后只是得到一堆有 getter 的数组。DTO 重点是传输,VO 的重点是不可变与合法性。比如 Email、Money、OrderId、PhoneNumber,这些对象一旦构造成功,就应该天然满足规则;如果构造失败,就不允许继续往下传。

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Money cannot be negative');
        }
        if (!in_array($currency, ['CNY', 'USD'], true)) {
            throw new InvalidArgumentException('Unsupported currency');
        }
    }
}

VO 最大的价值发生在“跨层移动”时

如果一个值只在单个函数里转一圈,VO 的收益不明显;但它一旦要跨控制器、应用服务、领域服务、仓储层、消息总线传递,VO 的好处会迅速放大。因为每经过一层,信息语义被稀释一次;而 VO 能强迫语义稳定下来。

避免“半成品 VO”

最糟糕的做法,是对象看起来像 VO,内部却允许任意 set,或者构造时根本不校验。那只是在增加代码长度,不是在降低错误率。真正有价值的 VO 通常具备几个特征:不可变、可比较、构造即合法、对外暴露明确语义。

不要为所有字段都造 VO

VO 不是越多越好。像普通备注、非关键排序字段、展示文案,这些不带强约束的值,继续用标量就够了。VO 应优先用在:金额、时间、标识、枚举、外部输入、协议边界这些“错一次就很恶心”的地方。

结论

PHP 不是静态类型很强的语言,所以更需要在边界处主动补语义。Value Object 不是“DDD 仪式感”,而是把错误尽可能提前暴露的一种成本很低的工程手段。