3领域驱动设计

实体

唯一标识的对象

实体标识生命周期

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章 实体 - 唯一标识的对象 (Entities - Objects with Unique Identity)

一、导读

3.1 本章学习目标

通过本章的学习,读者将能够:

理解实体(Entity)在 DDD 中的定义与核心特征

掌握实体与值对象的本质区别

学会设计实体的唯一标识符

理解实体的生命周期管理

掌握实体相等性判断的正确方式

能够在实际项目中正确识别和设计实体

理解实体与数据库主键的区别

掌握处理实体引用和关联的最佳实践

3.2 为什么本章如此重要

实体是领域模型中最基本的构建块之一。在大多数业务系统中,都存在一些需要长期跟踪、具有连续性的概念,如客户、订单、产品、账户等。这些概念无论其属性如何变化,我们仍然认为它们是"同一个"对象。这种连续性正是由实体的唯一标识来保证的。

正确理解和使用实体对于构建高质量的领域模型至关重要。如果错误地将应该作为实体设计的对象设计为值对象,会导致无法正确跟踪对象的变化历史;反之,如果将应该是值对象的对象设计为实体,会引入不必要的复杂性,使系统难以理解和维护。

在实际开发中,实体常常被误解为仅仅是数据库表的映射。这种误解导致开发人员过度关注实体的持久化细节,而忽视了其业务含义。DDD 强调实体首先是业务概念,其次才是技术实现。实体的设计应该由业务需求驱动,而不是由数据库结构决定。

此外,实体的相等性判断是一个常见的问题领域。在许多编程语言中,默认的相等性比较是基于对象引用的,但这对于实体来说往往是不合适的。两个实体即使内存地址不同,只要它们的标识相同,就应该被认为是相等的。理解并正确实现实体的相等性语义是本章的重要内容。

3.3 核心问题引导

在学习本章之前,请思考以下问题:

什么是实体?它与普通对象有什么本质区别?

为什么实体需要唯一标识?这个标识可以是业务属性吗?

实体和值对象的根本区别是什么?如何判断一个概念应该是实体还是值对象?

实体的唯一标识与数据库主键是一回事吗?它们有什么区别?

如何正确实现实体的相等性判断?为什么不能直接使用语言提供的默认实现?

实体的属性可以变化吗?如果可以,变化的边界在哪里?

如何处理实体之间的关联?是否应该在实体中直接持有其他实体的引用?

实体的生命周期由谁管理?创建和销毁实体的责任归属是什么?


二、核心概念详解

3.1 实体的定义与本质

实体(Entity)是领域模型中的一种对象类型,它的核心特征是具有唯一的标识(Identity),并且这个标识在对象的整个生命周期中保持不变。无论实体的其他属性如何变化,只要标识不变,它就仍然是同一个实体。

实体的本质在于其连续性(Continuity)。以人为例,一个人的姓名、年龄、地址、职业等属性都可能随着时间而变化,但我们仍然认为他是同一个人。这是因为我们有一个内在的标识机制——无论是身份证号、生物特征还是社会关系——来识别个体的连续性。在软件系统中,我们需要显式地定义这种标识机制。

实体不仅仅是一个数据容器,它还包含业务行为。实体的方法应该表达与该实体相关的业务操作和规则。例如,一个 Customer 实体可能有 placeOrder() 方法来下订单,有 updateAddress() 方法来更新地址。这些方法内部封装了相应的业务规则。

需要注意的是,实体的标识是概念层面的,不依赖于任何特定的技术实现。虽然在实践中,我们常常使用数据库生成的自增 ID 或 UUID 作为实体的标识,但这些只是标识的技术载体。实体的标识首先是一个业务概念,它代表了该实体在业务领域中的唯一性。

3.2 实体的核心特征

实体具有以下几个核心特征:

唯一标识:这是实体最本质的特征。每个实体实例都有一个唯一的标识,这个标识在实体的整个生命周期中保持不变。标识用于区分不同的实体实例,即使它们的其他属性完全相同。

