迄今爲止最完整的 DDD 實踐

對於一個架構師來說,在軟件開發中如何降低系統複雜度是一個永恆的挑戰。

01 爲什麼需要 DDD

02 DDD 的價值

03 DDD 架構

3.1 分層架構

3.2 六邊形架構

3.3 調用鏈路

04 DDD 的基本概念

4.1 領域模型

✪ 4.1.1 核心域

決定產品和公司核心競爭力的子域是核心域,它是業務成功的主要因素和公司的核心競爭力。直接對業務產生價值。

✪ 4.1.2 通用域

沒有太多個性化的訴求,同時被多個子域使用的通用功能子域是通用域。例如,權限,登陸等等。間接對業務產生價值。

✪ 4.1.3 支撐域

支撐其他領域業務,具有企業特性,但不具有通用性。間接對業務產生價值。

✪ 4.1.4 爲什麼要劃分核心域、通用域和支撐域

一個業務一定有他最重要的部分,在日常做業務判斷和需求優先級判斷的時候可以基於這個劃分來做決策。例如:一個交易相關的需求和一個配置相關的需求排優先級,很明顯交易是核心域,規則是支持域。同樣我們認爲是支撐域或者通用域的在其他公司可能是核心域,例如權限對於我們來說是通用域,但是對於專業做權限系統的公司,這個是核心域。

4.2 限界上下文(戰略)

業務的邊界的劃分,這個邊界可以是一個領域或者多個領域的集合。複雜業務需要多個域編排完成一個複雜業務流程。限界上下文可以作爲微服務劃分的方法。其本質還是高內聚低耦合,只是限界上下文只是站在更高的層面來進行劃分。如何進行劃分,我的方法是一個界限上下文必須支持一個完整的業務流程,保證這個業務流程所涉及的領域都在一個限界上下文中。

4.3 實體(ENTITY)

定義: 實體有唯一的標識,有生命週期且具有延續性。例如一個交易訂單,從創建訂單我們會給他一個訂單編號並且是唯一的這就是實體唯一標識。同時訂單實體會從創建,支付,發貨等過程最終走到終態這就是實體的生命週期。訂單實體在這個過程中屬性發生了變化,但訂單還是那個訂單,不會因爲屬性的變化而變化,這就是實體的延續性。

實體的業務形態: 實體能夠反映業務的真實形態,實體是從用例提取出來的。領域模型中的實體是多個屬性、操作或行爲的載體。

實體的代碼形態: 我們要保證實體代碼形態與業務形態的一致性。那麼實體的代碼應該也有屬性和行爲,也就是我們說的充血模型,但實際情況下我們使用的是貧血模型。貧血模型缺點是業務邏輯分散,更像數據庫模型,充血模型能夠反映業務,但過重依賴數據庫操作,而且複雜場景下需要編排領域服務,會導致事務過長,影響性能。所以我們使用充血模型,但行爲裏面只涉及業務邏輯的內存操作。

實體的運行形態: 實體有唯一 ID,當我們在流程中對實體屬性進行修改,但 ID 不會變,實體還是那個實體。

實體的數據庫形態: 實體在映射數據庫模型時,一般是一對一,也有一對多的情況。

4.4 值對象(VALUEOBJECT)

定義: 通過對象屬性值來識別的對象,它將多個相關屬性組合爲一個概念整體。在 DDD 中用來描述領域的特定方面,並且是一個沒有標識符的對象,叫作值對象。值對象沒有唯一標識,沒有生命週期,不可修改,當值對象發生改變時只能替換(例如 String 的實現)

值對象的業務形態: 值對象是描述實體的特徵,大多數情況一個實體有很多屬性,一般都是平鋪,這些數據進行分類和聚合後能夠表達一個業務含義,方便溝通而不關注細節。

值對象的代碼形態: 實體的單一屬性是值對象,例如:字符串,整型,枚舉。多個屬性的集合也是值對象,這個時候我們把這個集合設計爲一個 CLASS,但沒有 ID。例如商品實體下的航段就是一個值對象。航段是描述商品的特徵,航段不需要 ID,可以直接整體替換。商品爲什麼是一個實體,而不是描述訂單特徵,因爲需要表達誰買了什麼商品,所以我們需要知道哪一個商品,因此需要 ID 來標識唯一性。

我們看一下下面這段代碼,person 這個實體有若干個單一屬性的值對象,比如 Id、name 等屬性;同時它也包含多個屬性的值對象,比如地址 address。

