关于阿里云:代码注释的艺术优秀代码真的不需要注释吗

39次阅读

共计 11691 个字符,预计需要花费 30 分钟才能阅读完成。

作者:聂晓龙(率鸽)

前言

前天回家路上,有辆车强行插到后面的空位,司机大哥火暴地拍着方向盘吐槽道“加塞最可恶了”,我问“还有更可恶的吗”,司机大哥淡定说道“不让本人加塞的”。仿佛和咱们很相似,咱们程序员届也有这 2 件相辅相成的事:最厌恶他人不写正文,更厌恶让本人写正文。

一段蹩脚的代码,往往大家最低的预期是把正文写分明,最正当的做法通常应该对代码做优化。 如果咱们将代码真正做到了优良,咱们是否还须要正文?

正文的意义

; **************************************************************************
; * RAMinit Release 2.0 *
; * Copyright (c) 1989-1994 by Yellow Rose Software Co. *
; * Written by Mr. Leijun *
; * Press HotKey to remove all TSR program after this program *
; **************************************************************************
; Removed Softwares by RI:
; SPDOS v6.0F, WPS v3.0F
; Game Busters III, IV
; NETX (Novell 3.11)
; PC-CACHE
; Norton Cache
; Microsoft SmartDrv
; SideKick 1.56A
; MOUSE Driver
; Crazy (Monochrome simulate CGA program)
; RAMBIOS v2.0
; 386MAX Version 6.01

正文是对代码的解释和阐明,实质目标是为了加强程序的可读性与可解释性。正文会随着源代码,在进入预处理器或编译器解决后会被移除。这是雷布斯 1994 年写的一段 MASM 汇编代码,正文与代码整体构造都十分清晰。如果说代码是为了让机器读懂咱们的指令,那正文齐全就是为了让咱们理解咱们本人到底收回了哪些指令。

争议与一致

正文的起源十分早,咱们甚至曾经查阅不到正文的由来,但当初任何一种语言,甚至简直任何一种文本格式都反对各式各样的正文模式。

但如何应用正文,其实始终是一个备受争执的话题。当咱们接手一段‘祖传代码’时,没有正文的感觉几乎让人抓狂,咱们总是心愿他人能提供更多的正文。但软件届也有一段神话传说,叫做『我的代码像诗一样优雅』。有正文的代码都存在着一些瑕疵,认为足够完满的代码是不须要正文的。

坏代码的救命稻草

The proper use of comments is to compensate for our failure to express ourself in code.– Robert C. Martin《Clean Code》译:正文的失当用法是补救咱们在用代码表白用意时遭逢的失败

Clean Code 的作者 Robert C. Martin 能够说是正文的竭力否定者了,他认为正文是一种失败,当咱们无奈找到不必正文就能表白自我的办法时,才会应用正文,任何一次正文的应用,咱们都应该意识到是本人表达能力上的失败。

PH&V 的零碎架构师和负责人 Peter Vogel,同样也是一名动摇的正文否定着,他发表了一篇文章 why commenting code is still bad 来表述为代码增加正文在某种程度上可能是必要的,但的确没有价值。

事实上,咱们也的确经验着十分多无价值的正文,以及齐全应由代码来承当解释工作的“职能错位”的正文。

零正文

蹩脚的代码加上齐全不存在的正文,我喜爱称说它们为『我和上帝之间的机密』,当然过 2 个月后也能够称之为『上帝一个人的机密』。

压垮程序员最初一根稻草的,往往都是零正文。能够没有文档,能够没有设计,但如果没有正文,咱们每一次浏览都是灾难性的。当咱们埋怨它一行正文都没有时,其实咱们是在埋怨咱们很难了解代码想要表白的含意,正文是间接起因,但根本原因是代码。

零正文往往和坏代码一起生存,“没有正文”的吐槽,其实实质上直击的是那堆歪七扭八的英文字母,到底它们想表白什么!

无用正文

/**
 * returns the last day of the month
 * @return the last day of the month
 */
public Date getLastDayOfMonth(Date date) {Calendar calendar = new GregorianCalendar();
    calendar.setTime(date);
    calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
    return calendar.getTime();}