可变性:实体的属性可以随时间变化。例如,客户的地址可以更改,订单的状态可以更新,员工的职位可以调整。这种可变性是实体与值对象的重要区别之一。但需要注意的是,并非实体的所有属性都必须可变,有些属性可能在创建后就不再改变。

连续性:实体在其生命周期内保持身份连续性。即使实体的状态发生了显著变化,只要标识不变,它仍然是同一个实体。这种连续性使得我们能够跟踪实体的历史变化和演进过程。

业务行为:实体不仅仅是数据的集合,它还封装了与该实体相关的业务规则和操作。实体的方法应该表达业务意图,而不是简单的数据访问。

生命周期管理:实体通常有明确的生命周期,从创建到活跃再到归档或删除。在不同的生命周期阶段,实体可能具有不同的行为和约束。

3.3 实体与值对象的区别

实体和值对象是 DDD 中两种最基本的对象类型,正确区分它们对于领域建模至关重要。以下是它们的主要区别:

标识 vs 属性:实体通过唯一标识来区分,而值对象通过属性值来区分。两个实体即使所有属性都相同,只要标识不同,就是不同的实体;两个值对象如果所有属性都相同,则被认为是相等的。

可变性 vs 不可变性:实体通常是可变的,其属性可以随时间变化;值对象应该是不可变的,一旦创建就不能修改。如果需要"修改"值对象,实际上是创建一个新的值对象实例。

连续性 vs 瞬时性:实体具有时间上的连续性,我们关心它的变化历史;值对象是瞬时的,我们只关心它当前的值,不关心它的历史。

共享引用 vs 独立副本:实体可以被多个其他对象共享引用,因为它们有明确的标识;值对象通常应该复制传递,避免共享引用带来的意外修改。

复杂度:实体通常比值对象更复杂,因为它们需要管理标识、生命周期和状态转换;值对象相对简单,主要是数据的封装。

判断一个概念应该是实体还是值对象,可以问以下问题:

  • 我们是否需要跟踪这个对象的变化历史?如果是,可能是实体。
  • 这个对象是否有自然的生命周期?如果是,可能是实体。
  • 两个具有相同属性的对象是否代表不同的事物?如果是,可能是实体。
  • 这个对象是否可以被替换为另一个具有相同属性的对象而不影响业务逻辑?如果是,可能是值对象。

3.4 实体标识的设计

实体标识的设计是实体建模中的关键环节。一个好的标识设计应该满足以下要求:

唯一性:标识必须在相应的范围内唯一。这个范围可能是一个系统、一个模块或一个聚合。确保唯一性是标识的基本职责。

稳定性:标识在实体的整个生命周期中应该保持不变。即使实体的其他属性发生剧烈变化,标识也不应该改变。

简洁性:标识应该尽可能简洁,便于存储、传输和比较。过于复杂的标识会增加系统的负担。

无关性:理想的标识不应该携带业务含义。带有业务含义的标识(如使用身份证号作为客户标识)可能在业务规则变化时出现问题。

标识的实现方式有多种选择:

代理键(Surrogate Key):由系统生成的无意义标识,如自增整数、UUID。这是最常用的方式,优点是简单、稳定、与业务无关。缺点是无法从标识本身获取任何业务信息。

自然键(Natural Key):基于业务属性的标识,如身份证号、邮箱地址。优点是具有业务含义,便于人工识别。缺点是可能变化、可能重复、可能不符合格式要求。

复合键:由多个属性组合而成的标识。在某些场景下使用,但增加了复杂性,应谨慎使用。

在实际项目中,推荐使用代理键作为实体的内部标识,同时保留自然键作为业务属性。这样既保证了标识的稳定性,又保留了业务含义。

3.5 实体的相等性判断

实体的相等性判断是一个容易出错的地方。在大多数面向对象语言中,默认的相等性比较是基于对象引用的,即只有指向同一内存地址的两个引用才被认为相等。但对于实体来说,这种默认行为通常是不合适的。

实体的相等性应该基于标识来判断:如果两个实体实例具有相同的标识,它们就应该是相等的,即使它们的其他属性不同。这是因为标识代表了实体的本质身份,而其他属性只是状态的反映。

