Project Lombok 是很實用且被廣泛運用的語法糖 library,它可以減少許多樣板程式例如 getter, setter 等,我們只需幾個簡單的 annotation,例如 @Data,就能幫助我們省下大量重複的工作,讓開發者更專注於關鍵的邏輯,進而提高開發效率。但我認為 Lombok 也存在缺點,若過度使用 Lombok 可能導致問題,使程式碼難以維護。

lombok

Lombok 的 annotation 都是在編譯時才被轉換成 java code,因此早期在使用 Lombok 時,開發人員還要在 IDE 安裝套件才能正常使用 Lombok。時至今日,它已直接被整合進了 IntelliJ IDEA,不需額外安裝套件,只要引入 dependency 即可;但使用其他 IDE 的開發者就要比較麻煩一點了,可以參考官方的安裝教學,支援 Eclipse, VS code 等等。

Lombok @Data

Lombok 中的 @Data 應該是最常被使用的 annotation,到處都看得見這樣的程式碼:

@Data
public class Student {
    private int id;
    private String name;
    private String email; 
    // ...
}

根據官方的文件,它是由5個 Lombok annotation 組合而成,詳細成分如下:

@Getter: 為每一個 field 產生一個 getter。

@Setter: 為每一個 non-final field 產生一個 setter。

@RequiredArgsConstructor: 建立一個 constructor,其參數為 class 中所有 @NonNull, final field。

@ToString: 將每個 field 按順序並以逗號分隔,以 name=value 的形式組成字串,大幅提高可讀性。

@EqualsAndHashCode: 以所有 non-static 和 non-transient field 來實作 equals()hashCode()。如果 class 不能夠良好的 override 這兩個 method,導致無法如預期判斷兩物件的相等,一旦放入 HashSet 或當作 HashMap 的 key 值時,可能會引發 memory leak

這就是 Lombok 的強大之處!只需短短一行 @Data 就能產出數十行甚至上百行的程式碼。Lombok 雖然能減少開發負擔,也正因如此就導致有些開發者過度依賴了它,因此在使用 Lombok 之餘,應關心以下幾個議題:

Lombok 的議題

StackOverflowError

如果兩個之間的有雙向依賴關係,例如 Student, Teacher 兩類別互相依賴著,並且都標記了 @Data@ToString@EqualsAndHashCode,就會導致 StackOverflowError。 透過 Delombok 可以發現它們的 hashCode(), toString() 都存在循環引用,導致無窮迴圈,最後因記憶體不足引發錯誤。最好的解法就是藉由重構來消除雙向依賴的關係。

@Builder 的危險性

@BuilderBuilder Pattern 的語法糖,標記在 class 上就能優雅的建立一個 instance:

Student student = Student.builder()
    .id(12345)
    .name("KaiSheng")
    .email("KaiSheng714@github.com")
    .score(92)
    .build();

這在建立物件時還挺方便的,尤其是在測試裡。不過,它彈性優雅的風格相對帶來副作用。例如 field 太多時,容易導致開發者忘了賦值而創建出一個不完整的 instance,最後可能導致難以發現的 bug。因此應盡量按規格在必填的 field 加上 Lombok 的 @NonNull:如果沒有賦值,則立即拋出 NullPointerException,fail fast。又或者可以透過重構手段,讓 class 變小一點,減少開發者忘記賦值的機率。

備註: @NonNull 僅適用於非 primitive type。

與第三方 library 的衝突

Jackson 是一個可以幫助我們簡單、快速轉換 Java 物件與 json 的 library。若標記 @Builder,則無參數的 constructor 會被設成 package-private,這時若我們反序列化 json 字串,就會導致 Jackson 無法找到 constructor 而拋出 Exception:

Error on Jackson Deserialization
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Cannot construct instance of  ...  cannot deserialize from Object value 
(no delegate- or property-based Creator)

