Skip to main content

entity

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

소스 정보

저장소
microwind/ai-skills
최근 소스 활동
2026년 5월 25일 23:32
감지된 SKILL.md 언어
중국어
스타
88
포크
24

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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` 字段化并要求外部维护一致性 → 能从其他字段算出的不必字段化;必须字段化时由聚合根内部计算与维护
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기