程序员真香定律:源码即设计
https://p1.pstatp.com/large/pgc-image/RP683b23vkWFCt作者 |我不想种地来源 | 码砖杂役
前言
我们经常谈论架构,讨论设计,却甚少关注实现和代码本身,架构和设计固然重要,但要说代码本身不重要,我不同意,RobertC.Martin大叔也不同意,Martin以为“源码即设计”。
在讨论具体的实施细则之前,我们不妨讨论一下什么是好代码?萝卜特 C.Martin以为:衡量代码质量的唯一标准是:WTF/min,也就是review代码的时候每分钟说“握草”的次数。这个定义虽有辱斯文,但粗野中不失旷达,淘气中又蕴含哲理。
好的代码犹如文笔精美的散文,行云流水,赏心悦目,阅读的时候,如沐东风,带给人愉悦与启迪。
好的代码犹如构思精巧的小说,它或许不够平铺直述,却富足引人入胜,读到末了,你会豁然开朗,卧槽,原来是这样的啊,那一刻,你会觉得过程中的曲折和探索都是值得的。
好的代码,透过一个个函数,你仿佛可以窥视到作者有趣的灵魂;透过一行行代码,你仿佛在与一个充满聪明的朋友聊天,她总是条理清晰,逻辑严谨,有条不紊,娓娓道来。
而坏的代码,犹如病毒,它不仅瘫痪你的程序,还有很强的流传效应,等到它扩散开来,神仙难治。
坏的代码,像一个泥团,又大概像一座屎山,阅读的时候,你仿佛被困于黑暗的迷宫,又仿佛在跟一个絮絮叨叨的人交谈,她的脑回路经常短路,说话含混不清,主次不分,叨逼半天,你依然get不到她的中央思想,你常常感觉智商受到了莫大的侮辱,乃至感觉像被人喂吃shit,你面露艰难神色,心中万马奔驰。
有许多区分好坏代码的规则,我也看过一些,对于文章中提到的一些标准做法,就不重复嚼舌头根子了,我想结合本身的工作经历,谈一谈本身的切身体会。
闲扯半日,言归正传,要编写弥漫好味道的代码,要遵照哪些约束和指引呢?
https://p3.pstatp.com/large/pgc-image/RC6F8263HibLH0一致性
持之以恒的服从一致性规则,在代码风格上,争论个三天三夜估计也定不出个好坏出来,但好的风格一定是强一致性的,这一点应该比较容易告竣一致吧?风格的好坏其实更多受习惯的影响,头发少一点的程序员应该都有本身风格变迁的经历,多年前本身笃信不疑的good style或许正是当前深恶痛绝的bad style。
所以我主张在style上搁置嘴炮,一个项目应该有一个编码规则,好的规则应该是以理服人的,好的规则应该是拒绝任性夹带私货的,规则定了之后,就遵照执行吧,大概某个风格跟你不符合,但没关系,你要知道,这并不意味,你在style之战败下阵来,也并不表示它说服了你,你遵守的是规则和纪律本身。
我经历过一些c的项目,函数定名有大驼峰,有小驼峰,有下划线连词,还有驼峰加下划线(有些是两个),还有函数名下划线大概两个下划线开头,总结一下它的规律就是没有规律,非常随心所欲,令我莫衷一是。
变量(包罗文件、类/结构体、函数)定名,比如ohmygod,你大概搞不清哪些字母是一伙的,所以须要界定单词。驼峰通过单词首字母大写来界定单词,另一个惯用做法是用下划线拼接单词。驼峰的弊端是丑,下划线拼接的弊端是增加了标识符长度(相比首字母大写),好处是跟std c/c++、linux kernel的做法一致,喜好kernel的码农容易找到如家般的归属感。
c++有namespace避免冲突,c经常用prefix防止定名污染全局空间,但我以为定名简洁扼要很重要,所以我支持简短的前缀而反对冗长的前缀。
https://p1.pstatp.com/large/pgc-image/RC6F82ZBQQOfLo代码密度
实现同样的功能,你喜好100行代码,还是20行代码?如果贵司不以代码行数考核绩效我发起你把代码写的精简,而如果贵司以代码行数考核绩效,我发起你去职,开滴滴,送外卖大概摆摊都行,因为在这种公司耗费芳华基本上也不会有什么发展前途。
把简单的东西搞复杂化很容易,你只须要找一个能力平庸的人就能实现化简为繁的愿望,而化繁为简则堪称化腐朽为神奇。大概你要说,我欠缺简化的能力,这并不希奇,坦白讲,这不是一件容易的事,你做不到没关系,但你拥有正确的理念更重要,它将帮助你认清前进的方向,而不是在错误的道路上越走越远。
有些项目,充斥各种无效代码,其实你只须要稍加思考,你就能识别出来。
比如大块注释掉的代码像发臭的尸体一样遍布其中。比如大量功能重复的代码像垃圾一样堆砌在那边。
比如本不须要返回值的函数,执拗的固定返回true,然后在调用的地方还要装模作样的check一下返回值,如果返回false,再记一条日志,再assert一下,再抛个异常,这样显得很有职业操守,美其名曰面向failed编程,处理了各种异常情况。
又大概函数一进来,不管三七二十一,对入参一顿无脑检查,一顿操纵猛如虎,一看代码二百五,宣称这是符合ISO XX标准的安全做法,全然忘记你在编写的是一个私有实现函数,你在调用它之前已经检查过一遍,私有函数是一个受控的安全上下文,这不仅不优雅而且不绿色(低效耗电)并且不安全(在该崩的时候没崩把雷埋到了更隐蔽的地方),话说你看标准库函数strcpy/strcat,vector operator检查传参了吗?
提高代码密度大概说浓度有利于理清思路,有利于突出重点,有利于提高维护性,而充斥各种无效语句的代码只会把关键语句沉没在汪洋大海,使得review代码的人get不到重点,看不清主次。像听一个絮絮叨叨的人做报告,满篇废话,像看一个剧情拖沓的一连剧,昏昏欲睡,像喝一瓶二锅头兑十斤白开水,口能淡出个鸟来。
重构是程序员的口头禅,重构是在保持程序功能稳定的情况下调整架构和实现,我以为提高代码密度应作为重构的一项寻求。
Linux kernel、Lua、Nginx、Skynet这些优秀的开源库代码浓度都很高,发起读者朋友品尝一下。
https://p3.pstatp.com/large/pgc-image/RTy4oyA1ozZTxK封装
我们最常干的一件事就是把重复编写的代码封装到一个函数里去,用多处调用替代重复编写,这个很好理解,但其实纵然不被多处调用,把相关的一段代码封装到一个实现函数也是有须要的,因为把代码平铺开来,把细节暴暴露来,容易掩盖重要的东西,即框架和脉络会变得不够清晰。
一个见名知义的函数调用比堆砌在那边的一段代码给我的感受好,我如果关心它是怎么做的,我可以跳转到定义看看实现。
封装的一个核心原则是单一职责,符合单一职责的函数更易于被复用。
https://p1.pstatp.com/large/pgc-image/RTy4oyNDan8UIe解耦
构建疏松耦合的体系一直是软件工程的一个目标,模块化的一个方向便是解耦,但我们口口声称心心念想的解耦,在实施层面又有几分体现呢?
比如,我们经常干的一件蠢事就是把类似配置文件,大概宏定义的东西集中的一个头文件里去,看起来很同一也很正规,起码我之前也是这样以为的,但突然有一天,发现本身这样做显得很不聪明的样子,为什么呢?因为你想把全部模块配置相关的东西都塞进配置公共文件真的符合吗?是不是把公共接口抽离出来更好,把配置相关的数据下沉到各模块更符合?
另外,把宏都定义到一起,这意味随便改点东西,都会须要修改宏头文件,而这个头文件就会成为程序天下的中央,修改公共宏文件几乎会引起整个体系的全部源文件rebuild,这简直就是AOE团灭啊。所以更好的方式是分而治之,去集中式。
我们知道c/c++的编译单位是source file(.c/.cpp),编译的第一步是预处理,全部include都会睁开替换,所以我们要避免引入任何不须要的头文件,也应该把本编译单位用到的头文件都include进来,这就是所谓的头文件自给自足。这点很重要,但许多人会不以为然,甚职苄些人会自作聪明的搞一个allincluded.h,把常用的一些头文件全部include进来,然后沾沾自喜的自以为一劳永逸的美满的办理了问题,包含不须要的头文件会增加编译时间,会增加依靠,我们不仅应该避免错误的包含,还应该经心设计和分别文件,使得每个文件的功能富足内聚单一。
https://p1.pstatp.com/large/pgc-image/RCZeKPFHsKMUt5缩写
慎用缩写,相比缩写带来的含混不清,我宁愿多敲几下键盘,如果要缩写请符合惯例服从通例,比如AI,比如App,比如cfg,但是你把threshold缩写成threshod,把Item缩写成Iem,我特木真的搞不懂你是拼错了还是缩错了?
https://p3.pstatp.com/large/pgc-image/RCZeKWyOE4VLM服从标准
我遇到过一些项目,每个模块单独定义本身的int32,float,char,void,比如定义MoKuaiA_int32,MoKuaiB_int32,MoKuaiC_CHAR,不仅如此,它还把inline,true,false,VOID,const,case,static等一众关键字全部redefine了,令人匪夷所思的是它竟然把标准C API全部重定义一遍,令人发指的是它竟然不让你用语言的标准写法。
我一直搞不懂为什么要这样做?如果你只是须要办理不同体系结构下long等整型的长度差异,我想偷偷告诉你,c库头文件stdint.h已经从标准层面同一办理了这个问题,里面int8_t/16_t/32_t/64_t,还有uint8_t等等包罗万象。
这样的做法是很不好的,会让符合标准的写法寸草不生,发起不要这么做。
https://p1.pstatp.com/large/pgc-image/RCZeKXlFnucNNV宏
宏是 C 的一个有效武器,在有些情况下确实行之有效,我是宏的中间派,我既反对禁用宏,也反对滥用宏,inline可以部分替代宏,但不能完全替代宏。
但我见过一些项目,随处都是宏,全大写,至少1/3的代码都是各种花里胡哨的宏,你review代码的时候,不停的跳来跳去,看了一眼,哦,就这样啊,然后切回来,频繁的上下文切换是低效的,它打断了你的思路,让你看代码有种撒尿撒到一半憋回去的感觉。其实许多时候完全没有须要,不须要整这些虚头巴脑的。
https://p3.pstatp.com/large/pgc-image/RCZeKlA55I7nkG定名
定名有一些指引,比如类/结构体应该用名词,函数应该用类似动词大概doSomething这样的动宾结构,这些规矩都是耳熟能详的。
我主张定名应该简明扼要,不要罗里吧嗦,要准确的表达出它要做的事情,如果你遇到定名困难,你大概须要思量你的类定义大概接口分别是否符合。
定名是接口的一部分,很重要,好的定名是自注释的。
如果你没有思路,那我发起你参考一下STD C/C++ API,究竟这些接口历经几十年没有大的变化,算是经受住了历史的考验,比如malloc/free/atoi,stl 容器的成员函数也有点意思:size, capacity,resize,reserve,push,pop,top,back,很干脆,不废话,我觉得很好。
所以,如果你编写的是某某管理器,比如ItemManager,我发起你直接取名add,remove,而不用AddItem,RemoveItem,因为你本身就是Item的Manager,操纵的必然是Item,而且从参数上也能体现出来,少即是多,多不如少。
https://p3.pstatp.com/large/pgc-image/RMpfEqc2kVoxqN扩展性
开闭原则是应对扩展性的rule,人无远虑必有近忧,说的是我们不能范围于眼前,但也请不要盲目迷信扩展性,戏太多也是病。
知乎有一篇神贴讲的是怎样把helloworld搞成一个bigproject,当你想给别人项目挑刺的时候,你可以用扩展性说事,但我发起你离开口闭口扩展性的人远一点,据我观察,这种人大多比较虚伪而且很水。
https://p1.pstatp.com/large/pgc-image/ROAcOrM5in3fAs避免特例
Linus大神分享过他心中的好代码,说的是针对链表的操纵,他更喜好同一性的处理方式,而不是做特例化的处理,我想这个例子很有代表性,它其实代表一种理念,那就是自始至终,我们的头脑里必须优先思量normal化的处理方式,当然这其实是一个比较高层次的要求,菜鸟互啄可以先跳过这一层要求。
https://p1.pstatp.com/large/pgc-image/ROAcOrZ333KQCD高效而鲁棒
有许多避免运行低效的做法,比如镌汰拷贝,提高局部性,buffer/cache,空间时间置换,内联,分支预测,判断前置,计算延迟,无锁技术。
提高鲁棒性的关键是面向failed编程,不信任/零信任设计,假设依靠的上下文,上卑鄙都是不可靠的,方法许多,不一一列举了。
搬砖不容易,累了,搁笔苏息,择日再议!
https://p1.pstatp.com/large/pgc-image/R69FpRH4d90a7dhttps://p3.pstatp.com/large/pgc-image/S1WAXMR6UpK0otLinux 之父怒删工程师提交的补丁,称“太蠢了”网友:怼得好!
初级管理者,怎样打通任督二脉?
滴滴辟谣被美团收购;苹果提交认证 9 款新手机;VS Code 1.46 发布 | 极客头条
干货!3 个重要因素,带你看透 AI 技术架构方案的可行性!
干货 | 明白话彻底搞懂 HBase RowKey 详细设计
热评 | 警惕新基建热潮中的区块链项目烂尾 转发了 转发了
页:
[1]