我的最佳實務:通用
1. 註解:是福也是禍
好的註解是一門藝術。寫得用心是恩惠,卻也常是徒勞。
自我說明的程式碼(Self-documenting Code)並不代表「沒有註解」。程式碼本身應該盡可能表達怎麼做;註解則用來補充為什麼、不明顯的邊界條件,以及 API 的契約。
以下是一些自我說明程式碼的例子,以及好註解和壞註解的例子。
常見的一種說法是:註解很容易過時。但事實上,變數名稱和方法名稱也有一模一樣的問題!
哎唷,千萬別這樣寫:
int age = 32; // set the value of the age integer to 32
自我說明:變數名稱取得好,就成功了一半
不好的寫法:
var dateTime = DateTimeUtil.getNow();
好的寫法:
var now = DateTimeUtil.getNow();
再舉一個例子:
不好的寫法:
User user = this.getBruce();
不好的寫法:
// testing user without permission
User user = this.getBruce();
好的寫法:
User userWithoutPermission = this.getBruce();
自我說明:替方法取有意義的名稱
不好的寫法:
public String sanitizeInput(String input) {
// replace underscore with spaces
return input.replaceAll("_", " ");
}
好的寫法:
public String replaceUnderscoreWithSpaces(String input) {
return input.replaceAll("_", " ");
}
自我說明:私有方法
不好的寫法:這個方法逼讀者同時解讀驗證、計算和業務規則。
public Invoice createInvoice(Order order, Customer customer) {
if (order.items().isEmpty()) {
throw new IllegalArgumentException("Order needs at least one item");
}
BigDecimal subtotal = order.items().stream()
.map(item -> item.unitPrice().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal discount = customer.isPremium()
? subtotal.multiply(new BigDecimal("0.10"))
: BigDecimal.ZERO;
return new Invoice(order.id(), subtotal, discount, subtotal.subtract(discount));
}
好的寫法:公開方法敘述整體流程。私有的無狀態(stateless)方法替每個獨立的意圖命名,並透過參數和回傳值把相依關係呈現出來。
public Invoice createInvoice(Order order, Customer customer) {
requireItems(order);
BigDecimal subtotal = calculateSubtotal(order.items());
BigDecimal discount = loyaltyDiscountFor(customer, subtotal);
return invoiceFor(order, subtotal, discount);
}
private static void requireItems(Order order) {
if (order.items().isEmpty()) {
throw new IllegalArgumentException("Order needs at least one item");
}
}
private static BigDecimal calculateSubtotal(List<OrderItem> items) {
return items.stream()
.map(item -> item.unitPrice().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private static BigDecimal loyaltyDiscountFor(Customer customer, BigDecimal subtotal) {
return customer.isPremium()
? subtotal.multiply(new BigDecimal("0.10"))
: BigDecimal.ZERO;
}
private static Invoice invoiceFor(
Order order,
BigDecimal subtotal,
BigDecimal discount
) {
return new Invoice(order.id(), subtotal, discount, subtotal.subtract(discount));
}
並不是每一行程式碼都需要一個獨立的方法。值得抽出的是一個能被精確命名的完整想法。而像 processData() 或 handleStuff() 這樣的名稱,抽出來也毫無幫助。
經驗法則:註解為什麼這樣做,而不是做了什麼
這裡做了什麼一目了然:
public Duration retryDelay(int retryCount) {
long seconds = Math.min(1L << retryCount, 60);
return Duration.ofSeconds(seconds);
}
但為什麼要這樣做?
更好的寫法:
public Duration retryDelay(int retryCount) {
// Cap the delay so an outage never blocks queued jobs for more than one minute.
long seconds = Math.min(1L << retryCount, 60);
return Duration.ofSeconds(seconds);
}
再來一個實際專案中的例子。下面這些方法用來顯示和隱藏 Ajax 載入動畫(spinner)。可是總共有四個方法,乍看之下不太清楚為什麼:
function showAjaxLoader() {
$('.layout-ajax-loader').children().first().show();
}
function hideAjaxLoader() {
$('.layout-ajax-loader').children().first().hide();
}
function suppressAjaxLoader() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = 'none';
}
function releaseAjaxLoader() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = '';
}
這些方法看起來都很相似,似乎都是透過 CSS 以某種方式控制可見性。加上一句簡短的註解,各自的使用情境就一目了然了:
showAjaxLoader: function() {
$('.layout-ajax-loader').children().first().show();
},
hideAjaxLoader: function() {
$('.layout-ajax-loader').children().first().hide();
},
// take the control from primefaces and don't allow the spinner to show, no matter what
suppressAjaxLoader: function() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = 'none';
},
// reset the state after hiding, to give the control back to primefaces
releaseAjaxLoader: function() {
getOnlyElementByClassOrNull('layout-ajax-loader').style.display = '';
},
再舉一個例子:
不好的寫法:
/**
* Sets the tool tip text.
*
* @param text the text of the tool tip
*/
public void setToolTipText(String text) {}
好的寫法:
/**
* Registers the text to display in a tool tip. The text
* displays when the cursor lingers over the component.
*
* @param text the string to display. If the text is null,
* the tool tip is turned off for this component.
*/
public void setToolTipText(String text) {}
2021-06-21 工作坊上的討論:所有開發者一致認為,原則上應該避免使用 Javadoc(參數說明是一場惡夢、老是過時,維護起來也很不順手),只有當某個參數確實需要進一步解釋,或是不寫就得讀完整個冗長的方法時,才應該使用。
經驗法則的例外:註解做了什麼,這時候很有必要
如果不加註解程式碼就不夠清楚,而註解能大幅提升可讀性:
let isAlwaysAllowedKey = (
e.which === 8 || /* BACKSPACE */
e.which === 35 || /* END */
e.which === 36 || /* HOME */
e.which === 37 || /* LEFT */
e.which === 38 || /* UP */
e.which === 39 || /* RIGHT*/
e.which === 40 || /* DOWN */
e.which === 46 || /* DEL*/
e.ctrlKey === true && e.which === 65 || /* CTRL + A */
e.ctrlKey === true && e.which === 88 || /* CTRL + X */
e.ctrlKey === true && e.which === 67 || /* CTRL + C */
e.ctrlKey === true && e.which === 90 /* CTRL + Z */
)
註解:Linux 核心程式碼裡的髒話
最後再分享一則 Linux 核心的趣聞:原始碼中所有髒話的數量統計:
https://www.vidarholen.net/contents/wordcount/#fuck*,shit*,damn*,idiot*,retard*
祝你寫註解寫得開心!
2. 大型程式常式中的私有方法很危險
注意!在現今物件導向的世界裡,私有方法總是很多。這通常不是問題。只有當這些方法存取欄位(也就是狀態),而且位於大型常式之中時,問題才會出現。這類子方法應該是私有且無狀態的。否則它們就有一個缺點:單獨被呼叫時常常會惹出麻煩。總會有開發者想到要單獨呼叫這個子方法。而非法狀態正是 bug 的主要來源之一。這裡有一篇有趣的文章,介紹了 John Carmack(id Software,Doom)的一些觀點:在程式碼必須極度穩定的應用中(例如太空飛行),副程式常常是被禁止的,這讓程式碼更具確定性:http://number-none.com/blow/john_carmack_on_inlined_code.html
這個故事只能在一定程度上套用到現代無狀態的 Java/Spring 世界,但仍不失為一個很好的思考起點。