用Java編寫更好的不可變DTO的技巧 - Seb

發表於2021-04-27

為了使用來自外部服務的資料,我們通常將JSON有效負載轉換為資料傳輸物件(DTO)。快速處理DTO的程式碼變得很複雜,但是一些技巧可以有所幫助。我們可以編寫易於互動的DTO,使客戶端程式碼更易於編寫和閱讀的DTO。這些技巧一起使用,有助於使其保持簡單。

讓我們從使用JSON的典型方法開始:

{ 
  "name": "Regina", 
  "ingredients": ["Ham", "Mushrooms", "Mozzarella", "Tomato purée"]
}

建立了一個名為的簡單DTO PizzaDto:

import java.util.List;

public static class PizzaDto {
  private String name;
  private List<String> ingredients;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
  
  public List<String> getIngredients() {
    return ingredients;
  }
  
  public void setIngredients(List<String> ingredients) {
    this.ingredients = ingredients;
  }
}

PizzaDto是一個普通的Java舊物件:帶有屬性,獲取器,設定器的物件,僅此而已。它反映了JSON結構,因此物件和JSON之間的轉換隻是一種形式。這是Jackson庫的示例:

String json = """
  { 
    "name": "Regina",
    "ingredients": [ "Ham", "Mushrooms", "Mozzarella", "Tomato purée" ]
  }
""";

// from JSON to Object
PizzaDto dto = new ObjectMapper().readValue(json, PizzaDto.class);

// from Object to JSON
json = new ObjectMapper().writeValueAsString(dto);

轉換很簡單。所以有什麼問題?

在現實生活中,DTO可能非常複雜。建立和初始化DTO的程式碼可能非常龐大:有時需要數十行程式碼。有時更多。這是一個問題,因為複雜的程式碼包含更多的錯誤,並且對更改的響應較慢。

我簡化DTO建立的第一步是使用不可變的 DTO:建立後無法修改的DTO。

 

建立不可變的DTO

當物件在構造後無法更改時,它是不可變的。

讓我們重寫PizzaDto使其不可變

import java.util.List;

public class PizzaDto {

    private final String name;              
    private final List<String> ingredients;

    public PizzaDto(String name, List<String> ingredients) {
        this.name = name;
        if (ingredients != null) {
            ingredients = List.copyOf(ingredients);
        }
        this.ingredients = ingredients;
    }

    public String getName() {
        return name;
    }
    
    public List<String> getIngredients() {
        return ingredients;
    }
}

不可變版本沒有設定器setter。所有屬性均為最終屬性,必須在構造時進行初始化。

Effective Java的作者Joshua Bloch提出了建立不可變類的建議:

“如果您的類具有引用可變物件的任何欄位,請確保該類的客戶端無法獲取對這些物件的引用。” 約書亞·布洛赫(Joshua Bloch)

如果DTO的任何屬性是可變的,則需要製作防禦性副本。使用防禦性副本,可以保護DTO免受外部修改。

好的。現在,我們有了一個不變的DTO。但是,它如何簡化程式碼?

 

不變性的好處

不變性帶來很多好處,但這是我的最愛:不變變數沒有副作用。

我們來看一個例子。此程式碼段中有一個錯誤:

var pizza = make();
verify(pizza);
serve(pizza);

執行此程式碼後,pizza沒有達到預期的狀態。哪裡引起了問題?

我們將考慮2個答案:首先是一個可變變數,然後是一個不可變變數。

第一個答案,加上可變的比薩餅。pizza由make()建立,但可以在verify()和serve()中進行修改。因此,該錯誤可能來自3條可能中的任何一個。

現在,第二個答案:如果是一個不變的披薩。make()返回一個比薩餅,但verify()並serve()不能對其進行修改。問題只能來自make()。在這裡,查錯範圍要小得多。該錯誤更容易找到。

由於比薩餅是不變的,verify()因此不能僅將其修復。它必須建立並返回一個修改過的披薩,並且必須修改客戶端程式碼:

var pizza = make();
pizza = verify(pizza);
serve(pizza);

在這個新版本中,很明顯會verify()返回一個新的不變披薩。不變性使您的程式碼更加明確。它變得更容易閱讀和發展。

您可能不知道,但是我們已經每天都在使用不可變的物件。java.lang.String,java.math.BigDecimal,java.io.File是不可改變的。不變性還具有許多其他優點。約書亞·布洛赫(Joshua Bloch)在他的《有效Java》中只是建議“最小化可變性”。

不可變的類比可變的類更容易設計,實現和使用。它們不太容易出錯,並且更安全。約書亞·布洛赫(Joshua Bloch)

 

序列化DTO

Jackson是Java中最常見的JSON庫。當你DTO有getters和setters時, ,Jackson無需任何額外配置即可將物件對映到JSON。但是對於不可變的物體,傑克遜需要一點幫助。它需要知道如何構造物件。

必須使用註釋物件的建構函式@JsonCreator,並使用註釋每個引數。

傑克遜還有另一個好處。如果我們在建構函式中放入一些邏輯,則無論DTO是由應用程式程式碼建立還是由JSON生成,都將始終呼叫該邏輯。

我們可以利用這一點,避免使用null值。我們可以改進建構函式以使用非空值初始化欄位。

/ new import :

// import static org.apache.commons.lang3.ObjectUtils.firstNonNull;

@JsonCreator

public PizzaDto(

        @JsonProperty("name") String name,

        @JsonProperty("ingredients")

List<String> ingredients) {

    this.name = firstNonNull(name, "");  // replace null by empty String 

    this.ingredients = List.copyOf(

        firstNonNull(ingredients, List.of())  // replace null by empty List

    );

}

如果我們用“”值替換空null值,則客戶端可以使用DTO屬性,而無需先檢查它是否不為空。另外,它降低了獲得NullPointerExceptions的機會。

有了這個技巧,您可以減少編寫程式碼,並提高魯棒性。我們如何做得更好?

 

使用Builders建立DTO

構建器提供了一個流暢的API,以促進DTO初始化。

var pizza = new PizzaDto.Builder()
        .name("Regina")
        .ingredients("Mozzarella cheese", "Basil leaves", "Olive oil", "Tomato purée")
        .build();

使用複雜的DTO,構建器可以使程式碼更具表現力。這種模式是如此出色.

public static final class Builder {
    
    private String name;
    private List<String> ingredients;

    public Builder name(String name) {
        this.name = name;
        return this;
    }

    public Builder ingredients(List<String> ingredients) {
        this.ingredients = ingredients;
        return this;
    }
    
    /**
      * overloads {@link Builder#ingredients(List)} to accept String varargs
      */
    public Builder ingredients(String... ingredients) { 
        return ingredients(List.of(ingredients));
    }

    public PizzaDto build() {
        return new PizzaDto(name, ingredients);
    }
}

有些人在編譯時使用Lombok來建立構建器。這使DTO變得簡單。

我更喜歡使用Builder生成器IntelliJ外掛生成生成器程式碼。然後,可以像上一片段一樣新增方法過載。構建器更靈活,客戶端程式碼更精簡。

 

結論

這些是我用來編寫DTO的主要技巧。一起使用,它們確實可以改善您的程式碼。該程式碼庫更易於閱讀,更易於維護,並且最終更易於與您的團隊共享。

 

相關文章