这是典型的废话正文,读代码时代码自身就能很好的表白具体的含意,咱们齐全不须要看正文,并且正文也不会给咱们提供更多无效的信息。无用正文或者是零正文的另一个极其,咱们放心本人写的代码被人所吐槽,于是尽可能去补全正文,当你为 getLastDayOfMonth() 补一段 get last day of month 的正文时,祝贺你,你失去了双倍的代码。

代码优于正文

“Comments Do Not Make Up for Bad Code”– Robert C.Martin《Clean Code》译:正文不能丑化蹩脚的代码

当须要为一段代码加上正文时,阐明代码曾经不能很好的表白用意,于是大家开始为这段代码增加正文。Robert C.Martin 在 Clean Code 中提出一个观点:正文不能丑化蹩脚的代码。能用代码表白的间接用代码表白,不能用代码表白的,你再想想,如何能用代码表白。

简单的代码最间接的体现就是不够直观、难以了解,加上正文后往往会清晰很多,但你是违心看这段代码:

// 判断是否沉闷用户
if((customer.getLastLoginTime().after(dateUtils.minusDays(new Date(),15)) && customer.getCommentsLast30Days() > 5) 
    || orderService.countRecentDaysByCustomer(customer,30) > 1)

还是这段代码?

if(customer.isActive())

蹩脚代码的存在,通常是咱们写正文的常见动机之一。这种试图掩饰可读性差的代码的正文称之为『拐杖式正文』,即便赫赫有名的 JDK,也存在这样的拐杖式正文。

public synchronized void setFormatter(Formatter newFormatter) {checkPermission();
    // Check for a null pointer
    newFormatter.getClass();
    formatter = newFormatter;
}

这是取自 JDK java.util.logging.Handler 类的 setFormatter 办法,作者为了不让空指针异样下传,提前做一次空指针查看。没有这段正文咱们齐全不晓得游离的这句 newFormatter.getClass() 到底要做什么,这段正文也充沛表白了作者本人也晓得这句代码难以了解,所以他加上了正文进行阐明。但咱们齐全能够用 Objects.requireNonNull() 来进行代替。同样的代码作用,但可读性可了解性大不一样,JDK 里的这段代码,的确让人遗憾。
04

正文否定论

“If our programming languages were expressive enough, or if we had the talent to subtly wield those languages to express our intent, we would not need comments very much—perhaps not at all.”\
— Robert C.Martin《Clean Code》\
译:若编程语言足够有表达力,或者咱们长于用这些语言来表白用意,就不那么须要正文 – 兴许基本不须要

通过代码进行论述,是正文否定论的核心思想。当你花功夫来想如何写正文,让这段代码更好的表白含意时,咱们更应该重构它,通过代码来解释咱们的用意。每一次正文的编写,都是对咱们代码表达能力上的差评,晋升咱们的演绎、表白、解释能力,更优于通过正文来解决问题。当代码足够优良时,正文则是非必须的。并且需要在一直调整,代码肯定会随之变动,但正文可能缓缓被人忘记,当代码与正文不匹配时,将是更大的劫难。

软件设计的乌托邦

好吧你很优良

已经我确实对优良的代码一直钻研,对代码自身所蕴含的能量无比深信。如同当迷信代替鬼神论走上历史舞台时,即便存在有迷信解释不了,咱们仍然深信只是迷信还须要倒退。当代码他人无奈了解时,我会认为是我表述不够精准,形象不够正当,而后去重构去欠缺。

有一次给老板 review 代码,过后老板提出,“你的代码缺短少正文”,我说不须要正文,代码就能自解释。于是老板现场读了一段代码,“query-customer-list 查问客户”、“transfer-customer-to-sales 散发客户到销售”、“check-sales-capacity 查看销售库容”,每一个类每一个函数,一个单词一个单词往外蹦时,你会发现如同的确都能读懂,于是老板回了一个“好吧”。

漂亮的乌托邦

“‘good code is self-documenting’ is a delicious myth”\
— John Ousterhout《A Philosophy of Software Design》\
译:‘好的代码自解释’是一个漂亮的谎话

