DDD分層架構最佳實踐

還在單體應用的時候就是分層架構一說,我們用得最多的就是三層架構。而現(xiàn)在已經(jīng)是微服務時代,在微服務架構模型比較常用的有幾個,例如:整潔架構,CQRS(命令查詢分離)以及六邊形架構。每種架構模型都有自己的應用場景,但其核心都是“高內(nèi)聚低耦合”原則。而運用領域驅動設計(DDD)理念以應對日常加速的業(yè)務變化對架構的影響,架構的邊界越業(yè)越清晰,各施其職,這也符合微服務架構的設計思想。以領域驅動設計(DDD)為理念的分層架構已經(jīng)成為微服務架構實踐的最佳實踐方法。

一、什么是DDD分層架構

1. 傳統(tǒng)三層架構

要了解DDD分層架構,首頁先了解傳統(tǒng)的三層架構。

image

傳統(tǒng)三層架構流程:

  • 第一步考慮的是數(shù)據(jù)庫設計,數(shù)據(jù)表如何建,表之間的關系如何設計
  • 第二步就是搭建數(shù)據(jù)訪問層,如選一個ORM框架或者拼接SQL操作
  • 第三步就是業(yè)務邏輯的實現(xiàn),由于我們先設計了數(shù)據(jù)庫,我們整個的思考都會圍繞著數(shù)據(jù)庫,想著怎么寫才能把數(shù)據(jù)正確地寫入數(shù)據(jù)庫中,這時CRUD的標準作法就出現(xiàn)了,也就沒有太多考慮面向對象,解耦的事情了,這樣的代碼對日常的維護自然是越來越困難的
  • 第四步表示層主要面向用戶的輸出

2. DDD分層架構

image

為了解決高耦合問題并輕松應對以后的系統(tǒng)變化,我們提出了運用領域驅動設計的理念來設計架構。

此段落部分總結來源于歐創(chuàng)新《DDD實踐課》的《07 | DDD分層架構:有效降低層與層之間的依賴》讀后感

1)領域層

首先我們拋開數(shù)據(jù)庫的困擾,先從業(yè)務邏輯入手開始,設計時不再考慮數(shù)據(jù)庫的實現(xiàn)。將以前的業(yè)務邏輯層(BLL)拆分成了領域層應用層。

領域層聚焦業(yè)務對象的業(yè)務邏輯實現(xiàn),體現(xiàn)現(xiàn)實世界業(yè)務的邏輯變化。它用來表達業(yè)務概念、業(yè)務狀態(tài)和業(yè)務規(guī)則,對于業(yè)務分析可參照:《使用領域驅動設計分析業(yè)務

2)應用層

應用層是領域層的上層,依賴領域層,是各聚合的協(xié)調(diào)和編排,原則上是不包括任何業(yè)務邏輯。它以較粗粒度的封閉為前端接口提供支持。除了提供上層調(diào)用外,還可以包括事件和消息的訂閱。

3) 用戶接口層

用戶接口層面向用戶訪問的數(shù)據(jù)入向接口,可按不同場景提供不一樣的用戶接口實現(xiàn)。面向Web的可使用http restful的方式提供服務,可增加安全認證、權限校驗,日志記錄等功能;面向微服務的可使用RPC方式提供服務,可增加限流、熔斷等功能。

4) 基礎設施層

基礎設施層是數(shù)據(jù)的出向接口,封裝數(shù)據(jù)調(diào)用的技術細節(jié)。可為其它任意層提供服務,但為了解決耦合的問題采用了依賴倒置原則。其它層只依賴基礎設施的接口,于具體實現(xiàn)進行分離。

二、DDD分層代碼實現(xiàn)

1. 結構模型

image

2. 目錄結構

