Lombok是不少Java開發者會用到的一個很是方便的Java庫。在lombok的幫助下,開發者將更加集中於業務邏輯的開發,而不受重複的,無聊的,非業務邏輯代碼書寫的影響。一個比較明顯的例子就是咱們不須要再手動爲每個類成員變量寫Setter和Getter方法。java
Lombok在爲咱們帶來便利的同時,不只解放了開發者的雙手,必定程度上也讓開發者忘記了思考到底Lombok爲咱們自動生成了什麼代碼。若是不深究Lombok的自動代碼生成原理,則會讓咱們忽略掉不少Java基礎知識。程序員
先看下面一段代碼,請問request爲」/hello」的時,咱們所獲得的Response的body是什麼?json
@Getter
@Setter
abstract class A {
private boolean success;
}
@Setter
@Getter
class B extends A {
public Boolean success;
}
@Controller
@RequestMapping("/hello")
public class Hello {
@GetMapping
@ResponseBody
public B returnB() {
B b = new B();
b.setSuccess(true);
return b;
}
}
複製代碼
答案是:安全
body = {success: null}
複製代碼
其實原理很簡單,可是很容易被咱們忽略掉。我問過幾個老Java開發,他們雖然知道其中的原理,可是在一開始問他們爲何success的值爲null的時候,都沒有人能馬上想到緣由。bash
其實形成success的值爲null的緣由不是Lombok, Lombok生成的Setter重載了父類的方法。數據結構
首先,咱們想象一下,若是咱們不使用Lombok的Setter和Getter的話,咱們本身也許會像下面手動寫Getter和Setter。app
class A {
public boolean success;
public void setSuccess(boolean success) {
this.success = success;
}
public boolean getSuccess() {
return this.success;
}
}
class B extends A {
public Boolean success;
public void setSuccess(Boolean success) {
this.success = success;
}
@Override
public boolean getSuccess() {
return this.success;
}
}
複製代碼
上面的代碼, B的setSuccess重載了A的setSuccess,同時,B的getSuccess重寫了A.getSuccess.ide
因爲B的setSuccess重載了A的setSuccess.因此B的對象裏面就會有兩個方法,Signature分別是:post
void setSuccess(boolean success), void setSuccess(Boolean success) 複製代碼
因此當咱們使用b.setSuccess(true)時, 這裏不是設置的b.success(Boolean)的值,而是設置的b.A.success(boolean)的值,同時,b.A.success是一個隱藏值,因此對於外部來講,根本看不見b.A.success的值。以下圖所示。this
而此時b.success的值則爲初始值null。要想設置b.success的值的話,則須要使用b.setSuccess(Boolean.True)
若是這個setSuccess是本身去寫的話,咱們能夠能注意到子類和父類的入參類型的變化,而加以注意,不至於形成選錯setSuccess方法,而致使功能行爲與預想的不一致。
在咱們的Legacy代碼裏面,有一個父類,這個父類裏面有一個成員變量和一個方法。以下:
@Setter
@Getter
class A {
private boolean success;
public void doSomethingAndSetSuccess() {
// dosomething
this.setSuccess(true);
}
}
複製代碼
同時,在Legeacy代碼裏面,還有一個子類繼承了這個父類, 以下:
class B extends A {
// something not important
}
複製代碼
而後,在這個在這個Legacy系統裏面,有對B的對象調用。以下:
B b = new B();
b.doSomethingAndSetSuccess();
// something here
return b; (經過@ResponseBody)
複製代碼
這樣的結構以前一直運行得很好,所返回的json結果一直都是咱們想要的結果,好比{success: true(or false)}。直到有一天,有一個開發沒有注意到他們之間的關係。在類B上加入了以下代碼:
@Getter
@Setter
class B extends A {
public Boolean success = null;
// the last code same as before.
}
複製代碼
Lombok的Setter,重載了A類裏面的setSuccess(boolean success),進而,在不修改A.doSomethingAndSetSuccess裏面的setSuccess方法的話,則永遠沒有地方調用B的setSuccess(Boolean success)。也就是說B的success永遠是初始值null.
到底類的成員變量使用封裝的數據類型仍是使用原始數據類型?
從上面的事故中,咱們會容易得出一個結論,就是在子類中新添加的成員變量數據類型和父類不一樣形成了事故,亦即開發規範不一樣引發了意外。
那麼,爲了團隊之後再也不出現相似的類成員變量類型不一致的狀況,咱們到底應該是選擇像父類那樣,使用原始變量類型,仍是使用包裝類型呢?
先來看看阿里巴巴開發手冊(下手冊)上是怎麼說的吧。
手冊上舉了兩個例子來論證類屬性強制使用包裝數據類型。總結其論點就是 1. 包裝數據類型的RPC返回值能夠是null,代示其「不存在」。 2. 由於類屬性是引用類型,就算直接賦值null,不會有NPE風險。
這兩個論據很好理解,我也表示贊同。有一點須要補充的是,我認爲比起返回原始數據的包裝數據類型(Integer,Boolean,etc),返回一個自定義的數據類型會更加有語意。
// 返回原始數據類型
class A {
public int a;
public int getA(){
// do some logic.
return this.a;
}
}
// code 1
複製代碼
class A {
public Integer a;
public Integer getA(){
// do some logic
return this.a;
}
}
//code2
複製代碼
正如手冊所說,code 2比起code 1,可以多返回一個null。所以也能告訴調用者你想調用的值根本不存在。
可是,我認爲做爲一個開發,當你知道當前所要返回的值將會是 -x,0,x和不存在(姑且叫null),雖然返回Integer是一個不錯的解決方案,但我以爲從語意的角度上來講,仍是不夠好。畢竟null不是一個Integer,它只是一個空的Integer對象的引用。
因此我以爲在這種狀況,最有語意的作法是自定義一個新的數據結構。
@Setter
@Getter
class SomeResult {
// 使用這種編碼方式,能夠不用每個POJO類屬性都爲包裝數據類型。只須要保證RPC類屬性爲自定義數據類型便可。
private boolean exist = true;
private int value = 0;
}
// 能夠再把SomeResult再抽象,以減小重複代碼。這裏再也不列出。
class A {
public SomeResult someResult;
public SomeResult returnSomeResult() {
// after doing some logic, return result
// 我以爲這裏,做爲程序員,不該該主動返回null. 返回null一時爽,語意不清,形成後續Debug難度增長。
// 關於返回null的博客,詳細請移步 [https://juejin.im/post/5ba88342e51d450e6475f7cd](https://juejin.im/post/5ba88342e51d450e6475f7cd)
if(someResult exists) {
return someResult;
} else {
someResult.setExist(fasle);
return someResult;
}
}
}
class B {
public void someMethod() {
SomeResult someResult = getSomeResultBySomeService();
// 做爲調用者,確實有責任和義務確認RPC返回值是否爲null。null在語意上來講這是一個Exception.不該該參與業務邏輯的意外。
// 使用這種自定義數據結構,明確地代表是調用出了問題仍是自己被調用者系統出了問題。
if(someResult == null) {
// 表明調用失敗。
} else if (!someResult.isExist) {
// 調用雖然成功,可是所獲得的a不存在。
} else {
// 調用成功,使用其值。
a.value
}
// 我以爲更優美的寫法應該是
try {
if(!someResult.isExist){}
else {}
} catch(Exception e) {
// 處理以NPE爲表明的Exception.
}
}
}
複製代碼
人是會犯錯的。我認爲光是強調規範並不能保證開發者必定100%遵照規範。同時,我也不認爲這些規範就能覆蓋全部的開發質量問題。因此我認爲還應該有更多的機制保證上線代碼的質量。
TDD(Test-Driven Development)就是一個很是好的開發方法論。根據我本身對TDD的實踐,我以爲TDD的最大好處就是讓開發者知道本身正在作什麼。
拿上面的例子來講,若是應用了TDD,開發者在代碼裏面所添加的任何一行代碼都會獲得Test-Case的檢驗。並且開發者會在編寫業務邏輯代碼以前,認真思考我爲何會加入這一行代碼,這一行代碼對個人邏輯有什麼做用。
這樣,就不至於會無腦地添加一些所謂的最佳實踐卻對業務邏輯沒有幫助的代碼。
好比上面的例子,當咱們的Legacy系統有類A與其子類B。當前運行良好,咱們假定當前的代碼都是通過Test過的。
那麼Legacy的當前代碼snapshot以下:
@Setter
@Getter
class A {
private boolean success;
public void doSomeLogicAndSetSuccess() {
// after doing some logic
this.setSuccess(true);
}
}
class B extends A {
}
class AUseCaseClass() {
private B b = getBFromSomeService();
public B doSomeLogicAndReturnB() {
//after doing some logic
this.b.doSomeLogicAndSetSuccess();
return this.b;
}
}
//code 3
複製代碼
這時有個開發由於有新的業務需求要修改code 3的代碼,若是他是遵照TDD的話,他應該第一時間根據新的需求着手寫Test-Case。根據筆者自身的實踐經驗,若是對新的需求瞭解的越是不明白,越是寫不出Test-Case來,就更別說在原有代碼上作改動了。就是這讓開發者思考業務需求的過程和強制先寫Test-Case的要求,讓其每一行寫入代碼中的代碼都不會成爲廢代碼和錯誤代碼。
好比上面的事故,要是遵照TDD的話,這個開發者在添加以下代碼以前,他至少會問本身,我爲何添加success,難道A中沒有success嗎。同時,假如他沒有發現A中有success,他也會首先寫Test-Case,去驗證本身要添加的這一行是否會讓整個系統出現問題。
class B extends A {
public Boolean success = null;
}
複製代碼
本文分析了筆者在工做中發現的事故,詳細描述了事故發生的原由,通過和結果;同時,根據事故的根本緣由,從團隊將來預防一樣錯誤的角度和提高團隊總體代碼質量的角度進行了深刻的思考。筆者提出兩個反思結果:1. 根據須要,類屬性使用自定義數據類型(RPC方法返回值強制使用自定義數據類型)。2.使用TDD來保證代碼質量(最好是作code-review的代碼帶上其Test-Case,不然其實和裸奔沒有什麼區別)。