《程序员修炼之道》小抄

第一次看这本书还是在大三的时候,当时只看了一半就放弃了。如今工作三年多了,昨天晚上大概花了2个小时的时间看完这本书,发现讲的其实还是挺基础的,有些之前不懂的地方现在也豁然开朗了,相信过几年再回读,可能会有新的收获。此之谓常读常新。

不要容忍破窗户

不要留着“破窗户”(低劣的设计、错误决策、或是糟糕的代码)不修。发现一个就修一个。如果没有足够的时间进行适当的修理,就用木板把它钉起来。或许你可以把出问题的代码放入注释(comment out),或是显示“未实现”消息,或是用虚设的数据(dummy data)加以替代

不要追求完美

不要因为过度修饰和过于求精而毁损完好的程序。继续前进,让你的代码凭着自己的质量站立一会儿。它也许不完美,但不用担心:它不可能完美

想要开发一个 App 或者实现一个功能的时候,先让代码跑起来,允许不完美。后续再快速迭代增加新功能和修复 Bug,效果会比什么都规划好之后再动手更好

批判地分析你读到的和听到的

批判地思考你读到的和听到的。你需要确保你的资产中的知识是准确的

Web搜索引擎把某个页面列在最前面,并不意味着那就是最佳选择;内容供应商可以付钱让自己排在前面。书店在显著位置展示某一本书,也并不意味着那就是一本好书,甚至也不说明那是一本受欢迎的书;它们可能是付了钱才放在那里的

从谷歌或百度搜索到的知识进行思考和验证,确保每个进入脑海的知识都是准确的。一般来说,书籍更加权威

DRY – Don’t Repeat Yourself

代码注释

糟糕的代码才需要许多注释。DRY法则告诉我们,要把低级的知识放在代码中,它属于那里;把注释保留给其他的高级说明。否则,我们就是在重复知识,而每一次改变都意味着既要改变代码,也要改变注释。注释将不可避免地变得过时,而不可信任的注释比完全没有注释更糟

注释应该讨论为何要做某事、它的目的和目标。代码已经说明了它是怎样完成的,所以再为此加上注释是多余的,而且违反了 DRY 原则

Use the Power of Command Shells

Always Use Source Code Control

总是。即使你的团队只有你一个人,你的项目只需一周时间;即使那是“用过就扔”的原型;即使你的工作对象并非源码

学习一种文本操纵语言

至少精通一种

编写能编写代码的代码

我自己写的代码生成器:Jce 生成工具、播放器事件生成工具、多终端配置工具

DBC:按合约设计(Design with Contracts)

前条件:为了调用例程,必须为真的条件

后条件:例程保证会做的事情,例程完成时世界的状态

类不变项:从调用者的角度来说,该条件总是为真。例程内部处理过程中,不变项不一定会保持,但在例程退出的时候,不变项必须为真

1
2
3
4
5
6
7
8
9
10
/**
* @类不变项:列表是升序排列
*/
public class dbc_list {
/**
* @pre contains(aNode) == false
* @post contains(aNode) == true
*/
public void insertNode(final Node aNode) {
// ...

谁负责检查前条件,应该是调用者。比如 sqrt(int) 这个函数,参数不能为负数这个前条件应该由调用者来保证

如果它不可能发生,用断言确保它不会发生

不要用断言代替真正的错误处理。断言检查的是决不应该发生的事情

1
2
3
printf("Enter 'Y' or 'N': ");
ch = getchar();
assert((ch == 'Y') || (ch == 'N')); /* bad idea! */

处理用户的异常输入应该属于错误处理,不可用断言检查,因为用户输入异常不是不可能发生的事情

在C++异常机制下配平资源

1
2
3
4
5
6
7
8
9
10
11
void doSomething(void) {
Node *n = new Node;
try {
// do something
}
catch (...) {
delete n;
throw;
}
delete n;
}

注意我们创建的节点是在两个地方释放的——一次是在例程正常的退出路径上,一次是在异常处理器中。这显然违反了DRY原则,可能会发生维护问题。

一种最简单的解决方法是不使用指针即可,栈对象会自动释放;另一种优雅的解决方法是,使用新定义的对象包装好指针对象,这个其实就是 auto_ptr 的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Wrapper class for Node resources
class NodeResource {
Node *n;
public:
NodeResource() { n = new Node; }
~NodeResource() { delete n; }
Node *operator->() { return n; }
};

void doSomething2(void) {
NodeResource n;
try {
// do something
}
catch (...) {
throw;
}
}

C++ 中,为什么对指针 delete 之后还要置空?

  1. NULL 指针可以防止多次 delete 出错
  2. 防止指针成为野指针;对 NULL 指针解引用会导致运行时错误(野指针更难定位,因为对野指针解引用不一定会出现崩溃)

迪米特法则(德墨忒耳法则/最少知识原则)

迪米特法则规定,某个对象的任何方法都应该只调用属于以下情形的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Demeter
{
public:
void example(B &b);
private:
A *a;
int func(){}
}
void Demeter::example(B &b)
{
C c;
int f = func(); <------------------1. 它自身
b.invert(); <----------------------2. 传入该方法的任何参数
a = new A();
a->setActive(); <------------------3. 它创建的任何对象
c.print(); <-----------------------4. 任何直接持有的组件对象
}

举个例子,以下调用不符合迪米特法则

1
2
3
4
5
void showBalance(Account account)
{
Money money = account.getBalance();
printToScreen(monney.printFormat());
}

将抽象放进代码,细节放进元数据

npm 的 package.json、hexo 的主题配置、markdown、 git 的 config 等这些都是以元数据的形式进行配置

常识估算

  • 简单循环:O(n),比如查找数组最大值
  • 嵌套循环:O(n²),比如冒泡排序
  • 二分法:O(lg(n)),比如二分查找、遍历二叉树
  • 分而治之:O(nlg(n)),划分其输入,并独立地在两个部分上进行处理,然后再把结果组合起来的算法,比如快速排序、归并排序
  • 组合:O(Cⁿ),只要算法考虑事物的排列,其运行时间就可能失控,这是因为排列涉及到阶乘,比如旅行商问题

重构注意

  1. 不要试图在重构的同时增加功能
  2. 在开始重构之前,确保你拥有良好的测试
  3. 采用短小、深思熟虑的步骤,重构往往涉及到进行许多局部改动,继而产生更大规模的改动。如果你使你的步骤保持短小,并且在每个步骤之后进行测试,你将能够避免长时间的调试。
  1. 不要容忍破窗户
  2. 不要追求完美
  3. 批判地分析你读到的和听到的
  4. DRY – Don’t Repeat Yourself
  5. 代码注释
  6. Use the Power of Command Shells
  7. Always Use Source Code Control
  8. 学习一种文本操纵语言
  9. 编写能编写代码的代码
  10. DBC:按合约设计(Design with Contracts)
  11. 如果它不可能发生,用断言确保它不会发生
  12. 在C++异常机制下配平资源
  13. 迪米特法则(德墨忒耳法则/最少知识原则)
  14. 将抽象放进代码,细节放进元数据
  15. 常识估算
  16. 重构注意