.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── fun
    │   │       └── barryhome
    │   │           └── ddd
    │   │               ├── WalletApplication.java
    │   │               ├── application
    │   │               │   ├── TradeEventProcessor.java
    │   │               │   ├── TradeMQReceiver.java
    │   │               │   └── TradeManager.java
    │   │               ├── constant
    │   │               │   └── MessageConstant.java
    │   │               ├── controller
    │   │               │   ├── TradeController.java
    │   │               │   ├── WalletController.java
    │   │               │   └── dto
    │   │               │       └── TradeDTO.java
    │   │               ├── domain
    │   │               │   ├── TradeService.java
    │   │               │   ├── TradeServiceImpl.java
    │   │               │   ├── enums
    │   │               │   │   ├── InOutFlag.java
    │   │               │   │   ├── TradeStatus.java
    │   │               │   │   ├── TradeType.java
    │   │               │   │   └── WalletStatus.java
    │   │               │   ├── event
    │   │               │   │   └── TradeEvent.java
    │   │               │   ├── model
    │   │               │   │   ├── BaseEntity.java
    │   │               │   │   ├── TradeRecord.java
    │   │               │   │   └── Wallet.java
    │   │               │   └── repository
    │   │               │       ├── TradeRepository.java
    │   │               │       └── WalletRepository.java
    │   │               └── infrastructure
    │   │                   ├── TradeRepositoryImpl.java
    │   │                   ├── WalletRepositoryImpl.java
    │   │                   ├── cache
    │   │                   │   └── Redis.java
    │   │                   ├── client
    │   │                   │   ├── AuthFeignClient.java
    │   │                   │   └── LocalAuthClient.java
    │   │                   ├── jpa
    │   │                   │   ├── JpaTradeRepository.java
    │   │                   │   └── JpaWalletRepository.java
    │   │                   └── mq
    │   │                       └── RabbitMQSender.java
    │   └── resources
    │       ├── application.properties
    │       └── rabbitmq-spring.xml
    └── test
        └── java



此結構為單一微服務的簡單結構,各層在同一個模塊中。

在大型項目開發(fā)過程中,為了達到核心模塊的權限控制或更好的靈活性可適當調(diào)整結構,可參考《 數(shù)字錢包系統(tǒng)》系統(tǒng)結構

3. 領域層實現(xiàn)(domain)

在業(yè)務分析(《使用領域驅動設計分析業(yè)務》)之后,開始編寫代碼,首先就是寫領域層,創(chuàng)建領域對象領域服務接口

1)領域對象

這里的領域對象包括實體對象、值對象。

實體對象:具有唯一標識,能單獨存在且可變化的對象

值對象:不能單獨存在或在邏輯層面單獨存在無意義,且不可變化的對象

聚合:多個對象的集合,對外是一個整體

聚合根:聚合中可代表整個業(yè)務操作的實體對象,通過它提供對外訪問操作,它維護聚合內(nèi)部的數(shù)據(jù)一致性,它是聚合中對象的管理者

代碼示例:

// 交易
public class TradeRecord extends BaseEntity {
    /**
     * 交易號
     */
    @Column(unique = true)
    private String tradeNumber;
    /**
     * 交易金額
     */
    private BigDecimal tradeAmount;
    /**
     * 交易類型
     */
    @Enumerated(EnumType.STRING)
    private TradeType tradeType;
    /**
     * 交易余額
     */
    private BigDecimal balance;
    /**
     * 錢包
     */
    @ManyToOne
    private Wallet wallet;

    /**
     * 交易狀態(tài)
     */
    @Enumerated(EnumType.STRING)
    private TradeStatus tradeStatus;

    @DomainEvents
    public List<Object> domainEvents() {
        return Collections.singletonList(new TradeEvent(this));
    }
}

// 錢包
public class Wallet extends BaseEntity {

    /**
     * 錢包ID
     */
    @Id
    private String walletId;
    /**
     * 密碼
     */
    private String password;
    /**
     * 狀態(tài)
     */
    @Enumerated(EnumType.STRING)
    private WalletStatus walletStatus = WalletStatus.AVAILABLE;
    /**
     * 用戶Id
     */
    private Integer userId;
    /**
     * 余額
     */
    private BigDecimal balance = BigDecimal.ZERO;

}



  • 錢包交易例子的系統(tǒng)設計中,錢包的任何操作如:充值、消息等都是通過交易對象驅動錢包余額的變化

  • 交易對象錢包對象均為實體對象且組成聚合關系,交易對象是錢包交易業(yè)務模型的聚合根,代表聚合向外提供調(diào)用服務

  • 經(jīng)過分析交易對象錢包對象為1對多關系(@ManyToOne),這里采用了JPAORM架構,更多JPA實踐請參考>>

  • 這里的領域建模使用的是貧血模型,結構簡單,職責單一,相互隔離性好但缺乏面向對象設計思想,關于領域建模可參考《領域建模的貧血模型與充血模型

  • domainEvents()為領域事件發(fā)布的一種實現(xiàn),作用是交易對象任何的數(shù)據(jù)操作都將觸發(fā)事件的發(fā)布,再配合事件訂閱實現(xiàn)事件驅動設計模型,當然也可以有別的實現(xiàn)方式

2)領域服務

/**
 * Created on 2020/9/7 11:40 上午
 *
 * @author barry
 * Description: 交易服務
 */
public interface TradeService {

    /**
     * 充值
     *
     * @param tradeRecord
     * @return
     */
    TradeRecord recharge(TradeRecord tradeRecord);

