戲說領域驅動設計(十二)——服務

SKevin發表於2022-03-09

  上一章講解了軟體設計中主要用到的三個設計模型,本節講解三個服務。等我們們這次都講完了再最後進行一次歸納,即:系統開發流程中的三模型、軟體設計中的三模型和三個服務,我習慣管這個叫3*3*3。看完了您就會知道我為什麼常說軟體設計這活是樸素的,沒那麼多彎彎繞,只是因為我們在學習過程中沒有做思考和歸納。設計模式的那四個哥們兒不也是根據其經驗總結出了流傳至今且經久不衰的23個模式(其實常用的也沒幾個)以及6個原則。這裡還是要再多說兩句,23個設計模式其實屬於程式碼設計階段,應該是每個軟體工程師必須掌握的一門技術。但,如果是初學者千萬別刻意在產品中去用,強擰的瓜不甜,用完了搞不好還被領導罵你傻缺。我個人寫程式碼時間久了,所以遇到一些典型的場景會直接使用某個模式,非典型的一般是在重構時決定是否有合適的模式供使用。另外,還有一些企業級軟體的設計模式也需要去學習的,比如:熔斷器、IoC、工作單元等。

  書歸正傳,一提到“服務”,您肯定會想:這簡單啊,一個類裡面沒有欄位只有方法就可以稱其為服務。這樣的定義太大且模糊,我們既然要學習設計就把這個東西整細了。也就是要總結一下到底有哪些服務,每個服務的作用是什麼,應用於什麼場景,這才叫系統化學習。首先一點,服務的定義說白了的確就是“只有方法沒有欄位的類”,這是使用服務的一種常用模式。也還有一些特別的,比如計數器服務類裡面可能有“AtomicLong”型別的欄位標識計數值。一般來說,服務頂好是不要包含欄位且單例的,即使加了也儘量從技術角度做好併發訪問控制;第二,好的服務每個介面都可以提供一個完整的功能;第三,服務主要是用於提供特定的功能,其程式碼一般是以程式導向為主。

  上面對服務進行了一個白話版的定義,精確度略顯不足,不過本系列文章也不是站在高大上的角度來教化讀者的。談完了定義再說說服務的型別,軟體設計過程中主要包含三類服務:領域服務、應用服務和資料服務,具體如下圖所示。

一、應用服務

  如果您學習過軟體工程這本書,會發現在討論設計相關的知識的時總是提一個詞“控制類”。遙想當年,我在上這門課的時候對好多的詞都不懂,比如“資料字典”、“控制類”、“用例”等,聽著就不明覺厲,但實際上對這些東西完全沒有什麼概念,不過考試及格罷了。工作幾年後,做了一堆系統,還是不太明白所謂的“控制類”到底是何方神聖。這個事兒說起來挺玄幻的,再後來就突然“頓悟”了,也可能是看資料看得多了突然有所感應。接觸過DDD的您肯定知道有一個概念叫“應用服務”,再不濟程式導向的程式碼您總會寫過吧?裡面那個“service”就是應用服務,也就是所謂的“控制類”。

  說到這兒您肯定明白了應用服務的作用了吧?後面我們會詳細說明,在這裡面只給出一個概念性的解釋。應用服務的主要目的是控制一個事務內的(加緊拿小本本兒記下來,重點)業務流程的運轉,先幹啥後幹啥全靠它來搞,是業務的入口。從UML的角度來看,一般是一個用例對應一個控制類的介面。我們在設計領域模型的時候限制較多比如不能訪問基礎設施,應用服務沒這個限制,幾乎可以隨便玩兒,可能最重要的且強制性的要求就是隻能訪問其它包的應用服務,不能再向內進行入侵。需要注意的是不論您是用程式導向設計還是物件導向設計,控制類的程式碼永遠是程式導向的。除了訪問控制,應用服務的使用和設計還有許多的規範比如日誌、返回值、異常處理等,後面會細聊。

二、資料服務

  資料服務就是DAO,用於執行資料的持久化與反持久化,這東西全宇宙都知道,也沒什麼可講的。需要注意的是DAO不僅僅是用於MySQL這種關係型資料庫,涉及其它非關係型如Redis、MongoDB的操作都需要在DAO內搞定,別一會兒放應用服務內,一會兒放DAO內。既然說到這兒了,我給您展示一下如何在DAO內同時操作MySQL和Redis。我這裡的DAO基於MyBatis框架,使用了介面來與配置檔案對應。上面說了,Redis操作也算是DAO內的東西,但現在的DAO是個介面,不能寫程式碼的,so……ladies and gentlemen,請看示例。

public interface DictionaryMapper extends GenericDao<DictionaryDataEntity, Integer> {

    /**
     * 根據類別ID查詢資料字典
     * @param classId 類別ID
     * @return 資料字典列表
     */
    List<DictionaryDataEntity> selectByClassId(Integer classId);


    /**
     * 根據查詢條件查詢資料字典
     * @param criteria 查詢條件
     * @return 資料字典列表
     */
    List<DictionaryDataEntity> selectAll(DictionaryCriteria criteria);
}


