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

来源:https://www.cnblogs.com/javafront/archive/2023/05/12/17393782.html
-Advertisement-
Play Games

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




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」一起交流學習。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者:Yejinrong/葉金榮 文章來源:GreatSQL社區原創 繼續吹MySQL 8.0~ 在以前,當需要對MySQL資料庫進行維護操作時,通常需 ...
  • (資料庫定時備份linux篇) 1 序言 相信大家都還記得這則新聞吧,歐洲雲計算巨頭 OVH 位於法國斯特拉斯堡的機房發生嚴重火災,大火徹底摧毀了五層高、占地 500 平方米的 SBG2 數據中心。 當地報紙稱 115 位消防員投入 6 個小時才將其撲滅。經過長達 6 個小時的持續燃燒,SBG2 內 ...
  • 摘要:本文以華為雲圖引擎 GES 為例,來介紹如何使用圖查詢語言 Cypher 表達一些需要做數據局部遍歷的場景。 本文分享自華為雲社區《使用 Cypher 子查詢進行圖探索 -- 以華為雲圖引擎 GES 為例》,作者:蜉蝣與海。 在圖資料庫/圖計算領域,很多查詢可以使用圖查詢語言Cypher、Gr ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 組件是前端框架的基本構建塊。把它們設計得更好會使我們的應用程式更容易改變和理解。在這節課中,分享一下在過去幾年中工作中學到的 9 個技巧。 1. 你可能不需要創建一個組件 在創建一個組件之前,看看它是為了可重用性和為某些UI添加一個狀態, ...
  • 準備工作: 1.首先進入https://ecs.console.aliyun.com/ 領取或者購買一臺簡單的ECS雲伺服器。 進入網站註冊登錄後拉到頁面最下麵或者頂部搜索免費雲伺服器領取立即試用 ,當然富哥花錢買一臺伺服器也行。 創建完了以後可以進入雲服務ECS工作台,然後就是以下界面 點擊右邊的 ...
  • JS的預解析是指在代碼執行之前,JavaScript引擎會先對代碼進行一次掃描,將變數聲明和函數聲明提升到當前作用域的頂部,以便在代碼執行時能夠正確地訪問這些變數和函數。這個過程也被稱為“提升”。 具體來說,在預解析過程中,JavaScript引擎會將函數聲明和變數聲明提升到當前作用域的頂部,而不管 ...
  • Java設計模式【單例模式】 單例模式 單例模式(Singleton Pattern)是一種創建型設計模式,其主要目的是確保一個類只有一個實例,並提供對該實例的唯一訪問點。 優缺點 優點: 提供了對唯一實例的受控訪問。 由於在系統記憶體中只存在一個對象,因此可以節約系統資源。 缺點: 單例類的擴展有很 ...
  • Java設計模式簡介(總結) 什麼是設計模式 Java設計模式是一組經過驗證的解決特定問題的編程技術,這些技術可以幫助開發人員快速、有效地開發高質量的軟體。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 設計模式分類 設計模式一般分為三大類:創建型、結構型、行為型,具體分類如 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...