在软件设计中,总有一些软件工程师所深信的诗和远方,有的是大洋彼岸的美妙国家,有的或者是扑朔迷离的现实乌托邦。John Ousterhout 传授在 A Philosophy of Software Design 中提到一个观点,‘好的代码自解释’是一个漂亮的谎话。

咱们能够通过抉择更好的变量名,更精确的类与办法,更正当的继承与派生来缩小正文,但尽快如此,咱们还是有十分多的信息无奈间接通过代码来表白。这里的信息,或者不单单只是业务逻辑与技术设计,可能还包含了咱们的观感,咱们的体验,咱们的接收水平以及第一印象带来的首因效应。

好代码的最佳僚机

You might think the purpose of commenting is to ‘explain what the code does’, but that is just a small part of it.The purpose of commenting is to help the reader know as much as the writer did. 译:你可能认为正文的目标是“解释代码做了什么”,但这只是其中很小一部分,正文的目标是尽量帮忙读者理解得和作者一样多 – Dustin Boswell《The Art of Readable Code》

如同 John Ousterhout 传授一样,The Art of Readable Code 的作者 Dustin Boswell,也是一个动摇的正文支持者。与 Robert C.Martin 相似,Dustin Boswell 同样认为咱们不应该为那些从代码自身就能疾速推断的事实写正文,并且他也拥护拐杖式正文,正文不能丑化代码。

但 Dustin Boswell 认为正文的目标不仅解释了代码在做什么,甚至这只是一小部分,正文最重要的目标是帮忙读者理解得和作者一样多。编写正文时,咱们须要站在读者的角度,去想想他们晓得什么,这是正文的外围。这里有十分多的空间是代码很难论述或无奈论述的,配上正文的代码并非就是蹩脚的代码,相同有些时候,正文还是好代码最棒的僚机。

更精准表述

There are only two hard things in Computer Science: cache invalidation and naming things.– Phil Karlton 译:计算机科学中只有两个难题:缓存生效和命名

Martin Fowler 在他的 TwoHardThings 文章中援用了 Phil Karlton 的一段话,命名始终都是一件十分难的事件,因为咱们须要将所有含意稀释到几个单词中表白。很早之前学 Java,接触到很长的类名是 ClassPathXmlApplicationContext。可能有人认为只有能将含意精确地表达出来,名字长一些无所谓。那如果咱们须要有一段解决无关“一带一路”的内容,那咱们的代码可能是这样的:

public class TheSilkRoadEconomicBeltAndThe21stCenturyMaritimeSilkRoad {}

他十分精确的表白了含意,但很显著这不是咱们冀望的代码。但如果咱们辅以简略的正文,代码会十分清晰,阐明了简称,也阐明了全意,表述更精准。

/**
 * 一带一路
 * 丝绸之路经济带和 21 世纪海上丝绸之路
 */
public class OneBeltOneRoad {}

代码档次切割

函数抽取是咱们常常应用且老本最低的重构办法之一,但并非银弹。函数并非抽得越细越好,如同分布式系统中,并非有限的堆机器让每台机器解决的数据越少,整体就会越快。过深的嵌套封装,会加大咱们的代码浏览老本,有时咱们只须要有肯定的档次与构造帮忙咱们了解就够了,自觉的抽取封装是无意义的。

/**
 * 客户列表查问
 */
public List queryCustomerList(){
    // 查问参数筹备
    UserInfo userInfo = context.getLoginContext().getUserInfo();
    if(userInfo == null || StringUtils.isBlank(userInfo.getUserId())){return Collections.emptyList();
    }
    LoginDTO loginDTO = userInfoConvertor.convertUserInfo2LoginDTO(userInfo);
    // 查问客户信息
    List<CustomerSearchVO> customerSearchList = customerRemoteQueryService.query(loginDTO);
    Iterable<CustomerSearchVO> it = customerSearchList.iterator();
    // 排除不合规客户
    while(it.hasNext()){CustomerSearchVO customerSearchVO = it.next(); 
        if(isInBlackList(customerSearchVO) || isLowQuality(customerSearchVO)){it.remove();
        }
    }
    // 补充客户其余属性信息
    batchFillCustomerPositionInfo(customerSearchList);
    batchFillCustomerAddressInfo(customerSearchList);
}

