戲說領域驅動設計(十五)——核心元素

SKevin發表於2022-03-17

  前面細講了基於CQS的4層架構,其中的領域模型層也就是六邊型架構中的核心在整個開發流程中工作佔比最大,也是需要工程師最需要關注地方。那麼話說回來了,裡面到底包含了什麼東西需要投入如此高的關注度。答案還用說?必然是領域模型啊,比如實體、值型別、業務服務等,您別忘了我們們講的是領域驅動設計。具體可參看如下圖所示的領域模型層(後續簡稱BO層)中的元素。這裡面東西較多,基乎每一種都可以開一章來講,也就是可以水好多的文字。

戲說領域驅動設計(十五)——核心元素

 一、業務模型層中的特色元素

1、業務異常

  BO層中元素比較多,但這裡面最具特色應該是“業務異常”。您說把領域模型、領域服務歸結為BO中的元素這本是無可厚非的,因為它們本來面向的就是業務實體、業務邏輯。把異常也歸結為BO中的元素是幾個意思呢?而且,書上都沒這麼講過,我這是不是故意的譁眾取寵騙流量?這話不能這麼說,書上沒講過的東西多著呢,人家寫書的時候都會站在一定的前提之上,比如讀者應該會一門開發語言,應該做過實際的專案,應該有具備哪些基本知識等。我們這種部落格什麼樣的受眾都有,內容本身也是個人經驗的總結,有點特殊的東西很正常,而且我不僅要講書上沒有的東西,還會講得很細,您就踏實的看吧。

  回到業務異常這個事情上來,在軟體的分析設計過程中,那些有形的物體(一般是名詞)可以建成實體,比如訂單、賬戶、商品等;還有一些是無形的但在需求過程中也隱晦的提到了,最典型的就是“事件”,這是對動詞進行建模的經典案例。而異常則是典型的、經常被隱晦提及的需要被建模的實體,由於其隱蔽性所以在建模時很容易被忽略。舉個例子,需求中可能會這樣描述:下單失敗時應該告知使用者失敗的原因。這個場景中提及了“失敗原因”這個名詞,那應該用什麼東西來描述它呢?這時就需要業務異常來發揮熱量了。顯性的提及失敗的處理邏輯還好一些,還有一種常見的需求形式比如:下單成功後,給使用者傳送簡訊通知。這種需求只描述了正向的業務而沒有說明如果失敗了要如何處理。此時就需要工程師發揮自己的主動性,結合業務的使用形式為失敗的場景建立適宜的處理方式,當然也可以和客戶或產品經理就此等情況進行協商,實際的參與到需求完善的過程中。

  此種工作方式也應對了我在前面所說過的:軟體開發並不是無腦的堆程式碼。實際上,我也見過一些工程師,當面臨業務BUG時候很不願意承認自己的過失,而是將責任推給需求或其它人,常用的口頭語就是“你也沒說啊?”,對方的常見回答是“你為什麼不問”?然後就開始撕了……客觀來講,我個人比較不喜歡這種工程師,缺少必要的積極性和主動性。工作其實首先成全的是自己,能否讓自身成長也只能靠自己,畢竟“師傅領進門,修行在個人”;滿足公司的需要這本來就是義務,畢竟你從公司拿錢了;另一方面也需要考慮如何進行自我提升,有時候軟技能比會什麼什麼技術更加重要。我在以往的工作中也曾經歷了許多的故事,有些是個人的不足,有些是公司的政治,但一直在努力的進行自我提升包括寫這些文章這個事情的本身。不順是眼前的,有些時候並不是您自身的問題,但能否做一些事情為將來開闢出一條適合自己的路,這是可控的。

  有關業務異常還有一些補充,您在日常用的時候務必給他一個見名之意的名字。這麼說吧,給異常一個好名兒比您費半天勁想詞去描述異常原因更有價值,名起得好在拋異常的時候甚至不用寫具體原因。另外,實踐中最好讓業務異常繼承於“Exception”而不是“RuntimeException”,也就是使用檢查異常以避免異常未被正確捕獲。我寫了一個業務異常的示例供參考,注意名字啊,我個人覺得起得挺牛掰的,看名就知道什麼錯誤。另外一點,如果把程式碼再寫細一點,做一個業務異常的基類並讓所有的具體異常從它繼承,就可以使用一些全域性異常處理機制,比在每個方法裡做try...catch要強得多。

public class DeploymentApprovalFormCreationException extends Exception {
    public DeploymentApprovalFormCreationException() {
        super();
    }

    public DeploymentApprovalFormCreationException(String message) {
        super(message);
    }

    public DeploymentApprovalFormCreationException(String message, Throwable cause) {
        super(message, cause);
    }

    public DeploymentApprovalFormCreationException(Throwable cause) {
        super(cause);
    }

