文章目录
实体、聚合、值对象
在 DDD 的领域模型设计下,领域对象的设计是非常面向对象的。而且在整个风暴事件的四色建模过程也是在以领域对象为驱动进行的。
实体、聚合、值对象,三者位于每个领域下的领域对象内,服务于领域内的领域服务。
实体-有身份的对象
实体是依托于持久化层数据以领域服务功能目标为指导设计的领域对象。
实体 = 唯一标识 + 状态属性 + 行为动作(功能)
是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。
🔔核心特征:
- 唯一标识:具有全局唯一的身份标识
- 生命周期:会经历创建、修改、删除等状态变化
- 通过标识判断相等性:两个实体如果ID相同,就是同一个实体
🖊️实现手段:
- 定义实体类:在代码中定义一个类,该类包含实体的属性、构造函数、方法等。
- 实现唯一标识:为实体类提供一个唯一标识的属性,如ID,并确保在实体的生命周期中这个标识保持不变。
- 封装行为:在实体类中实现业务逻辑的方法,这些方法可以操作实体的状态,并执行相关的业务规则。
- 使用ORM框架:利用ORM框架将实体映射到数据库表中,这样可以简化数据持久化的操作。
- 实现领域服务:对于跨实体或跨聚合的操作,可以实现领域服务来处理这些操作,而不是在实体中直接实现。
- 使用领域事件:当实体的状态发生变化时,可以发布领域事件,这样可以通知其他部分的系统进行相应的处理。
java
/**
* 实体示例:用户
*/
public class User {
// 唯一标识是实体的核心
private final UserId id;
private String username;
private String email;
private UserStatus status;
private Instant createdAt;
private Instant updatedAt;
// 构造函数:创建新实体
public User(String username, String email) {
this.id = UserId.generate(); // 生成唯一ID
this.username = username;
this.email = email;
this.status = UserStatus.ACTIVE;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
validate();
}
// 业务方法:体现实体的行为
public void changeEmail(String newEmail) {
if (!isValidEmail(newEmail)) {
throw new IllegalArgumentException("无效的邮箱格式");
}
this.email = newEmail;
this.updatedAt = Instant.now();
}
public void deactivate() {
this.status = UserStatus.INACTIVE;
this.updatedAt = Instant.now();
}
public void activate() {
this.status = UserStatus.ACTIVE;
this.updatedAt = Instant.now();
}
// 验证业务规则
private void validate() {
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (!isValidEmail(email)) {
throw new IllegalArgumentException("邮箱格式无效");
}
}
// 通过ID判断相等性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return id.equals(user.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}
// 值对象:用户ID,封装标识
public class UserId {
private final String value;
private UserId(String value) {
this.value = value;
}
public static UserId generate() {
return new UserId(UUID.randomUUID().toString());
}
public static UserId of(String value) {
return new UserId(value);
}
public String getValue() { return value; }
}
值对象-无身份的描述
这个对象在领域服务方法的生命周期过程内是不可变对象,也没有唯一标识。它通常是配合实体对象使用。如为实体对象提供对象属性值的描述。
值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。
🔔核心特征:
- 无标识:没有唯一身份标识
- 不可变:创建后状态不能改变
- 通过属性判断相等性:所有属性相同就是相等的值对象
- 可替换性:可以随意创建和丢弃
🖊️实现手段:
- 定义不可变类:确保类的所有属性都是私有的,并且只能通过构造函数来设置。
- 重写equals和hashCode方法:这样可以确保值对象的等价性是基于它们的属性值,而不是对象的引用。
- 提供只读访问器:只提供获取属性值的方法,不提供修改属性值的方法。
- 使用工厂方法或构造函数创建实例:这有助于确保值对象的有效性和一致性。
- 考虑序列化支持:如果值对象需要在网络上传输或存储到数据库中,需要提供序列化和反序列化的支持。
java
/**
* 值对象示例:货币金额
*/
public class Money {
private final BigDecimal amount;
private final Currency currency;
// 构造函数通常私有,通过静态工厂方法创建
private Money(BigDecimal amount, Currency currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
this.amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
this.currency = currency;
}
// 静态工厂方法
public static Money of(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
public static Money of(String amount, Currency currency) {
return new Money(new BigDecimal(amount), currency);
}
public static Money zero(Currency currency) {
return new Money(BigDecimal.ZERO, currency);
}
// 业务方法:返回新对象(不可变)
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
BigDecimal result = this.amount.subtract(other.amount);
if (result.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
return new Money(result, this.currency);
}
public Money multiply(BigDecimal multiplier) {
return new Money(this.amount.multiply(multiplier), this.currency);
}
// 查询方法
public boolean isGreaterThan(Money other) {
checkCurrency(other);
return this.amount.compareTo(other.amount) > 0;
}
public boolean isLessThan(Money other) {
checkCurrency(other);
return this.amount.compareTo(other.amount) < 0;
}
private void checkCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
}
// 通过所有属性判断相等性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 &&
currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return amount + " " + currency.getCurrencyCode();
}
}
/**
* 地址值对象示例
*/
public class Address {
private final String country;
private final String province;
private final String city;
private final String street;
private final String zipCode;
public Address(String country, String province, String city, String street, String zipCode) {
this.country = country;
this.province = province;
this.city = city;
this.street = street;
this.zipCode = zipCode;
validate();
}
// 业务方法:返回新对象
public Address changeStreet(String newStreet) {
return new Address(country, province, city, newStreet, zipCode);
}
public Address changeCity(String newCity) {
return new Address(country, province, newCity, street, zipCode);
}
private void validate() {
if (country == null || country.trim().isEmpty()) {
throw new IllegalArgumentException("国家不能为空");
}
// 其他验证规则...
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address address = (Address) o;
return Objects.equals(country, address.country) &&
Objects.equals(province, address.province) &&
Objects.equals(city, address.city) &&
Objects.equals(street, address.street) &&
Objects.equals(zipCode, address.zipCode);
}
@Override
public int hashCode() {
return Objects.hash(country, province, city, street, zipCode);
}
}
聚合-一致性边界
当你对数据库的操作需要使用到多个实体时,可以创建聚合对象。一个聚合对象,代表着一个数据库事务,具有事务一致性。
聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理。
🔔核心概念:
- 聚合根实体(Aggregate Root):聚合的入口点,外部只能通过聚合根访问内部对象;根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。
- 一致性边界:聚合内的数据修改必须满足业务规则。
- 事务边界:通常一个聚合在一个事务中被修改。
🖊️实现手段:
- 定义聚合根:选择合适的聚合根是实现聚合的第一步。聚合根应该是能够代表整个聚合的实体,并且拥有唯一标识。
- 限制访问路径:只能通过聚合根来修改聚合内的对象,不允许直接修改聚合内部对象的状态,以此来维护边界和一致性。
- 设计事务策略:在聚合内部实现事务一致性,确保操作要么全部完成,要么全部回滚。对于聚合之间的交互,可以采用领域事件或其他机制来实现最终一致性。
- 封装业务规则:在聚合内部实现业务规则和逻辑,确保所有的业务操作都遵循这些规则。
- 持久化:聚合根通常与数据持久化层交互,以保存聚合的状态。这通常涉及到对象-关系映射(ORM)或其他数据映射技术。
java
/**
* 聚合根:订单
*/
public class Order {
// 聚合根标识
private final OrderId id;
private final CustomerId customerId;
// 聚合内部对象:值对象列表
private List<OrderItem> items;
// 值对象
private Money totalAmount;
private OrderStatus status;
private Address shippingAddress;
private Instant createdAt;
private Instant updatedAt;
// 构造函数
public Order(CustomerId customerId, Address shippingAddress) {
this.id = OrderId.generate();
this.customerId = customerId;
this.items = new ArrayList<>();
this.totalAmount = Money.zero(Currency.getInstance("USD"));
this.status = OrderStatus.CREATED;
this.shippingAddress = shippingAddress;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
}
// 核心业务方法:添加商品
public void addItem(Product product, int quantity) {
// 业务规则:已确认的订单不能修改
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("订单已确认,不能修改商品");
}
if (quantity <= 0) {
throw new IllegalArgumentException("商品数量必须大于0");
}
// 检查是否已存在相同商品
Optional<OrderItem> existingItem = findItemByProductId(product.getId());
if (existingItem.isPresent()) {
// 修改现有商品数量
existingItem.get().increaseQuantity(quantity);
} else {
// 添加新商品
OrderItem newItem = new OrderItem(product, quantity);
items.add(newItem);
}
// 重新计算总价
recalculateTotal();
this.updatedAt = Instant.now();
}
// 核心业务方法:移除商品
public void removeItem(ProductId productId) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("订单已确认,不能移除商品");
}
boolean removed = items.removeIf(item -> item.getProductId().equals(productId));
if (removed) {
recalculateTotal();
this.updatedAt = Instant.now();
}
}
// 核心业务方法:确认订单
public void confirm() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("订单状态无效");
}
if (items.isEmpty()) {
throw new IllegalStateException("订单不能为空");
}
if (totalAmount.isLessThan(Money.of(BigDecimal.ONE, Currency.getInstance("USD")))) {
throw new IllegalStateException("订单金额必须大于0");
}
this.status = OrderStatus.CONFIRMED;
this.updatedAt = Instant.now();
// 发布领域事件
DomainEventPublisher.publish(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount));
}
// 核心业务方法:取消订单
public void cancel() {
if (status != OrderStatus.CREATED && status != OrderStatus.CONFIRMED) {
throw new IllegalStateException("当前状态不能取消订单");
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = Instant.now();
DomainEventPublisher.publish(new OrderCancelledEvent(this.id, this.customerId));
}
// 内部方法:查找商品项
private Optional<OrderItem> findItemByProductId(ProductId productId) {
return items.stream()
.filter(item -> item.getProductId().equals(productId))
.findFirst();
}
// 内部方法:重新计算总价
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.zero(Currency.getInstance("USD")), Money::add);
}
// 查询方法
public boolean containsProduct(ProductId productId) {
return items.stream().anyMatch(item -> item.getProductId().equals(productId));
}
public int getTotalQuantity() {
return items.stream().mapToInt(OrderItem::getQuantity).sum();
}
// 只有getter,没有setter(通过业务方法修改状态)
public OrderId getId() { return id; }
public CustomerId getCustomerId() { return customerId; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
public Money getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public Address getShippingAddress() { return shippingAddress; }
}
/**
* 聚合内部对象:订单项(值对象)
* 注意:外部不能直接访问,只能通过Order聚合根操作
*/
public class OrderItem {
private final ProductId productId;
private final String productName;
private final Money price;
private int quantity;
public OrderItem(Product product, int quantity) {
this.productId = product.getId();
this.productName = product.getName();
this.price = product.getPrice();
this.quantity = quantity;
validate();
}
// 业务方法
public void increaseQuantity(int additionalQuantity) {
if (additionalQuantity <= 0) {
throw new IllegalArgumentException("增加数量必须大于0");
}
this.quantity += additionalQuantity;
validate();
}
public void updateQuantity(int newQuantity) {
if (newQuantity <= 0) {
throw new IllegalArgumentException("商品数量必须大于0");
}
this.quantity = newQuantity;
}
public Money getSubtotal() {
return price.multiply(new BigDecimal(quantity));
}
private void validate() {
if (quantity <= 0) {
throw new IllegalStateException("商品数量必须大于0");
}
}
// getter
public ProductId getProductId() { return productId; }
public String getProductName() { return productName; }
public Money getPrice() { return price; }
public int getQuantity() { return quantity; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderItem)) return false;
OrderItem orderItem = (OrderItem) o;
return productId.equals(orderItem.productId);
}
@Override
public int hashCode() {
return productId.hashCode();
}
}
🔗关系:
plain
聚合 (Order)
├── 聚合根:实体 (Order)
│ ├── 包含:值对象 (Money, Address)
│ └── 包含:内部对象 (OrderItem - 值对象)
└── 外部通过聚合根访问