主頁 > 軟體設計 > 長文多圖一步步講清楚:DDD理論、建模與代碼實作全流程

長文多圖一步步講清楚:DDD理論、建模與代碼實作全流程

2023-05-13 08:53:07 軟體設計



1 六個問題

1.1 為什么使用DDD

DDD方法論核心是將問題不斷分解,把大問題分解為小問題,大業務分解小領域,簡而言之就是分而治之,各個擊破,

分而治之是指直接面對大業務我們無從下手,需要按照一定方法進行分解,分解為高內聚的小領域,使得業務有邊界清晰,而這些小領域是我們有能力處理的,這就是領域驅動設計的核心,

各個擊破是指當問題被拆分為小領域后,因為小領域業務內聚,其子領域高度相關,我們在技術維度可以對其進行詳細設計,在管理維度可以按照領域對專案進行分工,需要指出DDD不能替代詳細設計,DDD是為了更清晰地行詳細設計,

在微服務流行的互聯網行業,當業務逐漸復雜時,技術人員需要解決如何劃分微服務邊界的問題,DDD這種清晰化業務邊界的特性正好可以用來解決這個問題,


1.2 方法與目標

我們的目標是將業務劃分清晰的邊界,而DDD是達成目標的有效方法之一,這一點是需要格外注意的,DDD是方法不是目標,不需要為了使用而使用,例如業務模型比較簡單可以很容易分析的業務就不需要使用DDD,還有一些目標是快速驗證型別的專案,追求短平快,前期可能也不需要使用領域驅動設計,


1.3 整體與區域

領域可以劃分多個子領域,子域可以再劃分多個子子域,限界背景關系本質上也是一種子子域,那么在業務分解時一個業務模塊到底是領域、子域還是子子域?

我認為不用糾結在這個問題,因為這取決于看待這個模塊的角度,你認為整體可能是別人的區域,你認為的區域可能是別人的整體,叫什么名字不重要,最重要的是按照高內聚的原則將業務高度相關的模塊收斂在一起,


1.4 粒度粗與細

業務劃分粒度的粗細并沒有統一的標準,還是要根據業務需要、開發資源、技術實力等因素綜合考量,例如微服務拆分過細反而會增加開發、部署和維護的復雜度,但是拆分過粗可能會導致大量業務高度耦合,開發部署起來是挺快的,但是缺失可維護性和可擴展性,這需要根據實際情況做出權衡,


1.5 領域與資料

領域物件與資料物件一個重要的區別是值物件存盤方式,在討論領域物件和資料物件之前,我們首先討論物體和值物件這一組概念,物體是具有唯一標識的物件,而唯一標識會伴隨物體物件整個生命周期并且不可變更,值物件本質上是屬性的集合,并沒有唯一標識,

領域物件在包含值物件的同時也保留了值物件的業務含義,而資料物件可以使用更加松散的結構保存值物件,簡化資料庫設計,

現在假設我們需要管理足球運動員資訊,對應的領域模型和資料模型應該如何設計?姓名、身高、體重是一名運動員本質屬性,加上唯一編號可以對應物體物件,跑動距離,傳球成功率,進球數是運動員比賽中的表現,這些屬性的集合可以對應值物件,

值物件在資料物件中可以用松散的資料結構進行存盤,而值物件在領域物件中需要保留其業務含義:


01 物體與值物件_足球.jpg


1.6 抽象與靈活

抽象的核心是找相同,對不同事物提取公因式,實作的核心是找不同,擴展各自的屬性和特點,例如模板方法設計模式正是用抽象構建框架,用實作擴展細節,

我們再回到資料模型的討論,可以發現腳本化是一種拓展靈活性的方式,腳本化不僅指使用groovy、QLExpress腳本增強系統靈活性,還包括松散可擴展的資料結構,資料模型抽象出了姓名、身高、體重這些基本屬性,對于頻繁變化的比賽表現屬性,這些屬性值可能經常變化,甚至屬性本身也是經常變化,例如可能會加上射門次數,突破次數等,所以采用松散的JSON資料結構進行存盤,


2 基本概念

2.1 領域、子域與限界背景關系

這三個詞雖然不同但是實際上都是在描述范圍這個概念,正如牛頓三定律有其適用范圍,程式中變數有其作用域一樣,DDD方法論也會將整體業務拆分成不同范圍,在同一個范圍內進行才可以進行分析和處理,

限界背景關系(Bounded contenxt)比較難理解可以從四個維度分析:

第一個維度是限界背景關系本身含義,限界表示了規定一個邊界,背景關系表示在這個邊界內使用相同語意物件,例如goods這個詞,在商品邊界內被稱為商品,但是快遞邊界內被稱為貨物,

第二個維度是子域與限界背景關系關系,子域可以對應一個,也可以對應多個限界背景關系,如果子域劃分足夠小,那么就是限界背景關系,如果子域可以再細分,那么可以劃分多個限界背景關系,

第三維度是服務如何劃分,子域和限界背景關系都可以作為微服務,這里微服務是指獨立部署的程式行程,具體拆分到什么維度是根據業務需要、開發資源、維護成本、技術實力等因素綜合考量,

第四個維度是互動維度,在同一個限界背景關系中物體物件和值物件可以自由交流,在不同限界背景關系中必須通過聚合根進行交流,聚合根可以理解為一個按照業務聚合的代理物件,


2.2 物體、值物件與聚合

領域模型分為三類:物體、值物件和聚合,物體是具有唯一標識的物件,唯一標識會伴隨物體物件整個生命周期并且不可變更,值物件本質上是屬性的集合,沒有唯一標識,

聚合包括聚合根和聚合邊界兩個概念,聚合根可以理解為一個按照業務聚合的代理物件,一個限界背景關系企圖訪問另一個限界背景關系內部物件,必須通過聚合根進行訪問,例如產品經理作為需求收口人,任何需求應該先提給產品經理,通過產品經理整合后再提給程式員,而不是直接提給開發人員,


2.3 領域事件

當某個領域發生一件事情時,如果其它領域有后續動作跟進,我們把這件事情稱為領域事件,這個事件需要被感知,

通過事件互動有一個問題需要注意,通過事件訂閱實作業務只能采用最終一致性,需要放棄強一致性,可能會引入新的復雜度需要權衡,同一個行程間事件互動可以用EventBus,跨行程事件互動可以用RocketMQ等訊息中間件,


3 分析七大步驟

3.1 七大步驟

每個維度描述系統的一個側面,組合在一起最終描繪出整個系統,這些維度分別是:

四色分領域

用例看功能

