目录
前言
战术设计
基本概念
领域内:
实体
值对象
领域服务
模块
对象生命周期:
聚合
工厂
仓库
其他:
领域事件
事件溯源
实例介绍
前言
上一篇文章 DDD-事件风暴 属于领域驱动设计中的战略设计,战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。而想要进行架构项目落地则需求另外一个模式,就是 战术设计。
战术设计
重落地实现,以战略设计构建的领域模型,建立了领域模型的边界与上下文,也就确认了微服务的边界,这个过程会涉及架构师、技术人员参与;
战术设计的作用是管理复杂性并确保领域模型中行为的清晰明确。可以使用这些基础元素和概念来捕获和传递领域中的概念、关系、规则。
每个构造块都具有单一职责:
1.代表领域中的概念。如实体、值对象、领域服务、领域事件、模块等;
2.用于管理对象的生命周期。如聚合、工厂、仓库等;
3.用于集成或跟踪。领域事件、事件溯源等。
基本概念
领域内:
实体[Entity]
在领域驱动设计(DDD)中,实体(Entity)是具有唯一标识符的对象,其生命周期可以跨越多个状态和时间点。实体通常用于表示具有状态和行为的领域对象,例如用户、订单、产品等。实体的唯一标识符用于区分不同的实体对象。
以下是实体的一些特点和定义:
-
具有唯一标识符:每个实体对象都具有唯一的标识符,用于区分不同的对象。这个标识符通常是不可变的,并且在整个生命周期内保持不变。
-
可变性:与值对象不同,实体的属性值是可变的,它可以在生命周期内随着业务逻辑的变化而改变其状态。
-
有状态和行为:实体通常具有状态和行为,可以执行某些操作,改变自身的状态或与其他实体进行交互。
-
生命周期:实体的生命周期可以跨越多个状态和时间点,通常会经历创建、更新、删除等不同的状态。
以下是一个简单的实体示例:
public class Product {
private final int id;
private String name;
private double price;
public Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
// 可能还有其他行为和方法
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Product other = (Product) obj;
return id == other.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
在这个例子中,Product
是一个实体,代表了一个产品。它具有唯一的标识符id
,以及名称name
和价格price
等属性。实体对象可以通过其唯一标识符进行区分。Product
类还提供了一些方法来获取和修改属性值,以及可能的其他行为和方法。
值对象[Value-Object]
特点:值对象 = 将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。
详解:在领域驱动设计(DDD)中,值对象(Value Object)是一种表示某个概念的不可变对象,它的身份由其属性(值)确定。值对象通常用于描述具有严格定义的特征和属性的概念,例如日期、时间、货币金额等。值对象之间相等性的比较通常是基于它们的属性值。
下面是值对象的一些特点和定义:
-
不可变性(Immutable):值对象的属性值在创建后不可变更。如果需要修改对象的属性,通常是通过创建一个新的值对象来表示修改后的状态。
-
按值相等性比较:值对象的相等性通常是基于其所有属性值的相等性。如果两个值对象的所有属性值都相等,则它们被认为是相等的。
-
没有唯一标识:与实体(Entity)不同,值对象没有自己的唯一标识符,其身份由其属性值决定。
-
表示概念:值对象表示某个概念的特征或属性,而不是具有唯一标识的实体。
public class EmailAddress {
private final String emailAddress;
public EmailAddress(String emailAddress) {
if (!isValid(emailAddress)) {
throw new IllegalArgumentException("Invalid email address format");
}
this.emailAddress = emailAddress;
}
public String getEmailAddress() {
return emailAddress;
}
private boolean isValid(String emailAddress) {
// 实现验证逻辑,确保邮箱地址格式正确
// 这里只是一个简单的示例,实际应用中可能需要更复杂的验证逻辑
return emailAddress != null && emailAddress.contains("@");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
EmailAddress other = (EmailAddress) obj;
return Objects.equals(emailAddress, other.emailAddress);
}
@Override
public int hashCode() {
return Objects.hash(emailAddress);
}
}
。
在这个例子中,EmailAddress是一个值对象,表示电子邮件地址的概念。它有一个私有属性emailAddress,表示邮件地址字符串。构造函数用于创建EmailAddress对象,并且会验证传入的邮箱地址是否符合预期的格式。equals和hashCode方法根据邮件地址的相等性来判断两个EmailAddress对象是否相等。由于邮箱地址是一个不可变对象,因此在创建后无法修改其值
领域服务[Domain-Service]
在领域驱动设计(DDD)中,领域服务(Domain Service)是一种封装了领域逻辑的服务,用于执行与特定领域相关的操作。领域服务通常用于处理那些不适合属于实体或值对象的操作,或者涉及多个实体或值对象之间的复杂交互的操作。
以下是领域服务的一些特点和定义:
-
操作领域对象:领域服务对领域对象执行操作,它可以协调多个实体或值对象之间的交互,并执行一些复杂的业务逻辑。
-
无状态:与领域对象不同,领域服务通常是无状态的,它们不保存任何状态信息,而是根据传入的参数执行操作。
-
具有领域知识:领域服务封装了领域专家的知识,它们负责执行领域相关的操作,确保业务逻辑的正确性和完整性。
-
解决领域复杂性:领域服务通常用于解决领域中的复杂问题,处理一些特定的业务需求,或者执行一些需要跨多个实体或值对象进行协调的操作。
以下是一个简单的领域服务的示例:
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void placeOrder(Customer customer, List<Product> products) {
// 执行下单逻辑,创建订单并保存到数据库
Order order = new Order(customer, products);
orderRepository.save(order);
}
public void cancelOrder(Order order) {
// 执行取消订单逻辑,更新订单状态并保存到数据库
order.cancel();
orderRepository.save(order);
}
// 其他领域服务方法...
}
在这个例子中,OrderService
是一个领域服务,用于处理订单相关的业务逻辑。placeOrder
方法用于创建订单并保存到数据库中,它接收一个顾客对象和产品列表作为参数。cancelOrder
方法用于取消订单并更新订单状态,它接收一个订单对象作为参数。这些方法封装了订单领域的操作,确保了业务逻辑的正确性。
模块[Module]
模块主要用于组织和封装相关概念(实体、值对象、领域服务、领域事件等),这样可以简化对较大模型的理解。
应用模块可以在领域模型中促成高内聚低耦合的设计。
模块作用于单个领域,用于分解模型规模。子域用于限定领域模型适用范围(有界上下文)。
在软件工程中,模块是指一个独立的、可重用的软件单元,它包含了相关功能的实现,并且可以被其他模块引用或组合以构建更复杂的系统。模块化设计有助于提高软件的可维护性、可扩展性和可重用性,同时也降低了系统的复杂度和耦合度。
以下是模块化设计的一些特点和定义:
-
独立性:模块应该是相互独立的,它们之间的变化不应该影响到其他模块,从而实现了高内聚低耦合的设计原则。
-
可重用性:模块应该是可重用的,它们可以被多个系统或应用程序共享和复用,从而提高了开发效率和代码的可维护性。
-
封装性:模块应该具有封装性,即模块内部的实现细节对外部是隐藏的,只暴露必要的接口和功能。
-
抽象性:模块应该具有抽象性,它们应该提供清晰的接口和抽象层,以隐藏内部实现的复杂性,并提供简单的使用方式。
-
单一责任:模块应该具有单一责任,即每个模块应该专注于完成一个特定的功能或任务,避免功能过于复杂和混杂。
对象生命周期:
聚合[Aggregate]
在领域驱动设计(DDD)中,聚合根(Aggregate Root)是一种特殊的实体,它是聚合(Aggregate)的根节点,负责管理和维护整个聚合内的一组相关对象。聚合根是聚合的入口点,外部对象只能通过聚合根来访问聚合内的其他对象。
以下是聚合根的一些特点和定义:
-
事务边界:聚合根定义了聚合内的一组对象之间的事务边界。所有的操作和变更都应该通过聚合根进行管理和协调,确保数据的一致性和完整性。
-
唯一标识:聚合根具有唯一的标识符,用于在系统中唯一标识和引用聚合。其他对象通过聚合根的标识符来访问聚合内的对象。
-
封装性:聚合根封装了聚合内的一组对象,外部对象只能通过聚合根来访问和操作聚合内的对象,聚合根隐藏了聚合内部的实现细节。
-
聚合内一致性:聚合根负责维护聚合内的一致性,确保聚合内的对象之间的关联和约束得到正确的维护和更新。
-
引用其他实体:聚合根可以引用其他实体对象,但这些实体对象通常是聚合根的直接子对象,并且它们的生命周期应该与聚合根的生命周期相关联。
聚合根的设计原则是领域驱动设计中的重要概念,它有助于构建具有良好结构和清晰边界的领域模型。通过定义聚合根和聚合之间的关系,可以更好地管理和组织领域对象,提高系统的可维护性和可扩展性。
在实际应用中,聚合根可以是任何具有聚合内部一致性边界的实体,例如订单、用户、博客文章等。设计良好的聚合根应该具有清晰的边界和单一责任,避免过于复杂和庞大的聚合结构。
工厂[Factory]
在领域驱动设计(DDD)中,工厂(Factory)是一种负责创建领域对象的对象,它封装了对象的创建逻辑,并提供了一种统一的方式来创建对象。工厂通常用于处理对象的创建过程,尤其是创建过程比较复杂或需要根据一些条件进行动态创建的情况。
以下是工厂的一些特点和定义:
- 封装对象创建逻辑:工厂封装了对象的创建逻辑,隐藏了创建过程的细节,外部对象可以通过工厂来创建领域对象,而无需知道对象创建的具体实现。
- 抽象对象创建:工厂提供了一种抽象的方式来创建对象,使得客户端代码与具体的对象实现解耦,增强了系统的灵活性和可维护性。
- 对象创建策略:工厂可以根据不同的条件或策略来创建不同的对象实例,例如基于参数、配置、环境等条件进行动态创建。
- 创建复杂对象:工厂通常用于创建那些具有复杂构造逻辑或需要进行一些初始化操作的对象,例如聚合根、值对象等。
- 可扩展性:工厂模式可以轻松地扩展和添加新的创建逻辑,使得系统可以应对不断变化的需求和业务场景。
工厂实现:
- 简单工厂方法
- 工厂方法
- 抽象工厂
- 反射工厂
以下是一个简单的工厂模式的示例:
public class ProductFactory {
public static Product createProduct(String type) {
if ("Book".equals(type)) {
return new BookProduct();
} else if ("Electronic".equals(type)) {
return new ElectronicProduct();
} else {
throw new IllegalArgumentException("Unknown product type: " + type);
}
}
}
public interface Product {
void display();
}
public class BookProduct implements Product {
@Override
public void display() {
System.out.println("This is a book product.");
}
}
public class ElectronicProduct implements Product {
@Override
public void display() {
System.out.println("This is an electronic product.");
}
}
在这个例子中,`ProductFactory`是一个工厂类,负责根据传入的参数创建不同类型的产品对象。`BookProduct`和`ElectronicProduct`是两种不同类型的产品,它们都实现了`Product`接口。客户端代码可以通过`ProductFactory`来创建不同类型的产品对象,而无需直接调用产品类的构造函数。
仓库[Repository]
在领域驱动设计(DDD)中,仓库(Repository)是一种负责持久化和管理领域对象的对象,它封装了领域对象的存储和检索逻辑,并提供了一种统一的方式来与持久化存储(如数据库、文件系统等)进行交互。仓库充当了领域对象和持久化存储之间的中介,负责将领域对象转换为持久化数据,并在需要时从持久化存储中检索出领域对象。
以下是仓库的一些特点和定义:
-
封装持久化逻辑:仓库封装了领域对象的持久化和检索逻辑,隐藏了与持久化存储的交互细节,使得客户端代码可以专注于领域逻辑而无需关心数据存储的具体实现。
-
抽象数据访问:仓库提供了一种抽象的方式来访问持久化数据,使得领域对象的存储和检索过程与具体的数据存储技术解耦,增强了系统的灵活性和可维护性。
-
领域对象管理:仓库负责管理领域对象的生命周期,包括创建、更新、删除等操作,并确保领域对象与持久化数据之间的一致性。
-
事务管理:仓库通常与事务管理器(Transaction Manager)结合使用,确保在数据持久化过程中的事务一致性和隔离性。
-
查询支持:除了存储和检索领域对象外,仓库还可以提供查询接口,支持根据一些条件或查询语言进行数据检索。
以下是一个简单的仓库示例:
在这个例子中,UserRepository
是一个仓库接口,定义了对用户对象进行存储和检索的方法。JpaUserRepository
是一个基于JPA(Java Persistence API)的具体实现,负责与数据库进行交互,并实现了仓库接口中定义的方法。客户端代码可以通过调用UserRepository
接口来进行用户对象的存储和检索,而无需关心具体的数据存储技术和实现细节。
public interface UserRepository {
User findById(int id);
void save(User user);
void delete(User user);
}
public class JpaUserRepository implements UserRepository {
private final EntityManager entityManager;
public JpaUserRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public User findById(int id) {
return entityManager.find(User.class, id);
}
@Override
public void save(User user) {
entityManager.persist(user);
}
@Override
public void delete(User user) {
entityManager.remove(user);
}
}
其他:
领域事件[DomainEvent]
在领域驱动设计(DDD)中,领域事件(Domain Event)是指领域中发生的具有重要意义的事情或状态变化,它们通常用于在领域内传递信息、触发业务流程或通知其他部分。领域事件可以是领域内部对象的状态变化,也可以是领域之间的交互。
以下是领域事件的一些特点和定义:
-
表达领域中的重要变化:领域事件用于表达领域中发生的重要变化或事情,例如订单创建、支付成功、库存变化等。
-
松耦合:通过使用领域事件,可以实现领域对象之间的松耦合,因为发送者不需要知道接收者的具体信息,只需要发送事件即可。
-
异步通信:领域事件通常用于异步通信,发送事件的对象不需要等待接收者的处理结果,而是继续执行后续操作。
-
触发业务流程:领域事件可以触发领域中的业务流程或业务规则,例如订单创建事件可以触发库存扣减和通知用户等操作。
-
事件溯源:通过记录领域事件,可以实现事件溯源(Event Sourcing),即根据事件序列重建系统的状态和历史。
在这个例子中,OrderCreatedEvent
表示订单创建事件,它包含了创建的订单对象。OrderService
负责创建订单,并在订单创建后发布订单创建事件。EmailService
是一个订阅者,它订阅订单创建事件,并在收到事件后发送订单确认邮件。通过使用事件总线(EventBus)来发布和订阅事件,可以实现领域内部对象之间的解耦合和异步通信。
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) {
this.order = order;
}
public Order getOrder() {
return order;
}
}
public class OrderService {
private final EventBus eventBus;
public OrderService(EventBus eventBus) {
this.eventBus = eventBus;
}
public void createOrder(Order order) {
// 创建订单逻辑
// ...
// 发布订单创建事件
OrderCreatedEvent event = new OrderCreatedEvent(order);
eventBus.publish(event);
}
}
public class EmailService {
@Subscribe
public void sendOrderConfirmationEmail(OrderCreatedEvent event) {
// 发送订单确认邮件
// ...
}
}
架构模式
分层架构[Layered Architecture]
领域驱动基本架构的四层架构图:
-
用户界面层(User Interface Layer):
- 该层包含了用户与系统交互的界面部分,包括网页、移动应用界面等。
- 用户界面层主要负责接收用户的输入请求,并将其传递给应用服务层处理。
-
应用服务层(Application Service Layer):
- 应用服务层是系统的核心,负责处理业务逻辑和业务流程。
- 应用服务接收来自用户界面层的请求,调用领域服务层的方法来完成具体的业务操作。
-
领域服务层(Domain Service Layer):
- 领域服务层包含了系统的领域逻辑和业务规则。
- 领域服务层负责实现复杂的业务逻辑,协调领域对象之间的交互,保持领域模型的一致性和完整性。
-
基础设施层(Infrastructure Layer):
- 基础设施层提供了系统的基础设施支持,包括数据访问、持久化、消息传递、日志等。
- 基础设施层负责与外部系统进行交互,实现与具体技术相关的功能,如数据库访问、文件操作等。
三层架构和四层架构区别
在这个四层架构中,各个层之间通过明确定义的接口进行交互,实现了各层之间的松耦合。同时,每个层都有明确的责任和职责,使得系统的结构清晰、易于维护和扩展。
六边形架构[Hexagonal]
六边形架构,又称为端口与适配器架构或者洋葱架构,是一种软件架构风格,旨在解耦应用程序的不同层和组件,并将关注点从具体的技术细节转移到业务逻辑上。它是由Alistair Cockburn提出的,后来由其他人进一步发展和推广。
六边形架构的核心思想是将应用程序划分为几个不同的层和环,每个环代表一个逻辑层,而六边形的边则代表了这些逻辑层之间的通信通道。这样的设计使得系统中的各个组件可以在不同的环境中运行,并且可以轻松地替换、重组,同时确保了各个组件之间的松耦合。
下面是六边形架构的一些关键概念:
-
内部端口(Internal Ports):内部端口是定义在六边形内部的接口,用于定义应用程序内部组件之间的通信规则和约定。内部端口负责将请求传递给适当的组件,并将响应返回给请求者。
-
外部端口(External Ports):外部端口是定义在六边形外部的接口,用于定义应用程序与外部环境(如UI、数据库、外部服务等)之间的通信规则和约定。外部端口负责将外部请求传递给应用程序内部的适当组件,并将响应返回给外部环境。
-
适配器(Adapters):适配器是用于连接外部端口和内部端口的组件,负责将外部请求转换为内部请求,并将内部响应转换为外部响应。适配器可以根据需要进行定制和扩展,以满足不同的外部环境要求。
-
业务逻辑(Business Logic):业务逻辑是定义在应用程序内部的核心逻辑,负责实现应用程序的具体业务功能和规则。业务逻辑通过内部端口与适配器进行通信,并处理外部请求。
-
外部资源(External Resources):外部资源是应用程序依赖的外部环境,如数据库、文件系统、消息队列、外部服务等。外部资源通过外部端口与应用程序进行通信。
六边形架构的设计目标是将关注点从技术细节转移到业务逻辑上,实现系统的灵活性、可维护性和可扩展性。通过将应用程序划分为不同的逻辑层和环,并定义清晰的通信规则和约定,六边形架构使得系统的各个组件可以独立开发、测试和部署,降低了系统的耦合度和复杂度。
CQRS 命令查询职责分离
命令查询职责分离(CQRS)是一种软件架构模式,旨在将系统的写操作(命令)和读操作(查询)分离开来,每个操作使用独立的模型和处理流程。在领域驱动设计(DDD)中,CQRS被广泛应用于实现复杂业务系统。
以下是CQRS的关键概念:
-
命令(Command):表示对系统进行改变或者修改的操作,通常是写操作。命令包含了要执行的操作以及操作所需的参数。
-
查询(Query):表示从系统中获取信息或者数据的操作,通常是读操作。查询不会修改系统的状态,只是返回系统当前的状态信息。
-
命令模型(Command Model):用于处理命令操作的模型,负责执行命令所指定的操作并更新系统的状态。
-
查询模型(Query Model):用于处理查询操作的模型,负责从系统中获取信息并返回给客户端。
-
命令处理器(Command Handler):负责接收并处理命令,并将其转发给命令模型进行处理。
-
查询处理器(Query Handler):负责接收并处理查询,并将查询转发给查询模型进行处理,并返回查询结果。
CQRS的主要思想是根据操作的性质,分别使用命令模型和查询模型进行处理。
命令模型专注于处理写操作,保证系统的一致性和完整性;
而查询模型专注于处理读操作,提高系统的性能和可伸缩性。
通过将命令和查询的职责分离开来,可以更好地满足系统的需求,提高系统的灵活性和可维护性。
在实践中,CQRS通常与事件驱动架构(EDA)相结合,通过领域事件来驱动命令和查询的处理流程,实现系统的异步和松耦合。
实际代码架构