「如何优雅的不写注释?」每个工程师都要不断追求的漫漫长路
引言 ✨
作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。
说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并不是银弹,也不是最好的手段,在我的眼里,注释更多承担的是一个兜底的作用。
于是便有了本篇文章,我想通过更多亲身经历和书籍参考来阐述证明我的观点:
- 什么是好注释
- 什么是坏注释
- 怎么不通过注释提高编码可读性「易于上手版」
- 挖一个关于前端架构的大坑「待我能力提升」
注意我并不是贬低使用注释,而是想给大家推销一种思想,即更多的用代码阐述自己的想法,把注释作为一种保底手段,而不是银弹。而且我切实的知道很多历史因素导致代码极其难以阅读,利用注释去表达信息也是没有办法的。但是希望大家读完本文之后可以更多去考量如何让代码更可读,而不是怎么写注释。
不堪回首的摸爬滚打 🌧️
我毕业工作这一年以来,一直在不断加深对整洁编码的理解,我也经历过迷惑和自我怀疑。但是通过不断的实践,思考,回顾,我渐渐的有了自己的理解。
希望大家在阅读完成我的经历与摸爬滚打后,有所收获,如果觉得没啥参考性,就当作看了一场平平无奇的故事。
求学 🎓
曾经在求学过程中,大家的老师应该都会鼓励大家去多些注释去阐述你的编码思想,我便一直认为写注释是一个标准,是我在开发中必须要去做的事情。
然而现在我回过头去看曾经老师的教诲,我会觉得是出于以下三点考虑:
- 对于一个计算机初学者,写注释可以让你去梳理你的编码思路
- 写注释的时候也会对代码逻辑进行思考,类比伪代码
- 很现实的一点,帮助老师理解你的代码,毕竟有的代码不写注释老师都不知道判卷的时候该给分还是不给分
实习 🧱
之后我便开始了我的第一段职业生涯,大四实习,我来到我现在所在的公司呆了两个月,做的也是比较简单的工作,改一改前人留下的遗产「bug」,到即将返校的时候,我接到了一个开发任务,当时我开发了一天,晚上进行 code-review,我记得当时我信心满满,给每个子方法和关键变量都写了注释,本来信心满满,但是最后我的代码被前辈批到自闭,其中不乏关于代码设计和命名相关的建议,也是在这次 review 中我听到了一句足以毁灭我学习生涯所建立的世界观的话:
好的代码是不需要注释的
工作 💻
工作之后,最开始我接触的项目也是一个恶臭代码的重灾区了,充斥着各种难以阅读的代码逻辑,但是那时候我还没有一个评判好坏的能力,一度去怀疑自己。
是不是我太菜了,才读不懂别人那些高端的代码
没错,我真的这么想过,甚至十分的自我怀疑,但是后期我经历了几件事情让我对这个自我怀疑的想法消除了:
- 在导师的指导下对代码的反复
code-review
并重写
当时我们发现该项目存在需求遗漏,于是这个需求便来到了我的头上,即使项目紧急,导师还是给我细心
review
,最后这个功能我重写了三四次,也让我对什么样的代码是好的有了一个粗略的概念。
- 对某一个模块的完全重新设计与编码
经历了从设计到评审,再到编码,最后
review
的过程。
- 相关书籍的阅读
本篇文章不做书籍推荐了,只是表达对于如何整洁编码是存在很多前人经验与指导原则存在的,新人可以优先阅读《代码整洁之道》
- 丰富的自我实践与思考
在我后来参与产线内部平台建设,负责安全运维大模块,负责冬奥会项目过程中也是不断的在追求整洁编码。思考,实践,回顾,一直伴随着我的职业道路,对于代码如何编写的更整洁也渐渐有了自己的想法。
在工作的过程中,我对于“好的代码是不需要注释的”这句话的理解也在不断加深。当然,如果对于某些难以处理的遗留问题,注释也是一个不错的方法对其进行注解描述。
总结 🔍
最开始我觉得注释是必要的,后经过经验的积累,前辈的教导,自己的学习,不断的思考与回顾,到现在有了自己的一套思想。当然我不会去说我的思想是正确的,可能过几年之后我会回来打我自己的脸,其实想法的改变,也能代表一种成长吧~
有关注释的杂七杂八 🌲
别给糟糕的代码加注释,重新写吧
- 什么也比不上放置良好的注释来的有用
- 什么也不会比乱七八糟的注释更有本事搞乱一个模块
- 什么也不会比陈旧,提供错误信息的注释更具破坏性
若编程语言有足够的表达力,或者我们长于用这些语言来表达意图,就不那么需要注释 —— 也许根本不需要。
上面的话引自《代码整洁之道》。但是从事这个行业越久我越无法否认其正确性,我们必须要知道的一件事是代码具有实时性,即你现在项目中的代码总是当前最新的,否则也无法正确运行。然而上面的注释我们根本无法知道是什么时候写的,不具备实时性。
- 代码是在变动,演化的。然而注释并不能随之变动
- 程序员不会长期维护注释
- 注释会撒谎,而代码不会
- 不准确的注释比没注释坏的多
- ...
所以我的想法很坚定,注释无法美化糟糕的代码,与其花时间为糟糕的代码编写解释不如花时间把糟糕的代码变得整洁。
用代码来阐述思想一直是最好的办法。
当然总有些注释是必须的或是有利的,还有一些注释是有害的,下面和大家聊一聊什么是好注释,什么是坏注释。
好注释 🌈
- 法律信息
比如版权或者著作权的声明
- 提供信息的注释
比如描述一个方法的返回值,但是其实可以利用函数名来传达信息
- 阐释
把某些晦涩难懂的参数或者返回值翻译成某种可读的形式,更好的方式是让参数和返回值自身就足够清楚
- TODO 注释
这个可能大家都会经常用,用来记录我们哪里还有任务没有完成
- 放大
比如一个看似不起眼却很重要的变量,我们可以用注释凸显它的重要性
- ...
坏注释 😈
- 喃喃自语
只有作者读的懂的注释,当你打算开始写注释,就要讲清楚原委,和读者有良好的沟通
- 多余的注释
有的注释写不写没啥作用,很简单的方法都要写注释,甚至读代码都比看注释快
- 误导性注释
程序员都已经够辛苦了,你还要用注释欺骗人家
- 循规式注释
要求每个方法每个变量都要有注释,很多废话只会扰乱读者
- 位置标记
比如打了一堆 ****** 来标注位置,这个我上学的时候经常干
- 废话注释
毫无作用的废话
- 注释掉的代码
很多读者会想,代码依然留在那一定有原因,最后不敢删除畏手畏脚
- 信息过多的注释
注释中包含很多无关的细节,其实对读者完全没有必要
- ...
优雅的不写注释 🌿
首先我再次阐述之前说过的话,编码实际上是一种社会行为,是需要沟通的。而如何让我们不借助注释来阐述我们的思想,其实是需要我们长期探索并在实践中积累经验的,从我的经验与视角出发,其实让我们的代码库更加整洁其实主要从以下两个方面考量:
- 整洁编码
- 前端架构
下面我分开来讲~
注意,编码不是一个人的事情,在我眼里如何做到团队成员编码风格的相近才是最具成效且需要长期努力的任务,也是相对理想且难以做到的。正所谓,就算我们写的代码很烂,但是烂的我们的成员可以相互理解,也是一种优秀「瞎说的,哈哈哈,代码可维护性还是要团队成员一起追求的」。
整洁编码 📚
首先我先引用几位前辈的话,带大家感受一下,什么样的代码是整洁的:
- Bjarne:我喜欢优雅和高效的代码,代码的逻辑应当直接了当,叫缺陷难以隐藏们。尽量减少依赖关系,使之便于维护,依据某种分层战略完善错误处理代码,性能调至最优,省得引诱别人做没有规矩的优化,搞出一堆混乱出来,整洁的代码只做好一件事。
- Grady: 整洁的代码简单直接,整洁的代码从不隐藏设计者的意图,充满干净利落的抽象和直截了当的控制语句。
对于整洁编码可以先简单总结:
- 尽量减少依赖关系,便于维护
- 简单直接,充满了干净利落逻辑处理和直截了当的控制语句。
- 能够全部运行通过,并配有单元测试和验收测试
- 没有重复的代码
- 写的代码能够完全提现我们的设计理念「这个可以通过类、方法、属性的命名,代码逻辑编码的清晰来体现」
在我们日常编码中,命名和函数可以说是我们最常接触的,也是最能影响我们代码整洁度的。于是本文中,我将围绕这两个方向为大家介绍几种易于上手的整洁编码方案。
下文参考我之前写过的一篇文章:关于整洁代码与重构的摸爬滚打
命名 🌟
- 只要命名需要通过注释来补充,就表示我们的命名还是存在问题
- 所有的命名都要有实际意义,命名会告诉你它为什么存在,它做什么事情,应该怎么用
比如列举一段曾经上学的时候可能写出的代码:
#include <stdio.h>
int main(){
printf("Hello, C! \n");
int i = 10;
int m = 1;
for(int j = 0; j < i; j+=m){
for(int n = 0; n< i-j;n++){
printf("*");
}
printf("\n");
}
return 0;
}
我们看这里命名都是一大堆 i,m,n,j
之类的根本不知道这些变量用来干嘛,其实这段代码最后仅仅打印出来的是 *
组成的直角三角形。但是当时写代码我确实就是这样,i, j,m,n
等等字母用了一遍,也不包含什么语义上的东西,变量命名就是字母表里面选。
当然现在的命名就高端多了,开始从词典里面找了,还要排列组合,比如 getUser
,isAdmin
。语义上提升了,通过命名我们也可以直观的判断这个方法是干嘛的,这个变量是干嘛的。
这样看其实命名是很有学问的事情,下面我开始列举几点命名中可以快速提升代码整洁度的方法:
- 避免引起误导
不要用不该作为变量的专有名词来为变量命名,一组账号
accountList
,却不是List类型,也是存在误导。命名过于相似:比如XYZHandlerForAORBORC
和XYZControllerForAORBORDORC
,谁能一眼就看出来呢~
- 做有意义的区分
let fn = (array1,array2) =>{
for(let i =0 ;i<array1.length;i++){
array2[i] = array1[i];
}
}
比如上面
array1
和array2
就不是有意义的区分,这只是一个赋值操作,完全可以是sourceArray
和DesArray
。
再比如 起的名字:userInfo
,userData
都是这种的,我们很难读懂这两个有啥子区别,这种区分也没啥意义,说白了这只是单词拼写的区分,而不是在语义上区分开了。
- 使用读的出来的名称
编程是社会活动,免不了与人交流对话,使用难以轻松读出来的声音会导致你的思想难于传达。并且人类的大脑中有专门处理语言的区域,可以辅助你理解问题,不加以运用简直暴殄天物。简单举个例子:getYMDHMS,这个方法就是获取时间,然而就是难以阅读,是不好的命名。
- 使用可以搜索的名称
之前的代码,我用个
i
作为变量。如果代码很长,我这想要追踪一下这个i
的变化,简直折磨。同理我不喜欢直接以value
,data
,info
等单词直接做变量,因为他们经常以其他变量的组成部分出现,难以追踪。
- 程序中有意义的数字或者字符串应该用常量进行替换,方便查找
export const DEFAULT_ORDERBY = '-updateTime'
export const DEFAULT_CHECKEDNUM = 0
比如采用上面的方式,既可以让代码更加语义化也方便集中修改
- 类名和对象名应为名词或名词词组,方法名应为动词或动词词组
比如我们常用的
updatexxx
,filteredXXX
都是这样的命名规则
- 属性命名添加有用必要的语境,但是短名称如果足够用清楚,就比长名称好,别添加不必要的语境
- 每个概念对应一个词
比如
tag
和label
,ticket
和workOrder
各种混着用岂不是乱糟糟的,这读者容易混淆,也会为以后造成负担,也可能会隐藏一些 bug。所以我们在项目开发前可以确定一个名词术语表来避免这种情况发生。
- ...
函数 🌟
大师写代码是在讲故事,而不是在写程序。
- 短小:20封顶最佳
- 函数的缩进层级尽可能的少
- 函数参数尽量少
- 使用具有描述性的函数名
当然函数越短小,功能越集中,就越便于取好名字
- 抽取异常处理,即
try-catch
就是一个函数 ,函数应该只做一件事,错误处理就是一件事 - 标识参数丑陋不堪
const updateList = (flag) {
if(flag){
// ...
} else {
// ...
}
}
比如一个方法,定义成上面这个样子,我们很难通过方法定义直接了解其能力以及参数的含义。
- 函数名是动词,参数是名词,并保证顺序
比如
saveField(name)
,assertExpectedEqualsActual(expected,actual)
- 无副作用
比如一个方法名是
updateList
,后来者应该顺理成章的认为这个方法只会更新列表,然而开发者在这个方法中加入了其他的逻辑,可能导致后来者在使用这个方法后导致副作用,而代码报错无法正常运行。
- 重复是软件中一切邪恶的根源,拒绝重复的代码
- ...
写代码和写文章一样,先去想你要写什么,最后再去打磨,初稿也许粗糙无序,那就要斟酌推敲,直到达成心中的样子。编程艺术也是语言设计的艺术。
前端架构 🎋
本人现在工作一年有余,一年半不足,对于前端架构并不能很好的输出给大家,所以在此给大家先挖一个大坑,本章节中如有错误理解,请大家不吝赐教,与我探讨交流,感谢。
首先,我先解释一下我为什么要把前端架构放在这样的一篇文章中,其实是存在两条原因:
- 从个人开发角度来看,优秀的前端架构可以增强代码的维护性
试想一个组织结构恶臭的项目,一定会影响你的阅读的,杂乱不堪的组件划分原则,不清晰的边界通通都会成为巨大的阻力。
- 最近换了组,到了天擎终端平台组,新的
leader
也分享了很多关于组件化的经验与理解
浅薄无知的小寒草🌿,在线求鞭策。
那么,大家在提到前端架构的时候,会想到什么呢,我反正会想到以下几点:
- 组件化
- 架构模式
- 规范 / 约定
- 分层
- ...
下面我逐条来讲~
架构模式 ✨
组件化我先跳过,最后再说,先说说架构模式,大家脑子里一定会想到 MVVM
,MVC
等模式,比如我们常用的 Vue
框架中的 MVVM
,以及普遍在 Spring
那一套中被提及并在在 nest.js
中有所应用的 MVC
。但是关于架构模式前端说的可能还是相对较少,我的水平也有限,而且说起来可能就会跑题了,于是也不在本文过多赘述。
规范&约定 ✨
关于规范或者约定,常见的包括:
- 目录结构
- 命名规范
- 术语表
- ...
其实这几点我们很好理解,我们会通过约定或者脚手架等方式来规范化我们的目录结构,使我们同一个产线下项目的目录结构保证一致。以及我们在开发前的设计阶段可能也需要出具一份术语表,这个前文也听到过一个含义用一个确定的词来表示,否则可能会导致代码的混乱。
关于命名规范,首先我们需要去约定一个统一的命名规则,我们常见的是变量命名为小驼峰,文件命名为连字符。但是这个命名规范其实我们可以做的事情不止这些,比如我说几个例子:
- 前端命名规范是小驼峰,服务端命名是下划线,我们怎么处理让前端编码中屏蔽掉命名规则差异。
- 同一个含义我们可以用很多命名来表示,比如:
handleStaffUpdate
/updateStaff
。在项目初期我们完全可以对其进行约束使用哪种命名风格,以让我们项目一致性加强。 - ...
分层 ✨
关于分层,大家的差异可能会比较大,比如我们可能会把我们的前端项目分为以下几层:
- 业务层
- 服务层
- 模型层「可能有也可能没有」
业务层就是我们比较熟悉的,各种业务代码。
服务层「server
」不知道大家的项目中有没有,我们项目使用 grpc
接口,由于接口粒度较高,我们通常会在 server
层对接口再次处理,合并,或者在这个层去完成一些服务端不合理设计的屏蔽。
模型层「model
」不常有,但是一些复杂的又需要复用的逻辑可能有这个层,就相当于逻辑的抽象,脱离于视图,之后如果我们需要复用这里的逻辑,而视图不同,我们就可以使用这个 model
。
合理的分层可以让我们的项目更清晰,减少代码冗杂,提升可维护性。
组件化 ✨
其实组件化一直都是前端架构中的大课题,首先我们可以通过组件化能得到什么,其实最重要的可能就是:
- 复用
不知道大家的项目有没有统计代码复用率,我们是有的,而且这也是前端工程质量很重要的一个指标。然而在追求组件化的过程中其实我们很少会拥有一个衡量标准:
- 什么情况需要拆分组件?
- 什么情况不需要拆分组件?
团队对这个问题没有一个统一认知的情况下很容易造成:
- 五花八门的组件拆分原则导致代码结构混乱
- 无效的组件拆分导致文件过多,维护困难
- 过深的组件嵌套层级「经历过的人一定会对此深恶痛绝」
- ...
其实我最开始的时候也喜欢把组件按照很细的粒度进行拆分,想的是总会有用到的时候嘛,但是从架构整洁的角度出发,过细或者过于粗糙的组件拆分都会导致维护困难,复用困难等问题,现在的我可能更会从复用性角度出发:
- 这个东西会不会复用?
只从复用性考量很容易的就会把组件区分为两大类:
- 需要复用的组件
- 几乎不会被复用的组件
注意我没有说什么组件是肯定不会被复用的,而是几乎不会被复用。
所以我们就可以坐下来思考,把我们工作中常见的场景拎出来,过一遍,因为我们工作的业务场景不同,所以我肯定还是以我的业务场景出发,那么我可以把我的组件分成几种:
- page 组件
- layout 组件
- 业务组件
其中我认为,page
组件是几乎不会复用的组件,layout
组件和业务组件在我眼里是可以复用的组件。
这只是很粗糙的的区分,之后还有很多问题:
- 如何把业务组件写的好用
- 如何确定一个组件的边界
- ...
这些我们就要从消费者角度考量了。
当然其实组件化也可以和分层一起考虑,因为组件其实也会有层级,比如:
- 基础 ui 组件[参考element-ui]
- 基础业务组件
基础业务组件也可以按照是否跨模块等原则继续进行分层,这个可以按照大家的业务场景自行考量。
总结 ✨
从实际经验出发,合理的架构确实是项目易于维护「从而优雅的不写注释🌿」,而这是一个自顶向下分析决策的过程,本章节篇幅有限,加上我水平有限,无法在此过多赘述,还请大家持续期待我的分享。
结束语 ☀️
那么本篇文章就结束了,涵盖了我个人经历上的摸爬滚打,解析什么样的注释是好的,什么样的注释是坏的,并从编码整洁度与前端架构的角度出发来考量如何提升代码的可维护性。以此来论述我的观点:
注释不是维护代码的银弹,而便于维护的代码需要从整洁编码与前端架构两个「或者更多」层面入手。
我工作的时间不长也不短了,已经一年出头了,我一直秉承着编码是社会性工作,需要协同合作,代码的可维护性也是一名职业软件工程师需要持续追求的观点。
思考,实践,回顾的过程没有停歇,我在此也希望大家多思考,作为一名工程师我们需要追求的不仅仅只有: