Java中的构造函数签名与Record:在领域驱动设计中的应用实践

Java中的构造函数签名与Record:在领域驱动设计中的应用实践

大家好!今天我们来聊聊Java中的构造函数签名与Record,以及它们在领域驱动设计(DDD)中的应用实践。在DDD中,我们强调通过业务领域知识来驱动软件设计,而构造函数和Record作为Java语言的核心特性,能在构建清晰、简洁且富有表达力的领域模型中发挥重要作用。

一、构造函数签名:领域模型的入口

构造函数是类的特殊方法,用于创建对象实例。构造函数签名,即构造函数的名称、参数类型和顺序,定义了创建对象的入口。在DDD中,我们应该精心设计构造函数签名,使其能够反映领域概念的本质,并强制执行领域规则。

1.1 构造函数签名的设计原则

  • 显式性: 构造函数参数应该清晰地表达创建对象所需的信息,避免使用魔术数字或隐藏的依赖关系。
  • 完整性: 构造函数应该要求传入创建对象所需的所有必要信息,确保对象在创建时处于有效的状态。
  • 不变性: 如果对象的状态应该是不可变的,那么构造函数应该负责初始化所有字段,并确保没有其他方法可以修改它们。
  • 验证性: 构造函数应该验证传入的参数是否符合领域规则,并在违反规则时抛出异常,防止创建无效的对象。

1.2 示例:订单的构造函数

假设我们正在设计一个电商系统的订单领域模型。一个订单需要包含客户、商品列表和订单日期。我们可以这样设计订单的构造函数:

import java.time.LocalDate;
import java.util.List;
import java.util.Objects;

public class Order {

    private final Customer customer;
    private final List<OrderItem> orderItems;
    private final LocalDate orderDate;

    public Order(Customer customer, List<OrderItem> orderItems, LocalDate orderDate) {
        Objects.requireNonNull(customer, "Customer cannot be null");
        Objects.requireNonNull(orderItems, "Order items cannot be null");
        Objects.requireNonNull(orderDate, "Order date cannot be null");

        if (orderItems.isEmpty()) {
            throw new IllegalArgumentException("Order must contain at least one item");
        }

        this.customer = customer;
        this.orderItems = List.copyOf(orderItems); // defensive copy
        this.orderDate = orderDate;
    }

    // Getters (omitted for brevity)

    public Customer getCustomer() {
        return customer;
    }

    public List<OrderItem> getOrderItems() {
        return orderItems;
    }

    public LocalDate getOrderDate() {
        return orderDate;
    }
}