值對象的運行形態: 值對象創建後就不允許修改了,只能用另外一個值對象來整體替換。當我們修改地址時,從頁面傳入一個新的地址對象替換調用 person 對象的地址即可。如果我們把 address 設計成實體,必然存在 ID,那麼我們需要從頁面傳入的地址對象的 ID 與 person 裏面的地址對像的 ID 進行比較,如果相同就更新,如果不同先刪除數據庫在新增數據。

值對象的數據庫形態: 有兩種方式嵌入式和序列化大對象。

案例 1:以屬性嵌入的方式形成的人員實體對象,地址值對象直接以屬性值嵌入人員實體中。

當我們只有一個地址的時候使用嵌入式比較好,如果多個地址必須有序列化大對象,同時可以支持搜索。

案例 2:以序列化大對象的方式形成的人員實體對象,地址值對象被序列化成大對象 Json 串後,嵌入人員實體中。

支持多個地址存儲,不支持搜索。

值對象的優勢和侷限:

  1. 簡化數據庫設計,提升數據庫操作的性能(多表新增和修改,關聯表查詢)。

  2. 雖然簡化數據庫設計,但是領域模型還是可以表達業務。

  3. 序列化的方式會使搜索實現困難(通過搜索引擎可以解決)。

4.5 聚合和聚合根

多個實體和值對象組成的我們叫聚合,聚合的內部一定的高內聚。這個聚合裏面一定有一個實體是聚合根。

聚合與領域的關係:聚合也是範圍的劃分,領域也是範圍的劃分。領域與聚合可以是一對一,也可以是一對多的關係

聚合根的作用是保證內部的實體的一致性,對外只需要對聚合根進行操作。

4.6 限界上下文,域,聚合,實體,值對象的關係

領域包含限界上下文,限界上下文包含子域,子域包含聚合,聚合包含實體和值對象

4.7 事件風暴

參與者

除了領域專家,事件風暴的其他參與者可以是 DDD 專家、架構師、產品經理、項目經理、開發人員和測試人員等項目團隊成員

事件風暴準備的材料

一面牆和一支筆。

事件風暴的關注點

在領域建模的過程中,我們需要重點關注這類業務的語言和行爲。比如某些業務動作或行爲(事件)是否會觸發下一個業務動作,這個動作(事件)的輸入和輸出是什麼?是誰(實體)發出的什麼動作(命令),觸發了這個動作(事件)… 我們可以從這些暗藏的詞彙中,分析出領域模型中的事件、命令和實體等領域對象。

實體執行命令產生事件。

業務場景的分析

通過業務場景和用例找出實體,命令,事件。

領域建模

領域建模時,我們會根據場景分析過程中產生的領域對象,比如命令、事件等之間關係,找出產生命令的實體,分析實體之間的依賴關係組成聚合,爲聚合劃定限界上下文,建立領域模型以及模型之間的依賴。領域模型利用限界上下文向上可以指導微服務設計,通過聚合向下可以指導聚合根、實體和值對象的設計。

05 如何建模

5.1 協同單自動化分單案例

✪ 5.1.1 領域建模

需求:我們需要把系統自動化失敗轉人工訂單自動分配給小二,避免人工挑單和搶單,通過自動分配提升整體履約處理效率。

✪ 5.1.2 領域劃分

溝通的過程就是推導和驗證模型的過程,最後進行域的劃分:

✪ 5.1.3 場景梳理

窮舉所有場景,重新驗證模型是否可以覆蓋所有場景。

wTJmZy

06 怎麼寫代碼

6.1 DDD 規範

每一層都定義了相應的接口主要目的是規範代碼:

/**
 * 實體屬性,update-tracing
 * @param <T>
 */
public final class Field<T> implements Changeable {
    private boolean changed = false;
    private T value;
    private Field(T value){
        this.value = value;
    }
    public void setValue(T value){
        if(!equalsValue(value)){
            this.changed = true;
        }
        this.value = value;
    }
    @Override
    public boolean isChanged() {
        return changed;
    }
    public T getValue() {
        return value;
    }
    public boolean equalsValue(T value){
        if(this.value == null && value == null){
            return true;
        }
        if(this.value == null){
            return false;
        }
        if(value == null){
            return false;
        }
        return this.value.equals(value);
    }
    public static <T> Field<T> build(T value){
        return new Field<T>(value);
    }
}

6.2 工程結構

✪ 6.2.1 application 模塊

✪ 6.2.2 domain 模塊

✪ 6.2.3 infrastructurre 模塊

所有技術代碼在這一層。mybatis,redis,mq,job,opensearch 代碼都在這裏實現,domain 通過依賴倒置不依賴這些技術代碼和 JAR。