    protected DeploymentApprovalFormCreationException(String message, Throwable cause, 
        boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

2、資源倉庫介面

  除業務異常另外需要著重說明的是“資源倉庫介面”。有些工程師會把資源倉庫當做DAO來用,這個其實是一種誤用。資源倉庫要包含哪些介面和您的業務是強關聯的,再說直白一點就是由業務來決定資源倉庫有哪些能力。兩者的目的有著本質的不同:DAO用於運算元據模型,資源倉庫用於操作領域模型。系統在實現的時候,領域模型最終還需要變成資料模型才能進行儲存和索引,這也是資源倉庫該乾的事情。

  具體來看,領域模型一般會有大量的巢狀物件,各物件間關係複雜,而且也不是所有的物件都需要進行持久化;資料模型相對簡單得多,常用的關係型資料庫對應的模型也不過是一種二維關係。想要實現兩者的轉換已經不僅僅是引入一個簡單的設計模式比如工廠就能解決的了,還需要再應用一種更為複雜的設計來實現領域模型和資料模型的解耦合,引入“資源倉庫”可以達到這個目的;另外,資源倉庫還能約束您在設計的時候要以業務模型為驅動以避免陷入面向資料庫設計的情況。為了實現洋蔥架構的效果,設計時還需要把資源倉庫的定義與實現進行分離。由於定義一般是以介面的形式,所以並不會為BO層引入更多的針對基礎設施的依賴,不得不感嘆DDD的那些先驅還真是聰明。

重點!

進行領域模型設計時,需要首先考慮領域模型的實現再決策儲存方式。資源倉庫的引入可以起到三個用:1)解決領域模型與資料模型間的異構問題,達到轉換器的作用;2)提供對領域模型的序列化和反序列化能力的支撐;3)約束您在考慮問題的時候應該使用業務驅動的方式。

  這裡有一個問題,為什麼說需要根據業務的需求來設計資源倉庫呢?舉一個例子,在我們常見的電商購物業務中有“下單”概念,下單的一個重要步驟是對訂單模型進行儲存,也就說資源倉庫應當具備訂單持久化的介面;支付後把訂單的狀態變成已支付,說明還需要有一個從儲存中查詢訂單的能力和變更訂單資訊的能力,分別對應查詢和編輯;訂單一般不能刪除只能作廢說明不需要有刪除的需求。針對上述需求,我們發現訂單資源倉庫應當只有三個介面:1)查詢單個訂單;2)更新訂單;3)儲存新訂單。具體到查詢單個訂單的介面,其引數是訂單ID還是編號亦或是其它的資訊,也是需要根據業務來定的。通過這個案例,相信您應該明白了資源倉庫設計的依據是業務而不是資料儲存,DAO才是面向資料的。不同的DAO雖然對應的資料模型不一樣,但有一些基本的功能是通用的比如:增加、刪除、更新等。由於責任單一且不包含業務邏輯,一般都會將DAO作為基礎設施層中的元件。

重點!

  • 資源倉庫所提供的命令型(對領域模型有變更)介面,需要保證每一個介面的事務性。
  • 資源倉庫介面所操作的是領域模型,所以命令型介面的引數一般是領域模型;查詢型介面返回的也是領域模型。
  • 資源倉庫中所有的介面都是為了應對命令類業務場景,不能僅為了查詢而增加新的介面。這裡需要注意:單純的查詢領域模型沒有任何意義,所查詢出的領域模型都是為了完成某一個命令型業務場景,應對查詢業務只需要使用DAO即可。
  • 引入資源倉庫不代表就沒有了DAO,資源倉庫實現時對資料儲存和查詢的責任仍然是由DAO進行代理。

  資源倉庫介面的實現邏輯上屬於基礎設施層的內容,系統設計過程中我個人一般會將其與基礎設施層分開至不同的包中。此外,真實專案中一般也會設計一個資源倉庫的基本介面,畢竟大部分場景中都需要對領域模型進行儲存、變更和根據ID查詢的能力。下面程式碼展示了兩個不同業務的資源倉庫介面的定義,您需要注意兩點:1)資源倉庫介面所在的包應該是BO;2)資源倉庫所定義的介面應該由業務來驅動的。

public interface Repository<TID extends Comparable, TEntity extends EntityModel> {

    /**
     * 根據ID返回領域模型
     * @param id 領域模型ID
     * @return 領域模型
     */
    TEntity findBy(TID id);

    /**
     * 刪除領域實體
     * @param entity 待刪除的領域實體
     */
    void remove(TEntity entity);

    /**
     * 刪除多個領域實體
     * @param entities 待刪除的領域實體列表
     */
    void remove(List<TEntity> entities);

    /**
     *將領域實體儲存至資源倉庫中
     * @param entity 待儲存的領域實體
     */
    void add(TEntity entity);

    /**
     * 將領域實體儲存至資源倉庫中
     * @param entities 待儲存的領域實體列表
     */
    void add(List<TEntity> entities);

    /**
     *更新領域實體
     * @param entity 待更新的領域實體
     */
    void update(TEntity entity);