@Repository
public class DictionaryDaoExtension implements DictionaryMapper {
    @Resource
    private RedisCacheUtils redisCacheUtils;
    @Resource
    private DictionaryMapper dictionaryMapper;

    @Override
    public List<DictionaryDataEntity> selectByClassId(Integer classId) {
        if (classId == null) {
            return new ArrayList<>();
        }
        RedisCacheUtils.RedisKey redisKey = this.buildRedisKey();
        String cached = this.redisCacheUtils.getHash(redisKey, classId);
        if (!StringUtils.isEmpty(cached)) {
            return JsonUtils.fromJsonToList(cached, DictionaryDataEntity.class);
        }
        List<DictionaryDataEntity> result = this.dictionaryMapper.selectByClassId(classId);
        if (result != null && !result.isEmpty()) {
            RedisCacheUtils.RedisKeyLifeCycle lifeCycle =
                    new RedisCacheUtils.RedisKeyLifeCycle(CACHE_TIME_OUT, TimeUnit.DAYS);
            this.redisCacheUtils.setHashValue(redisKey, classId.toString(), JsonUtils.toJson(result), lifeCycle);
        }
        return result;
    }

    @Override
    public DictionaryDataEntity getById(Integer id) throws DataAccessException {
        return this.dictionaryMapper.getById(id);
    }

    @Override
    public int deleteById(Integer id) throws DataAccessException {
        throw new UnsupportedOperationException();
    }

    ……
}

  這裡的“DictionaryDaoExtension”使用了一個裝飾模式,繼承於“DictionaryMapper”幷包含了一個“DictionaryMapper”型別的例項,“selectByClassId”方法中加上了快取相關的操作。

三、領域服務

  領域服務是DDD戰術階段中定義的一個重要模型,當某個方法無法區分其屬於哪個領域實體的時候一般會放到領域服務中。此外,如果一個方法涉及多個模型也就是所謂的跨模型操作,一般也會放到領域服務中。這裡面需要注意的是領域服務和領域模型是一樣的,最好只依賴於JDK。我見過一些設計,作者將“資源庫(Repository)”注入到領域服務或領域模型中,個人比較不推薦這種方式,與Spring框架耦合過於嚴重了。

  在繼續講之前還需要說一下所謂的物件導向程式設計(OOP)到底是什麼東西。作為對比,我們先說一下程式導向,簡單來說就是根據業務流程說明一行一行的寫程式碼最終完成整個用例,比較直觀和簡單。OOP如果用白話去說就是:在一個業務場景(用例)中,把涉及到的物件全拿出來,每個物件執行屬於自己責任的任務。一般來說,需要通過應用服務來控制各物件的行為。之所以在領域服務中介紹這一段內容,是因為有一種設計模式:為每一個用例都建立對應的領域服務,應用服務不再直接呼叫領域模型而是轉面呼叫此領域服務,再由後者呼叫領域模型。現實情況中大部分用例在每個子事務(一個用例可能涉及多個事務,使用最終一致性)中只會有一個領域模型參與,所以具體如何使用看個人習慣以及是否有必要。

  前後端分離和微服務架構已成為當前主流的設計方式,RESTfu、Web API或其它RPC是前後兩端以及微服務間互動的主要手段。所以在我們的開發過程中往往會設計一些介面卡元件比如“rest”層用於將當前系統的能力以RESTful介面的方式提供出去。雖然“rest”層的程式碼比較符合服務的定義,但一般並不會將其視為服務來看,其屬於介面卡(可參看六邊型架構。嚴格上來講DAO的實現也是一種介面卡,不過鑑於其在開發過程中的戲份較多,我將其看作一種服務來對待)的一種,除了用於實現REST能力幾乎不包含任何業務或資料邏輯,更多的是透傳操作,類似的還包括各類工具類等。

  三個服務講完了,雖然軟體設計中可能還包含各種其它的“類服務”元件,但由於分量不足達不到主角的層次,撐死了是個二線配角,沒流量。現在您仔細品味一下,會發現在開發過程中您所主要涉及的除了模型就是服務,沒其它的了。那是不是說開發其實就已經有了一個整體的思路或模式了呢?比如“先設計模型,後設計服務”這種?這屬於我個人的總結,您也許會有自己專用的模式。想要開發效率我們不能無腦的幹,也得想想是否有現成兒的最佳實踐或總結出適合於自己的方式,這叫態度。

四、歸納

  結合前面戰術部分所講的內容,我們總結出來在軟體開發與設計流程中會涉及9種物件,這9種物件幾乎涵蓋了系統中的方方面面。各物件的概念前面已經做了詳細介紹,下面的列表僅展示定義。

  • 軟體開發流程三類模型:業務模型、分析模型、設計模型
  • 軟體設計中的三個模型:資料模型、檢視模型、領域模型
  • 軟體設計中的三個服務:資料服務、業務服務、應用服務

  雖然重複,在這裡仍然把前面所用的圖總結性的貼出來供參考。

總結

  本章主要講了3個服務的概念和軟體開發流程中所涉及的9類物件。後面的內容中還會對部分內容做細講。其實很多的概念您仔細看都特別簡單,而我的只是把這些內容進行了總結歸納。最重要的是讓我們的開發有據可循、有理可循,不能盲目的進行。

相關文章