程式猿吃香蕉
Published in

程式猿吃香蕉

Java ♨️ 客製化 Lombok @Builder 的方法 (一):傳入 Null 採用預設值

筆者曾任職 Yahoo ,《軟體需求溝通 ─ 從外商公司學跨部門協作開發》線上課程講師,紛絲團《程式猿吃香蕉🍌

Lombok 是 Java 開發時常用的函式庫,它提供簡單的 Annotation ,在編譯時自動產生程式碼,讓工程師可以少打很多字。其中, Lombok 有一個很常用的 Annotation 叫做 @Builder ,可以幫助我們快速採用 Builder Pattern 建立物件。

▍@Builder 的妙用

《Effective Java》書中提到,Builder Pattern 是當物件有多個參數 (parameters) 時,建立物件更好的方式。

The builder pattern is a good choice when designing classes whose constructors or static factories would have more than a handful of parameters.

— Effective Java, 2nd Edition

Builder Pattern 具體長什麼樣子呢?我們直接來看程式碼,假設我們要建立一個 Person 物件,裡面分別有 name 和 age 兩個欄位要透過參數設定,Builder Pattern 可以這樣建立物件:

從 Builder Pattern 建立物件的方式,我們可以發現,有別於傳統使用建構子 (Constructor) 建立物件的方式,Builder Pattern 的寫法可以讓呼叫方更靈活地設定欄位值。接下來,我們分別用 Lombok @Builder和原生 Java 程式碼來實作 Builder Pattern 的細節,程式碼範例如下:

⊙ 採用 Lombok @Builder 程式碼實作

⊙ 原生 Java 程式碼實作

我們可以看到,在相同功能下,使用 Lombok 可以大幅減少程式碼撰寫的數量。

雖然 Lombok 很方便,但實際開發時,預設的 Builder 行為不一定完全符合我們的需要,對它做客製化是經常出現的需求。因此,我將整理系列文章,分享在不同情境下,幾種客製化 Lombok @Builder的方法。

首先來看第一個情境:有關於「預設值」的難題。當我們建立 Person 物件時,裡面的欄位 name 和 age (如下圖),要怎麼給它們預設值呢?

▍情境:傳入 Null 時,怎麼給欄位預設值 ?

大多數情況,若要給某個物件內欄位預設值,只要在該欄位加上 @Builder.Default 上就可以了,但這不包含該欄位被傳入 null 的情況。我們通常希望欄位被傳入 null 的時候,會採用預設值,如果你是這樣期待的話,很遺憾地

傳入 null 時,@Builder.Default 的預設值會失效

舉個例子,假設我們在 Person 物件多加一個 bonusPoint 欄位,給它預設值 0 ,我們可以這樣寫,如下圖所示:

當我們建立物件的時候 (如下圖),若沒有調用 bonusPoint() ,bonusPoint 會是預設值 0。注意!即使我們 bonusPoint 設定成 final 也會生效,在編譯時 Lombok 會幫我們覆寫掉原本的 Person 類別 (Class)。

看起來很美好,那問題出在哪呢?

如果我們建立物件時 (如下圖),不小心傳了 null 到 bonusPoint() ,bonusPoint 就會被設定成 null,而不是預設值 0。

讀者可能會想說,我怎麼可能傳 null 進去? 其實還真不一定,因為:

開發時,傳入值通常是來自另一個變數 (variable),而那個值可能是 null。

舉個例子,我們以外部變數 userRequest 來設定 Person (如下圖),其中 userRequest.bonusPoint 是從前端傳過來的參數,有可能是 null。

這情形通常發生在 API 新增欄位的情況 (經常發生),後端需要作向後相容 (Backward Compatible),某些尚未升級的前端程式發來的請求欄位值可能是 null,面對這個問題,當然,我們可以多寫一段 if 判斷式,來決定預設值:

只是這樣寫的話, bonusPoint 的不變量 (invaraint) 條件就洩漏出去了,沒有封裝在 Person 物件裏面,不符合物件導向開發的精神,也讓程式碼變得很亂。

那該怎麼做呢?以下分享兩種做法。

▍來客製化 @Builder 吧!

(以下實做基於 Lombok 1.18.8)

❶ 從 Getter 下手,若 null 回傳預設值

這個解法的思路是:

我們既然無法在「放入時」設定預設值,就改在「取出時」返回預設值。

