代码如诗和代码如屎只有一字之隔,但却能把代码人的境界分隔开来。Robert C. Martin 在《代码整洁之道》这本书中,给我们阐述了糟糕代码产生的原因,以及如何保持良好的代码习惯。事实上,这本书的目录就是一份教你如何写好代码的清单。倘若你想写好代码,又实在抽不出时间,我相信一览其目录也能有所收获。如果你连看目录的时间都没有,其实这本书反复推崇的原则很简单:减少重复代码,提高表达力。以下是我认为最有意思的部分。
我们为什么要简洁清晰的代码?
我们都或多或少有重写代码的经历。重新写代码的原因有很多,思维混乱、赶时间、或者是为了能让项目准时上线,不得不对代码进行临时性的修补。代码就像食物一样会腐烂。今天你为了赶时间写出了糟糕的代码,明天你思维混乱写出了更糟糕的代码。最后这就是一堆垃圾。只要项目不倒,它在后面的日子里会放出无数只恶魔困扰你。受够了,代码人要站起来,极力要求把一切打倒重新开始。可他们并不理解,重写一个项目往往不会把之前的问题解决掉,反而会带来新的未知问题。与其轻易地重来,不如一开始就写出简洁的代码。
关于函数
好的函数都有一个规律:一次只办一件事情,并且把事情办好。做到这一点,我们要把整个项目进行抽象层的分隔。比如说一个做菜的函数。
1 | def cook(ingredients): |
这一个函数很长。它把选材、洗菜、切菜、焯水、烹饪全部任务都揽过来,企图一口气讲问题解决。但是,运行的时候哪怕其中一个变量出现了问题,输出的结果都可能不对。这个函数的可维护性很差。如果你经常有这样的想法【中枪警告】:
- 这个东西我理解,但是不知道从哪里开始写起。
- 我明明在上面的代码中,把这个问题解决了,为什么现在它又出现了。
既然一个函数如此复杂,以至于你无法理解错误出在哪里,甚至不知道从哪里开始写,那为什么不把问题切割?
1 | def cook(ingredients): |
在炒菜这个抽象层,我只关心我炒菜需要做些什么。至于具体怎么做,我将它抽象到下一个层级来完成。这么做的好处是问题被细化了。本来要解决的是怎么炒菜,现在可以将每个步骤分别完成,逐个击破。
另外,注释并不能帮助你 debug。看看自己曾经做过的项目,那些灰色、绿色的注释在你 debug 的时候,能帮到你吗?好的代码比详细的注释更有说服力。比如说:
1 | //Check to see if the person is able to purchase a product |
能看懂吗?如果换一种写法
1 | if(person.is_able_to_buy(product)){ |
和上面说到的一样,将细节交给下一层来实现。代码可维护性提高的代价,仅仅是将问题细化成更小块的函数。函数就应该专注和短小。事实上很多公司的代码指南里,都有提到函数的规模。比如说 Google 的风格指南就提到
If a function exceeds about 40 lines, think about whether it can be broken up without harming the structure of the program. ^1
排版和规模
作者相信,开始工作之前。规定好的一系列风格指南,能大大地提高团队的工作效率。除了函数的简短之外,还有很多其他约定。比如说再比如说每行的字母最好不要超过80个字符,事实上,现在很多编译器都能显示一条虚线,告诉你尽量不要将代码写出超过这条虚线以外。

关于错误处理
不应该写各种if…else…或者switch来处理异常,现代大多数程序设计语言都有相应的异常处理方法。
1 | def printError(result): |
比起用各种if,更合理的方式是用异常处理将这些函数封装起来,提高代码的可读性以及可维护性。
这是我认为本书中的几个亮点,我对写代码的理解程度又深了一个层次。说到底,一个人写代码的水平和他阅读的代码是成正比的。比如我在这本书里面看到他介绍把函数分层来将一个问题解决,第二天我就能把它用在我自己的代码上。我在另一篇笔记(未发表)中提到我要实现一个函数,将一个Dataframe放进来,然后将其中某一列更改为1或0并输出。这在数据科学中被称为二元化(binarization) 当时对 Python 理解不深(现在也不, 哈),写出来的东西十分冗余。
1 | #Change price to only affordable (1) or unaffordable (0) |
直到我看到Hasan写的一个函数
1 | y = (df["target"] > 5).astype(np.int) |
看吧,一样的事情,不同的脑子写出来的就是不一样。这就是差距…写代码这种事,还是得多看多写。
总结
截取自 《代码整洁之道》 作者: [美]Robert C. Martin ISBN: 9787115216878。 引自第296页
什么是整洁代码
- 代码逻辑直接了当,让缺陷难以隐藏
- 尽量减少依赖关系,使之便于维护
- 依据某种分层策略完善错误处理代码
- 性能调至最优,省得引诱别人做没规矩的优化
- 整洁的代码只做一件事
- 简单直接,具有可读性
- 有单元测试和验收测试
- 有意义的命名
- 代码应在字面上表达其含义
- 尽量少的实体:类、方法、函数
- 没有重复代码
好的命名:
- 名副其实:名称不需要注释补充就可见其含义、用途
- 避免误导:
(1)系统专有名称不宜作为变量名
(2)提防差别很小的名称,在代码自动完成时容易选错
(3)用字母I和O作为变量名,让人看成1和0 - 做有意义的区分
不要用数字或废话来区分 - 使用读得出来的名称
- 使用可搜索的名称
少用单字母名称或没有名称的数字常量 - 避免使用编码
(1)不必在名称中标明类型,现代语言有完善的类型检查
(2)不必在名称中区别成员变量,应采用自动高亮成员的编译环境
(3)宁可对实现编码,也不要对接口编码 - 避免使用让别人理解成其他领域常用词的名称
- 类名应该是名词或名词短语,不应是动词
- 方法名应当是动词或动词短语
- 不要使用俗语
- 每个抽象概念用一个词,不要混用同义词
- 不要用双关语,例如add
- 尽量用术语(CS术语,算法,数学术语)命名
- 使用程序设计的领域的习惯名称
- 添加富有意义的语境,例如利用address封装各种个人信息
- 不要添加没用的语境
好的函数:
- 短小
(1)不该超过20行
(2)每个代码块(if, else,while)应该只有一行,包含一个函数 - 只做一件事
如果能拆成几个函数,就不是只做一件事 - 每个函数一个抽象层次
自顶向下的抽象层次 - switch语句
用于创建多态对象,并隐藏在抽象工厂中 - 使用描述性的名称
- 尽量避免多余2个参数
(1)使用返回值而不是输出参数
(2)不要用标识参数,即向函数传入bool值,应该写成两个函数
(3)如果一定需要多个参数,那么可能需要对参数进行封装 - 无副作用
尽量少做不是函数名称表明的事情 - 使用异常代替返回错误码
(1)把try,catch从正常流程中分离开 - 消除重复代码
- 在小函数中,偶尔出现return,continue,break没坏处,但不要用goto
- 先把函数写出来,再规范化
好的注释
- 尽量减少注释量,用代码本身说明问题
(1)代码常常变动,注释却不能跟着变
(2)只有代码是唯一准确的信息来源
(3)创建一个和注释所言相同的函数来代替注释 - 好的注释
(1)法律信息
(2)解释程序员的意图
(3)警示其他程序员某种后果
(4)TODO
定期删除不需要的
(5)公共API中的javadoc - 坏的注释
(1)喃喃自语,只有作者自己能看懂
(2)多余的注释
不一定比代码精到
(3)不是每个函数和变量都要有javadoc
(4)日志式注释,修改记录
(5)能用函数或变量时就别用注释
(6)用代码控制系统,而不是注释表明归属
(7)不要保留注释掉的代码,利用源代码控制系统
好的格式:
- 垂直距离
(1)变量声明尽可能靠近使用位置,本地变量应在函数顶部出现
(2)实体变量应在类的顶部声明
(3)相关函数放在一起
(4)函数的排列顺序保持其相互调用的顺序 - 水平位置
(1)一行代码尽量短,不超过100-120字符
(2)用空格将相关性弱的分开:加减法,赋值,乘法因子见无需空格
(3)声明和赋值不需要水平对齐
(4)缩进
空循环容易忽略行末的分号,要括号包围空循环。
测试
- TDD三定律
(1)在编写不能通过的单元测试前,不能编写生产代码
(2)只可编写刚好无法通过的单元测试,不能编译也不算通过
(3)只可编写刚好足以通过当前失败测试的生产代码 - 测试代码和生产代码一样重要,一样需要整洁
- 每个测试一个断言,每个测试一个概念
并发编程
- 并发防御原则
(1)单一权责:方法/类/组件应当只有一个修改的理由
(2)限制数据作用域
(3)使用数据副本避免共享数据
(4)线程应尽可能独立 - 了解常见的执行模型:生产者-消费者,读者-作者,宴席哲学家
- 警惕同步方法之间的依赖
- 保持同步区微小
- 尽早考虑关闭的代码,注意线程之间的依赖关系
- 测试线程代码
(1)将伪失败看作可能的线程问题
(2)先使非线程代码工作
(3)运行多于处理器数量的线程
(4)在不同平台上运行
(5)手动插入调试的试错代码