以 Java 为例,正确的实体相等性实现应该重写 equals() 和 hashCode() 方法:

java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Customer customer = (Customer) o;
    return id != null && id.equals(customer.id);
}

@Override
public int hashCode() {
    return id != null ? id.hashCode() : 0;
}

注意几个关键点:

首先检查是否为同一对象引用(this == o)。

检查类型是否匹配。

只基于标识进行比较,忽略其他属性。

处理标识为 null 的情况(通常在实体尚未持久化时可能出现)。

在 Python 中,可以通过重写 __eq__ 和 __hash__ 方法来实现类似的逻辑。

错误的相等性实现可能导致各种问题,如在集合中无法正确查找实体、缓存失效、测试失败等。因此,正确实现实体的相等性是非常重要的。

3.6 实体的生命周期

实体通常有明确的生命周期,从创建到消亡经历多个阶段。理解和管理实体的生命周期对于保证数据一致性和业务规则的正确执行至关重要。

典型的实体生命周期包括以下阶段:

创建阶段:实体被创建,分配唯一标识。在这个阶段,可能需要验证初始状态的合法性,设置默认值,触发创建事件等。

活跃阶段:实体处于正常使用状态,其属性可能发生变化,执行业务操作。这是实体生命周期的主要阶段,大部分业务规则都在这个阶段应用。

归档阶段(可选):实体不再活跃,但需要保留历史记录。归档的实体通常变为只读,不允许进一步修改。

删除阶段:实体从系统中移除。在某些业务场景中,物理删除可能不被允许,而是采用软删除(标记为已删除)的方式。

在不同生命周期阶段,实体可能具有不同的行为约束。例如,已取消的订单不能再添加商品,已关闭的账户不能再进行交易。这些约束应该在实体的方法中得到体现。

实体的生命周期管理通常涉及多个组件的配合:工厂负责创建实体,仓储负责持久化和检索实体,领域事件用于通知生命周期变化。

3.7 实体关联的处理

在领域模型中,实体之间往往存在关联关系。如何处理这些关联是实体设计中的重要问题。

一对一关联:一个实体引用另一个实体。例如,订单引用客户。这种关联相对简单,可以直接在实体中持有引用。

一对多关联:一个实体引用多个其他实体。例如,客户有多个订单。这种情况下,需要考虑是否需要在内存中加载所有关联实体。对于大量关联的情况,可能更适合使用仓储按需查询。

多对多关联:最复杂的关联类型。例如,学生选修课程,一个学生可以选修多门课程,一门课程也可以被多个学生选修。这种关联通常需要中间实体来管理。

在处理实体关联时,需要注意以下原则:

避免循环引用:如果 A 引用 B,B 又引用 A,可能形成循环引用,导致序列化问题和内存泄漏。应该尽量设计单向关联。

延迟加载:对于可能包含大量数据的关联,应该支持延迟加载,只在需要时才获取关联实体。

聚合边界:在同一个聚合内的实体可以直接引用,跨聚合的实体应该通过标识引用,而不是直接持有对象引用。

一致性责任:明确哪个实体负责维护关联的一致性。通常,聚合根负责管理聚合内部的关联。


三、程序员的视角

3.1 在实际项目中应用实体模式

作为程序员,在日常工作中设计和实现实体时,应该注意以下几个方面:

首先,在创建新类时,先判断它应该是实体还是值对象。问自己:这个对象是否需要唯一标识?是否需要跟踪它的变化?两个属性相同的对象是否代表不同的事物?根据答案来决定类型。

其次,为实体设计合适的标识策略。优先使用代理键(如 UUID 或自增 ID),避免使用可能变化的业务属性作为标识。如果业务上有自然键,将其作为普通属性保留,但不要用作唯一标识。

第三,正确实现相等性判断。重写 equals() 和 hashCode() 方法(Java)或 __eq__ 和 __hash__ 方法(Python),只基于标识进行比较。确保在集合操作中实体能够正确工作。