流程三劍客

領域與資料

縱橫做設計

分層看架構

介面看對接

本文我們分析一個足球運動員資訊管理系統,這個系統大家可能也沒有做過,我們一起分析這個系統,需要說明本文著重介紹方法論的落地,業務細節難以面面俱到,


3.2 四色分領域

3.2.1 流程梳理

首先梳理業務流程,這里有兩個問題需要考慮,第一個問題是從什么視角去梳理?因為不同的人看到的流程是不一樣的,答案是取決于系統需要解決什么問題,因為我們要管理運動員從轉會到上場比賽整條鏈路資訊,所以從運動員視角出發是一個合適的選擇,

第二個問題是對業務不熟悉怎么辦?因為我們不是體育和運動專家,并不清楚整條鏈路的業務細節,答案是梳理流程時一定要有業務專家在場,因為沒有真實業務細節,無法領域驅動設計,同理在互聯網梳理復雜業務流程時,一定要有對相關業務熟悉的產品經理或者運營一起參與,

假設足球業務專家梳理出了業務流程,運動員提出轉會,協商一致后到新俱樂部體檢,體檢通過就進行簽約,進入新俱樂部后進行訓練,訓練指標達標后上場比賽,賽后參加新聞發布會,當然實際流程會復雜很多,本文還是著重講解方法論,


02 流程梳理.jpg


3.2.2 四色建模

(1) 時標物件

四色建模第一種顏色是紅色,表示時標物件,時標物件是四色建模最重要的物件,可以理解為核心業務單據,在業務進行程序中一定要對關鍵業務留下單據,通過這些單據可以追溯出整個業務流程,

時標物件具有兩個特點:第一是事實不可變性,記錄了過去某個時間點或時間段內發生的事實,第二是責任可追溯性,記錄了管理者關注的資訊,現在我們分析本系統時標物件有哪些,需要留下哪些核心業務單據,

轉會對應轉會單據,體檢對應體檢單據,簽合同對應合同單據,訓練對應訓練指標單據,比賽對應比賽指標單據,新聞發布會對應采訪單據,根據分析繪制如下時標物件:


03 四色分析_時標物件.jpg


(2) 參與方、地、物

這三類物件在四色建模中用綠色表示,我們以電商場景為例進行說明,用戶支付購買商家的商品時,用戶和商家是參與方,物流系統發貨時配送單據需要有配送地址物件,地址物件就是地,訂單需要商品物件,物流配送需要有貨品,商品和貨品就是物,

我們分析本例可以知道參與方包含總經理、隊醫、教練、球迷、記者,地包含訓練地址、比賽地址、采訪地址,物包含簽名球衣和簽名足球:


04 四色分析_參與方、地、物.jpg


(3) 角色物件

在四色建模中用黃色表示,這類物件表示參與方、地、物以什么角色參與到業務流程:


05 四色分析_角色.jpg


(4) 描述物件

我們可以為物件增加相關描述資訊,在四色建模中用藍色表示:


06 四色分析_描述.jpg


3.2.3 劃分領域

在四色建模程序中我們體會到時標物件是最重要的物件,因為其承載了業務系統核心單據,在劃分領域時我們同樣離不開時標物件,通過收斂相關時標物件劃分領域,


07 領域劃分表格.jpg


3.2.4 領域事件

當業務系統發生一件事情時,如果本領域或其它領域有后續動作跟進,那么我們把這件事情稱為領域事件,這個事件需要被感知,

例如球員比賽受傷了,這是比賽子域事件,但是醫療和訓練子域是需要感知的,那么比賽子域就發出一個事件,醫療和訓練子域會訂閱,球員比賽取得進球,這也是比賽子域事件,但是訓練和合同子域也會關注這個事件,所以比賽子域也會發出一個比賽進球事件,訓練和合同子域會訂閱,

通過事件互動有一個問題需要注意,通過事件訂閱實作業務只能采用最終一致性,需要放棄強一致性,可能會引入新的復雜度需要權衡,


08 領域事件表格.jpg


3.3 用例看功能

目前為止領域已經確定了,大領域已經拆分成了小領域,我們已經不再束手無策,而是可以對小領域進行用例分析了,用例圖由參與者和用例組成,目的是回答這樣一個問題:什么人使用系統干什么事,

下圖表示在比賽領域,運動員視角(什么人)使用系統進行進球統計,助攻統計,犯規統計,跑動距離統計,比賽評分統計,傳球成功率統計,受傷統計(干什么事),同理我們也可以選擇四色建模中其它參與者視角繪制用例圖,


03 用例圖_藍色.jpg


include關鍵字表示包含關系,例如比賽是基用例,包含了進球統計,助攻統計,犯規統計,跑動距離統計,比賽評分統計,傳球成功率統計,受傷統計七個子用例,包含關系表示法有兩個優點:第一是可以清晰地組織子用例,第二是有利于子用例復用,例如主教練視角用例圖也包含比賽評分,那么就可以直接指向比賽評分子用例,

extend關鍵字表示擴展關系,例如點球統計是進球統計的擴展,因為不一定可以獲得點球,所以點球統計即使不存在,也不會影響進球統計功能,黃牌統計、紅牌統計是犯規統計的擴展,因為普通犯規不會獲得紅黃牌,所以紅黃牌統計不存在,也不會影響犯規統計功能,

用例圖不關心實作細節,而是從一種外部視角描述系統功能,即使不了解實作細節的人,通過看用例圖也可以快速了解系統功能,這個特性規定了用例圖不宜過于復雜,能夠說明核心功能即可,


3.4 流程三劍客

用例圖是從外部視角描述系統,但是分析系統總是要深入系統內部的,其中流程視圖就是描述系統內如何流轉的視圖,活動圖、序列圖、狀態機圖是流程視圖中最重要的三種視圖,我們稱為流程三劍客,三者側重點有所不同:活動圖側重于邏輯分支,順序圖側重于互動,狀態機圖側重于狀態流轉,


3.4.1 活動圖

活動圖適合描述復雜邏輯分支,設想這樣一種業務場景,球隊需要選出一名球員成為球隊的足球先生,選拔標準如下:前場、中場、后場、門將各選出一名候選球員,前場隊員依次比較進球數、助攻數,中場隊員依次比較助攻數、搶斷數,后場隊員依次比較解圍數、搶斷數,門將依次比較撲救數、撲點數,如果所有指標均相同則抽簽,每個位置有人選之后,全體教練組投票,如果投票數相同則抽簽,

