應用分層和領域模型規約

前言

本文講述的應用分層和領域模型,是我自己根據業務實踐過程的一些思考,以及結合目前業界主流的業務規範和技術框架,綜合形成的一份實踐規約(說明文檔)。規約不是標準,主要用於指導自己日後的項目研發,歡迎大家參考討論。

應用分層

這是阿里巴巴 Java開發手冊(嵩山版) 第 6 章節 【工程結構】中推薦的分層結構,如下圖:

分層結構圖

分層解釋說明,請參考手冊原文,這裡僅講述我自己的理解。

  • 終端顯示層 和 開放API層 可以簡單理解為客戶端,主要用於發起 HTTP 或 RPC 請求。

  • Web層,也就是 Controller層,主要用於請求參數校驗、調用 Service層 處理業務邏輯和返回結果。

  • Service層,主要用於封裝實現業務邏輯,這裡重點說明一下 Manager層。
    Manager層,主要用於封裝 Service層 中的 通用 業務邏輯,實現業務邏輯的復用。注意,業務邏輯也是可復用的組件之一。

    通用的業務邏輯可能有哪些?舉幾個常用的場景:

    1. 快取;
    2. Dao 的組合復用;
    3. 其他,Service 層中多次出現的 套路 程式碼都可以考慮(不是必須)遷移至 Manager 層;
  • Dao層,主要用於封裝數據訪問邏輯,不只局限於資料庫,也可以是數據介面或其他第三方服務;

簡化一下分層結構圖:

分層結構圖

左邊的箭頭表示數據流入方向:客戶端 -> Controller 層 -> Service(Manager) 層 -> Dao 層;
右邊的箭頭表示數據流出方向:Dao層 -> Service(Manager)層 -> Controller 層 -> 客戶端;

數據在每一層之間的流動(流入和流出),它的邏輯業務含義和物理數據結構並不是完全一樣的,為了清晰地定義數據位於某一層時的狀態,就有了 領域模型 的概念。

領域模型

領域模型本質就是 POJO(Plain Old Java Object)。

什麼是 POJO?

Plain:簡單的、樸素的;
Old:老舊的,我曾經一度很好奇為什麼是 old?後來才理解這裡引申為最原始的、最開始的;
Java Object:Java 對象;

POJO 就是最簡單的、最原始的普通 Java 對象。

什麼是不普通的 Java 對象?

現在大部分的技術框架,都會要求 Java 對象繼承特定的類或實現特定的介面,或者被要求打上各式各樣的註解,這些 Java 對象就可以看作是不普通的。

領域模組由三部分組成:

  • 類名,表示業務含義;
  • 欄位,表示數據結構;
  • 方法,表示支援的操作;

阿里巴巴 Java開發手冊(嵩山版) 也給出了領域模型的參考:

  • DO(Data Object):此對象與資料庫表結構一一對應,通過DAO層向上傳輸數據源對象。
  • DTO(Data Transfer Object):數據傳輸對象,Service或Manager向外傳輸的對象。
  • BO(Business Object):業務對象,可以由Service層輸出的封裝業務邏輯的對象。
  • Query:數據查詢對象,各層接收上層的查詢請求。注意超過2個參數的查詢封裝,禁止使用Map類來傳輸。
  • VO(View Object):顯示層對象,通常是Web向模板渲染引擎層傳輸的對象。

網路上關於應用有哪些O,以及每一個O的解釋 五花八門,沒有對錯之分,各有各有道理。本文以 數據在應用分層之間的流動 為視角,講述一下我自己的理解。

QO(Query Object)

查詢對象,用於 Controller 層方法接收客戶端的請求參數。

以查詢對象 MyQuery 對例:

public class MyQo {
  private String param1;
  private String param2;

  ......
}

Controller 層方法中,查詢對象的創建有兩種形式:

  1. 框架自動創建 MyQo 對象,且完成請求參數和對象欄位的映射;
@PostMapping("/post")
public String post(@RequestBody MyQo qo) {
  ......
}
  1. 人工手動創建 MyQo 對象,且逐一完成請求參數和對象欄位的映射;
@GetMapping("/get")
public String get(@RequestParam String param1, @RequestParam String param2) {
  MyQo qo = new MyQo();

  qo.setParam1(param1);
  qo.setParam2(param2);

  ......
}

查詢對象創建完成之後,即可作為 Service 層方法的參數:

  service.doSomething(qo);
  ......

這一過程,數據由 客戶端 流入 Controller 層。

注意:如果請求參數數目較少,如:1個或2個,則可以不創建查詢對象,直接使用請求參數即可。

BO(Business Object)

業務對象,用於 Service 層方法內部邏輯處理,以及向上層(Controller 層)輸出業務對象。

  1. Service 層方法內部邏輯處理

以 CRUD 中的 Create 為例,假如我們需要創建一個業務對象(BO),Service 層方法大致可以劃分為三步:

1.1 查詢對象向業務對象的映射;

  MyBo bo = mapper.map(qo);

客戶端 將創建業務對象需要的多個欄位使用請求參數的形式流入 Controller 層;Controller 層使用查詢對象 qo 接收請求參數,並將查詢對象 qo 流入 Service 層,查詢對象 qo 中包含有創建業務對象所需的多個欄位;查詢對象中的欄位和業務對象中的欄位,欄位名稱和欄位數目不一定是一樣的(取決於業務場景),因此需要映射(mapper.map)。

:映射工具由不少的開源技術框架,本文不討論。