第四,将业务规则封装在实体内部。实体的方法应该表达业务意图,如 order.cancel() 而非 order.setStatus(CANCELLED)。通过方法名传达业务语义。

第五,保护实体的不变量。在构造函数和方法中进行必要的验证,确保实体始终处于合法状态。对于无效的操作,抛出有意义的异常。

第六,谨慎处理实体关联。避免在实体中直接加载大量关联数据,考虑使用仓储按需查询。对于跨聚合的引用,使用标识而非对象引用。

3.2 常见陷阱与误区

在实体设计和实现中,常见的陷阱包括:

陷阱一:将实体等同于数据库表。很多开发者习惯先设计数据库表,然后生成对应的实体类。这种方式产生的"实体"实际上只是数据映射对象,缺乏业务行为。实体应该是业务概念的体现,而不是技术的产物。

陷阱二:使用业务属性作为标识。例如用邮箱地址作为用户标识。当用户更换邮箱时,会出现问题。应该使用无意义的代理键作为标识。

陷阱三:基于所有属性实现相等性。这会导致实体在属性变化后在集合中"消失"。实体的相等性应该只基于标识。

陷阱四:贫血实体。只有 getter/setter,没有业务方法。所有的业务逻辑都放在服务层。这违背了面向对象的封装原则。

陷阱五:过度关联。在实体中持有大量其他实体的引用,导致加载性能问题和循环依赖。应该按需加载关联实体。

陷阱六:忽视不变量。没有在构造函数和方法中验证状态,导致实体可能处于非法状态。

3.3 与其他模式的协作

实体不是孤立存在的,它与其他 DDD 模式紧密协作:

与值对象协作:实体可以使用值对象作为属性。例如,客户实体可以有地址值对象。值对象提供不可变的、可复用的数据结构。

与聚合协作:实体可以是聚合根,也可以是聚合内部的实体。聚合根负责管理聚合内部的一致性。

与仓储协作:仓储负责实体的持久化和检索。实体不应该关心自己如何被存储,这是仓储的职责。

与工厂协作:工厂负责创建复杂的实体,特别是当创建过程涉及多个步骤或需要外部依赖时。

与领域事件协作:实体在状态变化时可以发布领域事件,通知其他组件。这使得实体之间可以松耦合地协作。


四、代码实战

4.1 Java 示例:订单实体

java
/**
 * 订单实体
 * 订单具有唯一标识,其状态和内容可以随时间变化
 */
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private OrderStatus status;
    private final List<OrderItem> items;
    private Money totalAmount;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // 构造函数
    public Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
        this.status = OrderStatus.CREATED;
        this.items = new ArrayList<>();
        this.totalAmount = Money.zero();
        this.createdAt = LocalDateTime.now();
        this.updatedAt = this.createdAt;
    }
    
    /**
     * 添加商品项
     * 只有在订单处于 CREATED 状态时才能添加商品
     */
    public void addItem(Product product, int quantity) {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("Can only add items to created orders");
        }
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        
        OrderItem item = new OrderItem(product.getId(), product.getName(), product.getPrice(), quantity);
        this.items.add(item);
        recalculateTotal();
        touch();
    }
    
    /**
     * 移除商品项
     */
    public void removeItem(ProductId productId) {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("Can only remove items from created orders");
        }
        
        boolean removed = this.items.removeIf(item -> item.getProductId().equals(productId));
        if (removed) {
            recalculateTotal();
            touch();
        }
    }
    
    /**
     * 确认订单
     * 订单确认后不能再修改
     */
    public void confirm() {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("Order can only be confirmed from CREATED status");
        }
        if (this.items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm empty order");
        }
        
        this.status = OrderStatus.CONFIRMED;
        touch();
    }
    
    /**
     * 取消订单
     */
    public void cancel(CancellationReason reason) {
        if (this.status == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Cannot cancel delivered order");
        }
        
        this.status = OrderStatus.CANCELLED;
        touch();
    }
    
    private void recalculateTotal() {
        this.totalAmount = this.items.stream()
                .map(OrderItem::subtotal)
                .reduce(Money.zero(), Money::add);
    }
    
    private void touch() {
        this.updatedAt = LocalDateTime.now();
    }
    
    // 相等性判断:只基于标识
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return id != null && id.equals(order.id);
    }
    
    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }
    
    // Getters
    public OrderId getId() { return id; }
    public CustomerId getCustomerId() { return customerId; }
    public OrderStatus getStatus() { return status; }
    public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
    public Money getTotalAmount() { return totalAmount; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public LocalDateTime getUpdatedAt() { return updatedAt; }
}

