0Pricing
PHP Academy · 课时

聚合、仓储与工厂

使用聚合保护不变性,并以清晰的方式持久化数据。

聚合、仓储与工厂 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. DDD 构建模块:实体与值对象
  2. 聚合、仓储与工厂
  3. 领域事件与领域服务
  4. 限界上下文与上下文映射
← 返回 PHP Academy