我們經常說一圖勝千言,其中一個重要原因是文字是線性的,所以表達邏輯分支能力不如流程視圖,而在流程視圖中表達邏輯分支能力最強的是活動圖,


04 活動圖.jpg


3.4.2 順序圖

順序圖側重于互動,適合按照時間順序體現一個業務流程中互動細節,但是順序圖并不擅長體現復雜邏輯分支,

如果某個邏輯分支特別重要,可以選擇再畫一個順序圖,例如支付流程中有支付成功正常流程,也有支付失敗例外流程,這兩個流程都非常重要,所以可以用兩張順序圖體現,回到本文實體,我們可以通過順序圖體現球員從提出轉會到比賽全流程,


05 序列圖.jpg


3.4.3 狀態機圖

假設一條資料有ABC三種狀態,從正常業務角度來看,狀態只能從A流轉到B,再從B流轉到C,不能亂序也不可逆,但是可能出現這種例外情況:資料當前狀態為A,接收異步訊息更改狀態,B訊息由于延時晚于C訊息,最終導致狀態先改為C再改為B,那么此時狀態就是錯誤的,

狀態機圖側重于狀態流轉,說明了哪些狀態之間可以相互流轉,再結合狀態機代碼模式,可以解決上述狀態例外情況,回到本文實體,我們可以通過狀態機圖表示球員從提出轉會到簽約整個狀態流程,


06 狀態機圖.jpg


3.5 領域與資料

上述章節從功能層面和流程層面進行了系統分析,現在需要從資料層分析系統,我們首先對比兩組概念:值物件與物體,領域物件與資料物件,

物體是具有唯一標識的物件,唯一標識會伴隨物體物件整個生命周期并且不可變更,值物件本質上是屬性的集合,沒有唯一標識,

領域物件與資料物件一個重要的區別是值物件存盤方式,領域物件在包含值物件的同時也保留了值物件的業務含義,而資料物件可以使用更加松散的結構保存值物件,簡化資料庫設計,

現在我們需要管理足球運動員基本資訊和比賽資料,對應領域模型和資料模型應該如何設計?姓名、身高、體重是一名運動員本質屬性,加上唯一編號可以對應物體物件,跑動距離,傳球成功率,進球數是運動員比賽表現,這些屬性的集合可以對應值物件,


01 物體與值物件_足球.jpg


我們根據圖示撰寫領域物件與資料物件代碼:

// 資料物件
public class FootballPlayerDO {
    private Long id;
    private String name;
    private Integer height;
    private Integer weight;
    private String gamePerformance;
}

// 領域物件
public class FootballPlayerDMO {
    private Long id;
    private String name;
    private Integer height;
    private Integer weight;
    private GamePerformanceVO gamePerformanceVO;
}

public class GamePerformanceVO {
    private Double runDistance;
    private Double passSuccess;
    private Integer scoreNum;
}

如果需要根據JSON結構中KEY進行檢索,例如查詢進球數大于5的球員,這也不是沒有辦法,我們可以將MySQL表中資料平鋪到ES中,一條資料根據JSON KEY平鋪變成多條資料,這樣就可以進行檢索了,


3.6 縱橫做設計

復雜業務之所以復雜,一個重要原因是涉及角色或者型別較多,很難平鋪直敘地進行設計,所以我們需要增加分析維度,其中最常見的是增加橫向和縱向兩個維度,本文也著重討論兩個維度,總體而言橫向擴展的是思考廣度,縱向擴展的是思考深度,對應到系統設計而言可以總結為:縱向做隔離,橫向做編排,

我們首先分析一個下單場景做鋪墊,當前有ABC三種訂單型別,A訂單價格9折,物流最大重量不能超過8公斤,不支持退款,B訂單價格8折,物流最大重量不能超過5公斤,支持退款,C訂單價格7折,物流最大重量不能超過1公斤,支持退款,按照需求字面含義平鋪直敘地寫代碼也并不難:

public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Override
    public void createOrder(OrderBO orderBO) {
        if (null == orderBO) {
            throw new RuntimeException("引數例外");
        }
        if (OrderTypeEnum.isNotValid(orderBO.getType())) {
            throw new RuntimeException("引數例外");
        }
        // A型別訂單
        if (OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType())) {
            orderBO.setPrice(orderBO.getPrice() * 0.9);
            if (orderBO.getWeight() > 9) {
                throw new RuntimeException("超過物流最大重量");
            }
            orderBO.setRefundSupport(Boolean.FALSE);
        }
        // B型別訂單
        else if (OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType())) {
            orderBO.setPrice(orderBO.getPrice() * 0.8);
            if (orderBO.getWeight() > 8) {
                throw new RuntimeException("超過物流最大重量");
            }
            orderBO.setRefundSupport(Boolean.TRUE);
        }
        // C型別訂單
        else if (OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType())) {
            orderBO.setPrice(orderBO.getPrice() * 0.7);
            if (orderBO.getWeight() > 7) {
                throw new RuntimeException("超過物流最大重量");
            }
            orderBO.setRefundSupport(Boolean.TRUE);
        }
        // 保存資料
        OrderDO orderDO = new OrderDO();
        BeanUtils.copyProperties(orderBO, orderDO);
        orderMapper.insert(orderDO);
    }
}

上述代碼從功能上完全可以實作業務需求,但是程式員不僅要滿足功能,還需要思考代碼的可維護性,如果新增一種訂單型別,或者新增一個訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會影響原有邏輯,

為了避免牽一發而動全身這種情況,設計模式中的開閉原則要求我們面向新增開放,面向修改關閉,我認為這是設計模式中最重要的一條原則,

需求變化通過擴展,而不是通過修改已有代碼實作,這樣就保證代碼穩定性,擴展也不是隨意擴展,因為事先定義了演算法,擴展也是根據演算法擴展,用抽象構建框架,用實作擴展細節,標準意義的二十三種設計模式說到底最終都是在遵循開閉原則,

如何改變平鋪直敘的思考方式?這就要為問題分析加上縱向和橫向兩個維度,我選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場景,


07 訂單_分析矩陣.jpg


3.6.1 縱向做隔離

縱向維度表示策略,不同策略在邏輯上和業務上應該是隔離的,本實體包括優惠策略、物流策略和退款策略,策略作為抽象,不同訂單型別去擴展這個抽象,策略模式非常適合這種場景,本文詳細分析優惠策略,物流策略和退款策略同理,

// 優惠策略
public interface DiscountStrategy {
    public void discount(OrderBO orderBO);
}

// A型別優惠策略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {

    @Override
    public void discount(OrderBO orderBO) {
        orderBO.setPrice(orderBO.getPrice() * 0.9);
    }
}

