设计中的核心实践)
通用语言是领域专家与工程师之间的共享词汇。领域层中的每个类名、方法和变量都必须来源于此而非框架约定或持久化术语。错误信号名为OrderManager、OrderHelper或OrderService当Service不在领域词汇表中时的类说明实现行话正在污染领域语言。应使用领域专家所称的名称Order、OrderFulfillment、Shipment。将发现的通用语言直接映射为类型。如果某位专家说订阅在连续两次付款失败后变为逾期这句话蕴含了一条领域规则——它应归属于Subscription::markDelinquent()而非SubscriptionController。值对象值对象没有标识。两个实例如果值相等则视为相同。它们必须不可变——任何改变状态的操作都应返回一个新实例。实现?php // src/Domain/Order/Money.php declare(strict_types1); namespace App\Domain\Order; use InvalidArgumentException; final class Money { public function __construct( private readonly int $amount, // cents private readonly Currency $currency, ) { if ($amount 0) { throw new InvalidArgumentException(Amount cannot be negative); } } public function add(Money $other): self { if (!$this-currency-equals($other-currency)) { throw new InvalidArgumentException(Currency mismatch); } return new self($this-amount $other-amount, $this-currency); } public function equals(Money $other): bool { return $this-amount $other-amount $this-currency-equals($other-currency); } public function amount(): int { return $this-amount; } public function currency(): Currency { return $this-currency; } }值对象候选表值对象说明Money金额 币种算术运算返回新实例EmailAddress构造时校验统一转换为小写DateRange在构造函数中强制start end不变式Percentage0–100 范围无标识Coordinates经纬度对按值判等OrderStatus类似枚举PHP 8.1 优先使用枚举PHP 8.1 枚举后端枚举enum Status: string本质上是值对象——对于有限状态集应使用它们代替字符串常量或独立的值对象类。实体实体具有跨状态变更持久存在的标识。两个具有相同 ID 的Order对象是同一个订单无论它们的当前字段值如何。标识类型优先使用领域生成的 UUID而非数据库分配的自增整数 ID。自增 ID 强制在实体存在于内存之前进行一次数据库往返这会破坏聚合的一致性保证。?php final class OrderId { public function __construct(private readonly string $value) { if (!preg_match(/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i, $value)) { throw new InvalidArgumentException(Invalid OrderId: {$value}); } } public static function generate(): self { return new self(sprintf( %04x%04x-%04x-%04x-%04x-%04x%04x%04x, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), )); } public function value(): string { return $this-value; } public function equals(self $other): bool { return $this-value $other-value; } }聚合聚合是由单个聚合根组织的一组实体和值对象。所有外部访问都通过聚合根进行。跨越集群中多个对象的不变式由聚合根强制执行。规则仅通过 ID 引用其他聚合——切勿跨聚合边界持有直接对象引用。每次访问都加载外部聚合会降低性能并耦合边界。?php // src/Domain/Order/Order.php final class Order { private OrderStatus $status; /** var OrderLine[] */ private array $lines []; /** var DomainEvent[] */ private array $events []; public function __construct( private readonly OrderId $id, private readonly CustomerId $customerId, // foreign ID, not object private readonly Money $shippingFee, ) { $this-status OrderStatus::Draft; } public function addLine(ProductId $productId, int $qty, Money $unitPrice): void { $this-assertStatus(OrderStatus::Draft); if (count($this-lines) 50) { throw new OrderLineLimit(Max 50 lines per order); } $this-lines[] new OrderLine($productId, $qty, $unitPrice); } public function place(): void { $this-assertStatus(OrderStatus::Draft); if ($this-lines []) { throw new EmptyOrderException(Cannot place an empty order); } $this-status OrderStatus::Placed; $this-events[] new OrderPlaced($this-id, $this-customerId, $this-total()); } public function total(): Money { return array_reduce( $this-lines, fn(Money $carry, OrderLine $line) $carry-add($line-subtotal()), $this-shippingFee, ); } public function pullEvents(): array { $events $this-events; $this-events []; return $events; } private function assertStatus(OrderStatus $expected): void { if ($this-status ! $expected) { throw new InvalidOrderOperation( Operation requires status {$expected-name}, got {$this-status-name} ); } } }聚合大小保持聚合小巧。一个包含数百个子实体的聚合通常表明其中一些子实体应作为独立的聚合通过 ID 引用。较大的聚合会导致更长的事务锁和更多的合并冲突。聚合边界内不变式检查清单问题如果是该实体能否脱离聚合根存在它可能是独立的聚合聚合根是否对这些实体强制执行不变式将它们保留在同一聚合中它们是否在同一事务中一起持久化良好的信号说明它们应在一起聚合是否只是为了读取某个子实体而被加载考虑拆分仓储仓储提供类似集合的接口来访问聚合。它抽象了持久化机制——领域层定义接口基础设施层实现它。?php // src/Domain/Order/OrderRepository.php interface OrderRepository { public function get(OrderId $id): Order; // throws if not found public function find(OrderId $id): ?Order; // null if not found public function save(Order $order): void; public function delete(Order $order): void; /** return Order[] */ public function findByCustomer(CustomerId $customerId): array; }?php // src/Infrastructure/Persistence/DoctrineOrderRepository.php final class DoctrineOrderRepository implements OrderRepository { public function __construct(private readonly EntityManagerInterface $em) {} public function get(OrderId $id): Order { return $this-find($id) ?? throw new OrderNotFound($id); } public function find(OrderId $id): ?Order { return $this-em-find(Order::class, $id-value()); } public function save(Order $order): void { $this-em-persist($order); $this-em-flush(); } public function delete(Order $order): void { $this-em-remove($order); $this-em-flush(); } public function findByCustomer(CustomerId $customerId): array { return $this-em -createQuery(SELECT o FROM Order o WHERE o.customerId :id) -setParameter(id, $customerId-value()) -getResult(); } }仓储与查询服务仓储用于按标识或简单条件检索聚合。对于复杂的读模型仪表板、报表应使用专用的查询服务或直接发出 SQL 的读模型——不要将领域模型扭曲为 DTO 工厂。测试用内存实现?php final class InMemoryOrderRepository implements OrderRepository { /** var arraystring, Order */ private array $store []; public function get(OrderId $id): Order { return $this-store[$id-value()] ?? throw new OrderNotFound($id); } public function find(OrderId $id): ?Order { return $this-store[$id-value()] ?? null; } public function save(Order $order): void { $this-store[$order-id()-value()] $order; } public function delete(Order $order): void { unset($this-store[$order-id()-value()]); } public function findByCustomer(CustomerId $customerId): array { return array_values(array_filter( $this-store, fn(Order $o) $o-customerId()-equals($customerId), )); } }领域事件领域事件记录了领域中发生的事件。它们以过去时态命名不可变并携带处理程序所需的所有数据。?php // src/Domain/Shared/DomainEvent.php interface DomainEvent { public function occurredAt(): DateTimeImmutable; }?php // src/Domain/Order/OrderPlaced.php final readonly class OrderPlaced implements DomainEvent { public DateTimeImmutable $occurredAt; public function __construct( public readonly OrderId $orderId, public readonly CustomerId $customerId, public readonly Money $total, ) { $this-occurredAt new DateTimeImmutable(); } public function occurredAt(): DateTimeImmutable { return $this-occurredAt; } }分发策略策略保证适用场景收集 保存后分发仅当保存成功时触发事件默认——适用于大多数用例事务性发件箱跨进程边界至少一次投递异步处理程序、微服务同步事务内处理程序在同一数据库事务中运行很少合理耦合限界上下文pullEvents()模式读取后清空事件队列是标准的收集-分发方式。应用层在$repository-save($order)之后调用它并将事件分发到事件总线。领域服务领域服务包含不属于单个实体或值对象的领域逻辑通常是需要多个聚合或外部领域接口的操作。过度使用警告最终落入领域服务的大部分逻辑实际上应归属于聚合或值对象。如果第一反应是创建服务不妨先问问该方法是否可以直接归属于某个输入对象。?php // src/Domain/Pricing/PriceCalculator.php // Needs access to multiple aggregates a domain-level discount policy final class PriceCalculator { public function __construct( private readonly DiscountRepository $discounts, ) {} public function calculate(Order $order, Customer $customer): Money { $base $order-total(); $discount $this-discounts-findFor($customer-tier()); return $discount ! null ? $discount-apply($base) : $base; } }应用层应用层编排用例。它不包含领域逻辑——它协调领域对象、提交事务并分发事件。标准单元是命令 处理程序对。?php // src/Application/Order/PlaceOrderCommand.php final readonly class PlaceOrderCommand { public function __construct( public readonly string $orderId, public readonly string $customerId, public readonly array $lines, // [{productId, qty, unitPrice}] public readonly int $shippingFee, public readonly string $currency, ) {} }?php // src/Application/Order/PlaceOrderHandler.php final class PlaceOrderHandler { public function __construct( private readonly OrderRepository $orders, private readonly EventBusInterface $events, ) {} public function __invoke(PlaceOrderCommand $cmd): void { $currency new Currency($cmd-currency); $order new Order( new OrderId($cmd-orderId), new CustomerId($cmd-customerId), new Money($cmd-shippingFee, $currency), ); foreach ($cmd-lines as $line) { $order-addLine( new ProductId($line[productId]), $line[qty], new Money($line[unitPrice], $currency), ); } $order-place(); $this-orders-save($order); foreach ($order-pullEvents() as $event) { $this-events-dispatch($event); } } }层依赖规则领域层 → 无依赖。应用层 → 仅依赖领域层。基础设施层 → 依赖领域层 应用层。表现层 → 依赖应用层。绝不允许领域层依赖基础设施层。限界上下文限界上下文是领域模型适用的显式边界。同一个词在不同上下文中可能有不同含义——计费上下文中的客户可能携带订单上下文不需要或不拥有的发票数据。上下文映射模式模式方向适用场景共享内核双向两个团队共享一个稳定的小规模子模型变更需双方共同协定客户/供应商上游/下游下游团队的需求正式影响上游的排期顺从者下游遵循上游下游按原样接受上游模型例如第三方 API防腐层下游进行转换下游需要隔离上游模型——见下文开放主机服务上游发布上游为多个消费者暴露稳定的协议发布语言双方文档完善的共享格式例如消息总线上的领域事件在 PHP 单体应用中限界上下文通过命名空间和自动化架构测试Deptrac、PHPArkitect来强制执行。命名空间违规在 CI 中捕获而非运行时。# deptrac.yaml — prevent Billing from importing Order internals layers: - name: Billing collectors: - type: namespace regex: ^App\\Domain\\Billing - name: Order collectors: - type: namespace regex: ^App\\Domain\\Order ruleset: Billing: - Order # disallowed — Billing must go via ACL or events防腐层防腐层在外部上下文或外部服务与领域模型之间进行转换。它防止外部概念渗入领域层。?php // src/Infrastructure/Legacy/LegacyOrderAcl.php /** * Translates the legacy ERPs order format into domain objects. * The domain never sees LegacyOrder. */ final class LegacyOrderAcl { public function __construct( private readonly LegacyErpClient $erp ) {} public function findOrder(OrderId $id): ?Order { $raw $this-erp-getOrder($id-value()); if ($raw null) { return null; } return $this-translate($raw); } private function translate(array $raw): Order { $currency new Currency($raw[curr_code]); $order new Order( new OrderId($raw[ord_uuid]), new CustomerId($raw[cust_ref]), new Money((int) ($raw[ship_fee] * 100), $currency), ); foreach ($raw[items] as $item) { $order-addLine( new ProductId($item[sku]), (int) $item[qty], new Money((int) ($item[unit_price] * 100), $currency), ); } return $order; } }目录结构src/ ├── Domain/ │ ├── Order/ │ │ ├── Order.php # aggregate root │ │ ├── OrderId.php # VO identity │ │ ├── OrderLine.php # child entity │ │ ├── OrderStatus.php # backed enum │ │ ├── OrderPlaced.php # domain event │ │ ├── OrderRepository.php # interface │ │ └── Exceptions/ │ ├── Pricing/ │ │ ├── PriceCalculator.php # domain service │ │ └── DiscountRepository.php │ └── Shared/ │ ├── DomainEvent.php │ ├── Money.php │ └── Currency.php ├── Application/ │ └── Order/ │ ├── PlaceOrderCommand.php │ └── PlaceOrderHandler.php └── Infrastructure/ ├── Persistence/ │ ├── DoctrineOrderRepository.php │ └── InMemoryOrderRepository.php └── Legacy/ └── LegacyOrderAcl.php共享内核范围Domain/Shared必须保持精简——只包含真正跨越所有上下文的类型如Money、DomainEvent、聚合基类。如果它不断膨胀应提取为按上下文划分的共享类型并明确哪些上下文共享哪些类型。常见错误错误后果修复方法贫血领域模型所有逻辑都位于服务中实体沦为 getter/setter 的集合将行为迁移到实体中实体应自行强制执行不变式胖聚合长数据库锁、合并冲突、慢速加载按事务边界拆分跨边界操作使用事件ORM 实体 领域实体持久化模式渗入领域层可空外键、代理标识将 ORM 映射与领域对象分离或谨慎使用 Doctrine 嵌入对象/映射仓储当作查询构建器大量findByX方法业务查询散落在基础设施层添加专用的读模型/查询服务保持仓储轻量跨聚合对象引用耦合加载、循环加载、聚合边界被破坏仅通过 ID 引用在应用层分别加载保存前触发领域事件处理程序可能看到未持久化的变更在成功执行save()之后再分发事件使用 int/string 作为 ID错误 ID 类型被静默传入缺乏领域校验使用类型化的 ID 值对象OrderId、CustomerId应用逻辑位于领域层领域层依赖 HTTP 请求、会话或基础设施领域层只包含纯 PHP 代码——不包含框架接口不包含超全局变量