关于写代码这件事,我提出三个问题,并先答为敬:
其一,什么样的代码是好代码?
很炫的代码。
每个函数都使用了STL。
每个类都散发着设计模式的味道。
每个处理流程都曲径通幽,需要读者焚香沐浴,静心细品,才能发现作者的匠心独运。
这就是好代码吗?
Oh no,也许不是这样的。
首先代码能跑起来,实现用户的功能需求。然后再谈写得好不好。
怎么算好呢?
简洁明了
代码(函数,类)功能单一
一个函数只干一件事情,一个类只抽象一个明确的主题。不要只盯着面向对象里面有一个SRP(单一职责原则),实际上,不管是面向过程,也不管是面向对象还是函数式编程,内聚是永远不会错的。
又想上班,又想炒股又想做生意,结果可能是什么也做不好。
又接收报文,又解析报文,又校验报文,又存库,又组装报文,又发送报文,全铺开在一个函数里面,结果这个函数就变成了脏乱差。
想想我们的项目代码里面,有多少用鼠标滚轮、总也滚不到结尾的函数?这是病,得治啊。
实际上,大部分的函数,按功能封装封装,接收和解析报文提取一个函数,校验报文一个函数,组装报文一个函数,发送报文一个函数,就能弄得汤清水净,各个函数都功能单一了。
只有很少的机会,才会碰到需要我们拆分接口,或者按聚合、组合还是继承关系划分出几个类,才能实现功能单一的情况。
简单就是美。
代码干净利落,不拖泥带水
如果功能单一,那么干净利落已经做到大半。
这个标题下是想专门说说头文件的包含。
生成目标文件越来越大。宏的重复定义、编译告警越来越多。
头文件包含另外的一堆头文件。源文件包含所有能想到的头文件。
有时候编译不过去,要把源文件里面#include的a.h放到b.h后面,才能编过去。
你确定,你真的需要这么多头文件吗?
你知道,编译器会把你#include的所有头文件的每行代码,都铺开并占领到你的源文件中,一起参与编译,因此你编译的代码行数,并不仅仅是你的源文件代码行数这么少吗?
拜托,不需要的头文件,别#include进来吧。
尤其是,尽量别在头文件中包含头文件好吗,亲?
变量、函数、类命名清晰、准确
这个本来是不消说的。
生个娃,取个好名字能让娃今后万事如意,这是所有家长都懂的了。
定义个变量,写个函数,创建一个类,写个宏,如果从名字根本看不出来是用作啥的,那读者看着多闹心。
同一个文件中的函数名:有的全小写,用下划线连起来各个单词,这是linux风格的命名;有的大小写相间,用大写字母分开各个单词,这是骆驼命名法;有的又有下划线,又有大小写,好像雨天披着雨衣还同时打着伞,这是混搭命名法。总体看这是不好的命名。
按编码规范,整齐统一,像给娃取名那样,谨慎的命名自己的变量、函数、类、宏的,这才是极好的。
表达到位
代码的意图清楚
意图清楚,就是读者一看代码,就知道是干什么的。
代码首先是给人看懂,其次才是给机器看。
简洁明了,是意图清楚的前提。
注释恰当,不多不少。
不要故弄玄虚。不要使用不必要的STL。不用使用不必要的复杂算法。
可以用简单数组搞定的,就不用指针、堆内存。
命名多用set、get、check这些标准单词。
更主要的,如果你非要大段注释才能说明白一段代码,这个代码多半需要重构。
你可以把一组、甚至一条语句提取为一个小函数,只为让函数名称说明这是要干啥。
代码的处理逻辑层次分明
写代码和写文章是一样一样的。
谋篇布局,主次分明,逻辑清楚,才好看。
如果你实在没啥逻辑,就给代码标上1、2、3、4吧。
写文章可以有章节编号,1,2,3,4,写代码为什么不能有?
真的可以有:
代码的处理逻辑,与问题领域的概念、规则吻合
这个是比较高的境界。可意会。言传就落了俗套。
与问题领域吻合,意思是代码的样子,符合我们对代码所描述的事物的直观认识。
非高手不能。非多次重构不能。
举个例子。
保龄球记分,领域规则就是,全中得分为本轮加后两次,补中得分为本轮加后一次,普通得分为本轮。
好的代码,主函数就展现了它的领域规则。
安全可靠
代码运行状态、时序管理得当
尽量保证函数是可重入的。尽量不用全局变量。
独占资源(锁定数据库、占用信号量)的时间尽量短。
不滥用assert。传入数据不合要求你就挂起,真的合适吗?
避免时序引起的偶发故障。别人的进程起来得比平时慢了,你就可以出错吗?
代码具有必要的测试安全网络
TDD、ATDD都是不错的选择。
对于嵌入式软件,上设备调试太慢。找设备太难。
还是PC仿真最方便。
用例跑完一遍要快。秒级最好,分钟也行,不超过10分钟可以接受。为啥?用例跑得太慢,就没人爱用,没人爱用,修改代码就不安全了呢。
改完代码,跑一遍用例。喝口茶,等着VC打印最后的OK。享受。
易于扩展
扩展新功能时,只需新增代码,无需改动老代码
不要只盯着面向对象里面有一个OCP(开放-封闭原则),实际上,不管是面向过程,也不管是面向对象还是函数式编程,不动老代码、也能扩展新功能,都是一种追求。
老代码都测试过了,能不动当然好啊。
容易做到吗?不容易。
但至少,你不能加一个小功能,都要翻天覆地改一遍吧?那只能说明你原来的设计太逊了。
C语言里面,函数功能单一化,在易变的地方采用注册回调机制、表驱动机制,都可以把改动限制在局部,保持老代码的稳定。
C++里面,工厂模式,让你创建新子类的对象时,不用改动使用这个对象的客户类代码;策略模式,让你面对不同产品的处理差异,不用改动基类和客户类代码,这都是一些具体办法。
其二,代码是资产还是债务?
黄蓉每天写500行代码。郭靖每天写100行代码。
黄蓉比郭靖厉害?
都是完成相同的功能,慕容复拥有2000行代码。乔峰只拥有200行代码。
慕容复比乔峰厉害?
用户要的是代码还是功能?
代码能按行收钱吗?
古龙写武侠小说,经常是一句话一行,一行一段。据说那时候台湾小说界是按行付稿费的。
问题是代码不是小说。
代码的目的是实现用户需求。
在满足用户需求的前提下,我们的代码应该——
尽可能少。
看看下面的代码。这样的代码,大量出没在超过10000000行的软件项目中。
维护这些代码,得花多少人力?
维护者是否会觉得上辈子欠了原作者的债呢。
公司和项目是否都在背着这些代码的债务,负重前行呢?
有些严谨又有趣的老外,好像已经把这个债务算清了:
Technical Debt Is Now Costing Us $3.61 Per Line Of Code(“CAST估计,现在公司要解决技术负债的花费,是每行代码3.61美元。”). -- Jonathan Bloom From CAST.
其三,代码只是设计的实现吗?
据说程序员都是码农。
真正的大牛只画UML图,偶尔提出一些软件思想和理论,供码农们崇拜。
码农们像蚂蚁那样coding,给别人的设计来作嫁衣裳。
真的是这样吗?
软件工程的理论基础,是和传统工程学做对比,尤其是和建筑业做对比。
设计主要是画图,好比画建筑图纸;实现就是coding,好比砌砖。
真的是这样吗?
建筑师画图纸,民工砌砖。没听说哪个民工砌砖砌得好变成了建筑师,没听说建筑师以前都是砌砖的。
但软件SE都是从coding过来的,而且必须是coding很好的程序员。
为什么呢?
所以,软件工程,无法解释软件系统工程师的来源问题。
如果说——代码也是设计,程序员编程的过程,也属于设计的过程。那就好解释了——
作为程序员,你一直在设计,设计能力提高了,你就成了软件SE。
工程活动分成设计和生产。
在软件开发中,编程完全属于设计活动。我们通常说的软件SE“设计”的过程,也是软件设计的一部分。
软件里的生产工人,就是编译器。
软件的测试和调试,也是设计的一部分,是设计的验证。
如果认可这一点,除了自豪感油然而生,是否你也应该把代码写得更好点?
上述观点,纯属作者自言自语。
作为有思想的程序员,你不必同意作者的观点。
你只需想想,对这三个问题,你从前是怎么看的、现在又怎么看,这么多年过去,自己的看法是否有变化、是否有进步,就OK了。