DDD | 03-什么是实体对象

二、什么是实体?

实体(Entity)是一种核心的领域模型组件,用于表示具有唯一标识符、生命周期和行为的对象。实体是领域中关键概念的具体实例,它们通常对应于现实世界中的事物,比如用户、订单、账户等。

主要特点

  • 唯一标识符(Identity):每个实体都有一个唯一的标识符,这个标识符是用来区分不同实体的关键,即使两个实体的其他所有属性都相同,只要标识符不同,它们就是两个独立的实体。

  • 生命周期:实体有明确的生命周期,从创建到最终被废弃。在这个过程中,实体的状态可能会发生变化,但其身份保持不变。

  • 可变性:实体的状态在其生命周期中是可以变化的,这意味着实体的属性值可以被更新,实体可以响应业务事件并改变其内部状态。

  • 业务行为:实体不仅仅是一堆数据的集合,它们还封装了相关的业务逻辑和行为。这些行为体现了实体在领域内的职责和功能,有助于维护实体状态的一致性和完整性。

结构设计

在编码实现时,实体类的设计应当遵循面向对象的原则,如封装、继承(如果适用)、多态等,同时要确保实体的不变性和业务规则得到正确实施。此外,考虑到与基础设施(如 ORM 映射)的集成,还需注意如何处理标识符的生成、持久化细节以及并发访问控制等问题。

唯一标识符(Identity)

  • 每个实体必须有一个明确的唯一标识符,用于区分不同实例。这个标识符通常是不可变的,并且在实体的整个生命周期中保持不变。

  • 在 Java 等面向对象语言中,唯一标识通常通过一个私有的 ID 字段和相应的 getter 方法暴露给外部,而 ID 的 setter 方法可能被省略或设为私有,以防止外部直接修改。

  • 在实现层面,开发者需要确保实体的唯一标识符得到妥善管理,并且在设计实体时,要关注其核心的业务属性和行为,避免将实体变成简单的数据容器。此外,实体通常关联到数据库中的表,其中标识符映射为表的主键(委派标识,如数据库中的自增主键),以确保数据的持久化和查询能力。

属性(Attributes)

  • 实体包含描述其特征的属性(成员变量)。这些属性反映了实体在领域中的状态,可以是基本类型,也可以是复杂类型。
  • 属性应尽可能精简,仅包含那些对业务有意义的数据。

行为(Behaviors/Methods)

  • 实体不仅仅是数据的容器,更重要的是封装了业务逻辑和规则。通过方法(行为)来操作和改变实体状态,确保业务规则得到执行。

  • 行为应该表达领域内的概念操作,如订单的“确认”、“取消”等,这些方法通常会改变实体的内部状态,并可能触发领域事件。

构造函数(Constructors)

  • 实体的构造函数通常用来初始化实体的必要属性,特别是唯一标识符。在某些情况下,构造函数可能需要参数来确保实体在创建时就处于有效状态。

领域事件(Domain Events)

  • 虽不是实体结构的直接部分,但实体的行为可能会触发领域事件,以通知系统中的其他部分有关实体状态的重要变更。

聚合(Aggregates)

  • 实体经常作为聚合的一部分存在。聚合根负责维护聚合内部的一致性,实体则是聚合内部的成员,它们之间的关系和交互受到聚合根的控制。

值对象(Value Objects)

  • 实体中可能会包含值对象作为属性,值对象没有独立的身份,仅通过其属性值来定义,用于描述实体的某些特性。

代码示例

创建一个商品实体对象

/**
 * @author dolphinmind
 * @ClassName ProductEntity
 * @description 产品实体类,用于表示产品的核心信息。
 * @date 2024/6/16
 */

public class ProductEntity {
    private final UUID productId;
    private String productName;
    private BigDecimal price;
    private String description;

    /**
     * 验证产品ID的有效性,不能为null。
     *
     * @param productId 产品的唯一标识
     * @throws NullPointerException 如果产品ID为null
     */
    // 验证方法
    private void validateProductId(UUID productId) {
        Objects.requireNonNull(productId, "Product ID cannot be null");
    }

