1. 程式人生 > 其它 >領域驅動在程式碼層面的落地感悟

領域驅動在程式碼層面的落地感悟

領域驅動在程式碼層面的落地感悟

小米有品技術團隊 2021年12月14日 10:47 ·  閱讀 8981

筆者楊濤12年網際網路從業經驗,8年技術管理經驗。先後從事搜尋、社交、線上教育、電商等行業相關工作,對高併發和複雜業務場景解決方案均有較深入的經驗積累。

背景

小米有品隨業務發展,推出了會員系統。包含滿5單返會費,開卡禮包、每月優惠券、會員價、優先購等等權益和福利等業務場景。在初期的需求調研過程中,會員系統業務複雜性已經得以顯現。按計劃,未來業務上還會有橫向和縱向上的擴充套件(如橫向上權益增加、多會員身份,縱向上的等級制度等等)。

業務特點與挑戰

  • 影響範圍廣,會員權益幾乎覆蓋所有主要業務場景,例如產品站、購物車、結算頁、會場頁和會員頻道等場景

  • 邏輯複雜,權益型別多,狀態多(例如有15個左右的退款狀態,未來還會增加)

  • 關聯服務較多,如會員卡購買與購物車、訂單、履約等系統相關,權益與卡券、優惠、商城相關,省錢計算器與訂單、卡券相關

  • 需要為業務擴充套件和升級留下充足空間,程式碼上模組間要儘可能的減少耦合(資料庫表設計上,我們本次幾個主要表都放棄加唯一鍵約束(非主鍵),使用程式來控制唯一性更有利於橫向擴充套件)

為什麼選擇領域驅動

  1. 會員中心定位是中臺服務,團隊之前也對領域驅動有了一些積累,加之要準確快速實現如此複雜的業務,還要滿足後續緊接著的各種擴充套件需求,領域驅動此時成了最佳選擇。
  2. 領域驅動設計簡稱DDD(Domain-Driven Design),是一種開發思想體系,旨在設計和管理複雜問題域編寫的軟體,由Eric Evans於2003年提出。由於種種原因,它在國內的應用並不廣泛,不過其中的一些概念早已經出現在我們的日常工作中了,例如我們在程式碼中看到的XxxEntity,XxxRepository ,XxxService,XxxFactory。
  3. 它本身理論的複雜度高,學習成本高,在國內一直沒有得到廣泛的應用,直到後來微服務和中臺的出現。領域驅動中領域和限界上下文等概念,如天然為服務拆分和治理所準備。不過畢竟是18年前提出的理念,當初的DDD並不能完全適用現在,我們需要搭配事件風暴、四色建模等分析和建模方法。
  4. 微服務經過這麼多年發展,相關架構理論越來越成熟,加上事件風暴、敏捷迭代等工程方法得到廣泛運用,再去看領域驅動已經不再那麼難了,甚至可以根據自己專案特色,設計合適自己的落地方案。

企業應用開發正規化比較:資料驅動、特性驅動與領域驅動:

引自:www.yyang.io/2016/06/01/…

領域驅動應該如何落地

領域驅動分戰略設計和戰術設計兩個階段,其中又有很多的概念,如:通用語言、領域/子域、限界上下文、領域模型、值物件、實體、聚合/聚合根、領域服務、領域事件、資源庫、工廠等。

這些概念不一定能全部用到,可以根據自己的專案特色做出選擇,即便同一個專案也不是必須將所有業務劃分領域,例如某管理系統中的操作日誌,則可以直接在應用層呼叫基礎設施層中的資源庫進行存庫操作。

戰略設計過程方法有事件風暴和四色建模(可根據專案團隊習慣選擇),參與者需要包括產品經理、領域專家、研發等,專案流程上需要做出相應改變,可以參考下圖:

 引自:zhangyi.xyz/overview-of…

架構上可以選擇(或組合)DDD分層架構、六邊形架構、整潔架構等,具體怎麼選擇要看具體的業務場景或全域性的架構風格。

從例子看落實到程式碼層面

領域驅動重心應該放在戰略設計階段,不過為了快速帶大家過一遍,看看與當前主要的資料驅動設計有何不同,我們就從程式碼層面瞭解領域驅動落地後的樣子。

我所在團隊成員,之前基本是基於資料驅動進行設計和研發,或者應用過阿里的領域模型規約(DO、DTO、BO、AO、VO、Query),因此我們選擇先以DDD分層架構落地,後續視情況升級。

 1. DDD分層架構圖

2.落地上,圖裡兩個地方需要注意:

① 業務邏輯只能存在於領域層和應用層,且應用層只能是跨多子域呼叫,主要業務邏輯都按領域驅動規範在領域層實現。如違反此原則,會讓大量的業務邏輯被“擠”到應用層

② 上層是可以呼叫下方所有層的。例如有一些資料,是沒有(或沒有必要)使用子域(領域模型)進行相關業務管理的,是可以直接從應用層訪問基礎設施層的資源庫的。(這裡不建議使用者介面層直接訪問資源庫,使用者介面層還是隻做引數校驗、冪等、結果封裝等就好)

3.各層說明(自下而上)