其实细看每一处代码,都很容易让人了解。但如果是一版没有正文的代码,可能咱们会有点头疼。短少构造短少分层,是让咱们大脑第一感观感觉它很简单,须要一次性消化多个内容。通过正文将代码档次进行切割,是一次抽象层次的划分。同时也不倡议大家一直去形象公有办法,这样代码会变得十分割裂,并且上下文的背景逻辑、参数的传递等等,都会带来额定的麻烦。

母语的力量

其实上述例子,咱们更易浏览,还有一个重要的起因,那就是母语的力量。咱们人造所经验的环境与咱们每天所接触到的事物,让咱们对中文与英文有齐全不一样的感触。咱们代码的编写实质上是一个将咱们沟通中的“中文问题”,翻译成“英文代码”来实现的过程。而浏览代码的人在做得,是一件将“英文代码”翻译成“中文表述”的事件。而这之中通过的环节越多,意思变味越重大。

TaskDispatch taskDispatch = TaskDispatchBuilder.newBuilder().withExceptionIgnore().build();
taskDispatch
        // 外贸信息
        .join(new FillForeignTradeInfoTask(targetCustomer, sourceInfo))
        // 国民经济行业、电商平台、注册资本
        .join(new FillCustOutterInfoTask(targetCustomer, sourceInfo))
        // 客户信息
        .join(new FillCustomerOriginAndCategoryTask(targetCustomer, sourceInfo))
        // 客户扩大信息
        .join(new FillCustExtInfoTask(targetCustomer, sourceInfo))
        // 珍藏屏蔽信息
        .join(new FillCollectStatusInfoTask(targetCustomer, sourceInfo, loginDTO()))
        // 详情页跳转须要的标签信息
        .join(new FillTagInstanceTask(targetCustomer, sourceInfo, loginDTO()))
        // 客户信息残缺度分数
        .join(new FillCustomerScoreTask(targetCustomer, sourceInfo))
        // 潜客分层残缺度
        .join(new FillCustomerSegmentationTask(targetCustomer, sourceInfo))
        // 填充操作信息
        .join(new FillOperationStatusTask(targetCustomer, sourceInfo, loginDTO))
        // 认证状态
        .join(new FillAvStatusTask(targetCustomer, loginDTO))
        // 客户地址和组织
        .join(new FillCompanyAddressTask(targetCustomer, loginDTO))
        // 违规信息
        .join(new FillPunishInfoTask(targetCustomer, sourceInfo))
        // 填充客户黑名单信息
        .join(new FillCustomerBlackStatusTask(targetCustomer, sourceInfo))
        // 填充客户志愿度
        .join(new FillCustIntentionLevelTask(targetCustomer, sourceInfo));
        // 执行
        .execute();

这是一段补齐客户全数据信息的代码,尽管每一个英文咱们都看得懂,但咱们永远只会第一眼去看正文,就因为它是中文。并且也因为有这些正文,这里非常复杂的业务逻辑,咱们同样能够十分清晰的理解到它做了哪些,分哪几步,如果要优化应该如何解决。这里也倡议大家写中文正文,正文是一种阐明,越直观越好,中文的亲和力是英文无法比拟的。当然,这条倡议并不适宜美国程序员。

正文的真正归属

简单的业务逻辑

// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
if (isPrototypeCurrentlyInCreation(beanName)) {throw new BeanCurrentlyInCreationException(beanName);
}
// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    // Not found -> check parent.
    String nameToLookup = originalBeanName(name);
    if (args != null) {
        // Delegation to parent with explicit args.
        return parentBeanFactory.getBean(nameToLookup, args);
    }
    else {
        // No args -> delegate to standard getBean method.
        return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
}

这是 Spring 中的一段获取 bean 的代码,spring 作为容器治理,获取 bean 的逻辑也非常复杂。对于简单的业务场景,配上必要的正文阐明,能够更好的了解相应的业务场景与实现逻辑。

截取自:org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

艰涩的算法公式

/**
 * Returns the value obtained by reversing the order of the bits in the
 * two's complement binary representation of the specified {@code long}
 * value.
 */
