Clean Code 系列之 DDD 分層參數轉換

先看一段簡單的代碼:

package com.zhuxingsheng.adapter
@PostMapping("/login")
public LoginResponse login(LoginRequest loginRequest) {
    return loginService.login(loginReqeust);
}

從代碼中,可以明顯看出這是一段處理登陸請求的方法。在大多數項目中,這種代碼很常見。

它有什麼壞味道呢?

分層穿透了,LoginRequest 類本應該屬於入口層,結果穿透到了 service 層。

細細追究,需要明確的問題:

1、LoginRequest 到底屬於哪一層,是 resource 層,還是 service 層?

2、沒有達到 DDD 防腐層的意義,resource 是隔離外部與核心業務的,但卻變成了透傳。

歸屬哪一層

《再議 DDD 分層》[1] 中,也討論過。

當前系統是以 REST 方式對外提供服務,如果後面需要以 RPC 方式對外提供服務,顯然 LoginRequest 可能不再適用。

從圖中可以看出 REST 方式是 Controller,而如果是 thrift 方式是 TService。controller 的 LoginRequest 參數,會在 TService 中失效。在實現層面,LoginRequest 本質上就是個 DTO,傳輸數據。而且不再像過去原始 servlet,傳輸數據時會有很多原生 API 類型,現在的框架都進化了,request 對象中只有業務屬性。

從這個角度講,request 對象是在 resource 層,並且是與各個實現框架綁定的。

另外,resource 層還需要處理 request 參數的檢驗與轉換。如果直接透傳到 service 層,不僅加重了 service 的職責,而且對於 service 層,我們更推薦使用 ADT 方式,讓代碼更有業務語義,不使用單純的技術基礎類型。

總結一下整體結構就是這樣:

DDD 防腐層

DDD 中有限界上下文,而且限界上下文之間需要高度自治、隔離變化,防腐層因運而生。

而這兒的 LoginRequest 就是兩個限界上下文的通信數據,而核心業務層是有對應的業務對象承接數據。

這樣有常見的兩個問題

1、代碼重複

《DDD 實戰指南》[2] 中提出,我們引入 CQRS 架構中的概念,業務層有對應 cmd 和 query 對象。

如 LoginRequest 到 LoginCmd 轉換,但兩個類的內容都一樣

package com.zhuxingsheng.adapter.pl
public class LoginCmd {
    private String username;
    private String password;
}

如果是這樣,那我們參數校驗邏輯是不是得寫到 service 裏面,不然校驗邏輯也要重複了。

當測試代碼時,controller 的測試與 service 的測試是一致的,use case 是相同的。

怎麼應對,還是上文提到的 ADT 方式,對於 service 層,不再提倡使用技術層面的基本類型,如 username 屬性包裝成 Username 類,而校驗邏輯可以封裝在 Username 中

package com.zhuxingsheng.adapter.pl
public class Username {
    private String username;
    public Username(String username) {
        if (username == null) {
            throw   new ValidationException("username不能爲空");
        } else if (isValid(username)) {
            throw   new ValidationException("username格式錯誤");
        }
        this.username = username;
    }
}

這樣對於 service 層的方法業務語義更加顯現化。

public void login(Username username,Password password) {
}

對於 login 方法的測試,use case 數量相對基礎類型也變少了。

對於複雜對象的轉換,可以使用 mapstruct,既方便,性能也高效。

2、代碼複用

比如創建文章,編輯文章,兩者入參差不多,只是創建時沒有 id, 而編輯時有 id,從代碼複用角度,不想類的膨脹,DTO 只創建一個。會出現一個 dto 會有很多很多的屬性。

但從業務語義角度,兩個業務行爲就不應該共用同一個對象。需要有 CreateArticleCmd 和 EditArticleCmd

而對於 request dto 的數量,從友好 API 角度,應該要有兩個 DTO,但如果是複雜的查詢操作,query dto 屬性數量比 command dto 更多些。

總結

clean code,這篇主要闡述了分層架構中傳輸對象與業務對象職責不清問題。

應對策略:

大到一個微服務,小到一個變量,SRP 原則無處不在。

DTO 屬性要明確簡單,業務對象要語義清晰顯現化。

References

[1] 《再議 DDD 分層》: https://www.zhuxingsheng.com/blog/further-discussion-on-ddd-layering.html
[2] 《DDD 實戰指南》: https://www.zhuxingsheng.com/blog/ddd-tactical-practice-guide.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wfFrf38CPRhDJnDf4we9qg