✪ 6.2.4 client 模塊

對外提供服務

✪ 6.2.5 model 模塊

內外都要用的共享對象

6.3 代碼示例

✪ 6.3.1 application 示例

public interface CaseAppFacade extends ApplicationCmdService {
    /**
     * 接手協同單
     * @param handleCaseDto
     * @return
     */
    ResultDO<Void> handle(HandleCaseDto handleCaseDto);
}
public class CaseAppImpl implements CaseAppFacade {
    @Resource
    private CaseService caseService;//域服務
    @Resource
    CaseAssembler caseAssembler;//DTO轉Param
    @Override
    public ResultDO<Void> handle(HandleCaseDto handleCaseDto) {
        try {
            ResultDO<Void> resultDO = caseService.handle(caseAssembler.from(handleCaseDto));
            if (resultDO.isSuccess()) {
                pushMsg(handleCaseDto.getId());
                return ResultDO.buildSuccessResult(null);
            }
            return ResultDO.buildFailResult(resultDO.getMsg());
        } catch (Exception e) {
            return ResultDO.buildFailResult(e.getMessage());
        }
    }
}

✪ 6.3.2 domainService 示例

public interface CaseService extends DomainService {
    /**
     * 接手協同單
     *
     * @param handleParam
     * @return
     */
    ResultDO<Void> handle(HandleParam handleParam);
}
public class CaseServiceImpl implements CaseService {
    @Resource
  private CoordinationRepository coordinationRepository;
    @Override
    public ResultDO<Void> handle(HandleParam handleParam) {
        SyncLock lock = null;
        try {
            lock = coordinationRepository.syncLock(handleParam.getId().toString());
            if (null == lock) {
                return ResultDO.buildFailResult("協同單handle加鎖失敗");
            }
            CaseAggregate caseAggregate = coordinationRepository.query(handleParam.getId());
            caseAggregate.handle(handleParam.getFollowerValue());
            coordinationRepository.save(caseAggregate);
            return ResultDO.buildSuccessResult(null);
        } catch (RepositoryException | AggregateException e) {
            String msg = LOG.error4Tracer(OpLogConstant.traceId(handleParam.getId()), e, "協同單handle異常");
            return ResultDO.buildFailResult(msg);
        } finally {
            if (null != lock) {
                coordinationRepository.unlock(lock);
            }
        }
    }
}

✪ 6.3.3 Aggregate,Entity 示例

public class CaseAggregate extends BaseAggregate implements NoticeMsgBuilder {
    private final CaseEntity caseEntity;
    public CaseAggregate(CaseEntity caseEntity) {
        this.caseEntity = caseEntity;
    }
    /**
     * 接手協同單
     * @param followerValue
     * @return
     */
    public void handle(FollowerValue followerValue) throws AggregateException {
        try {
            this.caseEntity.handle(followerValue);
        } catch (Exception e) {
            throw e;
        }
    }
}
public class CaseEntity extends BaseEntity {
    /**
     * 創建時間
     */
    private Field<Date> gmtCreate;
    /**
     * 修改時間
     */
    private Field<Date> gmtModified;
    /**
     * 問題分類
     */
    private Field<Long> caseType;
    /**
     * 是否需要支付
     */
    private Field<Boolean> needPayFlag;
    /**
     * 是否需要自動驗收通過協同單
     */
    private Field<Integer> autoAcceptCoordinationFlag;
    /**
     * 發起協同人值對象
     */
    private Field<CreatorValue> creatorValue;
    /**
     * 跟進人
     */
    private Field<FollowerValue> followerValue;
    /**
     * 狀態
     */
    private Field<CaseStatusEnum> status;
    /**
     * 關聯協同單id
     */
    private Field<String> relatedCaseId;
    /**
     * 關聯協同單類型
     * @see 讀配置 com.alitrip.agent.business.flight.common.model.dataobject.CoordinationCaseTypeDO
     */
    private Field<String> relatedBizType;
    /**
     * 支付狀態
     */
    private Field<PayStatusEnum> payStatus;
    省略....
    public CaseFeatureValue getCaseFeatureValue() {
        return get(caseFeatureValue);
    }
    public Boolean isCaseFeatureValueChanged() {
        return caseFeatureValue.isChanged();
    }
    public void setCaseFeatureValue(CaseFeatureValue caseFeatureValue) {
        this.caseFeatureValue = set(this.caseFeatureValue, caseFeatureValue);
    }
    public Boolean isPayStatusChanged() {
        return payStatus.isChanged();
    }
    public Boolean isGmtCreateChanged() {
        return gmtCreate.isChanged();
    }
    public Boolean isGmtModifiedChanged() {
        return gmtModified.isChanged();
    }
    public Boolean isCaseTypeChanged() {
        return caseType.isChanged();
    }
    省略....
    /**
    * 接手
    */
    public void handle(FollowerValue followerValue) throws AggregateException {
        if (isWaitProcess()||isAppointProcess()) {
            this.setFollowerValue(followerValue);
            this.setStatus(CaseStatusEnum.PROCESSING);
            this.setGmtModified(new Date());
            initCaseRecordValue(CaseActionNameEnum.HANDLE, null, followerValue);
        } else {
            throwStatusAggregateException();
        }
    }
    省略....
}

