你可能还想看
1. 阿里云容器服务负责人易立:云原生如何解企业降本提效难题?
2. 对话张建锋,关于云计算和阿里云的未来
3. 阿里云张建锋: Back to Basic, 定义下一代的云
4. 深入浅出eBPF: 你要了解的7个核心问题
5. 不忘初心,方得始终 | 龙蜥开发者说第7期
关注我们欢迎关注加星标✨ 每日推送不错过
凌云时刻
注释的意义
; **************************************************************************
; * 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》
译:注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败
事实上,我们也确实经历着非常多无价值的注释,以及完全应由代码来承担解释工作的“职能错位”的注释。
/**
* 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();
}
"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 里的这段代码,确实让人遗憾。
"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》
译:若编程语言足够有表达力,或者我们擅长用这些语言来表达意图,就不那么需要注释——也许根本就不需要。
"'good code is self-documenting' is a delicious myth"
-- 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》
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
译:计算机科学中只有两个难题:缓存失效和命名
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 一次。为意料之外的行为添加注释,可以减少对代码的误解读,并向读者说明必要的背景及逻辑信息。
/**
* <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
欢迎关注加星标✨ 每日推送不错过