Skip to main content

entity

在DDD中具有唯一身份标识和生命周期的对象,通过身份而非属性值相等判断。

Informações da origem

Repositório
microwind/ai-skills
Última atividade na origem
25 de maio de 2026 às 23:32
Idioma detectado do SKILL.md
chinês
Estrelas
83
Forks
24

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
实体 (Entity)
description
在DDD中具有唯一身份标识和生命周期的对象,通过身份而非属性值相等判断。
# 实体 (Entity) ## 概述 Eric Evans 在蓝皮书中将实体定义为:**拥有贯穿时间与不同表现形式的、独特身份的对象**("Objects that have a distinct identity that runs through time and different representations")。两个实体即使属性完全相同,只要身份标识不同,它们就是不同的对象。 **核心特征**: - **唯一身份**:每个实体有唯一的 ID(标识符),由身份而非属性决定相等 - **生命周期连续性**:实体在时间中演化,可历经多种状态与表现形式(数据库行、内存对象、序列化结果)仍是同一个对象 - **可变性**:实体在生命周期内可以改变属性(但身份不变) - **行为承载**:实体不只是数据,更应封装业务规则与状态转换 **对比值对象**: - 实体:有身份,关心身份相等 - 值对象:无身份,关心值相等 ## 何时使用Entity **应该是Entity的**: - 用户(User)- 每个用户有唯一 ID - 订单(Order)- 每个订单有唯一订单号 - 产品(Product)- 每个产品有唯一SKU - 账户(Account)- 每个账户有唯一账号 **不应该是Entity的**: - 金额(Money)- 只关心数值,不关心"哪个"金额 - 地址(Address)- 两个相同地址就是一样的 - 颜色(Color)- 只关心颜色值,不关心"哪个"颜色 ## 实体的关键特征 ### 1. 身份标识 ```java public class User { private final String userId; // 身份标识(不可变) private String name; // 属性(可变) private String email; // 属性(可变) public User(String userId, String name, String email) { this.userId = userId; this.name = name; this.email = email; } // 通过身份判断相等,不通过属性值 @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; User user = (User) o; return Objects.equals(userId, user.userId); // 只比对 ID } @Override public int hashCode() { return Objects.hash(userId); // 只用 ID 计算哈希 } } // 两个用户,相同属性但不同ID → 不相等 User user1 = new User("U001", "John", "john@example.com"); User user2 = new User("U002", "John", "john@example.com"); assertNotEquals(user1, user2); // 不相等,因为 ID 不同 ``` ### 2. 生命周期管理 ```java public class Order { private final String orderId; private OrderStatus status; private List<OrderLine> items; private double totalAmount; private Instant createdAt; private Instant modifiedAt; // 创建 public static Order create(String customerId, List<OrderLine> items) { Order order = new Order(UUID.randomUUID().toString(), items); order.status = OrderStatus.PENDING; order.createdAt = Instant.now(); return order; } // 状态变化 public void pay() { if (status != OrderStatus.PENDING) { throw new IllegalStateException("Cannot pay order in status: " + status); } this.status = OrderStatus.PAID; this.modifiedAt = Instant.now(); } public void ship() { if (status != OrderStatus.PAID) { throw new IllegalStateException("Cannot ship unpaid order"); } this.status = OrderStatus.SHIPPED; this.modifiedAt = Instant.now(); } public void deliver() { if (status != OrderStatus.SHIPPED) { throw new IllegalStateException("Cannot deliver unshipped order"); } this.status = OrderStatus.DELIVERED; this.modifiedAt = Instant.now(); } } // 订单的生命周期 Order order = Order.create("C001", items); order.pay(); // PENDING → PAID order.ship(); // PAID → SHIPPED order.deliver(); // SHIPPED → DELIVERED ``` ### 3. 可变性 ```java public class User { private final String userId; // 不可变 private String name; // 可变 private String email; // 可变 public void updateProfile(String newName, String newEmail) { this.name = newName; this.email = newEmail; } } // 实体的属性可以改变,但 ID 不变 User user = new User("U001", "John", "john@example.com"); user.updateProfile("Jane", "jane@example.com"); // 仍然是同一个用户,只是属性改变了 assertEquals("U001", user.getUserId()); ``` ## 最佳实践 ### 1️⃣ 使用合适的标识符 ```java // ❌ 使用自增数字(可能重用) public class User { private long id; // 1, 2, 3... 不够唯一 } // ✅ 使用 UUID(全局唯一) public class User { private String userId = UUID.randomUUID().toString(); } // ✅ 使用业务标识符(有含义) public class Product { private String sku; // 库存保留单位,业务上唯一 } ``` ### 2️⃣ 保护实体的一致性 ```java public class BankAccount { private final String accountId; private double balance; // ❌ 不保护不变式 public void setBalance(double balance) { this.balance = balance; // 允许负数! } // ✅ 保护不变式 public void withdraw(double amount) throws InsufficientFundsException { if (balance < amount) { throw new InsufficientFundsException(); } this.balance -= amount; } public void deposit(double amount) throws InvalidAmountException { if (amount <= 0) { throw new InvalidAmountException(); } this.balance += amount; } } ``` ### 3️⃣ 用值对象封装属性 ```java // ❌ 混乱的属性 public class User { private String firstName; private String lastName; private String emailValue; private String emailDomain; } // ✅ 用值对象组织 public class User { private final String userId; private FullName name; // 值对象:firstName + lastName private Email email; // 值对象:封装邮箱逻辑 private Address address; // 值对象:封装地址 } ``` ### 4️⃣ 明确边界和聚合 ```java public class Order { private final String orderId; private Customer customer; // 引用,不嵌入 private List<OrderLine> items; // 从属(聚合根) // ❌ 不要直接修改 items // public List<OrderLine> getItems() { return items; } // ✅ 提供受控的操作 public void addItem(Product product, int quantity) { OrderLine line = new OrderLine(product, quantity); this.items.add(line); } public void removeItem(String productId) { items.removeIf(line -> line.getProduct().getId().equals(productId)); } } ``` ## Java 实现示例 ```java public class Order { private final String orderId; private String customerId; private OrderStatus status; private List<OrderLine> items; private Money totalAmount; private Instant createdAt; private Instant modifiedAt; public Order(String orderId, String customerId) { this.orderId = orderId; this.customerId = customerId; this.status = OrderStatus.PENDING; this.items = new ArrayList<>(); this.createdAt = Instant.now(); } public void addItem(Product product, int quantity) { if (quantity <= 0) { throw new IllegalArgumentException("Quantity must > 0"); } OrderLine line = new OrderLine(product, quantity); items.add(line); recalculateTotal(); } public void pay() { if (status != OrderStatus.PENDING) { throw new IllegalStateException("Can only pay pending orders"); } this.status = OrderStatus.PAID; this.modifiedAt = Instant.now(); } private void recalculateTotal() { totalAmount = items.stream() .map(OrderLine::getTotal) .reduce(Money.ZERO, Money::add); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Order)) return false; Order order = (Order) o; return Objects.equals(orderId, order.orderId); } @Override public int hashCode() { return Objects.hash(orderId); } } ``` ## Python 实现示例 ```python from dataclasses import dataclass from enum import Enum from datetime import datetime from typing import List class OrderStatus(Enum): PENDING = "PENDING" PAID = "PAID" SHIPPED = "SHIPPED" DELIVERED = "DELIVERED" @dataclass class OrderLine: product_id: str quantity: int unit_price: float def get_total(self) -> float: return self.quantity * self.unit_price class Order: def __init__(self, order_id: str, customer_id: str): self.order_id = order_id # 身份标识 self.customer_id = customer_id self.status = OrderStatus.PENDING self.items: List[OrderLine] = [] self.total_amount = 0.0 self.created_at = datetime.now() self.modified_at = None def add_item(self, product_id: str, quantity: int, unit_price: float): if quantity <= 0: raise ValueError("Quantity must > 0") self.items.append(OrderLine(product_id, quantity, unit_price)) self._recalculate_total() def pay(self): if self.status != OrderStatus.PENDING: raise ValueError(f"Cannot pay order in status {self.status}") self.status = OrderStatus.PAID self.modified_at = datetime.now() def _recalculate_total(self): self.total_amount = sum(item.get_total() for item in self.items) def __eq__(self, other): # 通过 ID 判断相等 if not isinstance(other, Order): return False return self.order_id == other.order_id def __hash__(self): return hash(self.order_id) ``` ## 实体的四种"血液模型" 在实际工程中,实体的代码形态可以按**携带业务逻辑的多少**分为四种,选型直接影响领域层的厚度与可维护性。 > **术语对照**:英文 DDD 圈通常只区分两种——**Anemic Domain Model(贫血领域模型,Fowler 视为反模式)** 与 **Rich Domain Model(充血模型)**。下表"四种血液模型"是中文工程社区的进一步细化划分;阅读 Evans / Vernon 原著时,请把"贫血 / 失血"都对应到 Fowler 的 Anemic 概念,不要混淆。 | 形态 | 包含内容 | 评价 | |------|----------|------| | **失血模型** | 仅有数据字段和 getter/setter(Java 中的 POJO) | 不算领域对象,只是数据容器 | | **贫血模型** | 数据 + 不依赖持久层的业务逻辑;**依赖持久层的业务逻辑放到领域服务**中 | **工程推荐**,平衡可维护性与充血度 | | **充血模型** | 数据 + 所有业务逻辑(含依赖持久层的) | 领域对象最"纯",但与持久层耦合 | | **胀血模型** | 数据 + 业务逻辑 + 与业务无关的其他逻辑(事务、授权、日志等) | 应避免,违反单一职责 | ### 对比示例 ```java // ❌ 失血模型:只有字段,业务逻辑全部外泄 public class Order { private Long id; private Integer status; private BigDecimal amount; // 50 行 getter/setter ... } public class OrderService { public void pay(Long orderId) { Order o = orderDao.get(orderId); if (o.getStatus() != 0) throw new RuntimeException("状态错误"); o.setStatus(1); orderDao.update(o); } } // ✅ 贫血模型:数据 + 核心业务规则,持久化交给仓储 public class Order { private OrderId id; private OrderStatus status; private Money amount; public void pay() { // 业务规则在实体内 if (status != OrderStatus.PENDING) { throw new DomainException("只有待支付订单可以支付"); } this.status = OrderStatus.PAID; } } public class OrderAppService { public void pay(OrderId id) { Order order = orderRepo.findById(id); order.pay(); // 调业务方法 orderRepo.save(order); // 持久化由仓储负责 } } // 充血模型:持久化也在实体内(少用) public class Order { public void pay() { if (status != OrderStatus.PENDING) { ... } this.status = OrderStatus.PAID; OrderRepository.save(this); // 持久化耦合进实体 } } // ❌ 胀血模型:一锅端,权限/事务/日志全塞进来 public class Order { @Transactional @RequiresPermission("ORDER_PAY") public void pay(User operator) { AuditLog.log("..."); // ← 和业务无关的东西 // ... } } ``` **经验建议**: - **首选贫血模型**:实体带业务规则,但不依赖仓储;仓储由[应用服务](../application-service/)或[领域服务](../domain-service/)调用 - **实体 + 领域服务 = 完整领域模型** - 避免胀血模型:事务、权限、日志等横切关注点属于应用层或 AOP,不属于领域层 ## 与值对象的对比 | 特性 | Entity | Value Object | |------|--------|--------------| | 身份 | 有唯一 ID | 无 ID | | 相等性 | 基于 ID | 基于属性值 | | 可变性 | 通常可变 | 不可变 | | 生命周期 | 有明确生命周期 | 无生命周期 | | 例子 | User, Order | Money, Email, Address | ## 实体 vs DTO 实体经常被误用为 DTO(数据传输对象),导致领域层退化为"带方法的字段袋"。二者必须严格区分: | 维度 | Entity(实体) | DTO(数据传输对象) | |------|---------------|--------------------| | 所属层 | 领域层 | 接口层 / 应用层 | | 目的 | 承载业务规则与状态转换 | 在层与层、进程与进程之间搬运数据 | | 行为 | 有业务方法(pay、ship、approve) | 通常只有字段 + getter/setter | | 不变量 | 自我保护、构造即合法 | 不维护不变量,由发送方负责 | | 身份 | 有领域标识(OrderId) | 通常无标识 | | 序列化 | 不直接序列化(避免暴露内部结构) | 为序列化而生(JSON、ProtoBuf) | | 生命周期 | 与业务对象一致,长久存在 | 一次请求/响应即弃 | > **判别关键**:如果一个类**既要被序列化为 HTTP 响应,又要承载业务规则**,几乎可以肯定是把 Entity 和 DTO 揉在了一起——拆开。 ```java // ❌ Entity 与 DTO 混用 @RestController public class OrderController { @GetMapping("/orders/{id}") public Order getOrder(@PathVariable String id) { return orderRepository.findById(id); // 直接把 Entity 序列化返回 } } // ✅ 用 DTO 隔离边界 @RestController public class OrderController { @GetMapping("/orders/{id}") public OrderDTO getOrder(@PathVariable String id) { Order order = orderRepository.findById(id); return OrderDTO.from(order); // 在边界处转换 } } ``` ## 常见误区 ❌ **用属性判等**——`equals` 比对 `name + email`,导致两个不同 `userId` 的 User 因属性相同被认为相等 → 实体的相等性永远基于身份标识,与任何属性无关 ❌ **全字段 setter 暴露**——实体退化为数据袋,业务规则散落到 Service → 暴露**业务方法**(pay、ship、cancel),让状态变化必经规则校验 ❌ **持久化可计算字段**——把 `totalAmount` 字段化并要求外部维护一致性 → 能从其他字段算出的不必字段化;必须字段化时由聚合根内部计算与维护
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub