我的最佳实践:通用
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 世界,但仍不失为一个很好的思考启发。