// B型別優惠策略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {

    @Override
    public void discount(OrderBO orderBO) {
        orderBO.setPrice(orderBO.getPrice() * 0.8);
    }
}

// C型別優惠策略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {

    @Override
    public void discount(OrderBO orderBO) {
        orderBO.setPrice(orderBO.getPrice() * 0.7);
    }
}

// 優惠策略工廠
@Component
public class DiscountStrategyFactory implements InitializingBean {
    private Map<String, DiscountStrategy> strategyMap = new HashMap<>();

    @Resource
    private TypeADiscountStrategy typeADiscountStrategy;
    @Resource
    private TypeBDiscountStrategy typeBDiscountStrategy;
    @Resource
    private TypeCDiscountStrategy typeCDiscountStrategy;

    public DiscountStrategy getStrategy(String type) {
        return strategyMap.get(type);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeADiscountStrategy);
        strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBDiscountStrategy);
        strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCDiscountStrategy);
    }
}

// 優惠策略執行
@Component
public class DiscountStrategyExecutor {
    private DiscountStrategyFactory discountStrategyFactory;

    public void discount(OrderBO orderBO) {
        DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
        if (null == discountStrategy) {
            throw new RuntimeException("無優惠策略");
        }
        discountStrategy.discount(orderBO);
    }
}

3.6.2 橫向做編排

橫向維度表示場景,一種訂單型別在廣義上可以認為是一種業務場景,在場景中將獨立的策略進行串聯,模板方法設計模式適用于這種場景,

模板方法模式一般使用抽象類定義一個演算法骨架,同時定義一些抽象方法,這些抽象方法延遲到子類實作,這樣子類不僅遵守了演算法骨架約定,也實作了自己的演算法,既保證了規約也兼顧靈活性,這就是用抽象構建框架,用實作擴展細節,

// 創建訂單服務
public interface CreateOrderService {
    public void createOrder(OrderBO orderBO);
}

// 抽象創建訂單流程
public abstract class AbstractCreateOrderFlow {

    @Resource
    private OrderMapper orderMapper;

    public void createOrder(OrderBO orderBO) {
        // 引數校驗
        if (null == orderBO) {
            throw new RuntimeException("引數例外");
        }
        if (OrderTypeEnum.isNotValid(orderBO.getType())) {
            throw new RuntimeException("引數例外");
        }
        // 計算優惠
        discount(orderBO);
        // 計算重量
        weighing(orderBO);
        // 退款支持
        supportRefund(orderBO);
        // 保存資料
        OrderDO orderDO = new OrderDO();
        BeanUtils.copyProperties(orderBO, orderDO);
        orderMapper.insert(orderDO);
    }

    public abstract void discount(OrderBO orderBO);

    public abstract void weighing(OrderBO orderBO);

    public abstract void supportRefund(OrderBO orderBO);
}

// 實作創建訂單流程
@Service
public class CreateOrderFlow extends AbstractCreateOrderFlow {

    @Resource
    private DiscountStrategyExecutor discountStrategyExecutor;
    @Resource
    private ExpressStrategyExecutor expressStrategyExecutor;
    @Resource
    private RefundStrategyExecutor refundStrategyExecutor;

    @Override
    public void discount(OrderBO orderBO) {
        discountStrategyExecutor.discount(orderBO);
    }

    @Override
    public void weighing(OrderBO orderBO) {
        expressStrategyExecutor.weighing(orderBO);
    }

    @Override
    public void supportRefund(OrderBO orderBO) {
        refundStrategyExecutor.supportRefund(orderBO);
    }
}

3.6.3 綜合應用

上述實體業務和代碼并不復雜,其實復雜業務場景也不過是簡單場景的疊加、組合和交織,無外乎也是通過縱向做隔離、橫向做編排尋求答案,


08 訂單_縱向隔離橫向編排.jpg


縱向維度抽象出能力池這個概念,能力池中包含許多能力,不同的能力按照不同業務維度聚合,例如優惠能力池,物流能力池,退款能力池,我們可以看到兩種程度的隔離性,能力池之間相互隔離,能力之間也相互隔離,

橫向維度將能力從能力池選出來,按照業務需求串聯在一起,形成不同業務流程,因為能力可以任意組合,所以體現了很強的靈活性,除此之外,不同能力既可以串行執行,如果不同能力之間沒有依賴關系,也可以如同流程Y一樣并行執行,提升執行效率,

此時可以回到本文足球運動員管理系統,如果我們采用縱橫思維,分析3.3.1足球先生選拔業務場景可以得到下圖:


09 足球_縱向隔離橫向編排.jpg


縱向隔離出進攻能力池,防守能力池,門將能力池,橫向編排出前場、中場、后場、門將四個流程,在不同流程中可以任意從能力池中選擇能力進行組合,而不是撰寫冗長的判斷邏輯,顯著提升了代碼可擴展性,


3.7 分層看架構

3.7.1 維度一

第一種層次關系是指本專案在整個公司位于哪一層,持久層、快取層、中間件、業務中臺、服務層、網關層、客戶端和代理層是常見的分層架構,


10 總體分層_2_場景化.jpg


3.7.2 維度二

第二種層次是指中臺和前臺的關系,一個系統在業務上通常分為三個端:面向B端用戶,面向C端用戶,面向運營用戶,面對這種情況可以劃分前臺、中臺、后臺三類應用:


變化多端_2.jpg


第一中臺應用承載核心邏輯,暴露核心介面,中臺并不要理解所有端資料結構,而是通過client介面暴露相對穩定的資料,

第二針對面向B端、面向C端、面向運營三種端,各自拆分出一個應用,在此應用中進行轉換、適配和裁剪,并且處理各自業務,

第三什么是大中臺、小前臺思想?中臺提供穩定服務,前臺提供靈活入口,

第四如果后續要做秒殺系統,那么也可以理解其為一個前臺應用(seckill-front)聚合各種中臺介面,


3.7.3 維度三

第三種層次是代碼層次結構,分層優點是每層只專注本層作業,可以類比設計模式單一職責原則,或者經濟學比較優勢原理,每層只做本層最擅長的事情,

分層缺點是層之間通信時,需要通過配接器,翻譯成本層或者下層可以理解的資訊,通信成本有所增加,我認為工程分層需要從六個維度思考:

(1) 單一

每層只處理一類事情,滿足單一職責原則

(2) 降噪

資訊在每一層進行傳輸,滿足最小知識原則,只向下層傳輸必要資訊

(3) 適配