/**
 * 订单项 - 作为值对象嵌入订单实体
 */
public class OrderItem {
    private final ProductId productId;
    private final String productName;
    private final Money unitPrice;
    private final int quantity;
    
    public OrderItem(ProductId productId, String productName, Money unitPrice, int quantity) {
        this.productId = productId;
        this.productName = productName;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }
    
    public Money subtotal() {
        return unitPrice.multiply(quantity);
    }
    
    // Getters
    public ProductId getProductId() { return productId; }
    public String getProductName() { return productName; }
    public Money getUnitPrice() { return unitPrice; }
    public int getQuantity() { return quantity; }
}

/**
 * 订单ID - 值对象
 */
public class OrderId {
    private final String value;
    
    public OrderId(String value) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException("Order ID cannot be null or empty");
        }
        this.value = value;
    }
    
    public String getValue() {
        return value;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderId orderId = (OrderId) o;
        return value.equals(orderId.value);
    }
    
    @Override
    public int hashCode() {
        return value.hashCode();
    }
}

/**
 * 订单状态枚举
 */
public enum OrderStatus {
    CREATED, CONFIRMED, PAID, SHIPPED, DELIVERED, CANCELLED
}

4.2 Python 示例:用户实体

python
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional
import uuid


class UserStatus(Enum):
    """用户状态"""
    ACTIVE = "active"
    SUSPENDED = "suspended"
    DELETED = "deleted"


@dataclass(frozen=True)
class UserId:
    """值对象:用户ID"""
    value: str
    
    def __post_init__(self):
        if not self.value:
            raise ValueError("User ID cannot be empty")
    
    def __str__(self):
        return self.value


@dataclass(frozen=True)
class Email:
    """值对象:邮箱地址"""
    value: str
    
    def __post_init__(self):
        if not self.value or "@" not in self.value:
            raise ValueError("Invalid email address")
    
    def __str__(self):
        return self.value


class User:
    """实体:用户
    
    用户具有唯一标识,其属性可以随时间变化。
    即使用户更改了邮箱或姓名,他仍然是同一个用户。
    """
    
    def __init__(self, user_id: UserId, email: Email, name: str):
        self._id = user_id
        self._email = email
        self._name = name
        self._status = UserStatus.ACTIVE
        self._created_at = datetime.now()
        self._updated_at = self._created_at
    
    @property
    def id(self) -> UserId:
        return self._id
    
    @property
    def email(self) -> Email:
        return self._email
    
    @property
    def name(self) -> str:
        return self._name
    
    @property
    def status(self) -> UserStatus:
        return self._status
    
    def change_email(self, new_email: Email):
        """更改邮箱"""
        if self._status != UserStatus.ACTIVE:
            raise RuntimeError("Cannot change email for inactive user")
        
        self._email = new_email
        self._touch()
    
    def change_name(self, new_name: str):
        """更改姓名"""
        if self._status != UserStatus.ACTIVE:
            raise RuntimeError("Cannot change name for inactive user")
        
        if not new_name or len(new_name.strip()) == 0:
            raise ValueError("Name cannot be empty")
        
        self._name = new_name
        self._touch()
    
    def suspend(self):
        """暂停用户"""
        if self._status != UserStatus.ACTIVE:
            raise RuntimeError("Can only suspend active users")
        
        self._status = UserStatus.SUSPENDED
        self._touch()
    
    def reactivate(self):
        """重新激活用户"""
        if self._status != UserStatus.SUSPENDED:
            raise RuntimeError("Can only reactivate suspended users")
        
        self._status = UserStatus.ACTIVE
        self._touch()
    
    def delete(self):
        """删除用户(软删除)"""
        self._status = UserStatus.DELETED
        self._touch()
    
    def _touch(self):
        """更新时间戳"""
        self._updated_at = datetime.now()
    
    # 相等性判断:只基于ID
    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return self._id == other._id
    
    def __hash__(self):
        return hash(self._id)
    
    def __repr__(self):
        return f"User(id={self._id}, email={self._email}, name={self._name})"


