---
# System prepended metadata

title: '[如何設計軟體?] 領域驅動設計: 4層架構 + 3類物件'

---

[如何設計軟體?] 領域驅動設計: 4層架構 + 3類物件
======
`有想法 x 也有做法`


{%youtube mducM_tUUEY %}

大綱
------
+ 前人的專案
+ 領域驅動設計
+ 理論與分層結構
+ 領域層的物件
+ 專案架構實作
+ 有想法也有做法

<br>

前人的專案
------
日常開發時，面對前人傳承下來的專案，改 A 壞 B ，改 B 壞 C，

各種系統模組瘋狂耦合與隨意分類，亂糟糟的系統架構，開發得很痛苦 !

### 已經沒救了！

基本上，專案發展到這個程度，

完全修復，不是不可能，只是成本很大 :

    修復的收成本 > 修復的收益

你我都知道，在實務上的時程安排 : 

	「有空再修」-> 美麗，但不切實際的幻想

### 重新再來
也許，有機會砍掉重練，或者在其他的專案，有機會重頭開始 !

但當面對全新的專案時，不希望重蹈覆徹

### 卻發現 !

    不是很確定該怎麼做 !?

<br>

領域驅動設計
------
### 學習之路
我軟體設計的「學習之路」，是從「無瑕的程式碼 整潔的軟體設計與架構篇」，這本書開始。

部分的理解內容，闡述在「[軟體的本質]」。

[軟體的本質]:https://youtu.be/njAgG9e1Qco

### 知行合一
但當時，還有一個疑問，書本內有很多理論與重要的設計原則。

不能很好的結合在日常開發中，中間的轉換過程遇到了瓶頸。

直到在「Spring Cloud 微服務架構 開發實戰」

**提到的架構的規劃方法 :**

    領域驅動設計

補足了我實作層面遇到的問題 !

讓我可以將設計的理論原則，結合到 Java Spring 的專案中。

據此延伸，架構的規劃是沒有語言與平臺的局限性。

開發 Android 與 iOS 的 App 時，也可以完全套用這一套軟體設計的方法論。

<br>

理論與分層結構
------
### 映射現實
軟體提供的服務，一定是會對映現實世界的某件事物。但開發者與使用者，看待軟體的角度，通常是不一樣的。如何在兩者之間，使用共同的語言，溝通會是一個挑戰。

### 領域驅動設計
`Domain Driven Design  簡稱 DDD`

以領域知識為核心建立的模型，領域專家與開發人員，可以透過這個**模型**進行交流。

    確保最終設計出來的東西，是雙方共同想要達到的結果


### 分層架構
領域驅動設計的分層結構，會分成四個部份。

