DDD(领域驱动设计)之领域(Domain)内概念
=========================
上一篇: 已经阐述了领域(Domain)的基础知识,接下来我们将探讨与该领域相关的概念。
一、涉及的概念
领域层是领域驱动设计(DDD)中的核心层,它封装了业务领域的知识和逻辑。在领域层内,定义了一系列概念来表达和处理业务规则。以下是领域层内的一些关键概念:
1.实体(Entity):
实体是具有唯一标识符(ID)的对象,它们在领域中代表唯一的实例。即使实体的某些属性随时间改变,其身份保持不变。例如,用户、订单等都是实体的例子。
2.值对象(Value Object):
值对象没有独立的身份,它们通过其属性的组合来定义。如果两个值对象的属性完全相同,它们就被视为等价的。例如,地址、颜色、货币金额等都可以是值对象。
3.聚合(Aggregate)与聚合根(Aggregate Root):
聚合是一组相关对象的集合,它们作为一个整体被外界访问,以维护内部一致性。聚合根是聚合的入口点,是唯一允许外部对象直接引用的实体,它控制着对其内部成员的访问。
4.领域服务(Domain Service):
当某些业务逻辑不能自然地归属到任何一个实体或值对象时,领域服务便用来封装这样的逻辑。它通常涉及多个实体间的协作,比如复杂的业务规则处理。
5.领域事件(Domain Event):
领域事件表示领域中发生的重要的业务事件,如“订单已创建”、“库存不足”等。它们用于解耦系统各部分,促进松耦合的设计,并支持事件驱动的架构。
6.仓储(Repository):
仓储是一种设计模式,用于提供一种抽象化的数据访问机制,使得领域层可以透明地与数据持久层交互,无需关心具体的数据库操作细节。
7.工厂(Factory) 和 建造者(Builder):
这些模式用于创建复杂的领域对象,确保对象的创建过程遵循业务规则和不变性条件。
每个概念都在领域层内扮演着特定的角色,共同协作以确保软件系统能够准确地反映并处理现实世界中的业务逻辑和规则。
二、概念
1) 实体(Entity)
实体(Entity)是一个核心概念,用于表示具有唯一标识符且在业务领域中具有持续性存在的对象。实体是通过其标识来区分的,即使它的属性随时间发生变化,它仍然被认为是同一个对象。实体在业务操作中扮演着重要角色,因为它们代表了我们关心的主要业务对象,如用户、订单、账户等。
实体的特征:
- 唯一标识符(Identity):每个实体都有一个唯一的标识,这个标识符在整个生命周期中保持不变,即使实体的其他属性发生改变,这个标识也不会发生变化。可以将这个唯一标识视作人的身份证号码,一旦确立,就会伴随其直到生命周期结束,期间不会有任何变动。对外界而言,这个ID就成了实体的代名词,我们仅凭ID就能辨认实体,无需深入了解实体的具体属性。正如身份证ID之于个人,不论此人日后体型变化或有其他生理上的不同,身份证ID依然是其不变的身份象征。
- 业务逻辑:实体不仅仅是数据的容器,更重要的是它们封装了相关的业务逻辑。例如,一个订单实体可能包含计算总价、更新订单状态等业务规则。 换句话说,就是实体里有改变实体属性值的方法。这可以简单理解为充血模式(相对贫血模式中,对象只有属性,改变对象属性值的方法都在引用对象的类中)
- 生命周期:实体通常具有较长的生命周期,它们从被创建开始,经历各种状态变化,直到最终被删除或归档。在全生命周期中,对外都是用唯一ID来表达。
- 可变性:实体的状态(即属性值)是可以随时间变化的,但其身份(基于标识符)保持不变。
总之,可以理解实体对内有属性值,对外就是一个整体,通过唯一识别号来表达。实体对象属性相同,身份标识不同,那就是两个不同的实体。
举例:
用户(User)实体: 每个用户有一个唯一的用户ID(例如,用户12345),即使用户的姓名、年龄、头像等属性发生变化,用户ID保持不变,确保了用户身份的唯一性。用户实体可能包含登录、修改个人信息、添加好友等业务逻辑。
import java.util.UUID;
public class User {
private UUID userId; // 使用UUID模拟唯一用户ID,实际项目中可根据需求调整为Long或其他类型
private String username;
private int age;
private String avatarUrl;
// 构造函数
public User(String username, int age, String avatarUrl) {
this.userId = UUID.randomUUID(); // 实际应用中可能从数据库生成或使用其他策略
this.username = username;
this.age = age;
this.avatarUrl = avatarUrl;
}
// Getters 和 Setters
public UUID getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
// 示例业务逻辑方法:修改个人信息
public void updateProfile(String newUsername, int newAge, String newAvatarUrl) {
if (newUsername != null && !newUsername.isEmpty()) {
this.username = newUsername;
}
if (newAge > 0) {
this.age = newAge;
}
if (newAvatarUrl != null && !newAvatarUrl.isEmpty()) {
this.avatarUrl = newAvatarUrl;
}
}
// 示例业务逻辑方法:添加好友逻辑可能涉及Friendship类和FriendshipService,这里简化处理
// 实际应用中,添加好友可能需要事务处理、好友同意逻辑等,不宜直接在User类中实现
// 但为了示例,我们假设有个简单的方法记录操作
public void addFriend(User friend) {
// 假设有个地方记录或处理这个动作,实际应由领域服务处理
System.out.println(username + " 添加了好友: " + friend.getUsername());
}
}
// 注意:在真实项目中,业务逻辑(如添加好友)通常不会直接放在实体类中,而是通过领域服务来协调实体之间的交互。
订单(Order)实体: 在线购物系统中,订单也是一个实体。它有一个订单号作为唯一标识,即便商品列表、价格、订单状态等属性随时间而变,订单号依然不变,保证了订单的可追踪性和唯一性。订单实体可能封装了计算总价、更新订单状态(如从待付款变为已发货)等业务逻辑。
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class Order {
private UUID orderId; // 订单唯一标识
private List<OrderItem> items; // 商品列表
private BigDecimal totalAmount; // 订单总金额
private OrderStatus status; // 订单状态
private Date orderTime; // 下单时间
// 枚举订单状态
public enum OrderStatus {
UNPAID, // 待付款
PAID, // 已付款
SHIPPED, // 已发货
DELIVERED, // 已送达
CANCELED // 已取消
}
// 构造函数
public Order() {
this.orderId = UUID.randomUUID();
this.items = new ArrayList<>();
this.status = OrderStatus.UNPAID;
this.orderTime = new Date();
}
// Getters 和 Setters 省略...
// 计算总价的方法
public BigDecimal calculateTotalAmount() {
BigDecimal amount = BigDecimal.ZERO;
for (OrderItem item : items) {
amount = amount.add(item.getProductPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
}
return amount;
}
// 更新订单状态的业务逻辑方法
public void updateOrderStatus(OrderStatus newStatus) {
// 简单的状态转换逻辑,实际应用中可能更复杂,需要考虑状态合法转换
if (newStatus.ordinal() > status.ordinal()) { // 状态只能向前推进
this.status = newStatus;
// 实际应用中可能需要持久化更改到数据库等后续操作
} else {
throw new IllegalArgumentException("Invalid state transition attempted.");
}
}
// 示例:添加商品到订单
public void addItem(Product product, int quantity) {
items.add(new OrderItem(product, quantity));
// 更新总金额
totalAmount = calculateTotalAmount();
}
// OrderItem类作为订单内商品项的简单示例
private static class OrderItem {
private Product product;
private int quantity;
public OrderItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public Product getProduct() {
return product;
}
public int getQuantity() {
return quantity;
}
}
// Product类作为商品信息的简单示例
private static class Product {
private String productId;
private BigDecimal productPrice;
public Product(String productId, BigDecimal productPrice) {
this.productId = productId;
this.productPrice = productPrice;
}
public BigDecimal getProductPrice() {
return productPrice;
}
}
}
2) 值对象(Value Object)
领域驱动设计(DDD)中的另一个核心概念,用于表示没有唯一标识符但具有某种意义的、不可变的数据集合。与实体不同,值对象的重点在于其属性值的组合,而不是独立的身份标识。当两个值对象的属性完全相同时,它们被视为等价的,即使它们在内存中是不同的实例。
值对象的特征:
- 无标识性:值对象没有唯一标识符,它们通过属性值的相等性来判断是否相等。这意味着如果两个值对象的属性完全相同,它们就被认为是同一个值对象。
- 不可变性: 为了确保值对象的等价关系不会因状态改变而破坏,值对象通常设计为不可变的。一旦创建,其属性就不能被修改,如果需要改变,应创建一个新的值对象实例。
- 传递性:在对象之间传递值对象时,由于它们是基于值的等价性而非引用,因此可以直接比较值而不考虑实例身份。
- 聚合复用:值对象可以在多个实体或聚合中复用,因为它们代表的是抽象概念或度量,而不是特定实例的身份。
例子:
地址(Address)值对象: 在电子商务应用中,地址通常被设计为值对象。一个地址由街道、城市、省份、邮编等组成,如果两个地址的所有字段都相同,那么这两个地址就是相等的,无论它们属于哪个用户。即使用户搬家,旧地址和新地址也是两个不同的值对象实例,它们各自代表了一个确定的地理位置信息。
public class Address {
private String street; // 街道
private String city; // 城市
private String province; // 省份
private String postalCode; // 邮政编码
private String country; // 国家
// 构造函数
public Address(String street, String city, String province, String postalCode, String country) {
this.street = street;
this.city = city;
this.province = province;
this.postalCode = postalCode;
this.country = country;
}
// Getters 省略...
// 重写equals和hashCode方法以确保值对象的相等性比较基于其属性值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(street, address.street) &&
Objects.equals(city, address.city) &&
Objects.equals(province, address.province) &&
Objects.equals(postalCode, address.postalCode) &&
Objects.equals(country, address.country);
}
@Override
public int hashCode() {
return Objects.hash(street, city, province, postalCode, country);
}
// 可选:提供toString方法以便于打印和调试
@Override
public String toString() {
return "Address{" +
"street='" + street + ''' +
", city='" + city + ''' +
", province='" + province + ''' +
", postalCode='" + postalCode + ''' +
", country='" + country + ''' +
'}';
}
}
货币金额(Money)值对象: 在财务软件中,货币金额经常被设计为值对象,因为它是由金额数值和货币类型(如美元、欧元)组成的。两个金额如果数值和货币类型都相同,就表示相同的金钱价值,不论它关联的具体交易或账户如何。
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;
public class Money {
private final BigDecimal amount; // 金额,使用BigDecimal确保精度
private final Currency currency; // 货币类型
// 构造函数
public Money(BigDecimal amount, Currency currency) {
if (amount == null || currency == null) {
throw new IllegalArgumentException("Amount and currency cannot be null.");
}
this.amount = amount.setScale(2, BigDecimal.ROUND_HALF_UP); // 保证金额小数点后两位,四舍五入
this.currency = currency;
}
// Getters 省略...
// 重写equals和hashCode方法以确保值对象的相等性比较基于其属性值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) 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.getCurrencyCode());
}
// 可选:提供加法操作,用于处理同种货币的金额相加
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add amounts of different currencies.");
}
return new Money(amount.add(other.amount), currency);
}
// 提供toString方法以便于打印和调试
@Override
public String toString() {
return amount + " " + currency.getDisplayName();
}
}
通过这些例子,我们可以理解值对象在领域模型中如何帮助封装和管理那些不依赖于唯一标识,而依赖于其具体值的领域概念,从而简化设计并提高数据的一致性。
3)聚合(Aggregate)与聚合根(Aggregate Root)
在领域驱动设计(DDD)中,聚合(Aggregate)和聚合根(Aggregate Root)是用于组织领域对象并维护业务一致性的核心概念。
聚合
聚合是一组相关领域对象的集合,这些对象一起形成一个逻辑上的整体,共同完成某项业务功能。聚合内部的对象之间可以自由交互,而聚合外部的对象只能通过聚合根来访问和修改聚合内的其他成员。聚合的主要目的是确保数据的一致性和完整性,限制外部对聚合内部结构的直接访问,以此来简化并发控制和事务边界。
特征:
- 一致性边界:聚合定义了一致性边界的范围,确保在边界内的所有变更能作为一个整体保持一致。
- 唯一入口点:聚合根是聚合的唯一可外部访问的点,所有对聚合的操作都必须通过它进行。
- 内部不变性:聚合内部的业务规则和不变性条件由聚合自己维护,外部对象不应直接修改聚合内的非根对象。
- 事务边界:通常情况下,对聚合的修改应该在一个事务中完成,以保持数据的原子性和一致性。
聚合根
聚合根是聚合的一部分,它是外部世界访问该聚合内部其他成员的唯一途径。聚合根拥有全局唯一标识符,并且负责维护聚合内部的业务规则和数据完整性。
例子: 订单(Order)聚合:在电子商务系统中,一个订单(Order)可以作为一个聚合。订单本身是聚合根,它包含了订单详情(Order Line Items)、订单状态、总金额等信息。外部系统或服务想要修改订单的某个细节(如增加或删除订单项,更改订单状态)时,只能通过操作订单这个聚合根来进行。订单内部的订单详情项虽然也有自己的属性,但不能直接从外部修改,必须通过订单提供的方法来间接操作,确保了订单状态和订单详情之间的一致性。
import java.util.ArrayList;
import java.util.List;
import java.math.BigDecimal;
public class Order {
private String orderId; // 订单唯一标识
private List<OrderLineItem> lineItems; // 订单详情列表
private OrderStatus status; // 订单状态
private BigDecimal totalAmount; // 订单总金额
// 枚举订单状态
public enum OrderStatus {
CREATED, // 创建
PAID, // 已支付
SHIPPED, // 已发货
COMPLETED, // 完成
CANCELED // 已取消
}
// 构造函数
public Order() {
this.lineItems = new ArrayList<>();
this.status = OrderStatus.CREATED;
this.totalAmount = BigDecimal.ZERO;
�� }
// 添加订单项
public void addOrderLineItem(Product product, int quantity) {
OrderLineItem item = new OrderLineItem(product, quantity);
this.lineItems.add(item);
this.totalAmount = this.totalAmount.add(item.getTotalPrice());
}
// 删除订单项
public void removeOrderLineItem(OrderLineItem item) {
this.lineItems.remove(item);
this.totalAmount = this.totalAmount.subtract(item.getTotalPrice());
}
// 更新订单状态
public void updateStatus(OrderStatus newStatus) {
// 简单的状态转换逻辑,实际应用中可能更复杂
if (isValidStatusTransition(status, newStatus)) {
this.status = newStatus;
} else {
throw new IllegalArgumentException("Invalid status transition.");
}
}
// 确保状态转换的合法性,此处仅为示例,实际情况可能更复杂
private boolean isValidStatusTransition(OrderStatus current, OrderStatus next) {
// 简化逻辑:状态只能向前推进
return next.ordinal() > current.ordinal();
}
// 获取总金额等Getters省略...
// 订单详情项(OrderLineItem)类
public static class OrderLineItem {
private Product product;
private int quantity;
public OrderLineItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
// 获取总价等方法省略...
// Product类简化示例,实际可能包含更多属性
public static class Product {
private String productId;
private BigDecimal price;
public Product(String productId, BigDecimal price) {
this.productId = productId;
this.price = price;
}
public BigDecimal getPrice() {
return price;
}
}
}
}
//在这个示例中,
//Order类作为聚合根,管理着内部的
//OrderLineItem列表。所有对外的操作,如添加、删除订单项或更新订单状态,都必须通过
//Order类提供的方法进行,这确保了对订单状态和订单详情的一致性管理。外部代码无法直接修改
//OrderLineItem实例,体现了聚合的封装原则。
银行账户(Bank Account)聚合: 在银行系统中,一个银行账户也是一个典型的聚合。账户(Account)是聚合根,它包含了账户余额、账户持有者信息、交易历史(Transactions)等。外部操作(如存款、取款、转账)都必须通过账户这个聚合根进行,确保每次操作后账户的总余额和其他状态(如透支状态)都能保持正确的业务逻辑。
import java.util.ArrayList;
import java.util.List;
import java.math.BigDecimal;
public class Account {
private String accountId; // 账户唯一标识
private AccountHolder accountHolder; // 账户持有者信息
private BigDecimal balance; // 账户余额
private List<Transaction> transactionHistory; // 交易历史记录
// 构造函数
public Account(AccountHolder accountHolder, BigDecimal initialBalance) {
this.accountId = generateAccountId(); // 假设这是一个生成账户ID的方法
this.accountHolder = accountHolder;
this.balance = initialBalance;
this.transactionHistory = new ArrayList<>();
recordTransaction(Transaction.Type.DEPOSIT, initialBalance, "Initial deposit"); // 记录初始存款交易
}
// 存款
public void deposit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive.");
}
balance = balance.add(amount);
recordTransaction(Transaction.Type.DEPOSIT, amount, "Deposit");
}
// 取款
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive.");
}
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException("Insufficient funds for withdrawal.");
}
balance = balance.subtract(amount);
recordTransaction(Transaction.Type.WITHDRAWAL, amount, "Withdrawal");
}
// 记录交易
private void recordTransaction(Transaction.Type type, BigDecimal amount, String description) {
Transaction transaction = new Transaction(type, amount, description);
transactionHistory.add(transaction);
}
// 获取余额等Getters省略...
// 交易类
public static class Transaction {
public enum Type {
DEPOSIT,
WITHDRAWAL,
TRANSFER
}
private Type type;
private BigDecimal amount;
private String description;
public Transaction(Type type, BigDecimal amount, String description) {
this.type = type;
this.amount = amount;
this.description = description;
}
// Getters省略...
}
// 账户持有者类
public static class AccountHolder {
private String name;
private String email;
public AccountHolder(String name, String email) {
this.name = name;
this.email = email;
}
// Getters省略...
}
// 自定义异常类
public static class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
}
这个示例展示了如何设计一个银行账户的聚合,其中 Account作为聚合根,封装了账户余额、持有者信息和交易历史的管理。所有的资金操作都是通过聚合根提供的方法完成,确保了操作的原子性和账户状态的一致性。例如, withdraw方法在执行取款操作前会检查账户余额,防止透支,体现了业务规则的内置和执行。通过这种方式,聚合帮助我们在复杂业务逻辑下维持数据的一致性和事务边界。
通过这些例子,可以看到聚合和聚合根如何帮助设计出清晰的领域模型,有效地管理复杂业务逻辑下的数据一致性和事务边界。
4)领域服务(Domain Service)
领域驱动设计(DDD)中的领域服务(Domain Service)是一种设计模式,用于封装那些无法自然地归属到任何单个实体或值对象中的业务逻辑。领域服务代表了领域中的一种概念或操作,它们通常涉及多个领域对象之间的交互,或者执行复杂的业务规则,其主要目的是提供一个清晰、集中化的职责区域来处理特定的业务功能。
特征:
- 无状态性:领域服务通常不维护状态,它们接收输入参数,执行操作,并可能返回结果,但不持有业务状态信息。
- 业务逻辑集中:领域服务将跨实体或值对象的业务规则集中在一起,避免这些逻辑分散在各个实体中,从而保持实体的简洁和专注。
- 协调多个实体:当需要协调多个实体共同完成一项任务,或需要执行横切关注点(如验证、计算)时,领域服务特别有用。
- 独立于特定技术或基础设施:领域服务关注的是纯粹的业务逻辑,不直接依赖于数据库访问、网络通信或其他技术细节。
- 领域概念的体现:领域服务应当体现领域内的某个概念或操作,而非仅仅是一堆操作的集合。
例子:
转账服务(MoneyTransferService): 在一个银行系统中,转账是一个涉及多个实体(如源账户、目标账户)的复杂操作,需要确保账户余额的正确扣减和增加,同时可能要处理事务、账户冻结状态检查、转账限额验证等业务规则。转账服务作为一个领域服务,它可以接受转账请求,协调源账户和目标账户实体,执行必要的业务逻辑,如验证账户状态、计算手续费、更新账户余额,并确保整个转账过程的原子性和一致性。
import java.math.BigDecimal;
public class MoneyTransferService {
private final AccountRepository accountRepository; // 假定有一个账户存储库来检索和保存账户信息
public MoneyTransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
/**
* 执行转账操作
* @param sourceAccountId 源账户ID
* @param targetAccountId 目标账户ID
* @param amount 转账金额
* @param feeRate 手续费率
* @throws InsufficientFundsException 余额不足
* @throws AccountFrozenException 账户冻结
*/
public void transfer(String sourceAccountId, String targetAccountId, BigDecimal amount, BigDecimal feeRate)
throws InsufficientFundsException, AccountFrozenException {
Account sourceAccount = accountRepository.findById(sourceAccountId);
Account targetAccount = accountRepository.findById(targetAccountId);
// 检查源账户状态
if (sourceAccount.isFrozen()) {
throw new AccountFrozenException("Source account is frozen.");
}
// 检查目标账户状态
if (targetAccount.isFrozen()) {
throw new AccountFrozenException("Target account is frozen.");
}
// 验证转账金额是否合法
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Transfer amount must be positive.");
}
// 计算手续费
BigDecimal fee = amount.multiply(feeRate);
// 检查源账户余额是否足够
BigDecimal totalAmount = amount.add(fee);
if (sourceAccount.getBalance().compareTo(totalAmount) < 0) {
throw new InsufficientFundsException("Insufficient funds in the source account.");
}
try {
// 开始事务
accountRepository.beginTransaction();
// 扣除源账户余额
sourceAccount.withdraw(totalAmount);
// 增加目标账户余额(已扣除手续费)
targetAccount.deposit(amount);
// 提交事务
accountRepository.commitTransaction();
} catch (Exception e) {
// 发生错误时回滚事务
accountRepository.rollbackTransaction();
throw e;
}
}
}
// 假设的AccountRepository接口
interface AccountRepository {
Account findById(String accountId);
void save(Account account);
void beginTransaction();
void commitTransaction();
void rollbackTransaction();
}
// Account类中的辅助方法isFrozen()等省略...
订单处理服务(OrderProcessingService): 在电商平台中,订单处理可能包括创建订单、验证库存、计算总价、处理支付等多个步骤,这些步骤可能涉及到订单实体、库存实体、支付服务等多个领域对象。订单处理服务可以作为领域服务,整合这些操作,确保订单从创建到支付的整个流程按照业务规则正确执行。
import java.util.List;
public class OrderProcessingService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PricingService pricingService;
private final PaymentGateway paymentGateway;
public OrderProcessingService(OrderRepository orderRepository,
InventoryService inventoryService,
PricingService pricingService,
PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.pricingService = pricingService;
this.paymentGateway = paymentGateway;
}
/**
* 处理订单创建至支付的完整流程
* @param customerId 客户ID
* @param items 订单商品列表,包含商品ID和数量
* @param paymentMethod 支付方式
* @return 创建的订单ID,如果过程中出现错误则返回null
*/
public Long processOrder(Long customerId, List<OrderItem> items, PaymentMethod paymentMethod) {
try {
// 1. 创建订单草稿
Order draftOrder = createDraftOrder(customerId, items);
// 2. 验证库存
if (!inventoryService.validateStock(draftOrder)) {
throw new InsufficientStockException("Not enough stock for one or more products.");
}
// 3. 计算总价
draftOrder.setTotalPrice(pricingService.calculateTotalPrice(draftOrder));
// 4. 保存订单到数据库
Long orderId = orderRepository.save(draftOrder);
if (orderId == null) {
throw new OrderSaveException("Failed to save the order.");
}
// 5. 处理支付
boolean paymentSuccess = paymentGateway.processPayment(orderId, draftOrder.getTotalPrice(), paymentMethod);
if (!paymentSuccess) {
throw new PaymentProcessingException("Payment processing failed.");
}
return orderId;
} catch (Exception e) {
// 异常处理逻辑,比如记录日志、通知用户等
handleProcessingException(e);
return null;
}
}
private Order createDraftOrder(Long customerId, List<OrderItem> items) {
// 实现创建订单草稿的逻辑
}
private void handleProcessingException(Exception e) {
// 实现异常处理逻辑
}
}
// 辅助类和接口定义
class Order {
// 省略订单属性和getter/setter
void setTotalPrice(BigDecimal totalPrice) {}
}
class OrderItem {
// 商品ID和数量的定义
}
enum PaymentMethod {
CREDIT_CARD, PAYPAL, APPLE_PAY // 示例支付方式
}
interface OrderRepository {
Long save(Order order);
}
interface InventoryService {
boolean validateStock(Order order);
}
interface PricingService {
BigDecimal calculateTotalPrice(Order order);
}
interface PaymentGateway {
boolean processPayment(Long orderId, BigDecimal amount, PaymentMethod method);
}
class InsufficientStockException extends Exception {}
class OrderSaveException extends Exception {}
class PaymentProcessingException extends Exception {}
通过这些例子,可以看出领域服务在DDD中扮演着整合业务逻辑、协调多个领域对象、执行复杂规则的关键角色,有助于保持领域模型的清晰和模块化。
5) 领域事件(Domain Event)
领域事件(Domain Event)是领域驱动设计(DDD)中的一个核心概念,用于表示领域模型中发生的有意义的业务事件,这些事件通常是领域内状态改变的直接结果。领域事件不仅描述了“发生了什么”,还隐含了领域知识,是领域模型中对象间通信和解耦的重要机制。
特征:
- 领域内发生:领域事件源自业务逻辑的执行,反映了业务领域中的关键变化,如订单创建、库存减少、用户注册等。
- 异步与解耦:领域事件通常采用异步处理机制,允许事件生产者和消费者解耦,增强系统的灵活性和扩展性。
- 无副作用:领域事件本身不包含业务逻辑,它们只是简单地宣布一个事实的发生,不改变领域对象的状态。
- 持久化:为了保证事件的可靠传递和系统的可追溯性,领域事件通常会被持久化记录,比如存储在事件日志或事件存储中。
- 触发后续行为:领域事件可以触发一系列后续的业务操作或事件处理流程,实现事件驱动的架构。
例子:
订单创建事件(OrderCreatedEvent): 在一个电商系统中,当一个新订单被创建时,系统可以发布一个OrderCreatedEvent。这个事件包含了订单的基本信息,如订单ID、用户ID、商品列表等。事件发布后,可以触发一系列后续处理,如发送订单确认邮件给用户、更新库存系统、启动支付流程等。
订单创建事件类
首先,定义一个OrderCreatedEvent类,它将携带订单创建后的相关信息。
import java.util.List;
public class OrderCreatedEvent {
private final Long orderId;
private final Long userId;
private final List<OrderItem> items;
public OrderCreatedEvent(Long orderId, Long userId, List<OrderItem> items) {
this.orderId = orderId;
this.userId = userId;
this.items = items;
}
public Long getOrderId() {
return orderId;
}
public Long getUserId() {
return userId;
}
public List<OrderItem> getItems() {
return items;
}
}
订单项类(简化)
class OrderItem {
private String productId;
private int quantity;
public OrderItem(String productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
// Getters & Setters 省略
}
事件发布与订阅机制
为了演示事件发布,我们假设有这样一个EventPublisher接口,以及一个简单的事件处理器接口EventListener。
import java.util.ArrayList;
import java.util.List;
// 事件发布器接口
interface EventPublisher {
void publish(Event event);
}
// 事件处理器接口
interface EventListener<T extends Event> {
void onEvent(T event);
}
// 通用事件类
abstract class Event {}
// 简化的事件发布器实现
class SimpleEventPublisher implements EventPublisher {
private List<EventListener<Event>> listeners = new ArrayList<>();
@Override
public void publish(Event event) {
for (EventListener<Event> listener : listeners) {
listener.onEvent(event);
}
}
public void register(EventListener<Event> listener) {
listeners.add(listener);
}
}
订单创建事件处理器(示例:发送订单确认邮件)
class OrderConfirmationMailer implements EventListener<OrderCreatedEvent> {
@Override
public void onEvent(OrderCreatedEvent event) {
// 这里只是一个示例,实际中应使用邮件服务发送邮件
System.out.println("Sending order confirmation email for order ID: " + event.getOrderId());
// 实现发送邮件的逻辑
}
}
使用示例
最后,我们模拟一个场景,当订单创建后,发布OrderCreatedEvent并由相应的监听器处理。
public class Main {
public static void main(String[] args) {
// 初始化事件发布器
EventPublisher publisher = new SimpleEventPublisher();
// 注册事件监听器
OrderConfirmationMailer mailer = new OrderConfirmationMailer();
publisher.register(mailer);
// 模拟订单创建并发布事件
OrderItem item1 = new OrderItem("P12345", 2);
List<OrderItem> items = new ArrayList<>();
items.add(item1);
OrderCreatedEvent event = new OrderCreatedEvent(123L, 456L, items);
publisher.publish(event);
}
}
账户余额变更事件(BalanceChangedEvent): 在银行系统里,当一个账户的余额发生变化时(比如存款、取款、转账后),系统会生成一个BalanceChangedEvent。这个事件记录了账户ID、变更前后的余额、变更原因等信息。事件可以用来触发对账服务、更新用户账户视图、或是启动风险监测流程。
账户余额变更事件类
首先,定义一个BalanceChangedEvent类,用于封装账户余额变更的信息。
public class BalanceChangedEvent {
private final Long accountId;
private final double previousBalance;
private final double newBalance;
private final String changeReason;
public BalanceChangedEvent(Long accountId, double previousBalance, double newBalance, String changeReason) {
this.accountId = accountId;
this.previousBalance = previousBalance;
this.newBalance = newBalance;
this.changeReason = changeReason;
}
public Long getAccountId() {
return accountId;
}
public double getPreviousBalance() {
return previousBalance;
}
public double getNewBalance() {
return newBalance;
}
public String getChangeReason() {
return changeReason;
}
}
事件发布与订阅机制
接下来,定义事件发布和订阅的基本结构。这里简化处理,仅展示基本概念。
import java.util.ArrayList;
import java.util.List;
interface EventPublisher {
void publish(Event event);
void register(EventListener eventListener);
}
interface EventListener<T extends Event> {
void onEvent(T event);
}
abstract class Event {}
class SimpleEventPublisher implements EventPublisher {
private List<EventListener<Event>> listeners = new ArrayList<>();
@Override
public void publish(Event event) {
for (EventListener<Event> listener : listeners) {
listener.onEvent(event);
}
}
@Override
public void register(EventListener eventListener) {
listeners.add(eventListener);
}
}
账户余额变更事件处理器示例(对账服务)
假设我们有一个对账服务的事件处理器,用于处理账户余额变更后的对账任务。
class ReconciliationService implements EventListener<BalanceChangedEvent> {
@Override
public void onEvent(BalanceChangedEvent event) {
System.out.printf("Reconciliation triggered for account ID: %d. Previous balance: %.2f, New balance: %.2f, Reason: %s%n",
event.getAccountId(), event.getPreviousBalance(), event.getNewBalance(), event.getChangeReason());
// 实现具体的对账逻辑
}
}
使用示例
最后,我们模拟一个场景,当账户余额发生变更后,发布BalanceChangedEvent并由对应的监听器处理。
public class Main {
public static void main(String[] args) {
EventPublisher publisher = new SimpleEventPublisher();
// 注册事件监听器
ReconciliationService reconciliationService = new ReconciliationService();
publisher.register(reconciliationService);
// 模拟账户余额变更并发布事件
BalanceChangedEvent event = new BalanceChangedEvent(987654321L, 1000.00, 1500.00, "Deposit");
publisher.publish(event);
}
}
在这个例子中,当一个账户的余额因为存款操作而发生变化时,系统会生成一个BalanceChangedEvent,进而触发对账服务进行相应的处理。这仅仅是一个基础的示例,实际应用中可能还会涉及到更复杂的事件处理逻辑、异步处理机制以及分布式环境下的事件总线等
库存不足事件(StockOutEvent): 在库存管理系统中,当某商品的库存降至预设的警戒水平或售罄时,系统会产生一个StockOutEvent。这个事件通知供应链系统需要补货,同时可能停止商品销售页面的相关展示,以避免超卖情况发生。
库存不足事件类
首先,定义StockOutEvent类来封装库存不足时的相关信息,如商品ID、当前库存量、警戒库存量等。
public class StockOutEvent {
private final Long productId;
private final int currentStock;
private final int threshold;
public StockOutEvent(Long productId, int currentStock, int threshold) {
this.productId = productId;
this.currentStock = currentStock;
this.threshold = threshold;
}
public Long getProductId() {
return productId;
}
public int getCurrentStock() {
return currentStock;
}
public int getThreshold() {
return threshold;
}
}
事件发布与订阅机制 接着,使用事件发布者(EventPublisher)和事件监听器(EventListener)接口来构建事件驱动的基础架构。
import java.util.ArrayList;
import java.util.List;
interface EventPublisher {
void publish(Event event);
void register(EventListener eventListener);
}
interface EventListener<T extends Event> {
void onEvent(T event);
}
abstract class Event {}
补货通知处理器 创建一个补货通知服务的事件处理器,它负责处理StockOutEvent,例如通知供应链系统补货。
****
class ReplenishmentNoticeService implements EventListener1`<StockOutEvent>` {
@Override
public void onEvent(StockOutEvent event) {
System.out.printf("Stock out alert for Product ID: %d. Current stock: %d, Threshold: %d. Triggering replenishment process.%n",
event.getProductId(), event.getCurrentStock(), event.getThreshold());
// 这里可以添加实际的补货逻辑,如调用供应链API
}
}
商品销售状态处理器
另外,可以设计一个处理器来管理商品销售页面的状态,避免超卖。
class SalesStatusManager implements EventListener<StockOutEvent> {
@Override
public void onEvent(StockOutEvent event) {
System.out.printf("Updating sales status for Product ID: %d to 'out of stock'.%n", event.getProductId());
// 实际操作可能包括更新数据库中的商品状态,或者发送指令给前端改变商品显示状态
}
}
使用示例
最后,通过一个简单的主函数来模拟整个过程,包括事件的发布和处理。
public class InventoryManagementDemo {
public static void main(String[] args) {
EventPublisher publisher = new SimpleEventPublisher(); // 假设存在一个SimpleEventPublisher实现
// 注册事件监听器
ReplenishmentNoticeService replenishmentService = new ReplenishmentNoticeService();
SalesStatusManager salesStatusManager = new SalesStatusManager();
publisher.register(replenishmentService);
publisher.register(salesStatusManager);
// 模拟商品库存不足事件
StockOutEvent stockOutEvent = new StockOutEvent(12345L, 10, 20); // 商品ID为12345,库存降到10,警戒线为20
publisher.publish(stockOutEvent);
}
}
// 注意:上述代码中`SimpleEventPublisher`的具体实现未给出,你需要实现该类以完成事件的发布和监听器的注册逻辑。
通过这些例子,可以看出领域事件是如何作为一种机制,帮助领域模型中的不同组件以松耦合的方式相互通信,支持复杂业务流程的自动化和系统间的集成。
### 6) 仓储(Repository)> 仓储(Repository)是软件开发中,特别是领域驱动设计(DDD)中的一种设计模式,它作为领域层与数据持久层之间的桥梁,负责管理和提供领域对象的存储、检索和其他数据访问操作。仓储模式的核心目标是封装数据访问逻辑,使得领域层的代码更加专注于业务规则,而不必关心数据是如何从底层数据存储(如数据库)中获取或保存的。**基本概念:**1. 解耦:仓储模式帮助解耦领域逻辑与数据访问细节,使得领域模型能够独立于具体的数据库实现和技术细节,有利于维护和测试。
- 接口与实现分离:通常定义一个仓储接口(Repository Interface)在领域层,而具体的仓储实现(Repository Implementation)则位于基础设施层。这样,领域对象通过接口与仓储交互,不知道背后的具体实现。
- 集合概念:仓储通常被设计得像一个对象集合,可以进行查询、添加、删除等操作,使得领域开发者可以以面向对象的方式处理数据,而不是直接编写SQL或使用ORM工具的低级API。
- 事务管理:在某些情况下,仓储还负责事务管理,确保领域操作的原子性和一致性。
例子:
假设有一个在线书店的应用,其中有一个核心领域概念——书籍(Book)。我们可以为书籍定义一个仓储接口和其实现。
仓储接口(BookRepository Interface)示例:
public interface BookRepository {
Book findById(Long id);
List<Book> findAll();
void save(Book book);
void delete(Book book);
}
仓储实现(BookRepositoryImpl)示例:
public class BookRepositoryImpl implements BookRepository {
private final EntityManager entityManager;
public BookRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Book findById(Long id) {
return entityManager.find(Book.class, id);
}
@Override
public List<Book> findAll() {
TypedQuery<Book> query = entityManager.createQuery("SELECT b FROM Book b", Book.class);
return query.getResultList();
}
@Override
public void save(Book book) {
entityManager.persist(book);
}
@Override
public void delete(Book book) {
if (entityManager.contains(book)) {
entityManager.remove(book);
} else {
entityManager.remove(entityManager.merge(book));
}
}
}
在这个例子中,BookRepository接口定义了对书籍进行CRUD操作的方法,而BookRepositoryImpl则是这个接口的一个具体实现,它利用JPA的EntityManager来与数据库交互。领域层的代码通过调用BookRepository接口的方法来操作书籍,无需直接关注底层的数据库操作细节。这样的设计使得业务逻辑更加清晰,同时也便于未来更换数据存储技术或调整数据访问策略。
7) 工厂(Factory)和建造者(Builder)
在领域驱动设计(DDD)中,工厂(Factory)和建造者(Builder)模式都是用于创建复杂对象的设计模式,但它们关注的焦点和应用场景有所不同。 工厂(Factory)> 概念: 工厂模式是一种创建型设计模式,它提供了一个接口用于创建对象,但是让子类决定实例化哪一个类。工厂方法把实例化的操作延迟到了子类中,使得客户代码不需要知道所创建对象的具体类名,只需知道所对应的工厂即可。 目的: 工厂模式主要用于解决对象的创建过程,尤其是当这个过程需要根据条件选择不同的创建逻辑时,它可以隐藏创建逻辑的复杂性,使代码更易于维护和扩展。 例子: 假设有一个订单处理系统,其中订单(Order)可以根据不同类型(如普通订单、促销订单)采取不同的创建逻辑。可以定义一个订单工厂(OrderFactory),它有一个方法createOrder,根据传入的类型参数,返回不同类型的订单实例。
public abstract class OrderFactory {
public abstract Order createOrder();
}
public class NormalOrderFactory extends OrderFactory {
@Override
public Order createOrder() {
return new NormalOrder();
}
}
public class PromotionOrderFactory extends OrderFactory {
@Override
public Order createOrder() {
return new PromotionOrder();
}
}
建造者(Builder)
概念: 建造者模式是一种创建型设计模式,它允许你使用相同的构建过程创建具有不同表示的对象。一个Builder类会逐步构造一个复杂的对象,这个过程中可能涉及多个步骤,每一步只设置一部分属性,最后调用一个方法来获取完整的对象。这种模式适用于需要创建具有多个可选参数的对象,且不想暴露复杂的构造过程给客户端的情况。
目的: 建造者模式关注于对象的逐步构造过程,使得构造过程更加清晰和灵活,特别是当构造过程复杂,有多个可选步骤或参数时,可以避免构造函数参数过多,提高代码的可读性和可维护性。
例子:
继续使用订单处理系统的例子,如果一个订单(Order)对象的创建需要设置许多属性,且某些属性可能是可选的,可以使用建造者模式来简化创建过程。
public class Order {
private String orderId;
private String customerName;
private List<Item> items;
// 其他属性及getter、setter省略...
private Order(OrderBuilder builder) {
this.orderId = builder.orderId;
this.customerName = builder.customerName;
this.items = builder.items;
// 设置其他属性...
}
public static class OrderBuilder {
private String orderId;
private String customerName;
private List<Item> items = new ArrayList<>();
// 其他可选属性...
public OrderBuilder orderId(String orderId) {
this.orderId = orderId;
return this;
}
public OrderBuilder customerName(String customerName) {
this.customerName = customerName;
return this;
}
public OrderBuilder addItem(Item item) {
this.items.add(item);
return this;
}
// 其他设置方法...
public Order build() {
return new Order(this);
}
}
}
在客户端代码中,可以通过Order.Builder逐步构建订单对象,既清晰又灵活:
Order order = new Order.OrderBuilder()
.orderId("ORD123")
.customerName("Alice")
.addItem(new Item("Book", 29.99))
.addItem(new Item("Pen", 4.99))
.build();
总结来说,工厂模式主要用于决定创建哪个类的实例,而建造者模式则专注于如何一步步构造复杂的对象。两者可以结合使用,以满足不同场景下的对象创建需求。
回顾与示例
领域模型(Domain Model)是软件开发中的一个核心概念,它在领域驱动设计(DDD)框架中尤为重要。领域模型是对现实世界中业务领域的抽象和建模,旨在通过软件来解决实际的业务需求和问题。它是对业务知识、规则、流程和实体的系统性表达,使得软件能够反映并支持业务逻辑。
1)基本概念
- 实体(Entities):具有唯一标识符且具有持续性存在的对象,即使属性随时间变化,其身份保持不变。
- 值对象(Value Objects):没有唯一标识符,通过其属性值的相等性来判断是否相等。
- 聚合(Aggregates):一组相关的领域对象,作为一个整体来维护业务规则和数据一致性。聚合根是聚合的入口点,是外部可以访问的唯一对象。
- 领域服务(Domain Services):封装不属于任何实体或值对象的业务逻辑,处理跨实体的复杂业务规则。
- 领域事件(Domain Events):表示领域中发生的有意义的业务事件,用于触发后续的行为或处理流程。
- 仓储(Repositories):提供一个统一的接口来存储和检索领域对象,隔离领域层与数据访问层。### 2) 完整例子> 假设正在开发一个图书管理系统,领域模型可能包含以下组成部分:1. 实体:- Book:具有唯一ISBN的书籍,包括书名、作者、出版日期等属性。
public class Book {
private String isbn;
private String title;
private Author author;
private LocalDate publicationDate;
private Price price;
public Book(String isbn, String title, Author author, LocalDate publicationDate, Price price) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.publicationDate = publicationDate;
this.price = price;
}
// Getters and setters
}
- Author:具有唯一标识的作者,包含姓名、国籍等信息。
public class Author {
private String id;
private String name;
private String nationality;
public Author(String id, String name, String nationality) {
this.id = id;
this.name = name;
this.nationality = nationality;
}
// Getters and setters
}
-
值对象:
-
Price:表示书籍的价格,包含货币和金额,仅通过值来判断是否相等。
public class Price {
private Currency currency;
private BigDecimal amount;
public Price(Currency currency, BigDecimal amount) {
this.currency = currency;
this.amount = amount;
}
// Equals and HashCode based on currency and amount
// Getters
}
-
聚合:
-
Catalog:作为聚合根,管理书籍集合。它可能包含添加、删除书籍的操作,以及查询书籍的功能。
import java.util.ArrayList;
import java.util.List;
public class Catalog {
private List<Book> books = new ArrayList<>();
public void addBook(Book book) {
books.add(book);
// 发布事件等逻辑
}
public void removeBook(Book book) {
books.remove(book);
// 相关逻辑处理
}
// Other methods like findBook, listBooks, etc.
}
-
领域服务:
-
RecommendationService:根据用户历史阅读记录和书籍的流行程度,推荐相似或热门书籍。
public class RecommendationService {
public List<Book> recommendBooks(User user) {
// 基于用户历史和书籍流行度的逻辑
return new ArrayList<>(); // 实际实现应根据算法填充推荐书籍列表
}
}
5.领域事件:
- BookPublishedEvent:当新书上架时,发布此事件,触发库存更新、推荐系统重新计算等后续操作。
public class BookPublishedEvent {
private Book book;
public BookPublishedEvent(Book book) {
this.book = book;
}
// Getters
}
6.仓储:
- BookRepository:用于存储和检索书籍信息。
- AuthorRepository:用于存储和检索作者信息。
public interface BookRepository {
Book save(Book book);
Book findByIsbn(String isbn);
// 其他CRUD操作
}
public interface AuthorRepository {
Author save(Author author);
Author findById(String id);
// 其他CRUD操作
}
原文链接: https://juejin.cn/post/7371261710622720019
文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17213.html