    /**
     * 验证产品名称的有效性,不能为null或空字符串。
     *
     * @param productName 产品的名称
     * @throws NullPointerException     如果产品名称为null
     * @throws IllegalArgumentException 如果产品名称为空字符串
     */
    private void validateProductName(String productName) {
        Objects.requireNonNull(productName, "Product name cannot be null");
        if (productName.trim().isEmpty()) {
            throw new IllegalArgumentException("Product name cannot be empty");
        }
    }

    /**
     * 验证价格的有效性,不能为null且必须大于等于0。
     *
     * @param price 产品的价格
     * @throws NullPointerException     如果价格为null
     * @throws IllegalArgumentException 如果价格小于0
     */
    private void validatePrice(BigDecimal price) {
        Objects.requireNonNull(price, "Price cannot be null");
        if (price.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
    }

    /**
     * 验证产品描述的有效性,允许为空或空白字符串。
     *
     * @param description 产品的描述信息
     */
    private void validateDescription(String description) {
        // 这里可以根据业务需求决定是否需要对description进行验证
        // 示例中假设description可以为空或空白
    }

    /**
     * 构造函数,初始化产品实体。
     *
     * @param productId 产品的唯一标识
     * @param productName 产品的名称
     * @param price 产品的价格
     * @param description 产品的描述信息
     * @throws NullPointerException     如果产品ID、名称或价格为null
     * @throws IllegalArgumentException 如果产品名称为空字符串或价格小于0
     */
    public ProductEntity(UUID productId, String productName, BigDecimal price, String description) {
        validateProductId(productId);
        validateProductName(productName);
        validatePrice(price);
        validateDescription(description);

        this.productId = productId;
        this.productName = productName;
        this.price = price;
        this.description = description;
    }

    /**
     * 获取产品的唯一标识。
     *
     * @return 产品的唯一标识
     */
    public UUID getProductId() {
        return productId;
    }

    /**
     * 获取产品的名称。
     *
     * @return 产品的名称
     */
    public String getProductName() {
        return productName;
    }

    /**
     * 获取产品的价格。
     *
     * @return 产品的价格
     */
    public BigDecimal getPrice() {
        return price;
    }

    /**
     * 获取产品的描述信息。
     *
     * @return 产品的描述信息
     */
    public String getDescription() {
        return description;
    }

    /**
     * 更新产品的名称。
     *
     * @param newProductName 新的产品名称
     */
    public void updateProductName(String newProductName) {
        this.productName = newProductName;
    }

    /**
     * 更新产品的价格。
     *
     * @param newPrice 新的产品价格
     */
    public void updatePrice(BigDecimal newPrice) {
        this.price = newPrice;
    }

    /**
     * 更新产品的描述信息。
     *
     * @param newDescription 新的产品描述信息
     */
    public void updateDescription(String newDescription) {
        this.description = newDescription;
    }

    /**
     * 检查当前对象与另一个对象是否相等,基于产品ID进行比较。
     *
     * @param o 另一个对象
     * @return 如果两个对象相等返回true,否则返回false
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ProductEntity that = (ProductEntity) o;
        return Objects.equals(productId, that.productId);
    }

    /**
     * 计算当前对象的哈希码,基于产品ID计算。
     *
     * @return 当前对象的哈希码
     */
    @Override
    public int hashCode() {
        return Objects.hash(productId);
    }

    /**
     * 返回当前对象的字符串表示,包含产品ID、名称、价格和描述。
     *
     * @return 当前对象的字符串表示
     */
    @Override
    public String toString() {
        return "ProductEntity{" +
                "productId=" + productId +
                ", productName='" + productName + '\'' +
                ", price=" + price +
                ", description='" + description + '\'' +
                '}';
    }

}

ProductEntity 可能触发的领域事件

产品创建

  • 当一个新的ProductEntity 实例通过构造函数创建时,这标志着一个新的产品被创建到系统中。此时,可以触发一个如ProductCreatedEvent 的领域事件,携带产品 ID、名称、价格和描述等信息
    // 在构造函数内部触发事件
    public ProductEntity(UUID productId, String productName, BigDecimal price, String description) {
        // ...验证逻辑...

        this.productId = productId;
        this.productName = productName;
        this.price = price;
        this.description = description;

        // 触发产品创建事件
        DomainEventPublisher.instance().publish(new ProductCreatedEvent(this));
    }

产品信息更新

