戲說領域驅動設計(十一)——糾偏

  今兒寫這個題目膽子有點大,不過還是得冒險整一篇(我怕您看完了罵我),一是出於經驗分享,另外則是為了後面我們講案例的時候做好鋪墊。好的程式碼需要注意的事項其實挺多的,您真讓我一骨腦兒都列出來可能也差點意思,所以遵照我們常態化歪樓的習慣,我是想到哪寫到哪兒。  

  我沒事兒的時候就喜歡看別人寫的文章,也喜歡看書,收穫還是挺多的,不過歲數大了忘性也大,記不住。可有一個事情我記得倍兒清楚:咱們搞技術的尤其是後端開發都知道一類對象叫視圖模型(View Object簡稱VO),一說VO,大多數文章給的概念就是「後端傳向前端,用於承載需要在頁面上顯示的資訊的實體或對象(反方向是前端傳向後端用於存儲的資訊)」。概念雖然簡單,可是和我的想法就不一樣,誰叫咱這人個色呢,您聽我給您嘮嘮。

  我所認為的視圖相對來說要廣義一點:前面您所認為的自然是沒問題;還有一種(咱們以Java為例啊)我來舉例說明:訂單的服務需要調用客戶服務來查詢客戶資訊「CustomInfo」,這裡面的「CustomInfo」就是視圖模型。您肯定暴起反對:搞笑呢吧?「CustomInfo」是「DTO(數據傳輸對象)」。這種說法當然沒錯了,但DTO這個說法太廣義面且太模糊了。常用的數據實體也算是一種DTO,他承載了需要存儲和從資料庫查詢返回的數據;前端傳向後端的也是DTO,承載了用戶的輸入。所以您平常老是用DTO這個說法其實真的不夠準確。那為什麼說服務間相互傳的數據是VO呢?比如您去相親,對方給您的印象(注意:印象不僅是你主動獲取的,還包括對方傳給你的,就跟兩個函數一樣你調用我我調用你)比如長相、談吐等這些是對方想讓您知道的(別反對,女人畫起妝來你就不知道她到底長什麼樣)關於其自身屬性的部分資訊,當然還有一部分是您從對方身上獲得的資訊,所謂的「印象」是兩種資訊的集成,其實就是資訊視圖。所以您調用下游服務時對方返回給你的就是這個下游服務的視圖也就是下游服務想要展示給你的內容。而且,不僅僅是服務間有視圖,包與包之前也只能通過視圖了解彼此。

  上一段我提到的「服務間和包間只能通過視圖了解彼此」,引申的含義是說:服務間和包間只能通過視圖傳遞資訊,這種限制不論是3層還是4層構架都適用。DTO這種稱呼相對模糊,一般我在開發的時候也不會這麼叫。實際上,您是喜歡叫VO還是DTO也不耽誤什麼事兒,可我們在這裡給出了一個重要的約束也就是包或系統間的數據訪問限制。這東西特別容易出亂子,尤其是想把一個服務做二次拆分的時候,如果當前系統無訪問約束那拆的風險就會相當的高。

  除了視圖模型外,後端服務開發還會用到另外兩類:數據模型和領域模型,如下圖所示。您別看統共就三種,想用好了沒那麼容易。至少在我經歷的不少項目中很多人都是亂用的,要不是沒有訪問限制要不就是模型冗餘。

重點!

服務間和包間相互調用時,傳入和傳出的資訊(簡單欄位除外)只能通過視圖模型進行承載,不得將數據模型和領域模型作為傳輸資訊的載體,包括在消息中也不可以內嵌領域與數據模型,以避免內部資訊泄露。

  對於模型的濫用,讓我來厚著臉皮做一下糾偏,分別說一個每個對象的作用和主要的使用限制。看完了後您會覺得:「這也太誇張了,寫個程式碼有那麼麻煩嗎?」,答:有!好東西一般不會是多快好省出來的。

1、數據模型

  定義:描述資料庫設計並承載數據在持久層到應用層間之間的傳輸。限制:1)每一張表一個實體;2)級聯查詢的結果一般是多個表的集成,也需要建立對應的實體;3)不可以傳到包、服務之外;4)不要包含任何業務邏輯;5)DAO的輸入輸出只能是簡單欄位或數據模型;6)僅能使用基本類型如Integer、String等。有一個小技巧:如果數據模型和視圖模型的欄位一樣,賦值時可以使用一些工具如「BeanUtils」實現兩個對象間的欄位複製。