聚合根的 reconProcess 的方法的業務邏輯被 reconHandler 和 reconRiskHandler 處理,必然這些 handler 要訪問聚合根裏面的實體的屬性,那麼邏輯就會散落。修改後:

沒有引入其他概念,都是在聚合根裏面組織實體完成具體業務邏輯,去掉了 handler 這種技術語言。

修改了 mapstruct 生成轉換代碼的源碼,修改後生成的代碼:

if(caseEntity.isAppended() || caseEntity.isCaseTypeChanged()){
    casePO.setCaseType( caseEntity.getCaseType() );
}

當屬性被改變後就轉換到 po 中,這樣就可以實現修改後的字段更新。

✪ 6.3.4 Repository 示例

public interface CoordinationRepository extends Repository {  
  /**
     * 保存/更新
     * @param aggregate
     * @throws RepositoryException
     */
   void save(CaseAggregate aggregate) throws RepositoryException;
}
@Repository
public class CoordinationRepositoryImpl implements CoordinationRepository {
  @Override
    public void save(CaseAggregate aggregate) throws RepositoryException {
        try {
            //聚合根轉PO,update-tracing技術
            CasePO casePO = caseConverter.toCasePO(aggregate.getCase());
            CasePO oldCasePO = null;
            if (aggregate.getCase().isAppended()) {
                casePOMapper.insert(casePO);
                aggregate.getCase().setId(casePO.getId());
            } else {
                oldCasePO = casePOMapper.selectByPrimaryKey(casePO.getId());
                casePOMapper.updateByPrimaryKeySelective(casePO);
            }
            // 發送協同單狀態改變消息
            if (CaseStatusEnum.FINISH.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.WAIT_DISTRIBUTION.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.PROCESSING.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.APPOINT_PROCESS.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.WAIT_PROCESS.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.CLOSE.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.REJECT.getCode().equals(casePO.getStatus())
                || CaseStatusEnum.PENDING_ACCEPTANCE.getCode().equals(casePO.getStatus())) {
                FollowerDto followerDto = new FollowerDto();
                followerDto.setCurrentFollowerId(aggregate.getCase().getFollowerValue().getCurrentFollowerId());
                followerDto.setCurrentFollowerGroupId(aggregate.getCase().getFollowerValue().getCurrentFollowerGroupId());
                followerDto.setCurrentFollowerType(aggregate.getCase().getFollowerValue().getCurrentFollowerType());
                followerDto.setCurrentFollowerName(aggregate.getCase().getFollowerValue().getCurrentFollowerName());
                //拒絕和關閉都使用CLOSE
                String tag = CaseStatusEnum.codeOf(casePO.getStatus()).name();
                if(CaseStatusEnum.REJECT.name().equals(tag)){
                    tag = CaseStatusEnum.CLOSE.name();
                }
                statusChangeProducer.send(CaseStatusChangeEvent.build()
                    .setId(casePO.getId())
                    .setFollowerDto(followerDto)
                    .setStatus(aggregate.getCase().getStatus().getCode())
                    .setCaseType(aggregate.getCase().getCaseType())
                    .setOldStatus(null != oldCasePO ? oldCasePO.getStatus() : null)
                    .setAppointTime(aggregate.getCase().getAppointTime()), (tag));
            }
            // 操作日誌
            if (CollectionUtils.isNotEmpty(aggregate.getCase().getCaseRecordValue())) {
                CaseRecordValue caseRecordValue = Lists.newArrayList(aggregate.getCase().getCaseRecordValue()).get(0);
                caseRecordValue.setCaseId(casePO.getId());
                recordPOMapper.insert(caseConverter.from(caseRecordValue));
            }
        } catch (Exception e) {
            throw new RepositoryException("", e.getMessage(), e);
        }
    }
}

07 最後結束語

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/y6H8UG-g829o0V0EBeEwrw