![layer-1](https://lh3.googleusercontent.com/-Iwf7W_RraZI/X8EM95Qy52I/AAAAAAAACmo/BOwcHRqWJHU0yuLJMma4BuuHQ4Cmww5ewCLcBGAsYHQ/w640-h360/layer-1.png)


+ 用戶介面層 (User Interface)
+ 應用層 (Application Layer)
+ 領域層 (Domain Layer)
+ 基礎設施層 (Infrastructure Layer)

越往上，離使用者越接近，是客戶能夠理解的部分。

越往下，則離程式語言與平臺越接近，是開發者搭建系統的技術實作。

<br>

### 用戶介面層 (User Interface)
`用戶接口層 / 表示層（Presentaion）`

從字面意思上理解，就是 UI 介面。

這一層，負責向用戶顯示信息或解釋用戶指令。

除了給人看的介面，也有可能是給機器看的介面，使用者可能會透過另外一個系統來訪問你的系統。

**可以理解為這一層就是飛機的機場或貨輪的港口，負責國內外的進出口平台 :**

    進出的可以是人也可以是貨物


![user-interface](https://lh3.googleusercontent.com/-ZeC5LLiQG_8/X8ENFNhEZWI/AAAAAAAACms/N-O-MOdtcp0VawQILUzTwhmonm97OHYowCLcBGAsYHQ/w640-h360/user-interface.png)    

<br>

### 應用層 (Application Layer)

    定義軟體要完成的任務，並且指揮表達領域概念的對象來解決問題。


**用機場來說明的話，就是要上飛機前的海關檢查程序。**

![application](https://lh3.googleusercontent.com/-e4UOTMLbi1E/X8ENJAEqITI/AAAAAAAACm0/-4AILBBVqHglV7OakKoVG7DQiNwn3mZuACLcBGAsYHQ/w640-h360/application.png)

每一個站點，要調用的領域物件與服務，就是稽核人員、查核護照與檢查物品的動作。當出現異常時，依據程序，通知保安與巡警到達現場。


**整套協調任務、分配工作的流程，就是「應用」**

    應用 = 流程

**「應用」代表的是各種任務的背後的流程**

+ 用戶介面層 : 只需要知道，要完成他的工作，需要調用哪一個應用。
+ 應用層 : 只需要知道，要完成他的流程，需要調用領域層的哪個物件與服務。


    「應用層」只管流程步驟，本身並不介入任務的實際執行。



<br>

### 領域層 (Domain Layer)
`模型層(Model Layer)`

    主要負責表達業務概念、業務狀態信息及業務規則


**以海關的稽核人員來說 :**

![domain](https://lh3.googleusercontent.com/-J4txWy5gD2s/X8ENMIao4LI/AAAAAAAACm4/AoqxuiJ-dA0vOOv_0EsPhxzu4QsbeoDYwCLcBGAsYHQ/w640-h360/domain.png)

+ 業務 : 查核旅客的護照與身上的物品
+ 業務狀態訊息 : 查核哪些資訊與違禁物的清單
+ 業務規則 : 有違禁品時 要進行通報


    至於要調配哪一位，保安與巡警到達現場，則是管理中心 應用層的職責。

<br>

### 基礎設施層 (Infrastructure Layer)

    為上面各層提供通用的技術能力

**例如:**

+ **為應用層傳遞消息**
+ **為領域層提供數據訪問及持久化機制**
+ **為用戶介面層繪製屏幕組件**


**可以理解為，海關安檢時用的:**

+ X 光機
+ 金屬探測器
+ 電腦設備

![infra](https://lh3.googleusercontent.com/-GLRJy3qWrTE/X8ENNi0ZHbI/AAAAAAAACnA/I400deqGe4UEIDJ_pLZ0N4x0bxZn-nlUgCLcBGAsYHQ/w640-h360/infra.png)

<br>

### 層級關係

    層與層之間的調用，是由上而下，並且可以跨層呼叫
    但不會出現，由下而上或者平行呼叫的情況出現

![layer-2](https://lh3.googleusercontent.com/-Ql-9kUClXgg/X8ENR5FyHYI/AAAAAAAACnE/CL-PBRzgDCkHaEhR1Ub9VxDLvOHbH2ejwCLcBGAsYHQ/w640-h360/layer-2.png)

**就像是要請其他的部門協助支援:**

一定是循組織規章程序作業，遞交申請或者是向上通報，而不是直接跑到對方部門 ，在對方主管都不知情的情況下，要求同事協助幫忙。

**這個服務調用之間的順序與流程，也有一些原則可以遵循:**

    元件耦合性原則:
    ADP : 無循環依賴原則
    SDP : 穩定依賴原則
    SAP : 穩定抽象原則


這部分，是屬於「元件耦合性」的問題，後續會再獨立介紹。

<br>

領域層的物件
------
**領域驅動設計，可以理解為，以領域層為核心，驅動整個系統的設計方法。**

在跟領域專家討論與建構模型的順序: 

1. 領域層
2. 應用層
3. 用戶界面層

(領域專家不需要去管技術實作)

### 描述模型
領域層的模型建立，還缺了三個東西，用來描述這個模型 :

+ **實體**
+ **值對象**
+ **服務**

### 實體(Entity)

物件導向概念中的**「物件」**，並且帶有**「標示符」**的對象。

**POJO 物件**

```java
public class AppInfo {
    private Long id; // <- 「標示符」
    private String name;
    private String version;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}
```

**「標示符」**

    指的是當中的屬性有唯一識別碼，在經過軟體的各個分層結構時，仍然保持一致。

**例如:**

+ 人的身份字號
+ 資料庫產生的流水號主鍵




### 值對象(Value Object)
相似於實體(Entity)，兩者的差異在於值對象，沒有「標示符」。


**POJO 物件**

```java
public class AppInfo {
    private String name; // <- 沒有「標示符」
    private String version;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}
```

**為什麼沒有標示符 ?**

    因為有時候，人們對於某個對象是什麼並不感興趣，
    只關心他所擁有的屬性，能夠描述領域的特殊方面，即可完成任務。


### 服務(Service)
跟實體與值對象不太一樣，在與領域專家規劃模型時，會發現領域中有些方面是很難映射成對象。

尤其是領域中的動作，不屬於任何對象，卻代表了一個重要行為，此種行為通常會跨越若干的對象。

將此行為加入到任一對象中會破壞對象。


    對象 = 物件 = 實體 / 值對象

**最佳實做的方式是將它當成一個服務(Service)**

<br>

**可以簡單地理解:**

    實體與值對象 -> 名詞
    服務 -> 動詞

**名詞與動詞兩者互相搭配，描述了領域中的一項任務。**

<br>

專案架構實作
------
### 改造目標
`Java 最常見的 Spring 框架示範如何調整`

一般 Spring 框架，書籍中的範例，通常會切分成三個階層 : 

+ Controller : @Controller 的分一個資料夾放
+ Service : @Service 也分一個資料夾來放
+ DAO : 所有有關資料庫存取的服務與對象也分一個資料夾放

![spring-sample-layer](https://lh3.googleusercontent.com/-_cWY2Yia2gQ/X8ENWrxm9RI/AAAAAAAACnI/Sp4IidD4C64zal9k_YfRhIUnraELwRFJACLcBGAsYHQ/w640-h346/spring-sample-layer.png)

**優勢**  

+ 簡單
+ 快速實現

**缺點**

+ 服務多的時候會變得複雜
+ Service 與 DAO 深度耦合
+ Service 不能進行單元測試

<br>

### 領域分層
`套用領域驅動設計的四個分層`

**用戶界面層 (User Interface)**

對應的是 Controller 保持不變

<br>

**應用層 與 領域層 對應的是 Service**

拆分成兩個 : 

+ 應用服務層 (Application Service) 
+ 領域服務層 (Domain Service)

<br>

**基礎設施層，對應的是 DAO**

DAO 該算是基礎設施層的一個子項模組

所以在基礎設施層，劃分一個數據庫的區塊存放

    Infrastructure/repository

<br>

**基礎設施層，細節部分**

![spring-boot-infra](https://lh3.googleusercontent.com/-Z-uI_V3WPF8/X8ENcC2l9zI/AAAAAAAACnQ/L8FhLt9v_gk_f70Hb_c1KN83N2N1Iy4hgCLcBGAsYHQ/w640-h569/spring-boot-infra.png)

+ util/ : 共用元件的資料夾 
+ filter/ : Spring 設定

<br>

**領域層，細節的部分:**

![spring-boot-domain](https://lh3.googleusercontent.com/-zz23DFdbp6o/X8ENcC9Q7GI/AAAAAAAACnU/wXv8BCgmhTsP_lkFYPzlvGYIHaQl7LI3wCLcBGAsYHQ/w640-h569/spring-boot-domain.png)

會在切分 實體 Entity 資料夾 與 值對象 Value Object 資料夾

**此部分可以分類的更細**

    元件內聚性原則:
    REP : 再使用性 - 發佈等價原則
    CCP : 共同封閉原則
    CRP : 共同重複使用原則


這部分，是屬於「元件內聚性」的問題，後續會再獨立介紹。

<br>

**應用層，細節的部分:**

![spring-boot-application](https://lh3.googleusercontent.com/-1aMpjk5D5UA/X8ENdJ8Af9I/AAAAAAAACnY/FXPaIn1FcyM8YjYg77cwmq_1XBzr-dlTgCLcBGAsYHQ/w640-h569/spring-boot-application.png)

由於應用層，代表的是一連串動作的最終結果，返回的結果，可能包含了很多，實體與值對象的內容。

所以，會在應用層內，劃分一個 DTO 資料夾

    DTO 代表的是 Data Transfer Object，資料傳輸物件


資料傳輸物件，用於應用層的服務，資料物件的輸入與輸出。

**通常，會以領域層的實體或值對象繼承實作:**

```java
public class AppInfoDTO extends AppInfo {
    private String groupId;
    private String groupName;
    private String groupDescription;

    public String getGroupId() {
        return groupId;
    }

    public void setGroupId(String groupId) {
        this.groupId = groupId;
    }

    public String getGroupName() {
        return groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }

    public String getGroupDescription() {
        return groupDescription;
    }

    public void setGroupDescription(String groupDescription) {
        this.groupDescription = groupDescription;
    }
}
```

除了，直接擴展，省去了重新建構的時間以外，兩者之間也建立了強關聯，

一眼就可以看出這個傳輸物件，是屬於哪一類的領域項目。

<br>

**資料訪問服務，細節部分:**

![spring-boot-dao](https://lh3.googleusercontent.com/-XxAdHZqw-1Q/X8ENi1AodaI/AAAAAAAACnk/sz3DTs7jxMcvJdsyOaaiFfoTq0tDs0F9QCLcBGAsYHQ/spring-boot-dao.png)

若持久化框架，映射的「持久化物件」允許繼承(ORM 框架 : MyBatis )

     持久化物件: 
     PO , Persistent Object

同樣會讓該物件繼承領域層的實體或值對象

```java
public class AppInfoPO extends AppInfo {
    public String createUser;
    public String createTime;
    public String modifyUser;
    public String modifyTime;

    public String getCreateUser() {
        return createUser;
    }

    public void setCreateUser(String createUser) {
        this.createUser = createUser;
    }

    public String getCreateTime() {
        return createTime;
    }

    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }

    public String getModifyUser() {
        return modifyUser;
    }

    public void setModifyUser(String modifyUser) {
        this.modifyUser = modifyUser;
    }

    public String getModifyTime() {
        return modifyTime;
    }

    public void setModifyTime(String modifyTime) {
        this.modifyTime = modifyTime;
    }
}
```

擴增的部分，通常是資料庫資料表才會特別紀錄的數據:

+ 新增使用者
+ 修改使用者
+ 新增時間
+ 修改時間

<br>

### 單元測試
以上的安排規劃，使得領域層的服務，只會依賴於同層的領域物件;

不需要基礎設施層的支持，就可以單獨的進行單元測試。

![service-test](https://lh3.googleusercontent.com/-Z057-QBNu6g/X8ENmH8exxI/AAAAAAAACno/LPEiNQp4eAcs_ZP2BnnSgvezsBlLEyOzgCLcBGAsYHQ/service-test.png)

### 架構無國界
這種安排規劃，也可以套用在其他種程式語言與平台上

**Android**

![AndroidProjSE](https://lh3.googleusercontent.com/-oZM1ZR5o17s/X8ENoLpsxTI/AAAAAAAACns/1oOOVQQWZeE4521h_hcafpJSBX-g6dHggCLcBGAsYHQ/w581-h640/AndroidProjSE.png)

+ Android 的 Activity 就是 使用者介面層
+ Android SDK 提供的各種服務 就是基礎設施層
+ 應用層與領域層甚至連改都不用改

**iOS**

![iOSProjSE](https://lh3.googleusercontent.com/-SHMwb6YsPPA/X8ENpv1oj9I/AAAAAAAACnw/k8fe8kp7TKUHcUTd6PfXt4-f9gmRDv4NACLcBGAsYHQ/w445-h640/iOSProjSE.png)

+ iOS 的 Controller 與 SDK 也是同樣道理

<br>

有想法也有做法
------
伊隆 • 馬斯克 : 
```
如果你就讀工程科系，並且對設計東西很有想法的話
自己創業，是個相對簡單的事情。

你需要的就只是找幾個和你理念相同的夥伴。

-- Youtube : 伊隆‧瑪斯克對學生和大學畢業生的終極建議 - 如何擁有成功人生
```

你如果對設計有想法的話，現在更有了作法，起碼明天上班時，就可以試著去優化你的系統。

我這個結合了書籍知識以及個人經驗的，專案結構的規劃方案，也是經過了一段時間的討論，

才被團隊逐漸認可，並且套用到下一個新的專案項目中。

    事情並非一簇可幾，但起碼有了方向。


並且我認為軟體設計的思維模式，也是一種大局觀的思考方式 :

    如果你可以組織映射現實的軟體架構，那麼你當然也可以組織現實的實際事物。



**未來的各種挑戰，說不定哪時候就可以派上用場。**



<br>

參考資料
------
+ Clean Architecture 整潔的軟體設計與架構篇
+ Spring Cloud 微服務架構 開發實戰
+ [伊隆‧瑪斯克對學生和大學畢業生的終極建議 - 如何擁有成功人生]

[伊隆‧瑪斯克對學生和大學畢業生的終極建議 - 如何擁有成功人生]:https://youtu.be/iuNJLtj10Lg