当前位置: 首页 > 科技观察

程序员的真香法则:源代码就是设计

时间:2023-03-19 02:28:02 科技观察

本文转载自微信公众号“码砖杂工”,转载请联系码砖杂工公众号。我们经常谈论架构和设计,但很少关注实现和代码本身。架构和设计很重要,但是说代码本身不重要,我不同意。罗伯特·C·马丁叔叔也不同意。Martin相信“源代码就是设计”。在讨论具体的实现规则之前,我们不妨先讨论一下什么是好代码?CarrotC.Martin认为,衡量代码质量的唯一标准是:WTF/min,即review代码时,每分钟说“草”的频率。这个定义虽然有辱人格,但粗犷中不羁,调皮中蕴含哲理。好的代码就像优美的散文。好的代码就像一本构思巧妙的小说。它可能不是直截了当的,但它足够吸引人。读到最后,你会豁然开朗。妈的,原来是这个样子。那一刻,你会感受到过程中的曲折。这一切都值得探索。好的代码,通过每一个功能,你仿佛都能窥探到作者有趣的灵魂;透过每一行代码,你仿佛在和一位睿智的朋友聊天,她总是思路清晰,逻辑清晰,有条不紊,口若悬河。糟糕的代码就像病毒。它不仅使你的程序瘫痪,而且具有很强的传播作用。一旦蔓延,就很难治愈。糟糕的代码,像一坨泥巴,或者像一坨屎,看书的时候,你好像被困在一个漆黑的迷宫里,好像在跟一个喋喋不休的人说话,她的脑回路经常短路,而且她的讲话模棱两可。很明显,不管轻重缓急,时间长了,你还是摸不着她的中心思想。你常常觉得自己的智商受到了极大的侮辱,甚至有被喂屎的感觉。你面露难色,心跳加速。区分好代码和坏代码的规则有很多,我也看到了其中的一些。我不会重复文章中提到的一些标准做法的基础知识。我想结合我的工作经历谈谈我的个人经历。闲话半天,言归正传,写出有味道的代码应该遵循哪些约束和准则?一致性始终遵循一致性规则。代码风格上,折腾了三天三夜,估计也分不出好坏,但好的风格必须是强一致的。在这一点上应该更容易达成一致吧?其实风格的好坏更多的是受习惯的影响。头发少的程序员对于风格的变化应该都有自己的体会。很多年前我信奉的好风格可能就是现在讨厌的坏风格,所以我提倡抛开风格的话题。一个项目应该有一个编码规则。好的规则应该是有说服力的。好的规矩应该是拒绝随意携带私人物品。规则定下来之后,就要遵守。也许某种风格不适合你,但没关系。你必须知道,这并不意味着你输掉了风格之战,也不意味着它让你相信你是在遵守规则和纪律本身。经历过一些C项目,函数名有大驼峰、小驼峰、下划线连词、驼峰加下划线(有的是两个),函数名以一个下划线或两个下划线开头。总结一下它的规律就是没有规律,而且很随意,弄得我很迷茫。变量(包括文件、类/结构、函数)都是有命名的,比如ohmygod,你可能分不清哪些字母属于一起,所以需要定义单词。CamelCase通过首字母大写来分隔单词,另一个习语是用下划线连接单词。驼峰的缺点是丑陋。下划线拼接的缺点是增加了标识符的长度(相对于首字母大写)。优点是与stdc/c++和linuxkernel的做法一致。喜欢内核的码农很容易找到家的归属感。C++有命名空间来避免冲突,C也经常使用前缀来防止命名污染全局空间,但我认为命名简洁很重要,所以我支持短前缀,反对长前缀。代码密度实现相同的功能。您更喜欢100行代码还是20行代码?如果你的公司不以代码行数来评价性能,我建议你把代码写得精简一点。如果你的公司以代码行数来衡量绩效,我建议你可以离职,开滴滴,送外卖,摆地摊,因为你在这种公司度过青春基本上是没有前途的。很容易把简单的事情复杂化。你只需要找一个能力平平的人,就可以实现化繁为简的愿望。也许你想说我缺乏简化的能力,这并不奇怪。坦率地说,这不是一件容易的事。做不到不要紧,重要的是要有正确的想法。方向,而不是在错误的道路上越走越远。有些项目充斥着各种无效代码。其实只要稍加思考,就可以识别出来。例如,大块大块的注释掉的代码像发臭的尸体一样散落在各处。比如很多功能重复的代码像垃圾一样堆在那里。比如一个不需要返回值的函数,硬是返回true,然后在调用的地方检查返回值。如果返回false,重新记录一条日志,重新断言,又抛出异常,看起来很烦人。职业道德,美其名曰面向失败编程,处理各种异常情况。或者函数一进来,不管发生什么,想都没想就去检查输入的参数,操作起来如虎添翼。当你看代码250的时候,你会声明这是一个符合ISOXX标准的安全实践,你会完全忘记你写的是什么私有实现函数,调用之前你已经检查过了,私有函数是controlledsecuritycontext,不仅不优雅而且不绿色(低效耗电)和不安全(没有Beng把雷埋在更隐蔽的地方),顺便查一下标准库函数strcpy/strcat,vectoroperator[]检查传递的参数?增加代码密度或集中度,有利于理清思路,突出重点,有助于提高可维护性,而充斥着各种无效语句的代码,只会把关键语句淹没在汪洋大海中,让review代码的人摸不着头脑关键点,看不出主次。就像听一个喋喋不休的人报告,满嘴胡言乱语,就像看剧情拖沓的连续剧,昏昏欲睡,就像喝一瓶二锅头掺十斤白开水,嘴巴能淡出来一样鸟。重构是程序员的口头禅。重构就是在保持程序功能不变的情况下,对架构和实现进行调整。我认为提高代码密度应该是重构的一种追求。linuxkernel、lua、nginx、skynet等优秀的开源库,代码高度集中。推荐读者朋友品尝。封装我们最常做的事情之一就是将重复编写的代码封装成一个函数,用多次调用代替重复编写。还需要找到一个实现函数,因为把代码摊开,暴露细节,很容易掩盖重要的东西,也就是框架和脉络会变得不清晰。一个著名的函数调用给我的感觉比一段代码堆在那里更好。如果我关心它是如何完成的,我可以跳到定义看实现。封装的一个核心原则是单一职责,符合单一职责的函数更容易被复用。解耦构建一个松散耦合的系统一直是软件工程的目标。模块化的一个方向是解耦,但是我们号称要考虑的解耦有多少体现在实现层面呢?比如我们经常做的一件蠢事就是把类似的配置文件或者宏定义放到一个头文件中,这样看起来很统一很正式,至少之前我是这么认为的,但是突然有一天,我发现自己在做这个好像非常不聪明,为什么?因为要把模块配置相关的东西都塞到配置公用文件里,真的合适吗?是不是把公共接口抽出来,把配置相关的数据下沉到各个模块更合适?另外,把宏定义在一起意味着,如果你改了什么东西,你就需要修改宏头文件,而这个头文件将成为程序世界的中心。修改公共宏文件几乎会导致整个系统崩溃。所有源文件都重建,这简直就是AOE群灭。所以更好的办法是分而治之,去中心化。我们知道c/c++的编译单元是源文件(.c/.cpp),编译的第一步是预处理,所有的include都会被展开替换,所以要避免引入任何不必要的头文件,我们应该也把这个编译单元用到的头文件都包含进来,也就是所谓的头文件自给自足。这个很重要,但是很多人不会当真,有的人甚至会搞一个allincluded。头文件会增加编译时间,增加依赖。既要避免错误的包含,又要精心设计和划分文件,使每个文件的功能足够内聚和单一。缩写谨慎使用缩写。比起缩写带来的歧义,我宁愿多敲几个键盘。如果要简写,请按约定俗成,比如AI,比如App,比如cfg,但是你把threshold简写成threshold,把Item简写成Iem。我真的不明白你是拼写错误还是缩写错误。我遇到过一些按照标准的项目。每个模块都定义了自己的int32、float、char、void,比如定义了MoKuaiA_int32、MoKuaiB_int32、MoKuaiC_CHAR,不仅如此,它还重新定义了inline、true、false、VOID、const、case、static等所有关键字。它令人难以置信的是,它重新定义了所有标准的CAPI,使得人发指的是它不允许你使用标准的方式来编写语言。我一直想知道为什么要这样做?如果你只是需要解决long和其他整数在不同架构下的长度差异,我想偷偷告诉你,c库头文件stdint.h已经从标准层面统一解决了这个问题。有int8_t/16_t/32_t/64_t、uint8_t等。这种做法是很不好的,会让符合标准的写法无法长进。建议不要这样做。Macro宏是C的一个有效武器,在某些情况下确实有效。我是宏观的中间派。我反对禁用宏和滥用宏。inline可以部分替代宏,但不能完全替代宏。但是我看过一些项目,宏无处不在,全是大写,至少有1/3的代码是各种花里胡哨的宏,你review代码的时候一直跳来跳去,一看,哦,就是这样,然后再切换回来,频繁的上下文切换效率低下,打断了你的思路,让你看代码的时候有想尿到一半憋尿的感觉。其实很多时候是完全没有必要的,没必要去修理这些虚荣的脑筋。有一些命名准则。例如,类/结构应该使用名词,函数应该使用动词或类似doSomething的动宾结构。这些规则大家都很熟悉。我提倡命名要简明扼要,不要罗嗦,准确表达要做什么。如果你在命名上遇到困难,你可能需要考虑你的类定义或者接口划分是否合适。命名是界面的一部分,它很重要,好的命名是自我记录的。如果您不知道,那么我建议您参考STDC/C++API。毕竟这些接口几十年都没有太大变化,经受住了历史的考验,比如malloc/free/atoi,还有stl容器的成员函数。有点意思:size()、capacity()、resize()、reserve()、push()、pop()、top()、back(),很简单,不废话,我觉得很好。所以,如果你写的是某个Manager,比如ItemManager,我建议你直接命名为add(),remove()而不是AddItem(),RemoveItem(),因为你是Item的Manager,操作是不可避免的是Item,从参数上也能体现出来,lessismore,多不如少。可伸缩性的开闭原则是处理可伸缩性的法则。人无远见,必有近忧。这意味着我们不应该局限于现在,但请不要盲目相信可扩展性。戏太多也是一种病。知乎有一篇关于如何将helloworld变成一个大项目的精彩帖子。当你想批评别人的项目时,你可以用可扩展性来谈论事情,但我建议你远离那些谈论可扩展性的人。据我观察,这些人大多虚伪,水汪汪的。避免特殊情况。Linus大神分享了他心目中的好代码。他讲了链表的操作。他更喜欢统一的处理方式,而不是特例处理。我觉得这个例子很有代表性。它其实代表了一种思想,就是自始至终,我们的脑海中都要优先考虑归一化的处理方式。当然,这其实是一个比较高阶的需求,菜鸟可以先跳过这层需求。高效健壮有很多方法可以避免低效操作,例如减少复制、提高局部性、缓冲区/缓存、时空替换、内联、分支预测、预判断、计算延迟、无锁技术等。提高健壮性的关键是面向失败的编程,无信任/零信任设计,假设依赖上下文,上下游不可靠,方法很多,我就不一一列举了。最后喷了这么多,还给你们喷我的机会。我贴出自己的代码,https://github.com/ZhuanJia/mynet。这是我2013年刚进富昌的时候周末摆弄的一个玩具,可能有这样的问题。如果觉得不错,请给我点个赞。如果你觉得很水,那我可以给时间。毕竟是7年前写的。桥水CEO说,如果一年前你不觉得自己是白痴,那说明你今年没有任何进步,更何况是7年前。所有内容纯属虚构,请勿想当然,如有不适请尽快就医。搬砖不易,累了就放下笔休息,改天再讨论!