每層都需要一個配接器,翻譯資訊為本層或者下層可以理解的資訊

(4) 縱向

縱向做隔離,同一個領域內業務要在本領域內聚

(5) 橫向

橫向做編排,應用層聚合多個領域進行業務編排

(6) 資料

資料物件盡量純凈,盡量使用基本型別

代碼可以分為九層結構:

  • 工具層:util
  • 整合層:integration
  • 基礎層:infrastructure
  • 領域層:domain
  • 應用層:application
  • 門面層:facade
  • 客戶端:client
  • 控制層:controller
  • 啟動層:boot

SpringBoot九層結構_DDD.jpg


3.8 介面看對接

當一個介面代碼撰寫完成后,那么這個介面如何呼叫,輸入和輸出引數是什么,這些問題需要在介面檔案中得到回答,介面檔案生成有兩種方式:第一種是自動生成,例如使用Swagger,第二種方式是手工生成,

自動生成優點是代碼即檔案,還具有除錯功能,在公司內部進行聯調時非常方便,但是如果介面是提供給外部第三方使用,那么還是需要手工撰寫介面檔案,對于一個介面的描述無外乎介面名稱、介面說明、介面協議,輸入引數、輸出引數資訊,


11 介面檔案.jpg


4 代碼詳解

user-demo-service
    -user-demo-service-application
    -user-demo-service-boot
    -user-demo-service-client
    -user-demo-service-controller
    -user-demo-service-domain
    -user-demo-service-facade
    -user-demo-service-infrastructure
    -user-demo-service-integration
    -user-demo-service-util

4.1 util

工具層承載工具代碼

不依賴本專案其它模塊

只依賴一些通用工具包

user-demo-service-util
    -/src/main/java
        -date
            -DateUtil.java
        -json
            -JsonUtil.java
        -validate
            -BizValidator.java

4.2 infrastructure

基礎層承載資料訪問和entity

同時承載基礎服務(ES、Redis、MQ)


4.2.1 專案結構

user-demo-service-infrastructure
    -/src/main/java
        -base
            -service
                -redis
                    -RedisService.java
                -mq
                    -ProducerService.java
        -player
            -entity
                -PlayerEntity.java
            -mapper
                -PlayerEntityMapper.java
        -game
            -entity
                -GameEntity.java
            -mapper
                -GameEntityMapper.java
    -/src/main/resources
        -mybatis
            -sqlmappers
                -gameEntityMapper.xml
                -playerEntityMapper.xml

4.2.2 本專案依賴

  • util

4.2.3 核心代碼

創建運動員資料表:

CREATE TABLE `player` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `player_id` varchar(256) NOT NULL COMMENT '運動員編號',
  `player_name` varchar(256) NOT NULL COMMENT '運動員名稱',
  `height` int(11) NOT NULL COMMENT '身高',
  `weight` int(11) NOT NULL COMMENT '體重',
  `game_performance` text COMMENT '最近一場比賽表現',
  `creator` varchar(256) NOT NULL COMMENT '創建人',
  `updator` varchar(256) NOT NULL COMMENT '修改人',
  `create_time` datetime NOT NULL COMMENT '創建時間',
  `update_time` datetime NOT NULL COMMENT '修改時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

運動員物體物件,gamePerformance欄位作為string保存在資料庫,體現了資料層盡量純凈,不要整合過多業務,決議任務應該放在業務層:

public class PlayerEntity {
    private Long id;
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private String creator;
    private String updator;
    private Date createTime;
    private Date updateTime;
    private String gamePerformance;
}

運動員Mapper物件:

@Repository
public interface PlayerEntityMapper {
    int insert(PlayerEntity record);
    int updateById(PlayerEntity record);
    PlayerEntity selectById(@Param("playerId") String playerId);
}

4.3 integration

本層呼叫外部服務,轉換外部DTO成為本專案可以理解物件,


4.3.1 專案結構

本專案呼叫用戶中心服務:

user-demo-service-integration
    -/src/main/java
        -user
            -adapter
                -UserClientAdapter.java
            -proxy
                -UserClientProxy.java
            -vo                                    // 本專案物件
                -UserSimpleAddressVO.java
                -UserSimpleContactVO.java
                -UserSimpleBaseInfoVO.java

4.3.2 本專案依賴

  • util

4.3.3 核心代碼

(1) 外部服務

// 外部物件
public class UserInfoClientDTO implements Serializable {
    private String id;
    private String name;
    private Date createTime;
    private Date updateTime;
    private String mobile;
    private String cityCode;
    private String addressDetail;
}

// 外部服務
public class UserClientService {

    // RPC
    public UserInfoClientDTO getUserInfo(String userId) {
        UserInfoClientDTO userInfo = new UserInfoClientDTO();
        userInfo.setId(userId);
        userInfo.setName(userId);
        userInfo.setCreateTime(DateUtil.now());
        userInfo.setUpdateTime(DateUtil.now());
        userInfo.setMobile("test-mobile");
        userInfo.setCityCode("test-city-code");
        userInfo.setAddressDetail("test-address-detail");
        return userInfo;
    }
}

(2) 本專案物件

// 基本物件
public class UserBaseInfoVO {
    private UserContactVO contactInfo;
    private UserAddressVO addressInfo;
}

// 地址值物件
public class UserAddressVO {
    private String cityCode;
    private String addressDetail;
}

// 聯系方式值物件
public class UserContactVO {
    private String mobile;
}

(3) 配接器

public class UserClientAdapter {

    public UserBaseInfoVO convert(UserInfoClientDTO userInfo) {
        // 基礎資訊
        UserBaseInfoVO userBaseInfo = new UserBaseInfoVO();
        // 聯系方式
        UserContactVO contactVO = new UserContactVO();
        contactVO.setMobile(userInfo.getMobile());
        userBaseInfo.setContactInfo(contactVO);
        // 地址資訊
        UserAddressVO addressVO = new UserAddressVO();
        addressVO.setCityCode(userInfo.getCityCode());
        addressVO.setAddressDetail(userInfo.getAddressDetail());
        userBaseInfo.setAddressInfo(addressVO);
        return userBaseInfo;
    }
}

(4) 呼叫外部服務

public class UserClientProxy {

    @Resource
    private UserClientService userClientService;
    @Resource
    private UserClientAdapter userIntegrationAdapter;

    // 查詢用戶
    public UserBaseInfoVO getUserInfo(String userId) {
        UserInfoClientDTO user = userClientService.getUserInfo(userId);
        UserBaseInfoVO result = userIntegrationAdapter.convert(user);
        return result;
    }
}

