領域驅動設計實戰-DDD 領域驅動設計實戰
領域驅動(DDD,Domain Driven Design)為軟體設計提供了一套完整的理論指導和落地實踐,通過戰略設計和戰術設計,將技術實現與業務邏輯分離,來應對複雜的軟體系統。本系列文章準備以實戰的角度來介紹 DDD,首先編寫領域驅動的程式碼模型,然後再基於程式碼模型,引入 DDD 的各項概念,先介紹戰術設計,再介紹戰略設計。
> DDD 實戰1 - 基礎程式碼模型
> DDD 實戰2 - 整合限界上下文(Rest & Dubbo)
> DDD 實戰3 - 整合限界上下文(訊息模式)
> DDD 實戰4 - 領域事件的設計與使用
> DDD 實戰5 - 實體與值物件
> DDD 實戰6 - 聚合的設計
> DDD 實戰7 - 領域工廠與領域資源庫
> DDD 實戰8 - 領域服務與應用服務
> DDD 實戰9 - 架構設計
> DDD 實戰10 - 戰略設計
在 DDD 中,共有四層(領域層、應用層、使用者介面層、基礎設施層),其層級實際上是環狀架構。如上圖所示。根據整潔架構思想,在上述環狀架構中,越往內層,程式碼越穩定,其程式碼不應該受外界技術實現的變動而變動,所以依賴關係是:外層依賴內層
。按照這個依賴原則,DDD 程式碼模組依賴關係如下:
- 領域層(domain):位於最內層,不依賴其他任何層;
- 應用層(application):僅依賴領域層;
- 使用者介面層(interfaces):依賴應用層和領域層;
- 基礎設施層(infrastructure):依賴應用層和領域層;
- 啟動模組(starter):依賴使用者介面層和基礎設施層,對整個專案進行啟動。
注意:interfaces 和 infrastructure 位於同一個換上,二者沒有依賴關係。
DDD 各層職責
領域模型層 domain
包括實體、值物件、領域工廠、領域服務(處理本聚合內跨實體操作)、資源庫介面、自定義異常等
應用服務層 application
跨聚合的服務編排,僅編排聚合根。包括:應用服務等
使用者介面層 interfaces
本應用的所有流量入口。包括三部分:
- web 入口的實現:包括 controller、DTO 定義、DTO 轉化類
- 訊息監聽者(消費者):包括 XxxListener
- RPC 介面的實現:比如在使用 Dubbo 時,我們的服務需要開放 Dubbo 服務給第三方,此時需要建立單獨的模組包,例如 client 模組,包含 Dubbo 介面和 DTO,在使用者介面層中,去做 client 中介面的實現以及 DTO 轉化類
基礎設施層 infrastructure
本應用的所有流量出口。包括:
- 資源庫介面的實現
- 資料庫操作介面、資料庫實現(如果使用mybatis,則包含 resource/*.xml)、資料庫物件 DO、DO 轉化類
- 中介軟體的實現、檔案系統實現、快取實現、訊息實現 等
- 第三方服務介面的實現
基於 DDD 開發訂單中心
需求:基於 DDD 開發一個訂單中心,實現下訂單、查詢訂單等功能
程式碼:https://github.com/zhaojigang/ordercenter
ordercenter 根模組
├── order-application 應用模組
├── order-domain 領域模組
├── order-infrastructure 基礎設施模組
├── order-interfaces 使用者介面模組
├── order-starter 啟動模組
└── pom.xml 根模組
領域層程式碼模型
包依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
引入 spring-boot-autoconfigure:2.4.2,在領域工廠中需要用到 Spring 註解
DDD 標識註解 common.ddd.AggregateRoot
/**
* 標註一個實體是聚合根
*/
@Documented
@Retention(SOURCE)
@Target(TYPE)
public @interface AggregateRoot {
}
自定義異常 common.exception.OrderException
/**
* 自定義異常
*/
@Data
public class OrderException extends RuntimeException {
private Integer code;
private String message;
public OrderException(Integer code, String message) {
this.code = code;
this.message = message;
}
}
將自定義異常放在領域層,因為 DDD 推薦使用充血模型,在領域實體、值物件或者領域服務中,也會做一些業務邏輯,在業務邏輯中,可以根據需要丟擲自定義異常
資源庫介面 io.study.order.repository.OrderRepository
/**
* 訂單資源庫介面
*/
public interface OrderRepository {
/**
* 儲存訂單
*
* @param order 訂單
*/
void add(Order order);
/**
* 根據訂單ID獲取訂單
* @param orderId
*/
Order orderOfId(OrderId orderId);
}
- 資源庫介面放置在領域層,實現領域物件自持久化,同時實現依賴反轉。
- 依賴反轉:將依賴關係進行反轉,假設 Order 要做自持久化,那麼需要拿到資源庫的實現 OrderRepositoryImpl 才行,那麼 domain 包就要依賴 infrastructure 包,但是這不符合
外層依賴內層
的原則,所以需要進行依賴反轉,由 infrastructure 包依賴 domain 包。實現依賴反轉的方式就是在被依賴方中新增介面(例如,在 domain 包中新增 OrderRepository 介面),依賴包對介面進行實現(infrastructure 包中對 OrderRepository 進行實現),這樣的好處是,domain 可以完全僅關注業務邏輯,不要關心具體技術細節,不用去關心,到底是儲存到 mysql,還是 oracle,使用的資料庫框架是 mybatis 還是 hibernate,技術細節的實現由 infrastructure 來完成,真正實現了業務邏輯和技術細節的分離- 資源庫的命名推薦:對於資源庫,推薦面向集合進行設計,即資源庫的方法名採用與集合相似的方法名,例如,儲存和更新是 add、addAll,刪除時 remove、removeAll,查詢是 xxxOfccc,例如 orderOfId,ordersOfCondition,複數使用 xxxs 的格式,而不是 xxxList 這樣的格式
- 一個聚合具有一個資源庫:比如訂單聚合中,Order 主訂單是聚合根,OrderItem 子訂單是訂單聚合中的一個普通實體,那麼在訂單聚合中只能存在 OrderRepository,不能存在 OrderItemRepository,OrderItem 的 CRUD 都要通過 OrderRepository 先獲得 Order,再從 Order 中獲取 List<OrderItem>,再做邏輯。這樣的好處,
保證了聚合根值整個聚合的入口,對聚合內的其他實體和值物件的方訪問,只能通過聚合根,保證了聚合的封裝性
領域工廠 io.study.order.factory.OrderFactory
/**
* 訂單工廠
*/
@Component
public class OrderFactory {
private static OrderRepository orderRepository;
@Autowired
public OrderFactory(OrderRepository repository) {
orderRepository = repository;
}
public static Order createOrder() {
return new Order(orderRepository);
}
}
工廠的作用:建立聚合。
工廠的好處:
- 建立複雜的聚合,簡化客戶端的使用。例如 Order 的建立需要注入資源庫,訂單建立後,可以直接釋出訂單建立事件。
- 可讀性好(更加符合通用語言),比如 對於建立訂單,createOrder 就比 new Order 的語義更加明確
- 更好的保證一致性,防止出錯,假設建立兩個主訂單 Order,兩個主訂單下分別還要建立多個子訂單 OrderItem,每個子訂單中需要儲存主訂單的ID,如果由客戶端來設定 OrderItem 中的主訂單ID,可能會將A主訂單的ID設定給B主訂單下的子訂單,可能出現數據不一致的問題,具體的示例見 《實現領域驅動》P183。
實體唯一標識 io.study.order.domain.OrderId
import lombok.Value;
/**
* 訂單ID
*/
@Value
public class OrderId {
private Long id;
public static OrderId of(Long id) {
return new OrderId(id);
}
public void validId(){
if (id == null || id <= 0) {
throw new OrderException(400, "id 為空");
}
}
}
- 推薦使用強型別的物件作為實體的唯一標識,好處有兩個:
a. 用來避免傳參混亂,同時提升介面的可讀性,例如 xxx(Long orderId, Long goodsId),假設上述介面第一個引數傳了 goodsId,第二個傳了 orderId,那麼編譯期是無法發現的,改為 xxx(OrderId orderId, GoodsId, goodsId) 即可避免,同時可讀性也較高。
b. 唯一標識中會有一些其他行為方法,如果唯一標識使用弱型別,那麼這些行為方法將會洩露在實體中- 唯一標識類是一個值物件,推薦值物件設定為不可變物件,使用 @lombok.Value 標註值物件,既可標識該物件為值物件,也可以是該類變為不可變類。例如,表示後的 OrderId 沒有 setXxx 方法。
- 值物件的行為函式都是無副作用函式(即不能影響值物件本身的狀態,例如 OrderId 物件被建立後,不能再使用 setXxx 修改其屬性值),如果確實有屬性需要變動,值物件需要整個換掉(例如,重新建立一個 OrderId 物件)
聚合根 io.study.order.domain.Order
/**
* 訂單聚合根
*/
@Setter
@Getter
@AggregateRoot
public class Order {
/**
* 訂單 ID
*/
private OrderId id;
/**
* 訂單名稱
*/
private String name;
/**
* 訂單資源庫
*/
private OrderRepository orderRepository;
protected Order(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
/**
* 建立訂單
*
* @param order
*/
public void saveOrder(Order order) {
orderRepository.add(order);
}
public void setName(String name) {
if (name == null) {
throw new OrderException(400, "name 不能為空");
}
this.name = name;
}
public void setGoodsId(Long goodsId) {
if (goodsId == null) {
throw new OrderException(400, "goodsId 不能為空");
}
this.goodsId = goodsId;
}
public void setBuyQuality(Integer buyQuality) {
if (buyQuality == null) {
throw new OrderException(400, "buyQuality 不能為空");
}
this.buyQuality = buyQuality;
}
}
- 聚合根是一個特殊的實體,是整個聚合對外的使者,其他聚合與改聚合溝通的方式只能是通過聚合根
- 由於使用工廠來建立 Order,那麼 Order 的構造器需要設定為 protected,防止外界直接使用進行建立
- 實體單個屬性的校驗需要在 setXxx 中完成自校驗
- 實體是可變的、具有唯一標識,其唯一標識通常需要設計成強型別
- 聚合中的 XxxRepository 可以通過上述的工廠進行注入,也可以使用“雙委派”機制,即提供類似方法:
createOrder(Order order, XxxRepository repository)
,然後應用層在呼叫該方法時,傳入注入好的 repository 例項即可。但是這樣的方式,提高了客戶端使用的複雜性。
應用層程式碼模型
包依賴
<dependencies>
<dependency>
<groupId>io.study</groupId>
<artifactId>order-domain</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
應用服務 io.study.order.app.service.OrderAppService
/**
* 訂單應用服務
*/
@Service
public class OrderAppService {
/**
* 建立一個訂單
*
* @param order
*/
public void createOrder(Order order) {
/**
* 儲存訂單
*/
order.saveOrder(order);
/**
* 扣減庫存
*/
}
}
應用服務用於服務編排,如上述先儲存訂單,然後再呼叫庫存服務減庫存。(庫存服務屬於第三方服務,第三方服務的整合見下一小節)
基礎設施層程式碼模型
image.png包依賴
<dependencies>
<!-- 領域模組 -->
<dependency>
<groupId>io.study</groupId>
<artifactId>order-domain</artifactId>
<version>${project.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<scope>provided</scope>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
- mapstruct 用於實現模型對映器,關於其使用見 https://www.jianshu.com/p/53aac78e7d60
- 資料儲存採用 mysql,資料庫操作框架使用 mybatis,可以看到,領域層對具體的技術實現並不關注,僅關注業務,通過 DDD 實現了技術細節與業務邏輯的解耦。
資源庫實現 io.study.order.repository.impl.OrderRepositoryImpl
/**
* 訂單資源庫實現類
*/
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Resource
private OrderDAO orderDAO;
@Override
public void add(Order order) {
orderDAO.insertSelective(OrderDOConverter.INSTANCE.toDO(order));
}
@Override
public Order orderOfId(OrderId orderId) {
OrderDO orderDO = orderDAO.selectByPrimaryKey(orderId.getId());
return OrderDOConverter.INSTANCE.fromDO(orderDO);
}
}
資料庫操作介面 io.study.order.data.OrderDAO
/**
* 訂單 DAO
* 使用 mybatis-generator 自動生成
*/
@org.apache.ibatis.annotations.Mapper
public interface OrderDAO {
int insertSelective(OrderDO record);
OrderDO selectByPrimaryKey(Long id);
}
資料庫實現類 resources/mapper/OrderDAO.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.study.order.data.OrderDAO">
<resultMap id="BaseResultMap" type="io.study.order.data.OrderDO">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id, name
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long">
select
<include refid="Base_Column_List"/>
from `order`
where id = #{id,jdbcType=BIGINT}
</select>
<insert id="insertSelective" parameterType="io.study.order.data.OrderDO">
insert into `order`
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="name != null">
name,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=BIGINT},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
</trim>
</insert>
</mapper>
資料物件
/**
* 訂單資料庫物件
*/
@Data
public class OrderDO {
/**
* 訂單 ID
*/
private Long id;
/**
* 訂單名稱
*/
private String name;
}
資料物件轉換器 io.study.order.data.OrderDOConverter
/**
* OrderDO 轉換器
*/
@org.mapstruct.Mapper
public interface OrderDOConverter {
OrderDOConverter INSTANCE = Mappers.getMapper(OrderDOConverter.class);
@Mapping(source = "id.id", target = "id")
OrderDO toDO(Order order);
@Mapping(target = "id", expression = "java(OrderId.of(orderDO.getId()))")
void update(OrderDO orderDO, @MappingTarget Order order);
default Order fromDO(OrderDO orderDO) {
Order order = OrderFactory.createOrder();
INSTANCE.update(orderDO, order);
return order;
}
}
在建立實體物件時,需要使用工廠進行建立,這樣才能為實體注入資源庫實現。
使用者介面層程式碼模型
image.png包依賴
<dependencies>
<!-- 領域模組 -->
<dependency>
<groupId>io.study</groupId>
<artifactId>order-domain</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 應用模組 -->
<dependency>
<groupId>io.study</groupId>
<artifactId>order-application</artifactId>
<version>${project.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<scope>provided</scope>
</dependency>
<!-- springboot-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springfox -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
領域驅動(DDD,Domain Driven Design)為軟體設計提供了一套完整的理論指導和落地實踐,通過戰略設計和戰術設計,將技術實現與業務邏輯分離,來應對複雜的軟體系統。本系列文章準備以實戰的角度來介紹 DDD,首先編寫領域驅動的程式碼模型,然後再基於程式碼模型,引入 DDD 的各項概念,先介紹戰術設計,再介紹戰略設計。
> DDD 實戰1 - 基礎程式碼模型
> DDD 實戰2 - 整合限界上下文(Rest & Dubbo)
> DDD 實戰3 - 整合限界上下文(訊息模式)
> DDD 實戰4 - 領域事件的設計與使用
> DDD 實戰5 - 實體與值物件
> DDD 實戰6 - 聚合的設計
> DDD 實戰7 - 領域工廠與領域資源庫
> DDD 實戰8 - 領域服務與應用服務
> DDD 實戰9 - 架構設計
> DDD 實戰10 - 戰略設計