领域驱动设计:Domain Driven Design,DDD,一种软件设计的方法论,围绕业务构建领域模型,通过领域模型来设计软件,以此来控制软件的复杂性,解决软件难以理解、难以演进的问题。它提出了一系列的概念和模式,以帮助开发者更好地建模和实现业务逻辑。
这里先学习根据领域模型设计出来的代码大概是什么样子的,相较于其它软件架构有什么不同或好处。
领域模型将软件系统分为四层:用户接口层、应用层、领域层、基础层。
相较于传统的三层架构,controller、service、repository,领域模型中的分层架构,最主要的特点就是将原先属于服务层的代码分散到了应用层和领域层。因为领域驱动设计,会根据业务来设计领域模型和领域服务,这是系统最核心的业务逻辑,这些逻辑会被放在领域层,然后在应用层,通过把多个领域服务组合在一起的方式,来实现具体的业务功能。领域会被分的很详细,单个领域服务通常无法实现业务逻辑,应用层也相当于领域层的适配器,它会对领域层屏蔽外部变动,保证核心业务的稳定。
严格分层架构和非严格分层架构:
代码设计:
代码实现:
使用DDD设计代码,先写好的是领域层,实现核心业务逻辑,然后再设计数据库,决定领域层的实体要怎么存放在数据库中,最后设计用户层、应用接口层,将前端请求转换为领域模型,调用领域模型的代码,实现业务功能。
DD中的实体类类型:和三层架构的实体类类型基本相同,主要是对于DO的概念不一致。
实体类的模型转换发生在什么位置:
领域层不做模型转换,由其它层来做,所以其它层相当于领域层的适配器。
领域:Domain,业务的活动范围,系统所要解决的问题的业务背景。复杂的业务中,还可以细分为子域、核心域、通用域、支持域。
限界上下文:Bounded Context,一个明确的边界,定义了某个子域的模型及其责任范围,每个上下文内的模型和逻辑都是自洽的。领域边界就是通过限界上下文来定义的。限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务
聚合:aggregate,是一个或多个对象的集合、是一个边界,聚合是逻辑上的。聚合中的对象在业务上紧密相关,并且需要在业务操作中保持一致性。聚合是领域模型的基本单位。
领域就是业务背景,限界上下文就是对领域进行划分,划分成不同的小领域,也就是子域,直到子域不可以再分。
实体:对应业务对象,它具有业务属性和业务行为,实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。实体是最基础的领域对象,它的行为表现出的是个体的能力。
贫血模式和充血模式:
值对象:主要是属性集合,对实体的状态和特征进行描述,值对象是不可变的。
值对象的作用:
聚合:由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,是领域模型的基本单位。
聚合的作用:
聚合根:聚合中的一个特殊实体,它是聚合的入口点和唯一暴露给外部的接口。外部系统只能通过聚合根来访问和操作聚合内的其他对象。
判断一个实体是否是聚合根的方法:是否有独立的生命周期?是否有全局唯一ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体
聚合间的引用:通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式。
子聚合操作:在某些情况下,可以允许对子聚合的直接操作,前提是这些操作不会影响到聚合根的业务逻辑和一致性。如果发现频繁需要直接操作子聚合,可能需要重新评估聚合边界,考虑是否需要调整聚合设计。确保在直接操作子聚合时,仍然能通过某种机制来验证业务规则。
一个聚合通常对应一个领域服务,领域服务用于处理不属于单个实体的业务逻辑
实现了数据库访问、缓存、rpc等操作。有一点值得注意,DDD规定把数据库访问的接口定义在领域层,不过在实践中,数据库访问通常是定义在基础服务层
位于应用层,应用服务用来处理跨多个聚合的逻辑
案例:订单系统中订单的领域模型
1、值对象 Product(一个订单涉及的具体产品),一旦创建,就不会再变
public class Product {
private final String productId;
private final String name;
private final double price;
// 只有构造方法和get方法
}
2、实体 OrderItem 订单项
public class OrderItem {
private final Product product;
private int quantity; // 数量
// 构造方法 getter方法
public void setQuantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be greater than zero.");
}
this.quantity = quantity;
}
public double getTotalPrice() {
return product.getPrice() * quantity;
}
}
3、聚合根 Order 订单
public class Order {
private final String orderId;
private final List<OrderItem> items;
// 构造方法 getter方法
public void addItem(Product product, int quantity) {
OrderItem item = findItemByProduct(product);
if (item == null) {
items.add(new OrderItem(product, quantity));
} else {
item.setQuantity(item.getQuantity() + quantity);
}
}
public void removeItem(Product product) {
OrderItem item = findItemByProduct(product);
if (item != null) {
items.remove(item);
}
}
public double getTotalPrice() {
return items.stream().mapToDouble(OrderItem::getTotalPrice).sum();
}
private OrderItem findItemByProduct(Product product) {
return items.stream().filter(i -> i.getProduct().equals(product)).findFirst().orElse(null);
}
}
4、领域服务:
public class OrderDomainService {
// 计算订单的总价
public double calculateTotalPrice(Order order) {
return order.getItems().stream()
.mapToDouble(OrderItem::getTotalPrice)
.sum();
}
// 判断订单是否可以应用折扣,在这里就是判断订单的总价是否大于100
public boolean isOrderEligibleForDiscount(Order order) {
return order.getTotalPrice() > 100;
}
// 应用折扣
public void applyDiscount(Order order, double discountRate) {
if (isOrderEligibleForDiscount(order)) {
double totalPrice = calculateTotalPrice(order);
double discount = totalPrice * discountRate;
order.setTotalPrice(totalPrice - discount);
}
}
}
5、 仓储服务和实现,仓储服务在领域层,实现在基础服务层。
// 接口
public interface OrderRepository {
// 根据订单ID查找订单
Order findById(String orderId);
// 保存订单
void save(Order order);
// 删除订单
void delete(Order order);
// 获取所有订单
List<Order> findAll();
}
// 实现
public class MybatisOrderRepository implements OrderRepository {
}
6、应用服务:调用领域服务和基础服务,完成流程编排
@Service
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderDomainService orderDomainService;
// 保存订单
public void placeOrder(String customerId, List<OrderItem> items) {
Order order = new Order(customerId);
for (OrderItem item : items) {
order.addItem(item);
}
double totalPrice = orderDomainService.calculateTotalPrice(order);
if (orderDomainService.isOrderEligibleForDiscount(order)) {
orderDomainService.applyDiscount(order, 0.9); // 9折
// 在这里把订单的折扣信息也保存到数据库中,和订单信息分开保存
}
orderRepository.save(order);
}
// 获取订单
public Order getOrder(String orderId) {
return orderRepository.findById(orderId);
}
// 改变订单状态
public void changeOrderStatus(String orderId, OrderStatus newStatus) {
Order order = orderRepository.findById(orderId);
if (order != null) {
order.setStatus(newStatus);
orderRepository.save(order);
} else {
throw new IllegalArgumentException("Order not found");
}
}
}
这个案例描述了在DDD中代码按照领域模型应该如何组织,没有描述的代码,基本和其他设计模式下的代码差不多。
设计原则:
用户接口层:interfaces
应用层:application
领域层:domain
基础层:infrastructure
领域,就是具体的业务,从代码设计的角度讲,一个领域,是一项可以放在某个单独的微服务中实现的业务,通过边界,划分出不同的领域,每个领域实现各自的功能
领域模型,是这个领域中的业务模型,领域模型是根据业务来建模的,是可以百分百反映业务的,领域模型的基本单位是聚合,聚合是逻辑上的概念,是由一系列实体和值对象组成,聚合是增删改查的基本单位,聚合中的实体,一个实体在代码中就是一个类,它是某个业务对象。
对于领域模型的计算,应该放在领域服务。
聚合和聚合根是业务上紧密关联的实体的集合,这个集合中的数据需要保持状态一致。
我的使用经验,要根据业务来设计聚合,然后根据聚合来设计代码,如果聚合本身就很大,在某些情况下,可以越过聚合根,单独操作聚合中的实体,但是这种操作不能很多。不要因为代码反过来影响聚合的设计,最好的聚合就是可以直接反映业务的聚合。
如果两个实体之间是一对多的父子关系,外键或许合适,如果多对多的并列关系,只能使用关联表。
在DDD中,应用层直接调用仓储是符合规范的,因为应用层负责协调应用的工作流和管理事务,而领域层专注于业务逻辑的实现。
合理,基础层是为每一层服务的,在用户接口层也可以调用。
不合理,应该将 DTO 定义在应用层中,应用层的DTO可以在接口层使用,以便于接口层与外部系统交互。不过在微服务的系统中,和外部交互的DTO、接口,通常单独定义在client包下,然后接口层、应用层,共同依赖client包。
分离业务模型和数据模型是为了更好地应对复杂系统中的不同关注点和变化需求。虽然在某些情况下这可能会增加初期的开发工作量,但从长期来看,这种分离可以提高系统的灵活性、可维护性和适应性
领域模型和数据模型:
它们的缩写都是DDD,Data Driven Design、Domain Driven Design,数据驱动设计、领域驱动设计
数据模型负责的是数据存储,其要义是扩展性、灵活性、性能。而领域模型负责业务逻辑的实现,其要义是业务语义显性化的表达,以及充分利用OO的特性增加代码的业务表征能力。
ER模型:Entity Relationship,实体关系,一种用于数据建模的方法,帮助设计者在设计数据库时,直观地表示实体及其相互关系。它通常使用图形表示,包括以下几个基本组件:
防腐层:Anti-Corruption Layer, ACL,是一种架构模式,用于在不同的子系统或服务之间进行适配和隔离,以防止外部系统的变化或复杂性影响到核心系统。这个概念主要来源于领域驱动设计,旨在保护核心域模型不受外部系统的影响。
https://www.infoq.cn/article/s_LFUlU6ZQODd030RbH9