基礎設施層: 不含業務邏輯。其中包括各種連線池(mysql、redis等)、各種配置(對接配置中心、對接全域性唯一、或對接其他集團中介軟體服務)、專案內部的Util、資源庫(資料庫、redis、本地快取等);嚴格講,對資料儲存的訪問(資源庫)也屬於基礎設施層,包括資料庫、redis等中介軟體快取、jvm本地快取等,但為了之後服務拆分方便,我們在各個子域包下建立對應的資源庫包,存放子域內相關資料的操作類

注意:一些MVC架構下的常量配置類或一些列舉類,在這裡屬於領域層對應的子域內部(可歸屬於值物件),不放在基礎設施層

領域層: 主要業務邏輯模組,包含1個或多個子域,子域包括核心子域、支撐子域、通用子域(通用子域通常是單獨的或第三方的服務提供,如發郵件和簡訊)

應用層: 跨子域呼叫(如跨子域的事務)時,可以在應用層排程,也可以通過領域事件觸發;其他服務的訊息訂閱;其他服務(如RPC/Rest介面)的呼叫代理

使用者介面層: 對應dubbo服務API的實現類,以及web(如果web專案使用了領域建模)的controller

4.目錄結構一覽

5.用一個例子串一下各層呼叫(自上而下)『虛擬碼』

使用者介面層(介面實現類)有一個方法:

com.xiaomi.youpin.member.service.interfaces.DemoServiceImpl#refund{
    //校驗引數
    verifyParams(request); 
    //呼叫基礎設施層做請求冪等性校驗,重試1次,間隔50ms 
    IdempotentTools.try(request.getParamsForIdempotent(),() -> {        
        //呼叫應用層服務,執行具體業務        
        demoXxxxAppService.refund(request.getA(), request.getB());
    },1,50);
}
複製程式碼

對應的應用層類(統一AppService字尾)的方法:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
com.xiaomi.youpin.demo.service.application.service.Demodomain1AppService#refund{    
    //順序呼叫兩個子域的相關方法    
    Long xxId = demodomain1DomainService.invalid(a, b);     
    demodomain2DomainService.refund(a, xxId);
}
複製程式碼

呼叫的領域層服務(統一DomainService字尾)有2個,這裡以demodomain1DomainService為例:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
com.xiaomi.youpin.member.service.domains.demodomain1.service.Demodomain1DomainService#invalid{ 
    //從資源庫中查詢資料物件    .
    ..domains.demodomain1.repository.dao.mysql.bean.DbDemoUser dbDemoUser = demodomain1UserRepository.findById(a);    
    //使用防腐層將資料物件轉換為領域實體
    ...domains.demodomain1.aggregate.entity.DemoUser demoUser = DemoUserAdapter.parseToDemoUser(dbDemoUser);    
    //將此領域實體作為聚合根,建立對應聚合    
    ...domains.demodomain1.aggregate.DemoUserAggregate demoUserAggregate = DemoUserAggregate.builder().setAggregateRoot(demoUser).setUserRepository(memberUserRepository).build();    
    //呼叫聚合類的業務方法,執行具體的業務處理    
    demoUser = demoUserAggregate.invalid(b);    
    //後續流程(可以直接訪問Repository)    
    Long vvvId = demodomain1VVVRepository.vvv(a, demoUser.getUid, demoUser.getStatus());    return vvvId;
}
複製程式碼

以領域服務demodomain1的Demodomain1UserRepository為例

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
com.xiaomi.youpin.demo.service.domains.demodomain1.repository.Demodomain1UserRepository#update{    
    //更新資料    
    demoUserDbDao.update(dbDemoUser);    
    //DbDemoUserSummary類是資源庫內部使用的類,在資源庫內部轉換    ...domains.demodomain1.repository.dao.mysql.bean.DbDemoUserSummary summaryInfo = dbDemoUser.getSummaryInfo();    
    //重新整理簡要資訊快取    
    refreshSummaryCache(summaryInfo );}private void refreshSummaryCache(long a,int b){    //更新redis快取    
    demoUserRedisCacheDao.refreshCache(refresh);    
    //這裡只是示例,不討論本地快取的分散式資料同步問題    demoUserJvmCacheDao.refreshCache(refresh);
}
複製程式碼

聚合以及實體、值物件相關的,由於涉及業務以及篇幅原因這裡就不多做介紹了,大家在實施領域驅動的過程中,戰略設計之後這些地方都會非常清晰。

總結和感悟

落地領域驅動並不是目的,使用領域驅動的長處解決我們最需要解決的問題才是目的;一些領域驅動中的設計原則,我們要根據具體情況選擇是否遵守,一切以優先解決實際問題為出發點

領域驅動是複雜的、高學習成本的,團隊需要做好充足的學習準備,在實踐中不斷磨合出適合團隊的執行方案;它雖然複雜,但確實對複雜的業務場景的治理非常有效。(舉個栗子,當面對資料庫中幾十幾百個各種資料表時,或許你需要考慮領域驅動設計了)

領域驅動不是銀彈,專案複雜度不高,不建議使用DDD;亦不建議跳過學習,直接用簡單的專案來練手

當前的各類網際網路業務,不再是一味追求流量了(高併發也不再是難題),產品會越做越“好玩”,系統業務勢必也越來複雜,領域驅動也將得到越來越多的關注。