public static long reverse(long i) {
    // HD, Figure 7-1
    i = (i & 0x5555555555555555L) << 1 | (i >>> 1) & 0x5555555555555555L;
    i = (i & 0x3333333333333333L) << 2 | (i >>> 2) & 0x3333333333333333L;
    i = (i & 0x0f0f0f0f0f0f0f0fL) << 4 | (i >>> 4) & 0x0f0f0f0f0f0f0f0fL;
    i = (i & 0x00ff00ff00ff00ffL) << 8 | (i >>> 8) & 0x00ff00ff00ff00ffL;
    i = (i << 48) | ((i & 0xffff0000L) << 16) |
        ((i >>> 16) & 0xffff0000L) | (i >>> 48);
    return i;
}

这是 JDK 中 Long 类中的一个办法,为 reverse 办法增加了足够多的正文。对于简直没有改变且应用频繁的底层代码,性能的优先级会高于可读性。在保障高效的同时,正文帮忙咱们补救了可读性的短板。

截取自:

java.lang.Long#reverse

不明所以的常量

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

这是 JDK 中 HashMap 的一个常量因子,记录由链表转向红黑树的链表长度阈值,超过该长度则链表转为红黑树。这里记录了一个 8,不仅记录了该常量的用处,也记录了为什么咱们定义这个值。常常咱们会发现咱们代码中存在一个常量等于 3、等于 4,有时咱们不晓得这些 3 和 4 是干什么的,有时咱们不晓得为什么是 3 和 4。

截取自:

java.util.HashMap#TREEIFY_THRESHOLD

意料之外的行为

for (int i = 0; i < 3; i++) {
    // if task running, invoke only check result ready or not
    Result result = bigDataQueryService.queryBySQL(sql, token);
    if (SUCCESS.equals(result.getStatus())) {return result.getValue();
    }
    Thread.sleep(5000);
}

代码及正文所示为每 5 秒 check 一下是否有后果返回,近程服务将触发与获取放在了一个接口。没有正文咱们可能认为这段代码有问题,代码体现的含意更像是每 5 秒调用一次,而非每 5 秒 check 一次。为意料之外的行为增加正文,能够缩小对代码的误会读,并向读者阐明必要的背景及逻辑信息。

接口对外 API

/**
 * <p>Checks if a CharSequence is empty (""), null or whitespace only.</p>
 * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
 * StringUtils.isBlank(null)      = true
 * StringUtils.isBlank("")        = true
 * StringUtils.isBlank(" ")       = true
 * StringUtils.isBlank("bob")     = false
 * StringUtils.isBlank("bob") = false
 *
 * @param cs  the CharSequence to check, may be null
 * @return {@code true} if the CharSequence is null, empty or whitespace only
 */
public static boolean isBlank(final CharSequence cs) {final int strLen = length(cs);
    if (strLen == 0) {return true;}
    for (int i = 0; i < strLen; i++) {if (!Character.isWhitespace(cs.charAt(i))) {return false;}
    }
    return true;
}

咱们常常应用的 StringUtils 工具类中的 isBlank 办法,写了十分详情的正文,不仅包含办法的逻辑,入参的含意,甚至还包含具体示例。咱们平时定义的二方库中的 HSF、HTTP 接口定义,同样须要有清晰详尽的正文,这里的正文甚至常常会多过你的代码。

截取自:

org.apache.commons.lang3.StringUtils#isBlank

法律文件信息

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

与法律相干的正文,在开源软件库中较常常遇到。波及到一些版权及著述申明时,咱们须要在源文件顶部搁置法律相干正文。当然,咱们不须要将所有法律信息写到正文中,如例子中的跳链,援用一份规范的内部文档,会是一个更好的抉择。

写在最初

正文并不会障碍你写出优雅简洁的代码,它只是程序固有的一部分而已。咱们不必过分在意咱们的代码是否能够脱离正文,也不须要强调因为咱们的代码合乎什么准则,满足什么约定,所以代码是优良的正文是冗余的。代码是一门艺术,并不会因为满足三规九条它就肯定完满,因为艺术,是不可掂量的。

参阅书籍

《A Philosophy of Software Design》

《Clean Code》

《The Art of Readable Code》

正文完
 0