# 使用示例
def user_lifecycle_example():
    """演示用户实体的生命周期"""
    # 创建用户
    user_id = UserId(str(uuid.uuid4()))
    email = Email("john@example.com")
    user = User(user_id, email, "John Doe")
    
    print(f"Created: {user}")
    print(f"Status: {user.status.value}")
    
    # 更改邮箱
    user.change_email(Email("john.doe@example.com"))
    print(f"New email: {user.email}")
    
    # 暂停用户
    user.suspend()
    print(f"Status after suspension: {user.status.value}")
    
    # 尝试在暂停状态下更改邮箱(应该失败)
    try:
        user.change_email(Email("new@example.com"))
    except RuntimeError as e:
        print(f"Error: {e}")

4.3 实体工厂示例

java
/**
 * 订单工厂
 * 负责创建复杂的订单实体
 */
public class OrderFactory {
    
    /**
     * 创建新订单
     */
    public Order createOrder(CustomerId customerId, List<OrderItemRequest> items) {
        OrderId orderId = new OrderId(generateOrderId());
        Order order = new Order(orderId, customerId);
        
        for (OrderItemRequest itemRequest : items) {
            order.addItem(itemRequest.getProduct(), itemRequest.getQuantity());
        }
        
        return order;
    }
    
    /**
     * 从快照重建订单
     * 用于从持久化存储中恢复订单
     */
    public Order rebuildFromSnapshot(OrderSnapshot snapshot) {
        Order order = new Order(snapshot.getId(), snapshot.getCustomerId());
        
        // 恢复订单项
        for (OrderItemSnapshot itemSnapshot : snapshot.getItems()) {
            order.addItemDirectly(itemSnapshot.toOrderItem());
        }
        
        // 恢复状态
        order.setStatusForRebuild(snapshot.getStatus());
        
        return order;
    }
    
    private String generateOrderId() {
        return "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8);
    }
}

五、深入思考

5.1 历史背景

实体的概念源于面向对象编程中的对象标识理论。在早期的面向对象语言如 Smalltalk 中,每个对象都有唯一的对象标识,这与 DDD 中的实体概念有相似之处。

然而,在关系数据库主导的时代,实体的概念常常被简化为数据库表的行。ORM 框架的出现试图弥合对象模型和关系模型之间的鸿沟,但也带来了新的混淆。许多开发者将实体视为仅仅是持久化的载体,忽视了其业务含义。

Eric Evans 在《领域驱动设计》中重新强调了实体的业务本质,将实体从技术实现中解放出来。他指出,实体首先是业务概念,其次才是技术实现。这一观点对后来的软件设计产生了深远影响。

5.2 前沿方向

实体模式的发展呈现以下几个趋势:

首先是事件溯源(Event Sourcing)的应用。在传统模式中,实体保存当前状态;在事件溯源中,实体保存一系列事件,状态可以从事件中重建。这种方式更好地捕捉了实体的变化历史。

其次是函数式编程的影响。一些开发者探索使用不可变数据和纯函数来处理实体,通过版本控制来管理实体的变化。这种方式在某些场景下可以提供更好的可预测性和可测试性。

第三是与分布式系统的结合。在分布式环境中,实体的标识需要考虑全局唯一性,实体的状态同步变得更加复杂。CRDT(无冲突复制数据类型)等技术被应用于解决这些问题。

5.3 延伸阅读

Eric Evans, 《领域驱动设计》第5章 - 原始论述

Vaughn Vernon, 《实现领域驱动设计》第6章 - 实用指南