4.4 domain

4.4.1 概念說明

通過三組對比理解領域層:

  • 領域物件 VS 資料物件
  • 領域物件 VS 業務物件
  • 領域層 VS 應用層

(1) 領域物件 VS 資料物件

資料物件使用基本型別保持純凈:

public class PlayerEntity {
    private Long id;
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private String creator;
    private String updator;
    private Date createTime;
    private Date updateTime;
    private String gamePerformance;
}

領域物件需要體現業務含義:

public class PlayerQueryResultDomain {
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceVO gamePerformance;
}

public class GamePerformanceVO {
    // 跑動距離
    private Double runDistance;
    // 傳球成功率
    private Double passSuccess;
    // 進球數
    private Integer scoreNum;
}

(2) 領域物件 VS 業務物件

業務物件同樣會體現業務,領域物件和業務物件有什么不同?最大不同是領域物件采用充血模型聚合業務,

運動員新增業務物件:

public class PlayerCreateBO {
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceVO gamePerformance;
    private MaintainCreateVO maintainInfo;
}

運動員新增領域物件:

public class PlayerCreateDomain implements BizValidator {
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceVO gamePerformance;
    private MaintainCreateVO maintainInfo;

    @Override
    public void validate() {
        if (StringUtils.isEmpty(playerName)) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == height) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (height > 300) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == weight) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null != gamePerformance) {
            gamePerformance.validate();
        }
        if (null == maintainInfo) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        maintainInfo.validate();
    }
}

(3) 領域層 VS 應用層

第一個區別:領域層關注縱向,應用層關注橫向,領域層縱向做隔離,本領域業務行為要在本領域內處理完,應用層橫向做編排,聚合和編排領域服務,

第二個區別:應用層可以更加靈活組合不同領域業務,并且可以增加流控、監控、日志、權限,分布式鎖,相較于領域層功能更為豐富,


4.4.2 專案結構

user-demo-service-domain
    -/src/main/java
        -base
            -domain
                -BaseDomain.java
            -event
                -BaseEvent.java
            -vo
                -BaseVO.java
                -MaintainCreateVO.java
                -MaintainUpdateVO.java
        -player
            -adapter
                -PlayerDomainAdapter.java
            -domain
                -PlayerCreateDomain.java          // 領域物件 
                -PlayerUpdateDomain.java
                -PlayerQueryResultDomain.java
            -event                                // 領域事件
                -PlayerUpdateEvent.java
                -PlayerMessageSender.java
            -service                              // 領域服務
                -PlayerDomainService.java
            -vo                                   // 值物件
                -GamePerformanceVO.java                
        -game
            -adapter
                -GameDomainAdapter.java        
            -domain
                -GameCreateDomain.java
                -GameUpdateDomain.java
                -GameQueryResultDomain.java
            -service
                -GameDomainService.java

4.4.3 本專案依賴

  • util
  • client

領域物件進行業務校驗,所以需要依賴client模塊:

  • BizException
  • ErrorCodeBizEnum

4.4.4 核心代碼

// 修改領域物件
public class PlayerUpdateDomain extends BaseDomain implements BizValidator {
    private String playerId;
    private String playerName;
    private Integer height;
    private Integer weight;
    private String updator;
    private Date updatetime;
    private GamePerformanceVO gamePerformance;
    private MaintainUpdateVO maintainInfo;

    @Override
    public void validate() {
        if (StringUtils.isEmpty(playerId)) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (StringUtils.isEmpty(playerName)) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == height) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (height > 300) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == weight) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null != gamePerformance) {
            gamePerformance.validate();
        }
        if (null == maintainInfo) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        maintainInfo.validate();
    }
}

// 比賽表現值物件
public class GamePerformanceVO implements BizValidator {

    // 跑動距離
    private Double runDistance;
    // 傳球成功率
    private Double passSuccess;
    // 進球數
    private Integer scoreNum;

    @Override
    public void validate() {
        if (null == runDistance) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == passSuccess) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (Double.compare(passSuccess, 100) > 0) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == runDistance) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == scoreNum) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
    }
}

// 修改人值物件
public class MaintainUpdateVO implements BizValidator {

    // 修改人
    private String updator;
    // 修改時間
    private Date updateTime;

    @Override
    public void validate() {
        if (null == updator) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
        if (null == updateTime) {
            throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
        }
    }
}

// 領域服務
public class PlayerDomainService {

    @Resource
    private UserClientProxy userClientProxy;
    @Resource
    private PlayerRepository playerEntityMapper;
    @Resource
    private PlayerDomainAdapter playerDomainAdapter;
    @Resource
    private PlayerMessageSender playerMessageSender;

    public boolean updatePlayer(PlayerUpdateDomain player) {
        AssertUtil.notNull(player, new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT));
        player.validate();

        // 更新運動員資訊
        PlayerEntity entity = playerDomainAdapter.convertUpdate(player);
        playerEntityMapper.updateById(entity);

        // 發送更新訊息
        playerMessageSender.sendPlayerUpdatemessage(player);

        // 查詢用戶資訊
        UserSimpleBaseInfoVO userInfo 
            = userClientProxy.getUserInfo(player.getMaintainInfo().getUpdator());
        log.info("updatePlayer maintainInfo={}", JacksonUtil.bean2Json(userInfo));
        return true;
    }
}

4.5 application

本層關注橫向維度聚合領域服務,引出一種新物件稱為聚合物件,因為本層需要聚合多個維度,所以需要通過聚合物件聚合多領域屬性,例如提交訂單需要聚合商品、物流、優惠券多個領域,

// 訂單提交聚合物件
public class OrderSubmitAgg {

    // userId
    private String userId;

    // skuId
    private String skuId;

    // 購買量
    private Integer quantity;

    // 地址資訊
    private String addressId;

    // 可用優惠券
    private String couponId;
}

// 訂單應用服務
public class OrderApplicationService {

    @Resource
    private OrderDomainService orderDomainService;
    @Resource
    private CouponDomainService couponDomainService;
    @Resource
    private ProductDomainService productDomainService;

    // 提交訂單
    public String submitOrder(OrderSubmitAgg orderSumbitAgg) {

        // 訂單編號
        String orderId = generateOrderId();

        // 商品校驗
        productDomainService.queryBySkuId(orderSumbitAgg.getSkuId());

        // 扣減庫存
        productDomainService.subStock(orderSumbitAgg.getStockId(), orderSumbitAgg.getQuantity());

        // 優惠券校驗
        couponDomainService.validate(userId, couponId);

        // ......

        // 創建訂單
        OrderCreateDomain domain 
            = OrderApplicationAdapter.convert(orderSubmitAgg);
        orderDomainService.createOrder(domain);
        return orderId;
    }
}

