代码如诗和代码如屎只有一字之隔,但却能把代码人的境界分隔开来。Robert C. Martin 在《代码整洁之道》这本书中,给我们阐述了糟糕代码产生的原因,以及如何保持良好的代码习惯。事实上,这本书的目录就是一份教你如何写好代码的清单。倘若你想写好代码,又实在抽不出时间,我相信一览其目录也能有所收获。如果你连看目录的时间都没有,其实这本书反复推崇的原则很简单:减少重复代码,提高表达力。以下是我认为最有意思的部分。

我们为什么要简洁清晰的代码?

我们都或多或少有重写代码的经历。重新写代码的原因有很多,思维混乱、赶时间、或者是为了能让项目准时上线,不得不对代码进行临时性的修补。代码就像食物一样会腐烂。今天你为了赶时间写出了糟糕的代码,明天你思维混乱写出了更糟糕的代码。最后这就是一堆垃圾。只要项目不倒,它在后面的日子里会放出无数只恶魔困扰你。受够了,代码人要站起来,极力要求把一切打倒重新开始。可他们并不理解,重写一个项目往往不会把之前的问题解决掉,反而会带来新的未知问题。与其轻易地重来,不如一开始就写出简洁的代码。

关于函数

好的函数都有一个规律:一次只办一件事情,并且把事情办好。做到这一点,我们要把整个项目进行抽象层的分隔。比如说一个做菜的函数。

1
2
3
4
5
6
7
8
9
10
11
12
def cook(ingredients):
foods = getFoods()
for i in ingredients:
type = i.type
quantity = i.quantity
name = i.name
if food.ingredients.include(name):
if food.ingredient(name).quantity == quantity:

# and so on and so on

return food

这一个函数很长。它把选材、洗菜、切菜、焯水、烹饪全部任务都揽过来,企图一口气讲问题解决。但是,运行的时候哪怕其中一个变量出现了问题,输出的结果都可能不对。这个函数的可维护性很差。如果你经常有这样的想法【中枪警告】:

  • 这个东西我理解,但是不知道从哪里开始写起。
  • 我明明在上面的代码中,把这个问题解决了,为什么现在它又出现了。

既然一个函数如此复杂,以至于你无法理解错误出在哪里,甚至不知道从哪里开始写,那为什么不把问题切割?

1
2
3
4
5
6
def cook(ingredients):
select(ingredients)
wash(ingredients)
blanch(ingredients)
food = cook(ingredients)
return food

在炒菜这个抽象层,我只关心我炒菜需要做些什么。至于具体怎么做,我将它抽象到下一个层级来完成。这么做的好处是问题被细化了。本来要解决的是怎么炒菜,现在可以将每个步骤分别完成,逐个击破。

另外,注释并不能帮助你 debug。看看自己曾经做过的项目,那些灰色、绿色的注释在你 debug 的时候,能帮到你吗?好的代码比详细的注释更有说服力。比如说:

1
2
3
4
//Check to see if the person is able to purchase a product
if(person.money > (product.price + pruduct.price*INTEREST)){
target(person)
}

能看懂吗?如果换一种写法

1
2
3
if(person.is_able_to_buy(product)){
target(person)
}

和上面说到的一样,将细节交给下一层来实现。代码可维护性提高的代价,仅仅是将问题细化成更小块的函数。函数就应该专注和短小。事实上很多公司的代码指南里,都有提到函数的规模。比如说 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个字符,事实上,现在很多编译器都能显示一条虚线,告诉你尽量不要将代码写出超过这条虚线以外。

line-length

关于错误处理

不应该写各种if…else…或者switch来处理异常,现代大多数程序设计语言都有相应的异常处理方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
def printError(result):
if result == 1:
return 23211
if result == 2:
return 10054
...
def handleError():
code = handleError(result)
if code == 23211:
# do this
if code == 10054:
# do that
...

比起用各种if,更合理的方式是用异常处理将这些函数封装起来,提高代码的可读性以及可维护性。
这是我认为本书中的几个亮点,我对写代码的理解程度又深了一个层次。说到底,一个人写代码的水平和他阅读的代码是成正比的。比如我在这本书里面看到他介绍把函数分层来将一个问题解决,第二天我就能把它用在我自己的代码上。我在另一篇笔记(未发表)中提到我要实现一个函数,将一个Dataframe放进来,然后将其中某一列更改为1或0并输出。这在数据科学中被称为二元化(binarization) 当时对 Python 理解不深(现在也不, 哈),写出来的东西十分冗余。

1
2
3
4
5
6
#Change price to only affordable (1) or unaffordable (0)
for i, row in df.iterrows():
if df.at[I,'target'] > 5:
df.at[i, 'target'] = 1
elif df.at[i, 'target'] <= 5:
df.at[i, 'target'] = 0

直到我看到Hasan写的一个函数

1
y = (df["target"] > 5).astype(np.int)

看吧,一样的事情,不同的脑子写出来的就是不一样。这就是差距…写代码这种事,还是得多看多写。

总结

截取自 《代码整洁之道》 作者: [美]Robert C. Martin ISBN: 9787115216878。 引自第296页