    /**
     * 消費
     *
     * @param tradeRecord
     * @return
     */
    TradeRecord consume(TradeRecord tradeRecord);
}



  • 先定義服務接口,接口的定義需要遵循現(xiàn)實業(yè)務的操作,切勿以程序邏輯或數(shù)據(jù)庫邏輯來設計定義出增刪改查
  • 主要的思考方向是交易對象對外可提供哪些服務,這種服務的定義是粗粒度高內(nèi)聚的,切勿將某些具體代碼實現(xiàn)層面的方法定義出來
  • 接口的輸入輸出參數(shù)盡量考慮以對象的形式,充分兼容各種場景變化
  • 關于前端需要的復雜查詢方法可不在此定義,一般情況下查詢并非是一種領域服務且沒有數(shù)據(jù)變化,可單獨處理
  • 領域服務的實現(xiàn)主要關注邏輯實現(xiàn),切勿包含技術基礎類代碼,比如緩存實現(xiàn),數(shù)據(jù)庫實現(xiàn),遠程調(diào)用等

3)基礎設施接口

public interface TradeRepository {
    /**
     * 保存
     * @param tradeRecord
     * @return
     */
    TradeRecord save(TradeRecord tradeRecord);

    /**
     * 查詢訂單
     * @param tradeNumber
     * @return
     */
    TradeRecord findByTradeNumber(String tradeNumber);

    /**
     * 發(fā)送MQ事件消息
     * @param tradeEvent
     */
    void sendMQEvent(TradeEvent tradeEvent);

    /**
     * 獲取所有
     * @return
     */
    List<TradeRecord> findAll();
}



  • 基礎設施接口放在領域層主要的目的是減少領域層對基礎設施層的依賴
  • 接口的設計是不可暴露實現(xiàn)的技術細節(jié),如不能將拼裝的SQL作為參數(shù)

4. 應用層實現(xiàn)(application)

// 交易服務
@Component
public class TradeManager {

    private final TradeService tradeService;
    public TradeManager(TradeService tradeService) {
        this.tradeService = tradeService;
    }


    // 充值
    @Transactional(rollbackFor = Exception.class)
    public TradeRecord recharge(TradeRecord tradeRecord) {
        return tradeService.recharge(tradeRecord);
    }


     // 消費
    @Transactional(rollbackFor = Exception.class)
    public TradeRecord consume(TradeRecord tradeRecord) {
        return tradeService.consume(tradeRecord);
    }
}

// 交易事件訂閱
@Component
public class TradeEventProcessor {

    @Autowired
    private TradeRepository tradeRepository;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, condition = "# tradeEvent.tradeStatus.name() == 'SUCCEED'")
    public void TradeSucceed(TradeEvent tradeEvent) {
        tradeRepository.sendMQEvent(tradeEvent);
    }
}

// 交易消息訂閱
@Component
public class TradeMQReceiver {

    @RabbitListener(queues = "ddd-trade-succeed")
    public void receiveTradeMessage(TradeEvent tradeEvent){
        System.err.println("========MQ Receiver============");
        System.err.println(tradeEvent);
    }
}

應用服務

  • 應用層是很薄的一層,主要用于調(diào)用和組合領域服務,切勿包含任何業(yè)務邏輯
  • 可包括少量的流程參數(shù)判斷
  • 由于可能是多個領域服務組合操作調(diào)用,如果存在原子性要求可以增加@Transactional事務控制

事件訂閱

  • 事件訂閱是進程內(nèi)多個領域操作協(xié)作解耦的一種實現(xiàn)方式,它也是進程內(nèi)所有后續(xù)操作的接入口
  • 它與應用服務的組合操作用途不一樣,組合是根據(jù)場景需求可增可減,但事件訂閱后的操作是相對固化的,主要是滿足邏輯的一致性要求
  • TransactionPhase.AFTER_COMMIT配置是在前一操作事務完成后再調(diào)用,從而減少后續(xù)操作對前操作的影響
  • 事件訂閱可能會有多個消息主體,為了方便管理最好統(tǒng)一在一個類里處理
  • MQ消息發(fā)布一般放在事件訂閱中

消息訂閱

  • 消息訂閱是多個微服務間協(xié)作解耦的異步實現(xiàn)方式
  • 消息體盡量以統(tǒng)一的對象包裝進行傳遞,降低對象異構帶來的處理難度

5. 基礎設施層(infrastructure)

@Repository
public class TradeRepositoryImpl implements TradeRepository {

    private final JpaTradeRepository jpaTradeRepository;
    private final RabbitMQSender rabbitMQSender;
    private final Redis redis;