4.5.1 專案結構

user-demo-service-application
    -/src/main/java
        -player
            -adapter
                -PlayerApplicationAdapter.java
            -agg
                -PlayerCreateAgg.java
                -PlayerUpdateAgg.java
            -service
                -PlayerApplicationService.java
        -game
            -listener
                -PlayerUpdateListener.java            // 監聽運動員更新事件

4.5.2 本專案依賴

  • util
  • domain
  • integration
  • infrastructure

4.5.3 核心代碼

本專案領域事件互動使用EventBus框架:

// 運動員應用服務
public class PlayerApplicationService {

    @Resource
    private LogDomainService logDomainService;
    @Resource
    private PlayerDomainService playerDomainService;
    @Resource
    private PlayerApplicationAdapter playerApplicationAdapter;

    public boolean updatePlayer(PlayerUpdateAgg agg) {
        // 運動員領域
        boolean result = playerDomainService.updatePlayer(agg.getPlayer());
        // 日志領域
        LogReportDomain logDomain 
            = playerApplicationAdapter.convert(agg.getPlayer().getPlayerName());
        logDomainService.log(logDomain);
        return result;
    }
}

// 比賽領域監聽運動員變更事件
public class PlayerUpdateListener {

    @Resource
    private GameDomainService gameDomainService;

    @PostConstruct
    public void init() {
        EventBusManager.register(this);
    }

    @Subscribe
    public void listen(PlayerUpdateEvent event) {
        // 更新比賽計劃
        gameDomainService.updateGameSchedule();
    }
}

4.6 facade + client

設計模式中有一種Facade模式,稱為門面模式或者外觀模式,這種模式提供一個簡潔對外語意,屏蔽內部系統復雜性,

client承載資料對外傳輸物件DTO,facade承載對外服務,必須滿足最小知識原則,無關資訊不必對外透出,這樣做有兩個優點:

  • 簡潔性:對外服務語意明確簡潔
  • 安全性:敏感欄位不能對外透出

4.6.1 專案結構

(1) client

user-demo-service-client
    -/src/main/java
        -base
            -dto
                -BaseDTO.java
            -error
                -BizException.java
                -BizErrorCode.java
            -event
                -BaseEventDTO.java
            -result
                -ResultDTO.java
        -player
            -dto
                -PlayerCreateDTO.java
                -PlayerQueryResultDTO.java
                -PlayerUpdateDTO.java
            -enums
                -PlayerMessageTypeEnum.java
            -service
                -PlayerClientService.java

(2) facade

user-demo-service-facade
    -/src/main/java
        -player
            -adapter
                -PlayerFacadeAdapter.java
            -impl
                -PlayerClientServiceImpl.java
        -game
            -adapter
                -GameFacadeAdapter.java
            -impl
                -GameClientServiceImpl.java

4.6.2 本專案依賴

client不依賴本專案其它模塊,這一點非常重要:因為client會被外部參考,必須保證本層簡潔和安全,

facade依賴本專案三個模塊:

  • domain
  • client
  • application

4.6.3 核心代碼

(1) DTO

以查詢運動員資訊為例,查詢結果DTO只封裝強業務欄位,運動員ID、創建時間、修改時間等業務不強欄位無須透出:

public class PlayerQueryResultDTO implements Serializable {
    private String playerName;
    private Integer height;
    private Integer weight;
    private GamePerformanceDTO gamePerformanceDTO;
}

(2) 客戶端服務

public interface PlayerClientService {
    public ResultDTO<PlayerQueryResultDTO> queryById(String playerId);
}

(3) 配接器

public class PlayerFacadeAdapter {

    // domain -> dto
    public PlayerQueryResultDTO convertQuery(PlayerQueryResultDomain domain) {
        if (null == domain) {
            return null;
        }
        PlayerQueryResultDTO result = new PlayerQueryResultDTO();
        result.setPlayerId(domain.getPlayerId());
        result.setPlayerName(domain.getPlayerName());
        result.setHeight(domain.getHeight());
        result.setWeight(domain.getWeight());
        if (null != domain.getGamePerformance()) {
            GamePerformanceDTO performance 
                = convertGamePerformance(domain.getGamePerformance());
            result.setGamePerformanceDTO(performance);
        }
        return result;
    }
}

(4) 服務實作

本層可以參考applicationService,也可以參考domainService,因為對于類似查詢等簡單業務場景,沒有多領域聚合,可以直接使用領域服務,

public class PlayerClientServiceImpl implements PlayerClientService {

    @Resource
    private PlayerDomainService playerDomainService;
    @Resource
    private PlayerFacadeAdapter playerFacadeAdapter;

    @Override
    public ResultDTO<PlayerQueryResultDTO> queryById(String playerId) {
        PlayerQueryResultDomain resultDomain 
          = playerDomainService.queryPlayerById(playerId);
        if (null == resultDomain) {
            return ResultCommonDTO.success();
        }
        PlayerQueryResultDTO result 
          = playerFacadeAdapter.convertQuery(resultDomain);
        return ResultCommonDTO.success(result);
    }
}

4.7 controller

facade服務實作可以作為RPC提供服務,controller則作為本專案HTTP介面提供服務,供前端呼叫,

controller需要注意HTTP相關特性,敏感資訊例如登陸用戶ID不能依賴前端傳遞,登陸后前端會在請求頭帶一個登陸用戶資訊,服務端需要從請求頭中獲取并決議,


4.7.1 專案結構

user-demo-service-controller
    -/src/main/java
        -controller
            -player
                -PlayerController.java
            -game
                -GameController.java

4.7.2 本專案依賴

  • facade

4.7.3 核心代碼

@RestController
@RequestMapping("/player")
public class PlayerController {

    @Resource
    private PlayerClientService playerClientService;

    @PostMapping("/add")
    public ResultDTO<Boolean> add(
        @RequestHeader("test-login-info") String loginUserId, 
        @RequestBody PlayerCreateDTO dto) {
        dto.setCreator(loginUserId);
        ResultCommonDTO<Boolean> resultDTO = playerClientService.addPlayer(dto);
        return resultDTO;
    }

    @PostMapping("/update")
    public ResultDTO<Boolean> update(
        @RequestHeader("test-login-info") String loginUserId, 
        @RequestBody PlayerUpdateDTO dto) {
        dto.setUpdator(loginUserId);
        ResultCommonDTO<Boolean> resultDTO = playerClientService.updatePlayer(dto);
        return resultDTO;
    }

