书接上文:两文读懂DDD领域驱动设计,举例说明,通俗易懂【值得收藏】
~~
没看过的小伙伴先去看第一篇,然后再来看这个~
上文中我们看到DDD有几层如下,可以再看下
DDD服务依赖关系
有两种
1.松散分层架构
DDD示例如下
特点:
- 分层仍然存在,但各层之间的依赖关系较为灵活;
- 各层之间通过更灵活的机制进行通信,如事件机制、依赖注入等;
- 更灵活也更耦合,对新需求可以更快的实现,不用层层封装,但是可能导致设计不够稳定
以DDD说明
领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层,比较自由,开发便捷
问题是依赖混乱,如果实体发生变更,上层调用可能发现好几个地方需要改动
适用场景:
- 小型或中型项目:不需要过分严格的分层。
- 快速迭代的项目:需要快速响应需求变化。
- 微服务架构:在微服务之间可能存在灵活的依赖关系
2.严格分层架构
示例如下
特点:
- 每一层都有明确的职责和边界,可维护性好,更容易定位问题;
- 上层依赖下层,下层不依赖上层,改动一处通常不会影响其他层,更加稳定;
- 更容易进行单元测试和集成测试,因为可以单独测试每一层;
- 扩展性较好,新增功能通常只需要在某一层进行改动
- 跨层需要封装,所以工作量代码量比松散的要大
以DDD说明
每一层服务只能向紧邻的上一层提供服务。虽然实体、实体方法和领域服务都在领域层,但实体和实体方法只能暴露给领域服务,领域服务只能暴露给应用服务
服务如果需要跨层调用,下层服务需要在上层封装后,才可以提供跨层服务。比如实体方法需要向应用服务提供服务,它需要封装成领域服务
可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿,倒是好管理
适用场景:
- 大型企业级应用:需要高度解耦和可维护性的场景。
- 长期维护的项目:需要清晰的分层和职责划分。
- 高度可测试性要求:需要进行单元测试和集成测试。
如何考虑?
在实际项目中一般可以根据项目的具体需求和团队的经验来选择合适的架构风格。通常情况下可以结合两种架构的优点,采取一种混合式的分层架构:
- 核心业务逻辑部分采用严格分层,确保高内聚和低耦合。
- 辅助功能或非核心部分可以采用松散分层,提高灵活性。
通过这种方式可以在保证核心业务逻辑的稳定性的同时,提高系统的灵活性和可维护性,永远要记得不要为了技术而技术,选择适合我们的就好
DDD典型目录结构
下面我们举例看下DDD的目录结构,这只是相对典型的一种,其他大家可以自行调整,别较真
src/
└── main/
├── java/
│ └── com.example.myapp.domain/
│ ├── [限界上下文名称]/
│ │ ├── application/
│ │ │ ├── service/
│ │ │ │ ├── [具体服务].java
│ │ │ │ ├── [具体服务]Impl.java
│ │ │ │ ├── [具体服务]Facade.java
│ │ │ │ └── [其他服务]
│ │ ├── infrastructure/
│ │ │ ├── repository/
│ │ │ │ ├── [具体Repository].java
│ │ │ │ ├── [具体Repository]Impl.java
│ │ │ │ └── [其他Repository]
│ │ ├── domain/
│ │ │ ├── entity/
│ │ │ │ ├── [具体实体].java
│ │ │ │ └── [其他实体]
│ │ │ ├── value-object/
│ │ │ │ ├── [具体值对象].java
│ │ │ │ └── [其他值对象]
│ │ │ ├── aggregate/
│ │ │ │ ├── [具体聚合].java
│ │ │ │ └── [其他聚合]
│ │ │ ├── event/
│ │ │ │ ├── [具体领域事件].java
│ │ │ │ └── [其他领域事件]
│ │ │ ├── model/
│ │ │ │ ├── [具体模型].java
│ │ │ │ └── [其他模型]
│ │ │ ├── service/
│ │ │ │ ├── [具体领域服务].java
│ │ │ │ └── [其他领域服务]
│ │ │ └── util/
│ │ │ ├── [具体工具类].java
│ │ │ └── [其他工具类]
│ │ └── config/
│ │ ├── [配置文件].properties
│ │ └── [其他配置文件]
│ └── [其他限界上下文]
└── resources/
└── com.example.myapp.domain/
└── [限界上下文名称]/
├── messages/
│ └── [消息文件].properties
└── [其他资源文件]
目录解释
下面解释下这些目录
-
domain/:
- entity/: 包含所有的领域实体(Entity)。
- value-object/: 包含所有的值对象(Value Object)。
- aggregate/: 包含所有的聚合(Aggregate)。
- event/: 包含所有的领域事件(Domain Event)。
- service/: 包含所有的领域服务(Domain Service)。
- model/: 包含领域模型的其他组件,如枚举、常量等。
- util/: 包含领域相关的工具类。
-
application/:
- service/: 应用服务(Application Service),负责协调领域服务和基础设施服务。
- facade/: 外观(Facade),为外部系统提供统一的接口。
-
infrastructure/:
- repository/: 包含所有的仓库(Repository)实现。
- adapter/: 包含适配器模式的相关实现,如外部服务的适配器。
- config/: 包含配置文件和配置类。
-
config/:
- 包含项目的配置文件和其他配置相关的类。
-
resources/:
- 包含项目所需的资源文件,如消息文件、模板文件等。
示例
假设我们有一个限界上下文叫做 OrderManagement
,其目录结构如下,大家可以观摩下
src/
└── main/
├── java/
│ └── com.example.myapp.domain/
│ └── ordermanagement/
│ ├── application/
│ │ ├── service/
│ │ │ ├── OrderService.java
│ │ │ ├── OrderServiceImpl.java
│ │ │ └── OrderFacade.java
│ ├── infrastructure/
│ │ ├── repository/
│ │ │ ├── OrderRepository.java
│ │ │ ├── OrderRepositoryImpl.java
│ │ └── adapter/
│ │ ├── ExternalOrderAdapter.java
│ ├── domain/
│ │ ├── entity/
│ │ │ ├── Order.java
│ │ │ └── OrderLine.java
│ │ ├── value-object/
│ │ │ ├── Address.java
│ │ │ └── Money.java
│ │ ├── aggregate/
│ │ │ ├── OrderAggregate.java
│ │ └── event/
│ │ ├── OrderPlacedEvent.java
│ │ └── OrderShippedEvent.java
│ │ ├── service/
│ │ │ ├── OrderService.java
│ │ └── util/
│ │ ├── DateUtil.java
│ │ └── ValidationUtil.java
│ └── config/
│ ├── OrderManagementConfig.properties
│ └── ApplicationConfig.java
└── resources/
└── com.example.myapp.domain/
└── ordermanagement/
├── messages/
│ └── messages.properties
└── templates/
└── email-template.html
所以我们在设计的时候要考虑什么?我列了几个点
- 模块化:确保每个模块的功能相对独立,方便维护和扩展。
- 层次分明:层次结构清晰,便于查找和理解代码。
- 灵活性:根据项目需求,灵活调整目录结构,避免过于僵化。
- 一致性:在整个项目中保持一致的命名和组织方式,方便团队协作
问题来了:限界上下文的粒度怎么分?留个问题思考下,后面会说明
DDD数据对象转换
我发现很多人对于数据对象的使用都不合理,一方面Convert可能有性能问题,一方面需要写代码,亦或是功能实现就行了,数据对象乱用,下面我们先看下定义
- 数据持久化对象PO,与数据库结构一一映射,是数据持久化过程中的数据载体。
- 领域对象DO,微服务运行时的实体,是核心业务的载体,很多公司DO和数据库映射,这里我们区分开,DO是一个粒度的领域对象,里面有业务逻辑,不用和数据库映射。
- 数据传输对象DTO,用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。
- 视图对象VO,用于封装展示层指定页面或组件的数据
我们看下在DDD中这个数据对象是怎么转换的
- 接口层到应用层:使用RequestDTO或Context来传递请求参数。
- 应用层到领域层:使用DTO对象来传递命令请求,并使用ResponseDTO来返回响应数据。
- 领域层:数据库查询PO,供实体和领域服务使用
- 应用层返回给接口层:使用ResponseDTO返回数据,并转换为VO返回给客户端(很多企业应该把VO下沉到逻辑层了,返回时直接就返回VO了,这样少了一步convert)
这里这个requestDTO可以就是常规DTO,responseDTO如果是简单的返回可以直接返回特定的值或状态,如果是复杂的返回可以直接封装一个全局的ResultResponse,用泛型来返回各种数据即可
DDD设计思考
所以说整体几层看下来倒是有些像JVM中的双亲委派模型,业务能沉底的沉底去做,沉底不合适的聚合去做,最后给到外部使用
限界上下文的粒度怎么分?
大粒度
我们将电商系统划分为三个大的限界上下文:
-
前端门户
- 包括用户界面、登录、注册、搜索等功能。
- 跨多个后端服务进行调用。
-
后端服务
- 包括商品管理、订单管理、库存管理、用户管理、支付处理、物流管理、营销活动、客户服务等功能。
- 每个功能作为一个独立的服务。
-
基础设施(Infrastructure)
- 包括数据库、消息队列、日志、监控等基础组件。
- 支撑整个系统的运行
小粒度
- 商品管理(Product Management)
- 订单管理(Order Management)
- 库存管理(Inventory Management)
- 用户管理(User Management)
- 支付处理(Payment Processing)
- 物流管理(Logistics Management)
- 营销活动(Marketing Campaigns)
- 客户服务(Customer Service
在电商系统中 限界上下文的设计一般需要综合考虑业务功能、团队规模、技术栈等因素。通常情况可以先从较大的粒度开始,然后根据项目的进展逐步细化。在设计过程中要确保每个限界上下文的职责清晰、边界明确,便于后续的维护和扩展
关于耦合
在业务逻辑中没有绝对的解耦,只有相对的解耦,什么意思呢
比如有两个限界上下文,订单和商品,在订单限界上下文中要调用商品的逻辑,怎么办?
凉拌
- 使用领域服务协调
- 通过基础层查询商品(只适用于简单商品查询,商品要是有逻辑就不行了)
示例揣摩下再解释
// 订单限界上下文中的领域服务
public interface OrderService {
void placeOrder(Order order);
}
public class OrderServiceImpl implements OrderService {
private OrderRepository orderRepository;
private ProductService productService;
@Override
public void placeOrder(Order order) {
// 获取商品信息
List<ProductInfo> productInfos = productService.getProductInfos(order.getItems());
// 业务逻辑处理
if (isValidOrder(order, productInfos)) {
order.setStatus(OrderStatus.PLACED);
orderRepository.save(order);
} else {
throw new IllegalArgumentException("Invalid order");
}
}
private boolean isValidOrder(Order order, List<ProductInfo> productInfos) {
// 检查订单的有效性
return true;
}
}
// 商品限界上下文中的领域服务
public interface ProductService {
List<ProductInfo> getProductInfos(List<OrderLineItem> items);
}
public class ProductServiceImpl implements ProductService {
private ProductRepository productRepository;
@Override
public List<ProductInfo> getProductInfos(List<OrderLineItem> items) {
List<ProductInfo> productInfos = new ArrayList<>();
for (OrderLineItem item : items) {
Product product = productRepository.findById(item.getProductId());
productInfos.add(new ProductInfo(product.getName(), product.getPrice()));
}
return productInfos;
}
}
// 订单实体
public class Order {
private String orderId;
private List<OrderLineItem> items;
private OrderStatus status;
public Order(String orderId) {
this.orderId = orderId;
this.items = new ArrayList<>();
this.status = OrderStatus.NEW;
}
public void addItem(String productId, int quantity) {
OrderLineItem item = new OrderLineItem(productId, quantity);
items.add(item);
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
// Getters and setters
}
// 订单行项目
public class OrderLineItem {
private String productId;
private int quantity;
public OrderLineItem(String productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
}
// 商品实体
public class Product {
private String productId;
private String name;
private BigDecimal price;
public Product(String productId, String name, BigDecimal price) {
this.productId = productId;
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
// Getters and setters
}
// 商品信息(用于传递给订单限界上下文)
public class ProductInfo {
private String name;
private BigDecimal price;
public ProductInfo(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
}
可以看到是订单的领域层调用了商品的领域层,这就耦合了,也是不得不的耦合,我们尽量在解耦,一方面为了减少耦合,还有一方面也是为了各自的模块化,而此时订单和商品已经模块化了,只是在调用上会单方面耦合一下,可以研究下怎么尽量减少耦合,领域事件-消息队列可以用起来
调用链比较长怎么办?
各个层比较多,链路变成有些复杂,包括对性能也会有些影响,怎么办,给大家个思路
- 合理分层和模块化
- 领域事件解耦(能异步的尽量异步)
- 领域服务来协调,封装复杂业务
- 缓存机制,减少下面链路的访问
- 事务管理,尽量小事务
- 数据库优化,优化查询
- 异步处理
- ……
DDD使用注意tips
DDD是一种指导思想,不必吹毛求疵,可以在使用架构过程中吸取DDD合适的部分到自己的架构中,不必全盘拿过来,适合自己的架构就是最好的,不要为了DDD而DDD,否则会导致过度设计
好了,结束了伙伴们,相信大家有个基础的认识了,后面有机会可以实践一下,祝好运祝发财~