1. 程式人生 > 其它 >領域驅動設計實戰-DDD 領域驅動設計實戰

領域驅動設計實戰-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

本應用的所有流量入口。包括三部分:

  1. web 入口的實現:包括 controller、DTO 定義、DTO 轉化類
  2. 訊息監聽者(消費者):包括 XxxListener
  3. RPC 介面的實現:比如在使用 Dubbo 時,我們的服務需要開放 Dubbo 服務給第三方,此時需要建立單獨的模組包,例如 client 模組,包含 Dubbo 介面和 DTO,在使用者介面層中,去做 client 中介面的實現以及 DTO 轉化類

基礎設施層 infrastructure

本應用的所有流量出口。包括:

  1. 資源庫介面的實現
  2. 資料庫操作介面、資料庫實現(如果使用mybatis,則包含 resource/*.xml)、資料庫物件 DO、DO 轉化類
  3. 中介軟體的實現、檔案系統實現、快取實現、訊息實現 等
  4. 第三方服務介面的實現

基於 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);
}
  1. 資源庫介面放置在領域層,實現領域物件自持久化,同時實現依賴反轉。
  2. 依賴反轉:將依賴關係進行反轉,假設 Order 要做自持久化,那麼需要拿到資源庫的實現 OrderRepositoryImpl 才行,那麼 domain 包就要依賴 infrastructure 包,但是這不符合 外層依賴內層 的原則,所以需要進行依賴反轉,由 infrastructure 包依賴 domain 包。實現依賴反轉的方式就是在被依賴方中新增介面(例如,在 domain 包中新增 OrderRepository 介面),依賴包對介面進行實現(infrastructure 包中對 OrderRepository 進行實現),這樣的好處是,domain 可以完全僅關注業務邏輯,不要關心具體技術細節,不用去關心,到底是儲存到 mysql,還是 oracle,使用的資料庫框架是 mybatis 還是 hibernate,技術細節的實現由 infrastructure 來完成,真正實現了業務邏輯和技術細節的分離
  3. 資源庫的命名推薦:對於資源庫,推薦面向集合進行設計,即資源庫的方法名採用與集合相似的方法名,例如,儲存和更新是 add、addAll,刪除時 remove、removeAll,查詢是 xxxOfccc,例如 orderOfId,ordersOfCondition,複數使用 xxxs 的格式,而不是 xxxList 這樣的格式
  4. 一個聚合具有一個資源庫:比如訂單聚合中,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);
    }
}

工廠的作用:建立聚合。
工廠的好處:

  1. 建立複雜的聚合,簡化客戶端的使用。例如 Order 的建立需要注入資源庫,訂單建立後,可以直接釋出訂單建立事件。
  2. 可讀性好(更加符合通用語言),比如 對於建立訂單,createOrder 就比 new Order 的語義更加明確
  3. 更好的保證一致性,防止出錯,假設建立兩個主訂單 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 為空");
        }
    }
}
  1. 推薦使用強型別的物件作為實體的唯一標識,好處有兩個:
    a. 用來避免傳參混亂,同時提升介面的可讀性,例如 xxx(Long orderId, Long goodsId),假設上述介面第一個引數傳了 goodsId,第二個傳了 orderId,那麼編譯期是無法發現的,改為 xxx(OrderId orderId, GoodsId, goodsId) 即可避免,同時可讀性也較高。
    b. 唯一標識中會有一些其他行為方法,如果唯一標識使用弱型別,那麼這些行為方法將會洩露在實體中
  2. 唯一標識類是一個值物件,推薦值物件設定為不可變物件,使用 @lombok.Value 標註值物件,既可標識該物件為值物件,也可以是該類變為不可變類。例如,表示後的 OrderId 沒有 setXxx 方法。
  3. 值物件的行為函式都是無副作用函式(即不能影響值物件本身的狀態,例如 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;
    }
}
  1. 聚合根是一個特殊的實體,是整個聚合對外的使者,其他聚合與改聚合溝通的方式只能是通過聚合根
  2. 由於使用工廠來建立 Order,那麼 Order 的構造器需要設定為 protected,防止外界直接使用進行建立
  3. 實體單個屬性的校驗需要在 setXxx 中完成自校驗
  4. 實體是可變的、具有唯一標識,其唯一標識通常需要設計成強型別
  5. 聚合中的 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>
  1. mapstruct 用於實現模型對映器,關於其使用見 https://www.jianshu.com/p/53aac78e7d60
  2. 資料儲存採用 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 - 戰略設計