    @GetMapping("/{playerId}/query")
    public ResultDTO<PlayerQueryResultDTO> queryById(
        @RequestHeader("test-login-info") String loginUserId, 
        @PathVariable("playerId") String playerId) {
        ResultCommonDTO<PlayerQueryResultDTO> resultDTO 
            = playerClientService.queryById(playerId);
        return resultDTO;
    }
}

4.8 boot

boot作為啟動層承載啟動入口


4.8.1 專案結構

所有模塊代碼均必須屬于com.user.demo.service子路徑:

user-demo-service-boot
    -/src/main/java
        -com.user.demo.service
            -MainApplication.java

4.8.2 依賴本專案

  • 所有模塊

4.8.3 核心代碼

@MapperScan("com.user.demo.service.infrastructure.*.mapper")
@SpringBootApplication
public class MainApplication {
    public static void main(final String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

5 文章總結

本文第一提出并回答了六個問題,第二介紹了DDD相關基本概念,第三介紹了DDD分析七大步驟,第四介紹了代碼分層結構,希望本文對大家有所幫助,歡迎大家關注公眾號「JAVA前線」查看更多精彩分享文章,主要包括原始碼分析、實際應用、架構思維、職場分享、產品思考等等,同時歡迎大家加我個人微信「java_front」一起交流學習,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/552354.html

標籤:領域驅動設計

上一篇:詳解快取更新策略及如何選擇

下一篇:返回列表

標籤雲
其他(158965) Python(38129) JavaScript(25420) Java(18034) C(15226) 區塊鏈(8265) C#(7972) AI(7469) 爪哇(7425) MySQL(7179) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5339) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4572) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1972) 功能(1967) Web開發(1951) HtmlCss(1936) python-3.x(1918) C++(1915) 弹簧靴(1913) xml(1889) PostgreSQL(1875) .NETCore(1860) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 長文多圖一步步講清楚:DDD理論、建模與代碼實作全流程

    1 六個問題 1.1 為什么使用DDD DDD方法論核心是將問題不斷分解,把大問題分解為小問題,大業務分解小領域,簡而言之就是分而治之,各個擊破。 分而治之是指直接面對大業務我們無從下手,需要按照一定方法進行分解,分解為高內聚的小領域,使得業務有邊界清晰,而這些小領域是我們有能力處理的,這就是領域驅 ......

    uj5u.com 2023-05-13 08:53:07 more
  • 詳解快取更新策略及如何選擇

    概述 快取更新是指在資料發生變化時,保持快取和資料庫的資料一致性的問題。如果快取和資料庫的資料不一致,會導致用戶看到過期或者錯誤的資料,影響業務邏輯和用戶體驗。 為了實作快取更新,我們可以采用以下四種方式其中的一種: Cache Aside策略:應用程式直接與資料庫和快取互動,并負責維護快取的一致性 ......

    uj5u.com 2023-05-13 08:52:47 more
  • 結構型模式(Structural Pattern)

    模式介紹 結構型模式(Structural Pattern)的主要目的就是將不同的類和物件組合在一起,形成更大或者更復雜的結構體。該模式并不是簡單地將這些類或物件擺放在一起,而是要提供它們之間的關聯方式。不同的結構型模式從不同的角度來組合類或物件,它們盡可能滿足各種面向物件設計原則的同時為類或物件的 ......

    uj5u.com 2023-05-12 10:05:14 more
  • 譯:從分布式微服務到單體

    原文:https://www.primevideotech.com/video-streaming/scaling-up-the-prime-video-audio-video-monitoring-service-and-reducing-costs-by-90 從分布式微服務架構遷移到整體式應用 ......

    uj5u.com 2023-05-12 10:05:04 more
  • 領域驅動設計之認知篇

    學習DDD的意義 作為技術人,都有一個成為大牛的夢。 有些人可以通過自己掌握了比較底層、有深度、有難度的技術來證明自己的能力。 但對于絕大多數的應用研發工程師來說,其大部分的時間精力,會被消耗在讀不懂、講不清的屎山代碼中,以及復雜多變的業務迭代中。很少會有需要去接觸高深技術的機會,即便是接觸了,也很 ......

    uj5u.com 2023-05-12 10:04:51 more
  • 結構型模式(Structural Pattern)

    模式介紹 結構型模式(Structural Pattern)的主要目的就是將不同的類和物件組合在一起,形成更大或者更復雜的結構體。該模式并不是簡單地將這些類或物件擺放在一起,而是要提供它們之間的關聯方式。不同的結構型模式從不同的角度來組合類或物件,它們盡可能滿足各種面向物件設計原則的同時為類或物件的 ......

    uj5u.com 2023-05-12 10:04:35 more
  • 領域驅動設計之認知篇

    學習DDD的意義 作為技術人,都有一個成為大牛的夢。 有些人可以通過自己掌握了比較底層、有深度、有難度的技術來證明自己的能力。 但對于絕大多數的應用研發工程師來說,其大部分的時間精力,會被消耗在讀不懂、講不清的屎山代碼中,以及復雜多變的業務迭代中。很少會有需要去接觸高深技術的機會,即便是接觸了,也很 ......

    uj5u.com 2023-05-12 10:04:16 more
  • 譯:從分布式微服務到單體

    原文:https://www.primevideotech.com/video-streaming/scaling-up-the-prime-video-audio-video-monitoring-service-and-reducing-costs-by-90 從分布式微服務架構遷移到整體式應用 ......

    uj5u.com 2023-05-12 10:03:54 more
  • 原型模式(Prototype Pattern)

    模式動機 原型模式(Prototype Pattern)結構較為簡單,它是一種特殊的創建型模式,當需要創建大量相同或者相似物件時,可以通過對一個已有物件的復制獲取更多物件。Java語言提供了較為簡單的原型模式解決方案,只需要創建一個原型物件,然后通過在類中定義的克隆方法復制自己。該模式應用較為廣泛, ......

    uj5u.com 2023-05-11 08:27:25 more
  • 原型模式(Prototype Pattern)

    模式動機 原型模式(Prototype Pattern)結構較為簡單,它是一種特殊的創建型模式,當需要創建大量相同或者相似物件時,可以通過對一個已有物件的復制獲取更多物件。Java語言提供了較為簡單的原型模式解決方案,只需要創建一個原型物件,然后通過在類中定義的克隆方法復制自己。該模式應用較為廣泛, ......

    uj5u.com 2023-05-11 08:27:11 more