class Customer {
    private String name;
    public Customer(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

class OrderItem {
    private String productName;
    private int quantity;

    public OrderItem(String productName, int quantity) {
        this.productName = productName;
        this.quantity = quantity;
    }

    public String getProductName() {
        return productName;
    }

    public int getQuantity() {
        return quantity;
    }
}

在这个例子中,构造函数签名清晰地表达了创建订单所需的必要信息:客户、商品列表和订单日期。它还通过 Objects.requireNonNull 检查了参数是否为空,并通过 orderItems.isEmpty() 验证了订单是否包含至少一个商品。使用 List.copyOf(orderItems) 创建了一个防御性副本,确保外部无法修改订单中的商品列表。

1.3 构造函数重载与领域概念

有时候,创建对象的方式不止一种。我们可以使用构造函数重载来支持不同的创建场景,但需要注意保持一致性和清晰性。例如,我们可能需要一个允许指定订单号的构造函数:

public class Order {

    private final String orderNumber;
    private final Customer customer;
    private final List<OrderItem> orderItems;
    private final LocalDate orderDate;

    public Order(String orderNumber, Customer customer, List<OrderItem> orderItems, LocalDate orderDate) {
        this.orderNumber = orderNumber;
        this.customer = customer;
        this.orderItems = List.copyOf(orderItems);
        this.orderDate = orderDate;
    }

    public Order(Customer customer, List<OrderItem> orderItems, LocalDate orderDate) {
        this(generateOrderNumber(), customer, orderItems, orderDate);
    }

    private static String generateOrderNumber() {
        // Logic to generate a unique order number
        return "ORD-" + System.currentTimeMillis();
    }

    // Getters (omitted for brevity)
    public String getOrderNumber() {
        return orderNumber;
    }

    public Customer getCustomer() {
        return customer;
    }

    public List<OrderItem> getOrderItems() {
        return orderItems;
    }

    public LocalDate getOrderDate() {
        return orderDate;
    }
}

在这个例子中,我们重载了 Order 的构造函数。第一个构造函数允许指定订单号,而第二个构造函数则自动生成订单号。这种方式既方便了客户端使用,又保证了订单号的唯一性。

二、Record:不可变数据载体的完美选择

Record 是 Java 14 引入的一种新型类,它简化了不可变数据类的创建。Record 自动生成了构造函数、getter 方法、equals()hashCode()toString() 方法,极大地减少了样板代码。在 DDD 中,Record 非常适合表示值对象(Value Object),这些对象是不可变的,并且通过其属性值来识别。

2.1 Record 的优势

  • 简洁性: Record 减少了大量样板代码,使代码更加简洁易懂。
  • 不变性: Record 的组件默认是 final 的,保证了对象的状态不可变。
  • 可读性: Record 的声明方式更加直观,更容易理解对象的结构。
  • 性能: Record 的实现经过优化,具有良好的性能。

2.2 示例:地址值对象

假设我们需要表示一个地址,它包含街道、城市和邮政编码。我们可以使用 Record 来定义 Address 值对象:

public record Address(String street, String city, String postalCode) {
    public Address {
        Objects.requireNonNull(street, "Street cannot be null");
        Objects.requireNonNull(city, "City cannot be null");
        Objects.requireNonNull(postalCode, "Postal code cannot be null");

        if (postalCode.length() != 5) {
            throw new IllegalArgumentException("Postal code must be 5 digits");
        }
    }
}

在这个例子中,Address 是一个 Record,它自动生成了构造函数、getter 方法、equals()hashCode()toString() 方法。我们还在 Record 的 compact constructor 中添加了验证逻辑,确保邮政编码的长度是 5 位。

2.3 Record 与 DDD

Record 在 DDD 中可以用于表示以下类型的对象:

  • 值对象: 如地址、货币、颜色等。
  • DTO (Data Transfer Object): 用于在不同层之间传递数据。
  • 事件: 用于表示领域事件。

使用 Record 可以使领域模型更加简洁、易懂,并减少样板代码的编写。

三、构造函数签名与Record的结合:构建强大的领域模型

构造函数签名和 Record 可以结合使用,构建更加强大的领域模型。我们可以使用 Record 来表示值对象,并在实体类的构造函数中使用这些值对象。

3.1 示例:客户实体与地址值对象

假设我们需要表示一个客户,它包含姓名和地址。我们可以使用 Record 来定义 Address 值对象,并在 Customer 实体类的构造函数中使用它:

public class Customer {

    private final String name;
    private final Address address;

    public Customer(String name, Address address) {
        Objects.requireNonNull(name, "Name cannot be null");
        Objects.requireNonNull(address, "Address cannot be null");

        this.name = name;
        this.address = address;
    }

    // Getters (omitted for brevity)

    public String getName() {
        return name;
    }

    public Address getAddress() {
        return address;
    }
}

public record Address(String street, String city, String postalCode) {
    public Address {
        Objects.requireNonNull(street, "Street cannot be null");
        Objects.requireNonNull(city, "City cannot be null");
        Objects.requireNonNull(postalCode, "Postal code cannot be null");

        if (postalCode.length() != 5) {
            throw new IllegalArgumentException("Postal code must be 5 digits");
        }
    }
}

在这个例子中,Customer 实体类的构造函数接收 nameaddress 作为参数。address 是一个 Address Record,它表示客户的地址。通过这种方式,我们将客户的地址抽象成了一个独立的领域概念,使代码更加清晰易懂。

3.2 Record的局限性与替代方案

虽然Record在很多场景下都非常有用,但它也有一些局限性:

  • 不可变性: Record是不可变的,这意味着一旦创建,就无法修改其状态。这在某些情况下可能是一个限制。
  • 无法继承: Record无法被继承,这意味着你无法扩展Record的功能。
  • 有限的状态验证: 虽然Record支持 compact constructor,但它提供的状态验证机制相对有限。

对于需要可变状态或更复杂行为的领域对象,传统的类仍然是更合适的选择。此外,如果需要更强大的状态验证机制,可以考虑使用Bean Validation API (JSR 303)。

四、案例分析:使用构造函数签名和Record构建领域模型

让我们通过一个更完整的案例来演示如何使用构造函数签名和Record构建领域模型。假设我们需要设计一个银行账户系统,其中包含以下领域概念:

  • 账户 (Account): 包含账户号码、账户持有人和余额。
  • 账户持有人 (AccountHolder): 包含姓名和地址。
  • 地址 (Address): 包含街道、城市和邮政编码。
  • 金额 (Money): 包含金额和货币类型。

我们可以使用 Record 来表示 AddressMoney 值对象,并使用类来表示 AccountHolderAccount 实体:

import java.math.BigDecimal;
import java.util.Objects;

// Value Object: Address
public record Address(String street, String city, String postalCode) {
    public Address {
        Objects.requireNonNull(street, "Street cannot be null");
        Objects.requireNonNull(city, "City cannot be null");
        Objects.requireNonNull(postalCode, "Postal code cannot be null");
    }
}

// Value Object: Money
public record Money(BigDecimal amount, String currency) {
    public Money {
        Objects.requireNonNull(amount, "Amount cannot be null");
        Objects.requireNonNull(currency, "Currency cannot be null");

        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount must be non-negative");
        }
    }
}

// Entity: AccountHolder
public class AccountHolder {

