整洁架构/DDD:PHP应用中复杂值对象的最佳实践探讨
整洁架构下PHP复杂值对象(如Money)的最佳实践
针对你提到的四种方案的痛点,推荐**「领域接口定义+第三方库轻量适配」**的方案,既能复用成熟库的实现,又能保持领域层的独立性,同时避免过度设计。
核心思路
整洁架构的核心要求是领域层不依赖任何外部实现,但完全自行实现复杂值对象会重复造轮子且容易遗漏边界场景(比如货币不匹配处理、精度计算)。通过「领域定义行为契约,基础设施层适配第三方库」的方式,能在两者间找到平衡:
- 领域层只定义业务需要的行为接口,不涉及具体实现
- 基础设施层用第三方库实现该接口,仅做转换逻辑,不添加额外业务规则
- 依赖注入保证领域层只依赖接口,而非第三方库
具体实现步骤(以Money为例)
1. 领域层定义Money值对象接口
只保留业务必需的方法,避免过度抽象:
namespace App\Domain\ValueObject; interface Money { // 从金额和货币创建实例 public static function fromAmount(float $amount, string $currency): self; // 金额加法 public function add(Money $other): self; // 金额减法 public function subtract(Money $other): self; // 金额乘法 public function multiply(float $factor): self; // 获取金额 public function getAmount(): float; // 获取货币代码 public function getCurrency(): string; // 相等性判断 public function equals(Money $other): bool; }
2. 基础设施层实现适配器
封装第三方库(如brick/money),仅做转换:
namespace App\Infrastructure\ValueObject; use App\Domain\ValueObject\Money as DomainMoney; use Brick\Money\Money as BrickMoney; class BrickMoneyAdapter implements DomainMoney { private BrickMoney $money; private function __construct(BrickMoney $money) { $this->money = $money; } public static function fromAmount(float $amount, string $currency): self { return new self(BrickMoney::of($amount, $currency)); } public function add(DomainMoney $other): self { if (!$other instanceof self) { throw new \InvalidArgumentException('仅支持同类型Money实例相加'); } return new self($this->money->plus($other->money)); } public function subtract(DomainMoney $other): self { if (!$other instanceof self) { throw new \InvalidArgumentException('仅支持同类型Money实例相减'); } return new self($this->money->minus($other->money)); } public function multiply(float $factor): self { return new self($this->money->multipliedBy($factor)); } public function getAmount(): float { return $this->money->getAmount()->toFloat(); } public function getCurrency(): string { return $this->money->getCurrency()->getCode(); } public function equals(DomainMoney $other): bool { if (!$other instanceof self) { return false; } return $this->money->isEqualTo($other->money); } }
3. 领域层依赖接口而非具体实现
在领域服务或实体中,直接使用领域接口:
namespace App\Domain\Service; use App\Domain\ValueObject\Money; class OrderTotalCalculator { public function calculate(Money $subtotal, Money $tax): Money { return $subtotal->add($tax); } }
方案优势对比
- 避免重复造轮子:复用第三方库成熟的边界处理(如货币不匹配抛出异常、高精度计算)
- 不污染领域层:领域层仅含接口,无任何第三方依赖,符合整洁架构的依赖规则
- 无冗余抽象:接口仅定义业务必需方法,适配器逻辑极简,避免过度设计
- 保持充血模型:值对象的行为和数据绑定,不会变成贫血模型导致业务规则分散
通用扩展(适用于其他复杂值对象)
对于其他具备复杂行为的值对象(如DateRange、Email),可遵循相同逻辑:
- 在领域层定义仅包含业务必需行为的接口
- 在基础设施层用成熟库或自定义实现适配该接口
- 领域层通过依赖注入使用接口,隔离外部实现细节
内容的提问来源于stack exchange,提问作者Yurii Orlyk
相关产品推荐
相关产品推荐