  • 更新产品名称、价格或描述等操作可能代表了重要的业务状态变更,可以分别或统一触发如ProductNameUpdatedEventProductPriceUpdatedEventProductDescriptionUpdatedEvent等事件。
    public void updateProductName(String newProductName) {
        String oldProductName = this.productName;
        this.productName = newProductName;

        // 触发产品名称更新事件
        DomainEventPublisher.instance().publish(new ProductNameUpdatedEvent(this, oldProductName, newProductName));
    }

    // 类似地,为updatePrice和updateDescription添加事件触发逻辑

为实现领域事件的发布和订阅机制,需要一个事件发布器DomainEventPublisher,它负责管理事件的订阅者并分发事件。着通常涉及到事件总线Event Bus 或观察者模式的实现。

为什么商品通常被作为一个实体对象存在?

唯一标识

  • 商品拥有一个全局唯一的标识符(如商品ID),这是实体的基本特征,用于区分不同的商品实例。

属性和行为

  • 商品包含了一系列描述性的属性,如名称、价格、描述、库存量等,并可能附带有相应的业务行为,如更新价格、减少库存等。这些属性和行为直接关联到商品本身,体现了实体的特征。

关联性

  • 虽然商品可能参与到多个聚合中(如作为订单项的一部分出现在订单聚合中),商品本身的管理(如上架、下架、修改信息)并不依赖于其他更高级别的业务概念,因此它不需要作为一个聚合根来协调内部的一致性。

复用性

  • 商品作为实体,可以在多个上下文中被引用,比如在订单、购物车、推荐系统等多个地方,而不需要每次引用都包含其所有关联信息,这符合实体的复用性质。

总结来说,商品作为一个具有独立标识、属性和行为的对象,更适合被建模为实体对象。它在系统中扮演的角色主要是提供关于商品本身的详细信息,并参与其他更搞层次聚合(如订单)的构成,而不是作为一个包含内部复杂逻辑和多个关联对象的聚合根。

实体对象可否退化为值对象?

实体对象在某些场景下可以退化为值对象,但这种转变需谨慎考虑并基于具体的业务需求和技术背景。

业务语境变化

  • 从唯一到非唯一:当业务逻辑不再要求某个实体具有唯一标识,或者唯一标识变得不重要是,该实体可以退化为值对象。例如,如果订单行项目OrderLineItem不再需要通过唯一ID追踪,而是仅关注其组合属性时,它可以变为值对象

不变性增强

  • 提升数据完整性:将实体转换为值对象通常意味着将属性设置为不可变final,这能增强数据的不变性和安全性,减少并发问题

性能考量

  • 减少数据库交互:在某些高性能或分布式场景中,将实体转换为对象并随聚合根一起加载,可以减少数据库查询次数,提高系统性能

简化设计

  • 减少复杂度:对于简单的数据持有结果,特别是当对象的主要职责是携带数据而非维护状态时,采用值对象可以简化系统设计

注意事项

  • 重新评估相等性:实体转为值对象后,需要基于所有属性来定义equalshashCode 方法,确保逻辑上的相等性判断正确
  • 移除身份标识:原有的唯一标识符(如ID)可能不再适用,需要从类中移除
  • 考虑生命周期管理:实体通常有独立的生命周期,可能与聚合根相关联,而值对象的生命周期往往依赖于拥有它的实体或聚合根
  • 更新关联关系:如果其他实体或值对象原先引用了这个实体,需要调整这些引用,可能需要将引用改为直接包含值对象的属性集合
  • 领域逻辑迁移:如果实体中包含业务逻辑,需要考虑这部分逻辑如何处理。有时可能需要将逻辑移动到其他领域对象或服务中

综上,实体对象退化为值对象是一种设计上的权衡,应当基于实际业务场景和系统需求综合考虑。在做出改变前,彻底分析影响并进行必要的设计调整。

问题探究

实体对象与值对象的区别是什么?

实体对象(Entities)

