Code Review(代码评审)是一种流行的软件开发实践。通过在代码合入主分支前引入人工评审,能有效促进成员间的知识交流,提升软件质量。
我以评审者的身份参与过大量代码评审。在评审一份代码时,有些事项长期处在我的关注榜头部,比如设计是否考虑到了边界情况、代码是否有合理的单测覆盖。也有一些事项,因看似无关痛痒一直未引起足够重视,直到最近,我才渐渐发现它们的重要性。
以下是曾被我忽视的 3 件重要的小事。
1. 命名
小女孩千寻误入汤婆婆为神明开设的浴场。为了留在浴场内工作,千寻与汤婆婆签订了一份协议,但协议并非重点,重点是另一件看似无关紧要的小事——汤婆婆给千寻改了个名:从“千寻”改为“千”。一旦失去了原本的名字,人们便失去了逃离浴场所在的异世界的能力,甘心永世被汤婆婆所奴役。
——电影《千与千寻》
程序员们对“命名”的关注程度似乎呈一个“倒 U 形”曲线。缺乏经验时,对命名的关注度很低,代码中充斥着各类不准确、不精确的名字,无法有效描述各种抽象概念。
下面这段代码中的命名就存在不少问题:
def get_var(u):
"""获取环境变量列表"""
data1 = UserVarsManager.get(u)
data2 = SiteVarsManager.get(u.site)
return data1 + data2
随着经验逐渐增加,大家对命名的关注度逐步提升。项目中的名字开始变得更具有描述性,含糊不清的名字渐渐绝迹。名字至少不会成为他人理解代码时的屏障。
这个阶段,代码会逐渐演变成像是这样:
def list_environment_vars(user):
"""获取环境变量列表"""
items_user = UserVarsManager.get(user)
items_site = SiteVarsManager.get(user.site)
return items_user + items_site
在绝大多数评审中,这绝对算是一份合格的代码,至少不大可能因为命名应发争议。
自此之后,大部分程序员们对命名的关注度进入“倒 U 形”曲线的后半段:不再如从前那般关注命名,名字只要有一定描述性,不造成歧义就足够。我也曾经是其中一员。
但不应在这个阶段停留太久,作为代码评审人,我们应该不断提升自己对于名字的敏感度。比方说,对于前面那份代码,也许应该提出以下评审建议:
def list_environment_vars(user): # 1
"""获取环境变量列表"""
items_user = UserVarsManager.get(user) # 2
items_site = SiteVarsManager.get(user.site)
return items_user + items_site
- 评论 1: 项目中对于“环境变量”的统一缩写是
env_variables /env_vars
,此处应保持一致,使用list_env_variables
或list_env_vars
。 - 评论 2:
UserVarsManager.get
的命名可优化,因为Manager
是一个“万金油”名词,虽然放在各种场景下都不违和,但也是以损失名字(等同于“职责”)的精确指向性为代价,此处可考虑改用一个更精确的名字,比如:UserEnvVarsRetriever.get(user)
;SiteVarsManager
同理。
虽然只是两处小改进,但是积少成多。
每一次代码评审,必定涉及到许多新名字。但名字并非生来平等,不是所有名字都值得我们花费时间,应当尽量把关注点聚焦在那些最常被使用、最靠近用户的名字上,比如 URL 路径的资源名、数据库模型与字段名、工具函数(类方法)名,等等。
此外,与业务直接相关的领域词汇重要程度极高。评审时,每一个关键的领域词汇都值得仔细斟酌、反复推敲。举个例子,开发一个影评功能,”用户评分“、“媒体评分”、“平均分”分别该用哪些名字表示?你绝不会想要在一个文件里看到 movie_score
,在另一个文件里看到 movie_rating
。
命名这件小事,虽然看似不起眼,但项目规模越大、所跨越的时间维度越长,在名字质量上的细微差别就越容易累加出不可估量的巨大影响。
2. 指引性注释
夏洛已经在网上织出了光彩照人四个大字,威尔伯站在金色的阳光里,真是光彩照人。自从蜘蛛开始扶助它,它就尽力活得跟它的名声相衬。夏洛的网说它是王牌猪,威尔伯尽力让自己看上去是只王牌猪;夏洛的网说它了不起,威尔伯尽力让自己看上去了不起;现在网上说它光彩照人,它尽力让自己光彩照人。
——《夏洛的网》
关于注释,我向来信奉 Bob 大叔在《代码整洁之道》里的观点:“注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。” 这就是说,好代码应该总是能清晰说明自身意图,无需注释再来画蛇添足,注释只应该被用来描述那些代码之外的信息,比如解释“为什么”。
正因如此,注释总是应该被谨慎使用。假如一段代码很难理解,第一反应不应该是补注释,而是应该去追求用一种更易理解的方式重写它。
但随着时间的推移,我渐渐意识到,事情不能一概而论。“指引性注释”,或者说常被人们诟病为“近乎复述代码意图”的描述性文字,也有着不可替代的重要作用。
Redis 的作者 antirez 就是“指引性注释”的忠实拥护者,他曾写过一篇文章详细分析过指引性注释在 Redis 项目中的应用。下面这段代码摘自 Redis 源码,里面就有不少“指引性注释”:
/* Call the node callback if any, and replace the node pointer
* if the callback returns true. */
if (it->node_cb && it->node_cb(&it->node))
memcpy(cp,&it->node,sizeof(it->node));
/* For "next" step, stop every time we find a key along the
* way, since the key is lexicographically smaller compared to
* what follows in the sub-children. */
if (it->node->iskey) {
it->data = raxGetData(it->node);
return 1;
}
在这段代码中,两段注释并未提供任何在代码之外的新信息。所以,好处是什么?
最直观的好处,就是这些注释让代码变得更容易理解了,它们极大地降低了人们阅读代码时所需付出的心智成本。同样一份代码,在缺少指引性注释的情况下,完全理解它的行为可能得花费 10 分钟,而有了注释的帮助,时间也许就能缩短到 5 分钟甚至更短。
当新开发者加入项目时,这些指引性注释也能助力他们更快上手。
正因如此,在评审一份代码时,我常常会在一段复杂的代码逻辑上评论:“Nit:考虑增加一小段指引性注释,帮助理清代码行为。”(Nit=nitpick,表示“鸡蛋里挑骨头”式的并不强烈要求修改的意见)。
此外,如果一段代码曾在评审过程中引发过一些深度讨论,那么那些讨论内容,也许很适合被二次加工后,作为指引性注释加入代码中。对于理解代码来说,它们有时有奇效。
不过,在追求“指引性注释”的路上,也要避免踩入以下几个陷阱:
- 简单复述代码:指引性注释虽然是一种帮助理解代码的辅助性文字,但绝不能只是复述代码而已,简单来说,你可以这么理解两者在传递信息方面的风格差异:代码是一本厚厚的权威科学教材,指引性注释则是一小册面向中学生的科学启蒙读物
- 追求“注释率”:不要在“代码注释率”指标上设置硬性要求,指引性文字也需要讲究质量,盲目追求数量只会适得其反
- 不注重时效性:过时的注释比代码危害更大,要及时修改或删除已经过期的指引性注释
总而言之,你可以把指引性注释当成有针对性的代码“教学文本”。审阅代码时,如果你发现一段逻辑理解起来很吃力,而代码本身也没有太多优化空间,请不要迟疑,勇敢表达出你对于“教学文本”的需求吧!
3. 沟通方式
“我因为鲁思和萨拉不得不离开我们而痛苦万分。而令我感觉更加痛苦的是我当时以为自己是完全孤立无援的。”
“说真的,肯顿小姐……”我端起那个我用来放使用过的瓷器的托盘。“对那样的解雇我自然是极不赞同的。我还以为那是不言自明的。”
——《长日将尽》
时至今日,仍有许多人认为软件开发是一种单打独斗的工作。一位程序员捡起一块键盘,就能源源不断地产出代码,根本不需要其他人。但事实是,程序员单打独斗的黄金时代早已过去,现代软件开发已演变成一种多人参与的协作事务。正因如此,程序员的日常充斥着各类沟通工作,参与代码评审正是其中之一。
在代码评审时,评审者的工作内容似乎一句话就可简单概括:指出他人代码中的不足。 这听起来易如反掌,对不对?我曾经正是这么以为,所谓评审,只要做完下面的“123”即可:
- 找出所有可优化的点(有事说事!)
- 等提交者完成改动,或在讨论后确认维持原状(就事论事!)
- 合并代码(大功告成!)
而现实总是和理想相去甚远,代码评审很少会像上面这样顺利。因为一旦涉及到人与人之间的沟通,尤其其中一方还在给另外一方“挑毛病”,事情又能简单到哪儿去呢?
人类是一种神奇的智慧生物,阅读一段文字,不仅能从中获取到信息,更能从字里行间感受到情绪,有时,这份情绪甚至会盖过信息,影响他们做出判断。因此,当你在参与评审时,请谨记这一点:保持谦逊、尊重他人,无论对方的经验或背景如何。优秀的表达,能做到内容即使在批评,也能让对方感受到自己仍是被尊重的。
让我来举一个例子。团队内来了一位新人,用他不太熟悉的 Python 语言提交了一个 PR。作为 PR 的评审人,你在代码里发现了一段冗长的循环代码,于是写下评论:
代码比较啰嗦,建议改成列表推导式。
虽然你的观点没错,但这种表达方式值得商榷。下面是这条评论的另一种写法:
这里的循环体较简单,只有过滤和转换逻辑,很适合改成列表推导式,代码更精简。举个例子:
items = [to_item(obj) for obj in objs if obj.is_valid()]
参考: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
比起第一条评论,后面这条显然更不容易引起新人的抵触情绪,更可能被采纳。这就像那句老话所说:“你表达的方式跟所要表达的内容同样重要,如果不是更重要的话。”
除了保持谦逊和尊重,还有一些其他值得采用的评审沟通小技巧:
- 一例胜千言:有时用文字洋洋洒洒写一大堆,不如直接写几行代码,举一个实际的代码样例
- 见什么人说什么话:对于加入团队一个月和一年的开发者,你在评审时可以(也应该)区别对待;对缺乏经验的新人,组织语言时要谨慎,尽量避免让对方感觉不被尊重,产生太多的挫败感;对熟稔的老人,语言风格就可以相对随意,言简意赅即可,不必过于啰嗦
我相信也许大部分人在心底都认同:代码评审是一个“对事不对人”的过程,不应该把对代码的批评当成对人的否定。但这和提倡“好好说话”并不冲突。一次让双方满意的沟通,几乎等同于一次更高效的沟通。所以,改善沟通方式就能提升工作效率,你又何乐而不为呢?
总结
代码评审作为保障软件质量的重要手段,是大型软件开发中不可或缺的重要一环。本文总结了我作为评审的参与者,在命名、指引性注释和沟通方式三个方面的一些思考,要点如下:
- 程序员们应当不断提高在代码评审时对于命名的敏感度
- 检查命名的两个技巧:同类名词保持一致、用更精确的词代替那些“万金油”名字
- 对待名字不要一视同仁,多多关注那些最重要的名字
- 对任何一个项目,领域(业务)相关的名字最重要,值得仔细斟酌、反复推敲
- 指引性注释虽不提供太多代码之外的信息,但也有着不可替代的作用
- 指引性注释能大大降低人们为理解代码所付出的心智成本
- 留意指引性注释的几个陷阱:简单复述代码、追求“注释率”、不注重时效性
- 评审时,要勇于对复杂的代码逻辑提出补充指引性注释的请求
- 软件开发中包含许多与沟通有关的事项,代码评审正是其中之一
- 理想的评审是“有事说事,就事论事”的,但正因涉及人际沟通,导致现实往往偏离理想
- 文字不光能传达信息,更是情绪的载体,而情绪往往会影响沟通的效果
- 在评审中,永远保持谦逊、尊重他人,无论对方的经验或背景如何
- 对于有着不同经验的待评审者,应当采取不同的沟通风格
代码评审是一项涉及多人协作的复杂事务,里面藏着许许多多的学问。质量高的评审,对于提升质量和塑造团队氛围有着不可替代的作用。质量低下的评审,则可能沦落为形式主义,甚至让团队内部滋生矛盾和不满。
而影响评审质量的因素,往往藏在那些不起眼的小细节、小事情中。以上这些关于“小事情”的经验之谈,希望能对你的工作有所启发。
😊 如果你喜欢这篇文章,也欢迎了解我的书: 《Python 工匠:案例、技巧与工程实践》 。它专注于编程基础素养与 Python 高级技巧的结合,是一本广受好评、适合许多人的 Python 进阶书。