因此,我們實做一個 Getter 為 getBonusPoint(),判斷當 bonusPoint 被設定為 null 或是沒被設定時,就返回預設值,程式碼範例如下:

如果你習慣用 Lombok 的 @Getter 來做 Getter,可以這樣寫:

記得在欄位加上 @Getter(AccessLevel.NONE) ,讓我們可以自行實作 Getter。

這麼一來,當使用 Person 時,不管 userRequest.bonusPoint 是 null ,或 bonusPoint 沒設定,都可以返回預設值了,以下示範呼叫方的使用情境:

(userRequest.bonusPoint 是 null 的情境)
(bonusPoint 沒設定的情境)

因為我們是從 Getter 下手,所以 bonusPoint 的值其實沒有「真真正正地」被設定,它本身可能是 null,只是呼叫方都是透過 getBonusPoint() 讀取值,所以可以有預設值返回。

「從 Getter 下手,若 null 回傳預設值」的做法有兩個重點:

  • (1) 欄位要設定為 private 的,必須確保只有唯一的 getter (範例裡為 getBonusPoint) 可以取出來欄位。
  • (2) 當物件內部有其他邏輯時,要記得統一使用 getter 來取欄位,以免讀到 null 值。

以下程式碼是錯誤實作的例子:

從上述程式碼我們可以看到,第 6 行如果 bonusPoint 不是 private 而是 public,外部直接讀取 bonusPoint 就會讀到 null 而非預設值。另外,在第12 行物件裏面邏輯 getDiscount() 用到 bonusPoint 時,也要記得改用 getBonusPoint() 做欄位讀取,否則也可能會讀到 null。

接下來我們來看第二種做法。

❷ 放棄 final 修飾字,在 Builder 裡初始化

我們已知 Lombok 會自動幫我們生成 Builder 類別,因此這個解法的思路是:

我們何不自行定義 Builder 類別,來做欄位的初始化呢?

Lombok 建立的 Builder 類別名稱,會依據 @Builder它所在類別 (Class) 做決定,命名方式是加上 Builder 後綴。舉例來說,當 @Builder 放在 Person 類別,就會對應產生 PersonBuilder 類別 (加上了 Builder 後綴),因此我們就自行實做一個 PersonBuilder 類別,來初始化 bonusPoint 欄位,程式碼範例如下:

這個做法同樣有兩個重點:

  • (1) 欄位不能是 final,因為我們先自行初始化了預設值,final 會讓欄位無法被二次設定。如果設定成 final,值就永遠是預設值了。
  • (2) 要實作設定方法 bonusPoint() ,避免 null 被呼叫方設定進來。

雖然這個做法有個小缺點,就是不能使用 final ,但這個缺點需要看情況:

只有當物件內部要拿欄位值做操作時,不能使用 final 才會是問題。

否則一般情況下,採用 private 對欄位做保護就可以了。

此外,跟上個方法比起來,這個做法是「真真實實地」把值設定到 bonusPoint,當我們的物件內部邏輯有用到 bonusPoint 時,可以不用強制透過 getBonusPoint() 取用,誤用的情況會少很多。

▍小結

有時候函式庫很方便也很好用,但身為工程師不應該被函式庫束縛住。在需要客製化的時候,便考驗動手實作的能力。以上分享兩種方法:

  • ❶ 從 Getter 下手,若 null 回傳預設值
  • ❷ 放棄 final 修飾字,在 Builder 裡初始化

解題思路分別是從「輸出」和「輸入」兩端去掐頭捉尾,達成返回預設值的功能。不管未來是不是有更好用的函式庫,這樣的實作練習都是很有趣的。下篇文章也會繼續分享,在其他情境下,客製化 Lombok @Builder 的作法。

若是喜歡我分享的內容,可以訂閱我的粉絲團《程式猿吃香蕉🍌,在軟體開發的路上,一起分享交流,一起成長。

--

--

我們是一群軟體開發愛好者,喜歡將軟體知識以簡單生動的方式講給你聽,順口好消化,營養又健康!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Jayden Lin

Jayden Lin

1.7K Followers

Yahoo 擔任 Lead Engineer,負責廣告系統,帶團隊做跨國開發。也是《程式猿吃香蕉》團隊創辦人,喜歡將實用的軟體知識以簡單生動的方式講給大家聽 😄😄😄