我看到有些建議不斷重複出現,所以我決定整理一個清單,與諸位分享。
這是我的 3(額外 +1)個最常見的程式碼審查建議。
建議1:出錯時丟擲異常
我見過的一個常見模式是:
1 2 3 4 5 6 7 8 |
List<String> getSearchResults(...) { try { List<String> results = // make REST call to search service return results; } catch (RemoteInvocationException e) { return Collections.emptyList(); } } |
這種模式實際上導致了我做的移動應用程式崩潰,我們當時使用的搜尋後端開始丟擲異常。而應用程式的 API 伺服器中也有一些類似的程式碼。從應用程式的角度來看,它成功獲取了 200 個響應,只是每個搜尋請求返回的是空列表。
如果當時 API 直接丟擲異常,那麼我們的監控系統就會立即檢測到並修復它。
很多時候,當你捕獲一個異常時,你會希望它返回一個空物件。Java 中的空物件包括 Optional.empty()、null 和空列表,而它們在 URL 解析中經常出現。如果 URL 不能從一個字串中正確解析時,先不要返回 null,而是停下來問問自己:“為什麼 URL 格式不正確?這不是我們應該在上游解決的資料問題嗎?”
空物件不是解決此類問題的合適工具。如果有異常,你應該(及時)丟擲它。
建議2:使用盡可能具體的型別
這個建議基本上與字串型別程式設計相反。
我經常看到類似這樣的程式碼:
1 2 3 4 5 6 7 8 |
void doOperation(String opType, Data data); // where opType is "insert", "append", or "delete", this should have clearly been an enum String fetchWebsite(String url); // where url is "https://google.com", this should have been an URN String parseId(Input input); // the return type is String but ids are actually Longs like "6345789" |
儘可能具體的型別可以讓你避免整個類的錯誤,這基本上是大家選擇強型別語言(如 Java)的原因。
所以現在的問題是:那些想要寫強型別語言的程式設計師,最終是怎麼寫出糟糕的字串型別的程式碼哪?答案是:因為外部世界不是強型別的。字串通常來自許多不同的地方,比如:
- url 中的請求和路徑引數
- JSON
- 不支援列舉的資料庫
- 寫得很差的庫
這些情況下,你應該使用以下策略來避免這個問題:將字串解析和序列化保持到程式邊緣。這是一個例子:
1 2 3 4 5 6 7 8 9 10 |
// Step 1: Take a query param representing a company name / member id pair and parse it // example: context=Pair(linkedin,456) Pair<String, Long> companyMember = parseQueryParam("context"); // this should throw an exception if malformed // Step 2: Do all the stuff in your application // MOST if not all of your code should live in this area // Step 3: Convert the parameter back into a String at the very end if necessary String redirectLink = serializeQueryParam("context"); |
它帶給我們許多好處:1、能夠立刻發現格式不正確的資料;如果有任何問題,應用程式會提前顯示並終止。2、一旦資料通過驗證,你不必在整個應用程式中捕獲解析異常。3、強型別使得簽名具有更多資訊,這樣你就無需在每個方法上寫 javadoc。
建議3:使用 Optionals 而不是 null
Optional
(可選)類是 Java 8 中最好的特性之一,它表示一個可以合理存在或不存在的實體。
問題時間:唯一用縮寫來指代的異常是什麼?答:NPE (Null Pointer Exception,空指標異常)。這是迄今為止 Java 中最常見的異常,被稱為十億美元的錯誤。
Optional
允許你從程式中完全刪除 NPE。但是,你必須正確地使用它。這裡有一些關於如何使用 Optional
的建議:
- 你不應該在任何時候隨意呼叫
.get()
,而應仔細考慮Optional
不存在的情況,並給出合理的預設值。 - 如果你還沒有一個合理的預設值,那麼像
.map()
和.flatMap()
這樣的方法,可以讓你晚點做這個決定。 - 如果外部庫返回
null
來表示空值,則立即使用Optional.ofNullable()
以封裝它。相信我,以後你會感謝自己的。在程式內部,null 有往上“冒泡”的傾向,所以還是最好從源頭上阻止它們。 - 在返回型別的方法中使用
Optional
。這樣做可以讓你不用閱讀 javadoc 就可以獲知,值是否可能不存在。
額外建議:儘可能使用“Unlift”方法
你應該儘量避免類似的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// AVOID: CompletableFuture<T> method(CompletableFuture<S> param); // PREFER: T method(S param); // AVOID: List<T> method(List<S> param); // PREFER: T method(S param); // AVOID: T method(A param1, B param2, Optional<C> param3); // PREFER: T method(A param1, B param2, C param3); T method(A param1, B param2); // This method is clearly doing two things, it should be two methods // The same is true for boolean parameters |
所有要避免的方法有什麼共同點?他們都使用容器物件,比如 Optional,List 或 Task 作為方法的引數。當返回型別是同一型別的容器(即,一個引數的方法採用 Optional 並返回一個 Optional),情況會變得更糟糕。
為什麼?
相比 ① Promise<A> method(Promise<B> param)
,② A method(B param)
更具有靈活性。
如果你有 Promise<B>
,那麼你可以使用 ①,也可以通過 .map
函式“提升(lifting)” (比如 promise.map(method)
)來使用 ②.
但是,如果你只有 B,那麼你可以很容易地使用 ②,卻不能使用 ①,很明顯,② 是一個更靈活的選擇。
我喜歡稱之為“Unlift”,因為它與普通函式的實用方法“Lift”恰好相反。應用這些重寫能使方法更靈活,呼叫更輕鬆。