1.2 業務對象向數據對象(DO)的映射;

  MyDo do1 = mapper.map(bo);
  MyDo do2 = mapper.map2(bo);

數據對象(Do)對應資料庫的一張數據表,詳情見後。業務對象和數據對象不一定是一一對應的,通常一個業務對象對應著多個數據對象。

以簡歷對象為例,通常簡歷中會包含:

  • 教育經歷
  • 工作經歷
  • 項目經歷

這些經歷的數據會分別存儲在不同的數據表中,每一張數據表對應一個數據對象。也就是說,簡歷對象對應著 3 個或更多的數據對象,因此也需要映射(mapper.map 和 mapper.map2 分別表示將一個業務對象映射到不同的數據對象)。

1.3 調用 Dao 層方法保存數據對象;

  dao1.doSomething(do1);
  dao2.doSomething(do2);

Dao 層方法將數據對象持久化保存到對應的數據表內,然後向上層返回保存結果。

  1. Service 層方法向上層(Controller 層)輸出業務對象

以 CRUD 中的 Read 為例,假如我們需要讀取一個業務對象(BO),Service 層方法大致可以劃分為三步:

2.1 調用 Dao 層方法獲取數據對象;

  MyDo do1 = dao1.getSomething1(qo);
  MyDo do2 = dao2.getSomething2(qo);

Dao 層方法使用查詢對象(或者根據需要,將查詢對象映射為適合 Dao 層方法的查詢對象)為參數,讀取業務對象對應的若干數據對象。

2.1 數據對象向業務對象的映射;

  MyBo bo = mapper.map(do1, do2);
  
  return bo;

將若干數據對象映射為一個業務對象,然後向上層返回這個業務對象。

這一過程,數據由 Controller 層流入 Service 層,再由 Service 層流入 Dao 層;然後,數據反向流出。

DO(Data Object)

數據對象,每一個數據對象都對應著資料庫中的一張數據表,用於 Dao 層方法保存數據對象,以及向上層(Service 層)輸出數據對象。

  1. Dao 層方法保存數據對象;
  int saveMyDo(Mydo do);
  1. Dao 層方法向上層(Service 層)輸出數據對象;
  Mydo getMyDo(int id);

Dao 層數據對象的保存和讀取通常由技術框架幫助完成,不同技術框架實現細節不同,本文不討論相關內容。

VO(View Object)

顯示對象,用於 Controller 層方法返回客戶端的請求結果。顯示對象來源於 Service 層的數據反向流出,有以下3種情況:

  1. 業務對象

Service 層輸出 業務對象,通常見於 Read 場景,這時需要將業務對象映射為顯示對象:

  MyVo vo = mapper.map(bo);
  
  return vo;

客戶端不需要業務對象的全部欄位,或者業務對象的欄位需要組合/轉換之後才能符合客戶端的需求,因此需要映射;映射完成之後,即可以返回給客戶端。

  1. 操作結果

操作結果可能是ID、成功或失敗、0或1等,通過見於 Create/Update/Delete 場景,這時不需要映射,按協議(客戶端和服務端的約定)返回特定結果給客戶端即可。

  1. 什麼都沒有

Service 層方法的返回類型為 void,這種情況實際也是有返回結果的,如:有無異常,按協議返回特定結果給客戶端即可。

:Dao 層方法向 Service 層流出時也有類似的情況,不再贅述。

這一過程,數據由 Service 層流出至 Controller 層;然後,數據流出至客戶端。

DTO(Data Transfer Object)

數據傳輸對象,我自己理解用於客戶端和服務端之間數據交互的 QO 和 VO 就是很典型的 DTO,可參考這篇文章,不再贅述。

有另一種解讀,數據傳輸對象不僅可以用於端與端之間的數據交互,也可用於層與層之間的數據交互,這一點我也是贊同的。以業務對象為例,如果我們僅僅想查詢或更新業務對象的部分欄位,那麼是否需要為這些僅包含部分欄位的業務對象創建專門的模型對象,如:Dto。我的觀點是不需要,儘可能復用業務對象(注意不是必須)。那麼,如何使用業務對象表述部分欄位的業務對象?

業務對象的欄位要求全部使用對象類型,不使用基本類型。以 int 為例,定義欄位時使用 Integer 替代:

public class MyBo {
  private Integer param1;
  private Double param2;
  private String param3;
  private Object param4;
  ......
}

因為業務對象欄位的類型全部使用的是對象類型,我們就可以通過檢測欄位值是否為 null,以檢測結果為條件來執行條件操作

  • 查詢業務對象的部分欄位,那些不需要被讀取的欄位可以設置為 null,數據返回時忽略這些值為 null 的欄位;
  • 更新業務對象的部分欄位,那些不需要被更新的欄位可以設置為 null,數據更新時忽略這些值為 null 的欄位;

這個要求推薦應用至全部的領域模型對象。

這麼做的核心目標是保持系統設計的精簡,不為解決特定問題引入特定對象,盡最大程度復用已有對象。雖然會帶來實現過程具有一定的複雜度,但我認為值得。

小結

【規約】通俗的講就是 規則的約定,是一種大家(組織/團隊)共同約定好,並一起遵守執行的規則合集;它不是一般意義上的標準規則,不同的 大家 可以使用的規則是不一樣的,這一點特別注意。

另外,分層結構和領域模型的設計都是為業務服務的,設計的好壞最終還是要取決於業務效果,適合的就是好的。