聚合、仓储与工厂
使用聚合保护不变性,并以清晰的方式持久化数据。
聚合、仓储与工厂 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
保护不变量
拥有实体和值对象后,您需要一些模式来保持它们组成的集合一致,并将其干净地持久化。DDD 提供了三种战术模式:聚合(一致性边界)、仓储(类似集合的持久化抽象)和工厂(负责复杂构造)。本课将展示它们如何在 PHP 中协同工作。
什么是聚合
聚合是一个实体和值对象的集合,在进行数据变更时被视为一个整体。其中一个实体是聚合根——外部代码唯一可以持有其引用的成员。所有修改都必须经过根,由根强制执行聚合的不变量。聚合也是事务边界:它会以原子方式加载和保存。
根守护整体
外部调用方永远不会直接接触内部成员。要添加一个行项目,您需要调用根上的方法,由该方法验证并维护一致性(总额、限额)。这样可以将不变量集中在一个地方。
<?php
final class LineItem {
public function __construct(
public readonly string $sku,
public readonly int $qty,
public readonly int $unitCents
) {}
public function subtotal(): int { return $this->qty * $this->unitCents; }
}
final class Order { // Aggregate Root
/** @var LineItem[] */
private array $items = [];
public function __construct(public readonly string $id) {}
public function addItem(string $sku, int $qty, int $unitCents): void {
if ($qty < 1) { throw new DomainException('qty must be >= 1'); }
$this->items[] = new LineItem($sku, $qty, $unitCents);
}
public function total(): int {
return array_sum(array_map(fn(LineItem $i) => $i->subtotal(), $this->items));
}
}
$o = new Order('o1');
$o->addItem('A', 2, 500);
$o->addItem('B', 1, 300);
echo $o->total(), PHP_EOL; // 1300
设计小型聚合
一个常见错误是让聚合过大(例如一个同时拥有完整Order和Customer对象图的聚合)。经验法则如下:
- 保持聚合小型化;通过标识符而不是持有对象来引用其他聚合。
- 一次事务应当修改一个聚合;跨聚合协调应使用领域事件。
- 必须始终成立的不变量决定边界。
按标识引用
订单存储的是customerId值对象,而不是Customer实体。这样可以保持紧凑的一致性边界,并避免加载庞大的对象图。跨聚合的一致性会成为最终一致性,通过事件而不是一个巨大的事务来处理。
<?php
final class CustomerId {
public function __construct(public readonly string $value) {}
}
final class Order {
public function __construct(
public readonly string $id,
public readonly CustomerId $customerId // reference, not object
) {}
}
$order = new Order('o1', new CustomerId('cus_99'));
echo $order->customerId->value, PHP_EOL;
仓储契约
仓储营造出一个内存中聚合根集合的假象。领域只依赖接口;具体实现(Doctrine、PDO、内存实现)位于基础设施层。仓储处理的是完整聚合,而不是不完整的行。
<?php
interface OrderRepository {
public function ofId(string $id): ?Order;
public function save(Order $order): void;
public function nextIdentity(): string;
}
内存实现
内存仓储对于快速且无需数据库的单元测试非常宝贵。由于领域依赖的是接口,您可以自由替换实现(这正是依赖倒置的体现)。
<?php
interface OrderRepository {
public function ofId(string $id): ?object;
public function save(object $order): void;
public function nextIdentity(): string;
}
final class Order { public function __construct(public readonly string $id) {} }
final class InMemoryOrderRepository implements OrderRepository {
private array $store = [];
public function ofId(string $id): ?object { return $this->store[$id] ?? null; }
public function save(object $order): void { $this->store[$order->id] = $order; }
public function nextIdentity(): string { return 'o_' . bin2hex(random_bytes(4)); }
}
$repo = new InMemoryOrderRepository();
$repo->save(new Order('o1'));
var_dump($repo->ofId('o1') !== null);
仓储不是 DAO
仓储不是通用的增删改查 DAO。它公开具有领域含义的查询(findOverdueOrders()),并在保持不变量完整的情况下重新构建完整聚合。它会有意隐藏数据库查询和对象关系映射的细节,使领域不感知持久化。请避免将查询构造器或通用的save($anyEntity)方法泄漏到领域中。
使用工厂进行复杂创建
当构造聚合涉及真实逻辑——生成标识、组装值对象、强制执行创建时不变量——请将这些逻辑移入工厂(专用类或静态命名构造函数)。这样可以让实体构造函数保持清晰,并集中管理有效创建的规则。
<?php
final class Order {
private function __construct(
public readonly string $id,
public readonly string $customerId
) {}
public static function place(string $customerId): self {
if ($customerId === '') { throw new DomainException('customer required'); }
return new self('o_' . bin2hex(random_bytes(4)), $customerId);
}
}
$order = Order::place('cus_1');
echo $order->id, PHP_EOL;
它们如何协作
应用服务中的典型流程如下:
- 工厂(或命名构造函数)创建一个有效聚合。
- 聚合根的方法在使用过程中强制执行不变量。
- 仓储持久化整个聚合,并在之后重新构建它。
应用服务在每个聚合对应的一个事务中协调这些步骤,并且只依赖接口。
强制执行聚合整体不变量
聚合根的真正价值在于强制执行跨成员的不变量。在这里,如果添加一个行项目会使总额超过信用额度,订单就会拒绝该行项目——这是单个LineItem无法独自强制执行的规则。由于所有变更都必须经过根,该规则永远无法被绕过。
<?php
final class Order {
private array $items = [];
public function __construct(
public readonly string $id,
private int $creditLimitCents
) {}
public function addItem(int $cents): void {
if ($this->total() + $cents > $this->creditLimitCents) {
throw new DomainException('Exceeds credit limit');
}
$this->items[] = $cents;
}
public function total(): int { return array_sum($this->items); }
}
$o = new Order('o1', 1000);
$o->addItem(600);
try { $o->addItem(600); } catch (DomainException $e) { echo $e->getMessage(), PHP_EOL; }
echo $o->total(), PHP_EOL; // 600
快速检查
聚合设计。
回顾
您已经学习了如何保护不变量并实现干净的持久化。聚合构成一致性边界和事务边界,只能通过聚合根进行修改,并通过按标识引用其他聚合来保持小型化。仓储通过领域接口将聚合根呈现为集合,隐藏对象关系映射和数据库查询,并支持使用内存测试替身。工厂集中负责复杂且能够强制执行不变量的创建过程。三者共同使领域模型保持一致,并且不感知持久化。
常见问题解答
「聚合、仓储与工厂」课时是免费的吗?
是的 — 「聚合、仓储与工厂」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「聚合、仓储与工厂」这节课中我会学到什么?
使用聚合保护不变性,并以清晰的方式持久化数据。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「聚合、仓储与工厂」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。