    private final String name;
    private final Address address;

    public AccountHolder(String name, Address address) {
        Objects.requireNonNull(name, "Name cannot be null");
        Objects.requireNonNull(address, "Address cannot be null");

        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public Address getAddress() {
        return address;
    }
}

// Entity: Account
public class Account {

    private final String accountNumber;
    private final AccountHolder accountHolder;
    private Money balance;

    public Account(String accountNumber, AccountHolder accountHolder, Money initialBalance) {
        Objects.requireNonNull(accountNumber, "Account number cannot be null");
        Objects.requireNonNull(accountHolder, "Account holder cannot be null");
        Objects.requireNonNull(initialBalance, "Initial balance cannot be null");

        if (initialBalance.amount().compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Initial balance must be non-negative");
        }

        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.balance = initialBalance;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public AccountHolder getAccountHolder() {
        return accountHolder;
    }

    public Money getBalance() {
        return balance;
    }

    public void deposit(Money amount) {
        Objects.requireNonNull(amount, "Deposit amount cannot be null");

        if (!amount.currency().equals(this.balance.currency())) {
            throw new IllegalArgumentException("Currency mismatch");
        }

        this.balance = new Money(this.balance.amount().add(amount.amount()), this.balance.currency());
    }

    public void withdraw(Money amount) {
        Objects.requireNonNull(amount, "Withdrawal amount cannot be null");

        if (!amount.currency().equals(this.balance.currency())) {
            throw new IllegalArgumentException("Currency mismatch");
        }

        if (this.balance.amount().compareTo(amount.amount()) < 0) {
            throw new IllegalArgumentException("Insufficient funds");
        }

        this.balance = new Money(this.balance.amount().subtract(amount.amount()), this.balance.currency());
    }
}

在这个案例中,我们使用了 Record 来表示 AddressMoney 值对象,并在 AccountHolderAccount 实体类的构造函数中使用它们。构造函数签名清晰地表达了创建对象所需的必要信息,并强制执行了领域规则。例如,Money Record 的构造函数验证了金额必须为非负数,而 Account 类的构造函数验证了初始余额必须为非负数。depositwithdraw 方法验证了货币类型是否匹配,以及是否有足够的资金进行取款。

五、总结

通过精心设计的构造函数签名和 Record,我们能够构建清晰、简洁且富有表达力的领域模型。构造函数签名定义了创建对象的入口,并强制执行领域规则,而 Record 则简化了不可变数据类的创建,非常适合表示值对象。将两者结合使用,可以使领域模型更加健壮、易于理解和维护。在实践中,我们需要根据具体的领域需求选择合适的工具和技术,才能构建出高质量的软件。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注