Martin Fowler, 《Entity》- Patterns of Enterprise Application Architecture

Greg Young, 《Event Sourcing》- 事件溯源介绍


六、实验笔记

实验一:识别现有系统中的实体

实验目的:通过分析现有系统,练习识别实体和值对象。

实验步骤:

选择一个你熟悉的系统或模块。

列出其中所有的领域类。

对每个类,判断它应该是实体还是值对象,并说明理由。

检查实体的标识设计是否合理:是否唯一?是否稳定?是否与业务无关?

检查实体的相等性实现是否正确:是否只基于标识?

发现问题并提出改进建议。

预期收获:培养识别实体和值对象的能力,发现常见的设计问题。

实验二:设计一个博客系统的实体模型

实验目的:从零开始设计实体模型,练习实体设计的完整流程。

实验步骤:

确定博客系统的核心概念:文章、评论、用户、标签等。

对每个概念,判断它是实体还是值对象。

为每个实体设计唯一标识。

设计实体的属性和方法,确保业务规则被封装在实体内部。

设计实体之间的关联关系。

实现关键实体的代码,包括相等性判断。

编写单元测试验证实体的行为。

预期收获:体验完整的实体设计流程,掌握实体建模的技巧。


七、本章小结

本章详细介绍了 DDD 中的实体模式。我们首先定义了实体的本质:具有唯一标识且标识在生命周期中保持不变的对象。然后,我们探讨了实体的核心特征:唯一标识、可变性、连续性、业务行为和生命周期管理。

实体与值对象的区别是本章的重点内容。我们通过多个维度对比了这两种对象类型,并提供了判断标准。正确区分实体和值对象对于构建清晰的领域模型至关重要。

实体标识的设计是另一个关键主题。我们讨论了标识的要求(唯一性、稳定性、简洁性、无关性)和实现方式(代理键、自然键、复合键),推荐使用代理键作为内部标识。

实体的相等性判断是一个容易出错的领域。我们强调了实体的相等性应该只基于标识,并提供了 Java 和 Python 的实现示例。错误的相等性实现可能导致各种运行时问题。

通过代码实战,我们展示了如何实现订单实体、用户实体和实体工厂。这些示例体现了实体的最佳实践:封装业务规则、正确实现相等性、使用值对象作为属性。

最后,我们回顾了实体模式的历史背景,展望了前沿方向,并提供了两个实验任务帮助读者实践所学内容。

实体是领域模型的骨架,正确设计实体是构建高质量软件的基础。希望读者能够在实际项目中应用本章的知识,设计出清晰、健壮、易于维护的实体模型。


八、习题

概念理解题:用自己的话解释什么是实体,它的核心特征是什么?

辨析题:以下概念哪些应该是实体,哪些应该是值对象?说明理由。

- 地址

- 银行账户

- 电话号码

- 员工

- 日期范围

- 购物车

设计题:为一个图书馆系统设计实体模型。至少包括图书、会员、借阅记录三个实体。为每个实体设计唯一标识,并说明选择理由。

代码分析题:以下 Java 代码的相等性实现有什么问题?如何修正?

java
public class Product {
    private Long id;
    private String name;
    private BigDecimal price;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Product product = (Product) o;
        return Objects.equals(id, product.id) && 
               Objects.equals(name, product.name) && 
               Objects.equals(price, product.price);
    }
}

思考题:为什么推荐使用代理键而不是自然键作为实体标识?在什么情况下可以考虑使用自然键?

实践题:在你当前参与的项目中,找出一个实体类,检查它的标识设计和相等性实现。如果有问题,提出改进方案。

论述题:实体和数据库表有什么区别?为什么说将实体等同于数据库表是一种误解?

设计题:设计一个 Article(文章)实体,包括属性和方法。文章可以有标题、内容、作者、发布状态等。文章可以发布、下架、编辑。写出完整的 Java 或 Python 代码。

开放题:在分布式系统中,实体的标识生成面临哪些挑战?你有什么解决方案?

综合题:分析一个开源项目的实体设计。评价其优缺点,并提出改进建议。

3/12实体
0%