2、視圖模型

  定義:用於系統間、模組間、包間傳送和展示數據的載體,具體的解釋可參考上面內容。限制:1)僅包含最少的用於傳輸的資訊,不要使用一個對象包含所有的欄位即萬能對象;2)不可以從數據模型繼承(這個問題尤其普遍),可使用工具實現與數據模型的互轉;3)是包、服務間傳輸資訊的唯一載體;4)不得包含業務邏輯。下面給出了一個「數據字典」視圖模型的示例,請參考。

@ApiModel(value = "數據字典資訊")
public class DictionaryVO extends VOBase {
    private static final int MAX_VALUE_LENGTH = 32;

    @ApiModelProperty(value = "字典值,長度:32", required = true)
    private String value;
    

    @Override
    public ParameterValidationResult validate() {
        if (this.classId == null) {
            return ParameterValidationResult.failed(OperationMessages.INVALID_CLASS_ID);
        }
        if (StringUtils.isEmpty(this.value) || this.value.length() > MAX_VALUE_LENGTH) {
            return ParameterValidationResult.failed(String.format(OperationMessages.INVALID_VALUE_LENGTH, MAX_VALUE_LENGTH));
        }        
        return super.validate();
    }

    public static DictionaryVO of(DictionaryDataEntity entity) {
        if (entity == null) {
            return null;
        }
        DictionaryVO vo = new DictionaryVO();
        BeanUtils.copyProperties(entity, vo);
        return vo;
    }
}

  上述程式碼中,視圖模型繼承於「VOBase」自定義類,此類包含了「validate」方法用於對視圖模型中的資訊進行驗證。切記:前端、其它服務和包傳過來的資訊永遠是不可信的,通過在視圖模型中增加驗證邏輯可以讓程式碼更簡潔。「of」方法用於數據模型和視圖模型的轉換。「ApiModel」引入了「Swagger」用於對欄位進行說明。

3、領域模型

   定義:描述業務實體屬性和行為的模型。一般來說是充血模型,後續會細講。限制:1)不得依賴於架構中的其它層、第三方基礎框架如Spring、DAO、HTTPClinet等。這麼說吧,除了JDK外其它都不能依賴。就和狗一樣,依賴少表示血統純粹,越純粹越好;2)作用範圍只能在業務模型層中,不得外泄;3)嚴格注意每個模型的訪問限制,能不用public就不用。4)最好自行寫一些包含了公共能力(比如「對象判等」)的領域模型基類來保證程式碼的乾淨。

  這個裡面我覺得有可能爭議最大的是關於領域模型的依賴,有一些比如字元串工具、日期工具這種的第三方類庫其實很普遍,完全的不依賴是否會更加的極端?怎麼說呢,我個人在使用ODD開發的時候寫了一套基礎組件,包含了驗證、值類型、實體類型、領域倉庫、Saga等領域模型相關的組件(大部分都是抽象類)和少量的工具類,需要什麼拿來即用,我稱之為「通用能力庫」。這個庫本身是自己寫的(註:本系列文章只關注DDD知識的講解,不會推薦任何的、成熟度不夠高的尤其是標榜為DDD的框架),也的確沒用到JDK外的其它第三方組件。而且,領域模型本身專用於業務邏輯處理和計算,像什麼字元串格式化啊、日期格式化啊其實就不應該在領域模型里搞。寫通用能力庫畢竟也會佔用精力,我的推薦是您在領域模型中盡量少的依賴第三方組件,越少越好。有些書籍中會展示在領域模型中注入DAO來實現嵌套對象的「懶載入」,我個人認為這個是不化不類,只有在需要向性能妥協時才會使用。

總結

  本章講了三個模型,雖然內容不多,可真正使用起來也是有一定的要求的。軟體開發是個細活兒,想做好當然要負出精力了。您完全可以突破上述所提到的限制,也能出東西,可需求總是變化的,總得改程式碼,到時候吃虧的還是自己。這沒辦法,坑兒是您自己給自己挖的,除非您寫完了程式碼就打算跑路,那也給了後面接手的人罵爹的機會。另外,您可能認為本章和DDD沒關係,我得提前聲明:咱不是寫小說呢,一個字多少多少錢,這些經驗您還真在其它書上找不著。而且,越是細節越能體現開發者的能力,搞技術到後面比的是什麼?不再是會什麼不會什麼了,而是比誰開發的品質高和可維護性高。兩個員工做同樣的需求尤其是功能增強性相關的,一個5分鐘搞定,一個3天,你是老闆你用哪個 ?