  • 唯一标识:实体拥有唯一的标识符(ID),这个ID用来区分不同的实体实例,即使它们的其他属性相同。实体的标识符在整个生命周期中保持不变
  • 可变性:实体的状态可以在其生命周期中发生变化,即实体的属性可以被修改
  • 业务行为:实体通常包含业务逻辑,体现为方法或操作,这些行为可以改变实体自身的状态,反映领域内的业务规则
  • 生命周期:实体有明确的生命周期管理,从创建到最终可能的删除

值对象(Value Objects)

  • 无唯一标识:值对象没有独立的唯一标识,它们通过其属性的组合来确定相等性。如果两个值对象的所有属性都相同,那么它们就被认为是相等的,即使它们在内存中是不同的实例
  • 不可变性:值对象通常设计为不可变的,一旦创建,其属性就不能更改。如果需要改变,通常是通过创建一个新的值对象实例来表示变化后的状态
  • 传递值:值对象在领域模型中通常用来描述实体的属性或特征,它们可以被实体引用,也可以在多个实体间共享,而不改变其本质
  • 关注数据:值对象主要关注数据的封装,它们不包含业务行为,或者包含的行为仅限于验证自身数据的完整性

拓展示例

创建一个订单项OrderLineOrderLineItem实体对象

public class OrderLineItemEntity {

    // 订单行项目ID,唯一标识每个订单行项目
    private final UUID orderLineItemId;
    // 商品ID,关联到订单中的特定商品
    private final UUID productId;
    // 商品名称,描述订单中的商品
    private final String productName;
    // 商品数量,表示订单中该商品的件数
    private int quantity;
    // 商品单价,表示每件商品的价格
    private BigDecimal unitPrice;
    // 折扣金额,表示应用于商品总价的折扣数额
    private BigDecimal discountAmount;

    /**
     * 构造一个新的订单行项目。
     *
     * @param orderLineItemId 订单行项目ID
     * @param productId       商品ID
     * @param productName     商品名称
     * @param quantity        商品数量
     * @param unitPrice       商品单价
     * @param discountAmount  折扣金额
     */
    public OrderLineItemEntity(UUID orderLineItemId, UUID productId, String productName, int quantity,
                               BigDecimal unitPrice, BigDecimal discountAmount) {
        this.orderLineItemId = orderLineItemId;
        this.productId = productId;
        this.productName = productName;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
        this.discountAmount = discountAmount;
    }

    /**
     * 验证订单行项目ID的有效性。
     *
     * @param orderLineItemId 订单行项目ID
     * @throws NullPointerException 如果ID为null,则抛出异常
     */
    private void validateOrderLineItem(UUID orderLineItemId) {
        Objects.requireNonNull(orderLineItemId, "订单项ID不能为空");
    }

    /**
     * 验证商品ID的有效性。
     *
     * @param productId 商品ID
     * @throws NullPointerException 如果ID为null,则抛出异常
     */
    private void validateProductId(UUID productId) {
        Objects.requireNonNull(productId, "商品ID不能为空");
    }

    /**
     * 验证商品名称的有效性。
     *
     * @param productName 商品名称
     * @throws NullPointerException 如果名称为null,则抛出异常
     */
    private void validateProductName(String productName) {
        Objects.requireNonNull(productName, "商品名称不能为空");
    }

    /**
     * 验证商品数量的有效性。
     *
     * @param quantity 商品数量
     * @throws IllegalArgumentException 如果数量小于1,则抛出异常
     */
    private void validateQuantity(int quantity) {
        if (quantity < 1) {
            throw new IllegalArgumentException("商品数量不能小于1");
        }
    }

    /**
     * 验证商品单价的有效性。
     *
     * @param unitPrice 商品单价
     * @throws NullPointerException      如果单价为null,则抛出异常
     * @throws IllegalArgumentException 如果单价小于等于0,则抛出异常
     */
    private void validateUnitPrice(BigDecimal unitPrice) {
        Objects.requireNonNull(unitPrice, "商品单价不能为空");

        if (unitPrice.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("商品单价不能小于等于0");
        }
    }

    /**
     * 验证折扣金额的有效性。
     *
     * @param discountAmount 折扣金额
     * @throws NullPointerException      如果折扣金额为null,则抛出异常
     * @throws IllegalArgumentException 如果折扣金额小于0,则抛出异常
     */
    private void validateDiscountAmount(BigDecimal discountAmount) {
        Objects.requireNonNull(discountAmount, "折扣金额不能为空");

        if (discountAmount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("折扣金额不能小于0");
        }
    }