什么是整洁代码

  1. 代码逻辑直接了当,让缺陷难以隐藏
  2. 尽量减少依赖关系,使之便于维护
  3. 依据某种分层策略完善错误处理代码
  4. 性能调至最优,省得引诱别人做没规矩的优化
  5. 整洁的代码只做一件事
  6. 简单直接,具有可读性
  7. 有单元测试和验收测试
  8. 有意义的命名
  9. 代码应在字面上表达其含义
  10. 尽量少的实体:类、方法、函数
  11. 没有重复代码

好的命名:

  1. 名副其实:名称不需要注释补充就可见其含义、用途
  2. 避免误导:
    (1)系统专有名称不宜作为变量名
    (2)提防差别很小的名称,在代码自动完成时容易选错
    (3)用字母I和O作为变量名,让人看成1和0
  3. 做有意义的区分
    不要用数字或废话来区分
  4. 使用读得出来的名称
  5. 使用可搜索的名称
    少用单字母名称或没有名称的数字常量
  6. 避免使用编码
    (1)不必在名称中标明类型,现代语言有完善的类型检查
    (2)不必在名称中区别成员变量,应采用自动高亮成员的编译环境
    (3)宁可对实现编码,也不要对接口编码
  7. 避免使用让别人理解成其他领域常用词的名称
  8. 类名应该是名词或名词短语,不应是动词
  9. 方法名应当是动词或动词短语
  10. 不要使用俗语
  11. 每个抽象概念用一个词,不要混用同义词
  12. 不要用双关语,例如add
  13. 尽量用术语(CS术语,算法,数学术语)命名
  14. 使用程序设计的领域的习惯名称
  15. 添加富有意义的语境,例如利用address封装各种个人信息
  16. 不要添加没用的语境

好的函数:

  1. 短小
    (1)不该超过20行
    (2)每个代码块(if, else,while)应该只有一行,包含一个函数
  2. 只做一件事
    如果能拆成几个函数,就不是只做一件事
  3. 每个函数一个抽象层次
    自顶向下的抽象层次
  4. switch语句
    用于创建多态对象,并隐藏在抽象工厂中
  5. 使用描述性的名称
  6. 尽量避免多余2个参数
    (1)使用返回值而不是输出参数
    (2)不要用标识参数,即向函数传入bool值,应该写成两个函数
    (3)如果一定需要多个参数,那么可能需要对参数进行封装
  7. 无副作用
    尽量少做不是函数名称表明的事情
  8. 使用异常代替返回错误码
    (1)把try,catch从正常流程中分离开
  9. 消除重复代码
  10. 在小函数中,偶尔出现return,continue,break没坏处,但不要用goto
  11. 先把函数写出来,再规范化

好的注释

  1. 尽量减少注释量,用代码本身说明问题
    (1)代码常常变动,注释却不能跟着变
    (2)只有代码是唯一准确的信息来源
    (3)创建一个和注释所言相同的函数来代替注释
  2. 好的注释
    (1)法律信息
    (2)解释程序员的意图
    (3)警示其他程序员某种后果
    (4)TODO
    定期删除不需要的
    (5)公共API中的javadoc
  3. 坏的注释
    (1)喃喃自语,只有作者自己能看懂
    (2)多余的注释
    不一定比代码精到
    (3)不是每个函数和变量都要有javadoc
    (4)日志式注释,修改记录
    (5)能用函数或变量时就别用注释
    (6)用代码控制系统,而不是注释表明归属
    (7)不要保留注释掉的代码,利用源代码控制系统

好的格式:

  1. 垂直距离
    (1)变量声明尽可能靠近使用位置,本地变量应在函数顶部出现
    (2)实体变量应在类的顶部声明
    (3)相关函数放在一起
    (4)函数的排列顺序保持其相互调用的顺序
  2. 水平位置
    (1)一行代码尽量短,不超过100-120字符
    (2)用空格将相关性弱的分开:加减法,赋值,乘法因子见无需空格
    (3)声明和赋值不需要水平对齐
    (4)缩进
    空循环容易忽略行末的分号,要括号包围空循环。

测试

  1. TDD三定律
    (1)在编写不能通过的单元测试前,不能编写生产代码
    (2)只可编写刚好无法通过的单元测试,不能编译也不算通过
    (3)只可编写刚好足以通过当前失败测试的生产代码
  2. 测试代码和生产代码一样重要,一样需要整洁
  3. 每个测试一个断言,每个测试一个概念

并发编程

  1. 并发防御原则
    (1)单一权责:方法/类/组件应当只有一个修改的理由
    (2)限制数据作用域
    (3)使用数据副本避免共享数据
    (4)线程应尽可能独立
  2. 了解常见的执行模型:生产者-消费者,读者-作者,宴席哲学家
  3. 警惕同步方法之间的依赖
  4. 保持同步区微小
  5. 尽早考虑关闭的代码,注意线程之间的依赖关系
  6. 测试线程代码
    (1)将伪失败看作可能的线程问题
    (2)先使非线程代码工作
    (3)运行多于处理器数量的线程
    (4)在不同平台上运行
    (5)手动插入调试的试错代码