    /**
     *更新領域實體
     * @param entities 待更新的領域實體列表
     */
    void update(List<TEntity> entities);
}

  Repository介面是所有資源倉庫介面的基類,包含了新建、更新、根據ID查詢和刪除四類基本操作。有人說資源倉庫的介面都應該使用業務術語,類似於“update”、“add”已經偏向於技術,應當使用如“save”代替。我個人覺得這麼搞其實挺麻煩的,儲存的時候還需要區分到底是插入還是修改,程式碼會很髒。不過使用業務術語表達每一個介面這個倒是個很重要的規範,您應該遵守。另外有爭議的是“delete”介面,這介面其實不應該有,可能也是因為設計時腦子抽了才加上的,您在實踐時幹掉即可。有了基本介面後,下面就可以基於此來定義業務級資源倉庫介面。

package xx.workflow.bo.opresourceapply.repository;

import xx.common.odd.repository.Repository;
import xx.workflow.bo.opresourceapply.OprApplyForm;


public interface OprApplyFormRepository extends Repository<Long, OprApplyForm> {
}

  “OprApplyFormRepository”所面向的實體是“OprApplyForm”,其中沒有再定義任何其它介面,說明只需要使用“Repository”中的能力即可。

package xx.servicedeployment.bo.repository;


import xx.common.odd.repository.PersistenceException;
import xx.common.odd.repository.Repository;
import xx.servicedeployment.bo.DeploymentDetail;


public interface DeploymentDetailRepository extends Repository<Long, DeploymentDetail> {
    /**
     * 根據部署審批單查詢部署詳情
     * @param deploymentApprovalFormId 部署審批單ID
     * @return 部署詳情
     */
    DeploymentDetail findByDeploymentApprovalFormId(Long deploymentApprovalFormId) throws PersistenceException;
}

  “DeploymentDetailRepository”中多了一個“根據部署審批單查詢部署詳情”介面,說明某個命令型業務中有需要根據“部署審批單”查詢“DeploymentDetail”這個業務實體的需求。其它有關“DeploymentDetail”的能力仍然從基類中繼承。

  根據上面的演示,您可以看到資源倉庫介面的定義遵循了前面所說的全部規範尤其是其所操作的物件都應是領域模型;您應該也看到了類似查詢“XXX資訊列表”這種單純用於查詢的方法並沒有出現在資源倉庫介面中。

二、業務模型層中的程式碼結構

  根據上圖所示,您已經明確了BO層所包含的元素的種類。我們前面說BO層很厚,這麼多東西都在這個層裡,想不厚也不行啊。如果落實到程式碼中,這些元素一般會統一放到一個包中,包名即為業務名,如下圖所示。針對包的組織,我建議這麼做:根據業務能力將服務分成幾個子BC,以包的形式組織這些BC。比如訂單服務中需要包含兩項業務:訂單管理、發貨單生成,那針對這兩項分別建立兩個包,每個包都按下圖所示的結構進行程式碼組織。不建議建立如BO、DAO、VO、Service四個包,根據這些包對程式碼進行組織和分類。

  上圖展示了“審批服務”業務的程式碼結構,BO這個包中除了事件、業務異常和資源倉庫介面,其它的都是實體型別、值型別等元件。根據業務能力組織程式碼,您會發現即使是一個單體的系統,在遵循了DDD設計規範後仍能具備高內聚的屬性,後續如果需要拆分時只需要做一些簡單的工作即可。

三、BO層訪問限制

  既然BO層處於系統的核心位置,根據六邊型模型的要求就需要在依賴與訪問控制兩個方面進行約束。依賴相對簡單,只要讓其別依賴於其它層就OK,也就是限制這層對其它層元素的引用;特別常見的一個錯誤就是在領域實體中引入資源倉庫介面或DAO,雖然初衷是為了提升效能,但會造成程式碼結構的混亂,損害了程式碼的健壯性使系統成為了所謂的大泥球(其實球不球的也不是重點,有了BC的隔離最多是個小泥球,胡亂的引用體現出您的工作沒有規則)。訪問控制方面,針對每個物件的訪問級別包括public、package、private等需要進行充分考慮,做到最大化的隔離。除應用服務層和資源倉庫實現層,其它層不可以直接引用業務模型。下面的程式碼展示了BO層中實現領域模型時的反例,供參考。

   問題一:業務模型依賴Spring框架;問題二:訪問了其它包的DAO;問題三:反向依賴資源倉庫,記住:資源倉庫實現與領域模型的依賴是單向的。

總結

  本章講解了BO層中所包含的元素,尤其對於業務異常和資源倉庫介面進行了重點說明。通過本章的內容相信您已經對於所謂的六邊型架構中的核心及其構成有了一個感性的認識,也為後面的學習打下了一定的基礎,後續我們會深入到BO內部對各元素逐一的進行解釋。

相關文章