    /**
     * 获取订单行项目ID。
     *
     * @return 订单行项目ID
     */
    public UUID getOrderLineItemId() {
        return orderLineItemId;
    }

    /**
     * 获取商品ID。
     *
     * @return 商品ID
     */
    public UUID getProductId() {
        return productId;
    }

    /**
     * 获取商品名称。
     *
     * @return 商品名称
     */
    public String getProductName() {
        return productName;
    }

    /**
     * 获取商品数量。
     *
     * @return 商品数量
     */
    public int getQuantity() {
        return quantity;
    }

    /**
     * 设置商品数量。
     *
     * @param quantity 新的商品数量
     */
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    /**
     * 获取商品单价。
     *
     * @return 商品单价
     */
    public BigDecimal getUnitPrice() {
        return unitPrice;
    }

    /**
     * 设置商品单价。
     *
     * @param unitPrice 新的商品单价
     */
    public void setUnitPrice(BigDecimal unitPrice) {
        this.unitPrice = unitPrice;
    }

    /**
     * 获取折扣金额。
     *
     * @return 折扣金额
     */
    public BigDecimal getDiscountAmount() {
        return discountAmount;
    }

    /**
     * 设置折扣金额。
     *
     * @param discountAmount 新的折扣金额
     */
    public void setDiscountAmount(BigDecimal discountAmount) {
        this.discountAmount = discountAmount;
    }

    /**
     * 计算订单行项目的总价(数量 * 单价 - 折扣金额)。
     *
     * @return 订单行项目的总价
     */
    public BigDecimal calculateTotalPrice() {
        return unitPrice.multiply(BigDecimal.valueOf(quantity)).subtract(discountAmount);
    }

    /**
     * 返回订单行项目的信息字符串。
     *
     * @return 订单行项目信息的字符串表示
     */
    @Override
    public String toString() {
        return "OrderLineItem{" +
                "orderLineItemId=" + orderLineItemId +
                ", productId=" + productId +
                ", productName='" + productName + '\'' +
                ", quantity=" + quantity +
                ", unitPrice=" + unitPrice +
                ", discountAmount=" + discountAmount +
                '}';
    }

    /**
     * 检查两个订单行项目是否相等。
     * 相等性基于订单行项目ID。
     *
     * @param o 另一个对象
     * @return 如果两个订单行项目具有相同的ID,则返回true;否则返回false
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderLineItemEntity that = (OrderLineItemEntity) o;
        return Objects.equals(orderLineItemId, that.orderLineItemId);
    }

    /**
     * 计算订单行项目的哈希码。
     *
     * @return 订单行项目的哈希码
     */
    @Override
    public int hashCode() {
        return Objects.hash(orderLineItemId);
    }
}

为什么订单项通常被视为实体对象?

唯一性和标识

  • 虽然订单项通常作为订单的一部分存在,没有全局唯一的标识符要求,但在订单内部,每个订单项可能需要一个唯一标识来区分不同的商品及其购买详情。这种内部唯一性表明它具有实体的特征。

独立属性和行为

  • 订单项通常包含商品 ID、数量、单价、小计金额等属性,这些属性组合起来描述了一个具体的购买决策。它可能还会有自己的行为,比如调整数量、计算小计等,这些行为体现了它不仅仅是数据的容器,而是具有业务逻辑的实体。

生命周期依赖

  • 虽然订单项的生命周期通常于订单紧密相关,但它的存在和操作(如数量变更)在一定程度上是独立的,可以被看作是订单聚合内部的一个子实体。

相比之下,值对象通常标识没有唯一标识符且仅通过其属性来定义其相等性的对象,比如颜色、地址等,它们在领域模型中更多地用于描述属性,而不是拥有独立行为或生命周期。

因此,基于订单项具有自己的属性、可能的行为以及在订单聚合内的相对独立性,将其涉及为实体对象是合理的。这允许订单项在订单聚合的上下文中保持一定的灵活性和自治性,同时也便于处理于订单相关的复杂业务逻辑。

posted @ 2024-07-15 20:58  Neking  阅读(4)  评论(0编辑  收藏  举报