0Pricing
PHP Academy · 课时

DDD 构建模块:实体与值对象

使用丰富的实体和不可变值对象建模领域。

DDD 构建模块:实体与值对象 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

战术核心

领域驱动设计的战术模式为您提供了一套在代码中建模业务概念的词汇。两个基础构建块是实体(由其随时间保持的标识定义的对象)和值对象(由其属性定义并被视为不可变的对象)。正确理解这一差异,会影响领域层中的其他所有内容。

标识与值

实体具有稳定的标识,即使其属性发生变化,这一标识仍会持续存在:某个Customer搬家后,仍然是同一位客户。值对象没有标识;两个属性相等的值对象可以互相替代,就像两张 5 美元钞票一样。请思考:“如果每个字段都发生变化,它仍然是同一个东西吗?”如果是,那么它就是实体。

值对象

值对象是不可变的,并且能够自我验证。PHP 8.1 的readonly属性可以强制实现不可变性;构造函数会守护不变量,因此无效实例不可能存在。

<?php
final class Email {
    public function __construct(public readonly string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $value");
        }
    }
    public function equals(Email $other): bool {
        return strtolower($this->value) === strtolower($other->value);
    }
}
$a = new Email('Jane@Example.com');
var_dump($a->equals(new Email('jane@example.com'))); // true

按值相等

值对象依据结构相等性进行比较,而不是依据引用。请提供显式的equals()方法,而不要依赖==,因为 PHP 的宽松比较具有一些出人意料的规则。Money值对象必须同时考虑金额和货币。

<?php
final class Money {
    public function __construct(
        public readonly int $cents,
        public readonly string $currency
    ) {}
    public function equals(Money $o): bool {
        return $this->cents === $o->cents && $this->currency === $o->currency;
    }
    public function add(Money $o): self {
        if ($this->currency !== $o->currency) {
            throw new DomainException('Currency mismatch');
        }
        return new self($this->cents + $o->cents, $this->currency);
    }
}
$sum = (new Money(500, 'USD'))->add(new Money(250, 'USD'));
echo $sum->cents, PHP_EOL; // 750

不可变性与派生方法

由于值对象不可变,“修改”一个值对象意味着返回一个新实例。诸如add()或withAmount()这样的方法从不进行修改,而是生成一个新值。这样可以消除别名错误,也使值对象能够在整个领域中安全地自由共享。

<?php
final class DateRange {
    public function __construct(
        public readonly DateTimeImmutable $start,
        public readonly DateTimeImmutable $end
    ) {
        if ($end < $start) { throw new InvalidArgumentException('end before start'); }
    }
    public function withEnd(DateTimeImmutable $end): self {
        return new self($this->start, $end);
    }
}
$r = new DateRange(new DateTimeImmutable('2026-01-01'), new DateTimeImmutable('2026-01-10'));
$r2 = $r->withEnd(new DateTimeImmutable('2026-02-01'));
echo $r->end->format('Y-m-d'), ' / ', $r2->end->format('Y-m-d'), PHP_EOL;

实体

实体由标识定义,通常使用领域生成的标识符,而不是数据库自增值。相等性比较标识符。实体的属性可以通过富含行为且能够保护不变量的方法发生变化。

<?php
final class Customer {
    private Email $email;
    public function __construct(
        public readonly string $id,
        Email $email
    ) { $this->email = $email; }

    public function changeEmail(Email $new): void { $this->email = $new; }
    public function email(): Email { return $this->email; }
    public function sameIdentityAs(Customer $o): bool { return $this->id === $o->id; }
}
$c = new Customer('cus_1', new Email('a@b.com'));
$c->changeEmail(new Email('c@d.com'));
echo $c->email()->value, PHP_EOL; // c@d.com

生成标识

建议在持久化之前于领域中生成标识(例如 UUID),而不是等待数据库生成。这样您就可以在内存中构造一个完全有效的实体,在不同聚合之间引用它,并且无需数据库即可进行测试。标识符是模型的一部分,而不是存储产物。

富含行为,而非贫血数据

一种常见的反模式是贫血领域模型:实体只是装有公共获取器和设置器的数据袋,而所有逻辑都位于“服务”类中。DDD 主张将行为放入实体。$order->cancel()封装了取消操作的规则,而不是让调用方直接修改状态字段。

<?php
final class Order {
    private string $status = 'open';
    public function __construct(public readonly string $id) {}
    public function cancel(): void {
        if ($this->status === 'shipped') {
            throw new DomainException('Cannot cancel a shipped order');
        }
        $this->status = 'cancelled';
    }
    public function status(): string { return $this->status; }
}
$o = new Order('o1');
$o->cancel();
echo $o->status(), PHP_EOL; // cancelled

处处使用值对象

请用值对象替代原始类型偏执。不要到处传递string $email、int $cents和string $currency,而是将它们封装在Email和Money中。优点包括:

  • 验证在构造时执行一次。
  • 领域规则(货币匹配)与数据放在一起。
  • 类型签名能够说明意图并防止混用。

持久化映射说明

值对象通常映射到嵌入式列,而不是映射到自己的表(Doctrine 的#[Embeddable])。实体映射到以其标识为键的行。请将持久化关注点排除在领域对象本身之外:模型不应知道自己存储在 MySQL 中。对象关系映射器或手写映射器负责在富领域模型与数据库之间进行转换。

复合值对象

值对象可以进行组合。Address将多个原始类型聚合为一个内聚的概念,并拥有自己的相等性规则和格式化方式。这样,实体只需持有一个丰富类型,而不是五个松散的字符串,地址规则也集中在一个地方。

<?php
final class Address {
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $postcode
    ) {
        if ($postcode === '') { throw new InvalidArgumentException('postcode required'); }
    }
    public function equals(Address $o): bool {
        return $this->street === $o->street
            && $this->city === $o->city
            && $this->postcode === $o->postcode;
    }
    public function oneLine(): string {
        return "{$this->street}, {$this->city} {$this->postcode}";
    }
}
echo (new Address('1 Main St', 'Ankara', '06000'))->oneLine(), PHP_EOL;

快速检查

实体还是值对象?

回顾

您已经学习了 DDD 的两个核心构建块。实体由稳定的标识定义,承载丰富的行为,并通过方法保护自身不变量。值对象不可变、能够自我验证,并依据属性相等性进行比较,从而替代原始类型偏执。正确地进行建模,将逻辑放在对象内部而不是贫血数据袋中,是构建聚合、仓储以及领域层其余部分的基础。

常见问题解答

「DDD 构建模块:实体与值对象」课时是免费的吗?

是的 — 「DDD 构建模块:实体与值对象」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「DDD 构建模块:实体与值对象」这节课中我会学到什么?

使用丰富的实体和不可变值对象建模领域。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「DDD 构建模块:实体与值对象」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

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