解法是在 class 上多加一個 @Jacksonized 即可。不過,我並不喜歡這個作法,因為要讓團隊花額外時間去了解、學習它的意義,太累了,如果越能降低專案的學習門檻就越好。順帶一提,Google Gson 能很好的處理 @Builder class,無需額外設定或標記。

隱藏複雜性

之前提過,Constructor Injection 暴露了 constructor 中含有過多參數 (Long Parameter List) 的壞味道,正常的開發者看到過多參數肯定會不舒服,表示它可能違反了 SRP 原則,需要被重構。

但我看過這樣的用法:使用 Lombok @AllArgsConstructor 輕鬆建立 constructor,取代 Spring @Autowired

@Component
@AllArgsConstructor
public class HelloBean {
 
   private final AnotherBean anotherBean;
   private final AnotherBean2 anotherBean2;
   private final AnotherBean3 anotherBean3;
   // ...
   private final AnotherBean15 anotherBean15;

   // method   
}

這會讓開發者看不到巨大的 constructor,即使注入很多個 dependency 看起來好像也沒關係。Lombok + constructor injection 的組合用法反而隱藏了複雜性。但如果團隊成員都有發現程式壞味道的意識、熟悉重構的手段,這種用法能使程式更簡潔,我認為是可被允許的。

Tell, Don’t Ask.

因為 Lombok 能方便快速產出所有 getter,就容易讓人忽略了 Tell, Don’t Ask 原則的建議。若物件中有過多不適當的 getter,會導致本不應被暴露的內部狀態被洩漏出去了,破壞了封裝 (Encapsulation)。

此原則提醒開發者:所謂 OOP 就是將資料與操作該資料的 method 綁在一起,設計出內聚力強的類別。與其在 service 層用了一堆 getter 詢問然後進行邏輯運算,不如直接請它做完再回傳出來。因此,必要時可在指定的 field 個別去標記 @Getter@Getter(AccessLevel.NONE),來決定是否要對外暴露哪個 field,以盡量符合此原則的精神。

我常用的做法

綜上所述,我認為 Lombok 還是最適合用於 Entity, Model。這樣用就足以應付大部分的狀況了:

@Log4j2
@Data
@Accessors(chain = true)
public class Student {
    private final int id;   // 可在必填欄位加上 final
    private String name;
    private String email;
    private int score;

    // 可自行實作 setter
    public void setEmail(String email) {
        EmailUtil.validate(email);  
        this.email = email;
    }

    // 實作業務邏輯
    public boolean isPassExam() {
        return this.score >= 60;
    }

}

在 class 標註 @Accessors(chain=true) 後就能以 Method Chaining 的方式建立 instance。另外,將必要的 field 加上 final,使用者就必須在 constructor 裡填入它:

Student student = new Student(12345)
    .setName("KaiSheng")
    .setEmail("KaiSheng714@github.com")
    .setScore(92);

可能有些人會問我:用 @Builder 不是也差不多嗎?但我的經驗是,因為我有時會用 Factory 來建立物件,這時如果在工廠裡再用 Builder Pattern 反而有點多此一舉,此外,我的方式不需要額外呼叫 builder(), build(),而且也不必透過 Lombok 實作整個 Builder Pattern 的程式碼,而是只有 setter 而已,變得單純許多,而且 setter 也是可以自行實作的。

另外,標註 @Log4j2@Slf4j,就可以使用 log 功能,非常方便,適用於任何 class,通常建議是需要時再標註上去即可。

結論

雖然 Lombok 雖然很方便,但世上沒有完美的工具,也沒有完美的用法。開發者們不僅要懂得取捨,也要根據 context 來決定該如何使用。只要能符合本身需求,同時考慮到可讀性與維護成本,並且盡量減少出錯的風險,就會是個好的用法。

此外,我們也要明白 Lombok 到底做了什麼,並且不要標記過多的 annotation 讓程式看起來更複雜(尤其是有邏輯的地方),也要注意與其他 library 衝突的可能性,否則萬一出了問題就會很麻煩。

References

更多你可能會感興趣的文章

Java

view: