写好代码,我的三个 Code
国内很多大学的计算机专业,比较偏重基础和理论的“灌输”(就我当年上学的体验,现在可能会好一些),对于代码能力,虽然也有一些课程实验,但往往不太够用。于是,在进入正式工作前,很多同学就会对自己代码水平不太自信。下面我就根据我自身的写代码经历提供一些建议。
一些经历
我是 2010 年上的北邮,当时也是很迷糊的就进了计算机专业。自然的,在大学一开始也谈不上什么学习规划。只能是沿用着高中的学习方法,懵懂地跟着老师走——上课就听课,课余就自习做作业。结果便是,学习效率很低,上课听不太懂、题目做不通透。但总归,上完计算机导论后,编程作业都是自己啃出来的,跌跌撞撞的完成之后,慢慢地竟感受到了编程的乐趣。
我们当时大作业最多的几门课,C++ 程序设计、算法和数据结构、操作系统、计算机网络、微机原理等,现在想来,大部分都都跟玩具一样。后来做了国外一些知名大学公开课的实验才知道,要打造好一个实验项目,是非常难的事情:
首先,得适配学生的水平,准备详尽的实验材料。
其次,得搭好代码框架,在合适的地方“留白”,给学生“填空”。
最后,还得构建足够好的自动化测试平台,进行打分。
如果从头开发,这里面涉及到的复杂度、需要花的心思,并不比发一篇顶会论文简单。那作为教授来说,有这些时间,我为什么不去发一篇论文呢?毕竟国内高校都是科研第一、教学老末。
因此,我在本科课内,代码水平也并没有打下太好的基础。在后面在读研和工作中,不断摸索,代码水平才一点点提高。回头来看,对我代码能力提升有比较大影响的可以总结为 “Code”:LeetCode、Writing/Review Code Loop、Clean Code。
LeetCode
在说 LeetCode 前,想先说说工作后,见到的一类神奇的人——打过算法比赛(通称 ACM,其实是 ICPC 和 CCPC)的同学的印象。这类同学的一大突出特点,用最简单直接的语言来形容,就是:出活快。几年的竞赛经历,让他们只要在脑袋中对需求(题目)理解之后,就能在最短的时间内转化为代码。
由于太过懵懂,我自然是没有打过竞赛,等反应过来竞赛的诸般好处时,已经大三下了。当时,校队也不会招这么“大龄”的队员了,就算招,门槛也非常高,也是大学诸多憾事中的一件了。
后来读了研,在找工作前一年时,LeetCode 已经相当流行了,便也和同学组队,互相激励着刷了起来。当时题目还不是特别多,到研二暑假找实习时,大概把前两百多道刷了两遍。一开始,会不断思考题目是什么意思,该用什么算法解,有时半天想不出来,便去看高票答案。很多高票解真的是精妙而简练,这大概也是当时 LeetCode 最吸引人的地方之一。慢慢的对各种类型题目有些感觉之后,就开始练速度和通过率。也就是上文说的,在理解题目后,能够迅速转变为 bug free 的代码。
因此,虽然没有打过比赛,但是通过 LeetCode 的训练,确实也有了类似竞赛的收获。但自然,在深度、广度和速度上都远不及那些“身经百赛”的同学。不过于我已经是受益匪浅:
对常见数据结构和算法掌握纯熟。比如现在说起六种排序,特点、使用场景、背后原理,可以做到如数家珍;比如说起树的各种递归非递归遍历,脑动模拟递归执行过程,也是信手拈来;再比如链表、队列、图等特点,也能在脑中边模拟,边换成代码。
学到了很多精巧的代码片段“构件”。比如如何二分、如何迭代、如何处理链表头尾节点、如何设计基本数据结构的接口等等。这些偏“原子”的构件,是我后来工作中写代码的血肉来源。
但只有这些,是远远不够的,一到大项目里,写出的代码就很容易——“有佳句无佳章”。
Writing/Review Code Loop
遇到上述窘境,往往是因为缺少中大型项目的磨练。表现在空间上,不知道如何组织上万行的代码,如何划分功能模块、构建层次体系;体现在时间上,没有经过项目“起高楼、宴宾客、楼塌了”的构建-腐烂-重构循环。
工程中在理解代码和组织代码时有个矛盾:
可理解性。作为维护人员,我们学习代码时,多喜欢顺着数据流和控制流来理解,所谓根据某个头,一路追查到底,是为纵向。
可维护性。但作为架构人员,我们组织代码时,为了容易维护,多是按照围绕模块来组织代码——把关联紧密的代码聚合到一块,是为横向。
所以我们在拿到一个大工程时,如果立即地毯式的看代码,肯定会昏昏欲睡、事倍功半。不幸的是,由于多年读书养成的强大习惯,这个毛病,跟了我很多年。正确的打开方式是,要像对待团在一起的多条线一样,找到“线头”,然后慢慢往外揪。在项目中,这些线头是:service 的 main 函数、各种单测入口。
但我们在构建一个大工程时,又得反着来:先搭建一个揉在一起的主流程,然后逐渐迭代。就像盘古开天辟地一样,随着时间而演化,让天慢慢地升高、地慢慢下降,让整体化为地上四极、山川河流、太阳月亮。如是迭代,将一个混沌的流程,慢慢地模块化。比如常用的工具模块(utils)、业务相关基础模块(common)、控制模块(controller、manager)、RPC HTTP 等回调处理模块(processor)等等。
但当然,如果你已经有了构建某种类型系统的经验,则并不需要在构建初期经历这个漫长过程,可以直接按经验分出一些模块。更进一步,你已经形成了自己的一个代码库,比如时钟、网络、多线程、流控等等,可以直接拿来就用。
那剩下的问题就是细节的微调,我们在进行分层时,边界处的功能,是往上升,还是往下沉;某个较完整的结构,是拍平到使用类里,还是单独拎出来;这些形形色色的决策,都没有一个定则,更多的还是根据场景的需求、工期的长短等诸多实际情况,便宜行事。而这种背后的决策,则是在长时间对中大型项目的学习、对别人修改的 Review、自己上手搭架子和修修补补中,一点点形成的直觉。
就像股票市场有周期一样,工程代码也是有其周期。不经历一个股市牛熊周期,我们不敢轻言空多;不经历过一个工程构建-成熟-腐烂的周期,我们也不敢轻言取舍。即,没办法在工程构建初期,预见到其最常用的打开方式,进而面向主要场景设计,牺牲次要场景的便利性。
单元测试的重要性,怎么强调都不为过。一方面,能不能写出的单元测试,意味着你代码的模块边界是否清楚;另一方面,通过设计好的输入和输出,测试能够保证某种“不变性”,之后无论你怎么微调、重构,只要能跑过之前的测试,那就可以放心一半。另一半,就要靠自己和别人不断 Review 、测试集群线上集群不断地迭代了。
所以,这个过程是一个无休止的 loop,不断的磨,尔后不断地提升。
Clean Code
最后说说对代码的品味。小节标题是:Clean Code,是因为我对代码的品味,最初是从 Clean Code: A Handbook of Agile Software Craftsmanship[1] 这本书建立起来的。其第二章对命名——这个工程中“最难”的事情——的阐述,给我印象很深。
举几个例子:
单一职责。如果你不能清晰的对你的类或者函数命名,说明你的类或者函数干的事情太多了。
命名代替注释。比如不要直接使用字面值常量,最好给其一个名字;比如最好不要使用匿名函数,也要给其一个能看出含义的名字。
工作中,我们常说,某某对代码有“洁癖”。我也多少有一些,但我并不以为这是洁癖,而是一种对美的欣赏和追求。代码的美体现在哪里呢?我这里稍微抛个砖(当然,我之前也写文章就代码命名问题啰嗦过,感兴趣的可以点这里[2]可以去看看):
一致性。比如具有相同含义的实体,使用相同的命名;而需要区分的实体,则要通过命名阈、前缀来进行甄别。从而给读者造成最小的心智负担。
体系性。是指我们在做一组相关接口时,要考虑其体体系性。比如增删改查,比如生产消费,比如预处理、处理、处理后,比如读取写入等等。体系性又包括对称性和逻辑性,让人在拿到一组接口时,就能最小成本地理解其是如何相互联系、又是如何具有区别的。
没有赘肉。写代码,不要啰嗦,不要啰嗦,不要啰嗦。如果不小心啰嗦了,说明你可能没有想清楚所解决问题的本质。复杂的表象,在不断地剥离杂质后,往往有很简单的关窍。抓住这些关窍,再往其上附着骨肉,同时理清楚一对一、一对多、多堆多等依赖关系,往往能化简为繁。
不同概念(对应代码中的类)间的关系,在理解代码组织的时候至关重要,最好在名字上有所体现,比如一对一,一对多还是多对多。每个概念的内涵,以及多个概念之间的包含、连接关系,是在做模块设计的时候最需要考虑的事情之一。
在审美之外,还要说说建模(在某种程度上和隐喻是相通的)。毕竟,我们在说构建时,本身就是借助的建筑学中的隐喻。软件工程中,类似的隐喻随处可见。
我们大脑在认知新事物时,多建立在基于旧的模型推演上。因此,如果在处理模块时,如果能从经典的模型库中,找到一个相对合适的抽象,往往能够极大降低用户理解门槛。比如经典的生产者消费者模型、树形组织模型、路由器模型、线程调度模型、内存模型等等。此外,也可以使用某种常见意象、隐喻来命名项目,往往也能在易理解性上收获奇效。比如监控系统,可以叫“鹰眼”;比如各种流水线管控,可以叫“富士康”(手动斜眼);再比如更常见一些的数据采集,我们都知道他叫——“爬虫”。
The last Thing
世间的事情往往是多方印证、互相补足的——如果你想写好代码,就不能只是低头写代码,你得去读读历史、学学美术、写写文字、见见河山,建立一套你自己的审美偏好,然后将其理念平移到写代码里来,才能写出符合直觉、具有美感的好代码。
链接:https://juejin.cn/post/7214288126222467132
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。