我的最佳实践:通用

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 世界,但仍不失为一个很好的思考启发。