    public TradeRepositoryImpl(JpaTradeRepository jpaTradeRepository, RabbitMQSender rabbitMQSender, Redis redis) {
        this.jpaTradeRepository = jpaTradeRepository;
        this.rabbitMQSender = rabbitMQSender;
        this.redis = redis;
    }

    @Override
    public TradeRecord save(TradeRecord tradeRecord) {
        return jpaTradeRepository.save(tradeRecord);
    }

    /**
     * 查詢訂單
     */
    @Override
    public TradeRecord findByTradeNumber(String tradeNumber) {
        TradeRecord tradeRecord = redis.getTrade(tradeNumber);
        if (tradeRecord == null){
            tradeRecord = jpaTradeRepository.findFirstByTradeNumber(tradeNumber);
            // 緩存
            redis.cacheTrade(tradeRecord);
        }

        return tradeRecord;
    }

    /**
     * 發(fā)送事件消息
     * @param tradeEvent
     */
    @Override
    public void sendMQEvent(TradeEvent tradeEvent) {
        // 發(fā)送消息
        rabbitMQSender.sendMQTradeEvent(tradeEvent);
    }

    /**
     * 獲取所有
     */
    @Override
    public List<TradeRecord> findAll() {
        return jpaTradeRepository.findAll();
    }
}

  • 基礎設施層是數(shù)據(jù)的輸出向,主要包含數(shù)據(jù)庫、緩存、消息隊列、遠程訪問等的技術實現(xiàn)

  • 基礎設計層對外隱藏技術實現(xiàn)細節(jié),提供粗粒度的數(shù)據(jù)輸出服務

  • 數(shù)據(jù)庫操作:領域層傳遞的是數(shù)據(jù)對象,在這里可以按數(shù)據(jù)表的實現(xiàn)方式進行拆分實現(xiàn)

6. 用戶接口層(controller)

@RequestMapping("/trade")
@RestController
public class TradeController {

    @Autowired
    private TradeManager tradeManager;

    @Autowired
    private TradeRepository tradeRepository;

    @PostMapping(path = "/recharge")
    public TradeDTO recharge(@RequestBody TradeDTO tradeDTO) {
        return TradeDTO.toDto(tradeManager.recharge(tradeDTO.toEntity()));
    }

    @PostMapping(path = "/consume")
    public TradeDTO consume(@RequestBody TradeDTO tradeDTO) {
        return TradeDTO.toDto(tradeManager.consume(tradeDTO.toEntity()));
    }

    @GetMapping(path = "/{tradeNumber}")
    public TradeDTO findByTradeNumber(@PathVariable("tradeNumber") String tradeNumber){
        return TradeDTO.toDto(tradeRepository.findByTradeNumber(tradeNumber));
    }

}

  • 用戶接口層面向終端提供服務支持
  • 可根據(jù)不同的場景單獨一個模塊,面向Web提供http restful,面向服務間API調(diào)用提供RPG支持
  • 為Web端提供身份認證和權限驗證服務,VO數(shù)據(jù)轉換
  • 為API端提供限流和熔斷服務,DTO數(shù)據(jù)轉換
  • 將數(shù)據(jù)轉換從應用層提到用戶接口層更方便不同場景之前的需求變化,同時也保證應用層數(shù)據(jù)格式的統(tǒng)一性

7. 復雜數(shù)據(jù)查詢

以上可見并沒有涉及復雜數(shù)據(jù)查詢問題,此問題不涉及業(yè)務邏輯處理所以不應該放在領域層處理。

  • 如果復雜數(shù)據(jù)查詢需求較多可采用CQRS模式,將查詢單獨一個模塊處理。如果較少可由基礎設施層做數(shù)據(jù)查詢,應用層做數(shù)據(jù)封裝,用戶接口層做數(shù)據(jù)調(diào)用
  • JPA不太適合做多表關聯(lián)的數(shù)據(jù)庫查詢操作,可使用其它的靈活性較高的ORM架構
  • 在大數(shù)據(jù)大并發(fā)情況下,多表關聯(lián)會嚴重影響數(shù)據(jù)庫性能,可以考慮做寬表查詢

三、綜述

DDD分層主要解決各層之間耦合度問題,做到各層各施其職互不影響。各層中領域層的設計是整個系統(tǒng)的中樞,最能體現(xiàn)領域驅動設計的核心思想。它的良好設計是保證往后架構的可持續(xù)性,可維護性。

四、源代碼

文中代碼由于篇幅原因有一定省略并不是完整邏輯,如有興趣請Fork源代碼 https://gitee.com/hypier/barry-ddd

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容