< The Pragmatic Programmer > 读书笔记
Table of Contents

The Pragmatic Programmer: From Journeyman to Master

中文名:《程序员修炼之道》

英文名原意应为“注重实效的程序员”。


ch1 注重实效的哲学

1 我的源码让猫给吃了

注重实效?处理问题,寻找解决方案时的态度、风格、哲学。注意大的图景,大的语境。

最大的弱点就是害怕暴露弱点。

对自己的职业生涯负责。主动承担责任。应急计划。

提供各种选择,而不是找借口。不要说事情做不到,要说明做什么能够挽回局面。

不要害怕提出要求,也不要害怕承认你需要帮助。

2 软件的熵

熵(entropy),某个系统中的“无序”的总量。

软件中的无序增长->软件腐烂(software rot)

破窗理论。不要容忍破窗户。发现一个就修一个。

把出问题的代码放入注释。(comment out)

3 石头汤与煮青蛙

石头汤的故事:类似于“麦当劳效应”,先拿出石头来。做变化的催化剂。

煮青蛙:记住大图景,要持续不断地观察周围发生的事情,而不只是你自己在做的事情。

4 足够好的软件

让用户参与权衡。使质量成为需求问题。

知道何时止步,不要因为过度修饰和过于求精而损毁完好的程序。

5 你的知识资产

承认这样的事实:随着新技术、语言及环境的出现,你的知识会变得过时。

知识资产(Knowledge Portfolios),程序员知道的关于计算技术、工作的应用领域的全部事实及所有经验。

管理知识资产与管理金融资产非常相似:

  1. 严肃的投资者定期投资--作为习惯。
  2. 多元化是保持长期成功的关键。
  3. 聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的资产。
  4. 投资者应设法低买高卖,以获取最大回报。
  5. 应周期性重新评估和平衡资产。

目标

批判思考的重要性。提醒自己不要受到供应商或者媒体炒作的影响。小心不要被商业力量欺骗(如百度的竞价排名)

如何与Guru打交道:

6 交流

规划你要说的东西。写出大纲。问问自己:“这是否讲清了我想要说的所有内容?”

了解听众,WISDOM离合诗。

让文档美观。

让听众参与。

倾听。

回复他人。

关于电子邮件交流,最重要的一点,在按下“发送之前”,再次校对,确保附件和拼写无误。

将你的电子邮件加以组织并存档。

作者推荐阅读《人月神话》和《人件》还有《Dynamics of Software Development》


ch2 注重实效的途径

DRY原则

系统中的每一项知识都必须具有单一、无歧义、权威的表示。

DRY是最重要的工具之一。

重复:

imposed duplication

再思考一下头文件和实现文件中的注释,没有理由在这两种文件之间重复函数或类头注释。应该用头文件记载接口问题,实现文件记载代码使用者无需了解的实际细节。

inadvertent duplication

设计的不合理。例子:

class Line
{
public:
    Point start;
    Point end;
    double length;
};

上面的代码有重复,因为长度是由起点和终点决定的,应该令长度成为可计算字段

class Line
{
public:
    Point start;
    Point end;
    double length() { return start.distanceTo(end); }
};

但实际开发中往往会由于性能原因迫使我们违反DRY原则。例如我们需要缓存数据,以避免重复昂贵的计算操作。这里的窍门是使影响局部化。对DRY原则的违反没有暴露给外界,类中的方法要注意保持行为良好。

class Line
{
private:
    Point start;
    Point end;
    bool changed;
    double length;
public:
    void setStart(Point p) { start = p; changed = true; }
    void setEnd(Point p) { end = p; changed = true; }
    Point getStart(void) { return start; }
    Point getEnd(void) { return end; }
    double getLength()
    {
        if (changed)
        {
            length = start.distanceTo(end);
            changed = false;
        }
        return length;
    }
};

在可能的情况下,总是使用访问器(accessor)函数读写对象的属性。

impatient duplication

时间压力,偷懒。“欲速则不达”。

训练自己,为避免以后更大的痛苦预先花一些时间。

interdeveloper duplication

主动交流。

阅读他人的代码和文档。

8 正交性

几何中的正交。直角坐标系的两个坐标轴。沿着一条直线移动,你投影到另一条直线上的位置不变。

不相依赖性,解耦性。

高内聚,低耦合。

正交性的两个主要好处:提高生产率,降低风险。

正交性可以提高生产率:

降低风险:

正交设计的测试方法:如果我显著地改变某个特定功能背后的需求,有多少模块会受到影响?正交设计的答案应该是“一个”。

编码中的正交

测试中的正交

文档中的正交

9 可撤销性

灵活的架构

不存在最终决策

10 曳光弹

曳光弹较其他子弹弹头不同之处在于弹头在飞行中会发亮,并在光源不足或黑暗中显示出弹道,协助射手进行弹道修正,甚至作为指引以及联络友军攻击方向与位置的方式与工具。

曳光弹,反馈是即时的,它们与工作在与真正的弹药环境相同的环境中,外部影响得以降至最低。

适用于新的项目,特别是当你构建从未构建过的东西时

把系统定死,制作大量文档,逐一列出每项需求、确定所有未知因素、限定环境。根据死的计算射击。预先进行一次大量计算。然后射击并企望击中目标。

曳光代码并非用过就扔的代码:你编写它是为了保留它。它含有任何一段产品代码都拥有的完整的错误检查、结构、文档及自查。它只是功能不全而已。

项目永不会结束:总有改动需要完成,总有功能需要增加。这是一个渐进的过程。

曳光弹方法的优点:

与原型制作不同。原型制作生成用过就扔的代码。曳光弹虽然简约,但是完整。

11 原型与便签

原型应不关心细节。当你发现不能放弃细节时,或许曳光弹更合适。

应该制作原型的事物:

原型制作是一种学习经验。其价值不在于所产生的代码,而在于所学到的经验教训,那才是原型制作的要点所在。

构建原型时可以忽略的:

记住:原型应掩盖细节。

12 领域语言(Domain Language)

DSL。在具体的某个问题或领域中,用特定的小型语言描述。该语言无须是可执行的,它可以只是用于捕捉用户需求的一种方式--一种规范。更进一步,你可以实际实现该语言。

用大大接近应用领域的方式编程。

Windows编程中的资源文件(.rc文件)使用了特定的语言描述资源。

对于维护者来说,当应用发生变化时,可以只更新DSL的高级描述,无需钻入实现代码的各种细节中。

13 估算

在进行估算的过程中,你将会加深对你的程序所处的世界的理解。

估算以问题的模型为基础。去问已经做过这件事情的人。

理解问题。建模。把模型分解为组件。给每个参数指定值。计算答案。

追踪你的估算能力。

估算项目进度

在被要求估算时回答:“我等会儿回答你。”


ch3 基本工具

14 纯文本的威力

纯文本并非意味着没有结构

markdown

用纯文本保存知识。

Unix哲学:提供“锋利”的小工具,其中每一样都意在把一件事情做好。各种基于纯文本的工具。

15 Shell游戏

在shell下,你可以调用你的全套工具,并使用管道,以这些工具原来的开发者从未想过的方式将它们组合使用。

GUI的好处WYSIWYG(所见即所得),缺点WYSIAYG(所见即全部所得)。

Shell脚本便于实现自动化。

16 强力编辑

基本武器。

好的编辑器应具有这些特性:

17 源码控制

确保每一样东西都处在源码控制之下。(代码,文档,备忘录,shell脚本等)

好处:可以进行自动的和可重复的产品构建。

18 调试

专注于解决问题,而不是发出指责。

不要恐慌。

设法找出根源,而不是只解决表象。

不要容忍警告。

可视化。跟踪。

橡皮鸭是你的好朋友。(向橡皮鸭解释、描述,帮助自己思考、梳理)

记录你的bug,让下一次修正更容易。

19 文本操纵(Text Manipulation)

学习 Shell,Python 等脚本语言。

20 代码生成器

Write Code That Writes Code.

被动代码生成器

主动代码生成器


ch4 注重实效的偏执

你不可能写出完美的软件。

21 按合约设计

Design By Contact.

对在开始之前接受的东西要严格,而允诺返回的东西要尽可能少。

继承和多态 -> 合约。

子类必须要能通过基类的接口使用,而使用者无需知道其区别。

在基类中规定合约一次,那么合约会自动应用于子类。

DBC是一种设计方法,可以作为注释放在代码中。

循环不变项

int m = arr[0];
int i = 1;
// Loop invariant: m = max(arr[0:i-1])
while (i < arr.length) {
    m = Math.max(m, arr[i]);
    i = i + 1;
}

语义不变项

22 死程序不说谎

早崩溃。尽早检测,尽早崩溃。

让程序尽早崩溃。

当你的程序发现某件被认为不可能的事情发生时,应该尽早终止它。

Java,RunTimeException

C语言,使用宏。

#define CHECK(LINE, EXPECTED)    \
{  int rc = LINE;
    if (rc != EXPECTED)    \
        ut_abort(__FILE__, __LINE__, #LINE, rc, EXPECTED);  }
void ut_abort(char *file, int ln, char *line, int rc, int exp) {
    fprintf(stderr, "%s line %d\n'%s': expected %d, got %d\n", file, ln, line, exp, rc);
    exit(1);
}

// 包装调用
CHECK(stat("/tmp", &stat_buff), 0);

// 如果它失败了,你会得到stderr的提示信息

23 断言式编程

“这绝不会发生”--自我欺骗。

用断言确保它绝不会发生。

C/C++中往往有assert或_assert宏。

// 使用断言确保传入的指针不为NULL
void writeString(char *string) {
    assert(string != NULL);
}
// 使用断言检查排序算法能否工作
for (int i = 0; i < num_entries - 1; ++i) {
    assert(sorted[i] <= sorted[i + 1]);
}

断言不应该有副作用。

绝对不要把必须执行的代码放在assert中,因为断言可能在编译时被关闭

断言不能替代错误处理,断言检查的是绝对不应该发生的事情

下面是一个错误使用断言的例子:

printf("Enter 'Y' or 'N': ");
ch = getchar();
assert((ch == 'Y') || (ch == 'N'));    // BAD IDEA!!

断言增加了开销,对性能有一定的影响。只关闭真正对性能有较大影响的断言。

要警惕断言中执行的方法、语句的作用,防止出现“海森堡虫子(Heisenbug)”,即调试改变了被调试系统的行为。

24 何时使用异常

如果不使用异常,可能写出这种代码:(程序的正常逻辑被错误处理所遮蔽)

retcode = OK;
if (socket.read(name) != OK) {
    retcode = BAD_READ;
}
else {
    processName(name);
    if (socket.read(address) != OK) {
        retcode = BAD_READ;
    }
    else {
        processAddress(address);
        if (...) {
            ...
        }
    }
}

如果使用异常,代码会简洁很多(Java示例):

retcode = OK;
try {
    socket.read(name);
    process(name);
    socket.read(address);
    processAddress(address);
    ......
}
catch (IOException e) {
    retcode = BAD_READ;
    Logger.log("Error reading individual: " + e.getMessage());
}
return retcode;

何时使用异常,异常应该保留给意外事件,移走所有的异常处理器,代码应该仍能够运行。

异常处理破坏了封装:通过异常处理,例程和它们的调用者被更紧密地耦合在一起。

错误处理器是另一种选择。

25 怎样配平资源

要有始有终。Finish What You Start.

少用共享的全局变量。

建议:

  1. 以与资源分配次序相反的次序解除资源的分配
  2. 在代码的不同地方分配同一组资源时,总是以相同的次序分配它们。降低发生死锁的可能性。

将资源封装在类中很有用。

资源配平与异常

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

void doSomething() {
    Node *n = new Node();
    try {
        // do something
    }
    catch (...) {
        delete n;
        throw;
    }
    delete n;
}

上面的代码违反了DRY原则。如果不使用指针,即Node n;则不会有该问题。

有些情况不得不使用指针?

在另一个类中包装资源。(RAII ,或者用智能指针)

// Wrapper class for Node
class NodeResource {
    Node *n;
public:
    NodeResource()  { n = new Node(); }
    ~NodeResource()  { delete n; }

    Node *operator->()  { return n; }
};
void doSomething2() {
    NodeResource n;
    try {
        // do something
    }
    catch (...) {
        throw;
    }
}

注意这里重载了->运算符,这样它的使用者可以直接访问所包含的Node对象中的字段。

Java中利用finally字句配平:

public void doSomething() throws IOException {
    File tmpFile = new File(tmpFileName);
    FileWriter tmp = new FileWriter(tmpFile);
    try {
        // do some work
    }
    finally {
        tmpFile.delete();
    }
}

ch5 弯曲,或折断

26 解耦与得墨忒耳法则

把代码组织成最小组织单位(模块),并限制它们之间的交互。

public void plotDate(Date aDate, Selection aSelection) {
    TimeZone tz = aSelection.getRecorder().getLocation().getTimeZone();
    ...
}

上面三个类不必要的耦合在了一起:Selection, Recorder, Location。这种编码风格增加了耦合。

应该直接要求提供你所需的东西,而不是自行“挖通”调用层次。

public void plotDate(Date aDate, TimeZone aTz) {
    ...
}
plotDate(someDate, someSelection.getTimeZone);

增加包装方法,避免“组合爆炸”(如果n个对象之间全部都互相了解,那么对一个对象的改动就可能导致其他n-1个对象都需要改动)

函数的得墨忒耳法则:使模块之间的耦合减至最少,阻止你为了获得对第三个对象的方法的访问而进入某个对象。

在C++中,具有较大响应集的类更容易出错(响应集:类的各个方法直接调用的函数的数目)。

class Demeter {
    A *a;
    int func();
public:
    // ...
    void example(B &b);
};
void Demeter::example(B &b) {
    C c;                // 函数的得墨忒耳法则规定,对象的方法应该只调用属于以下情形的方法
    int f = func();     // 1. 它自身
    b.invert();         // 2. 传入方法的任何参数
    a = new A();    
    a->setActive();     // 3. 它创建的任何对象    
    c.print();          // 4. 任何直接持有的组件对象
}

得墨忒耳法则缩小了调用类中的响应集的规模。

但其也有代价:你将会编写大量包装方法,它们只是把请求转发给被委托者。这些包装方法会带来运行时代价,也会带来空间开销。

平衡、权衡。不要僵化使用一些法则。

27 元程序设计

让系统高度可配置。

元数据就是关于数据的数据。

以声明方式思考(规定要做什么,而不是怎么做),创建高度灵活和可适应的程序。

元数据驱动的好处:

28 时间耦合

考虑并发性,考虑解除任何时间或次序上的依赖。

分析工作流以改善并发性。

考虑用服务(service)进行设计。

服务:位于定义良好的、一致的接口之后的独立、并发的对象。

29 它只是视图

事件(event),一个特殊的消息,用于说明刚刚发生了某件有趣的事情,可以用事件将某个对象的状态变化通知给可能感兴趣的其他对象。

使用事件可以降低对象之间的耦合。(发送者、接收者都针对事件进行处理)

发布/订阅:通过单个例程推送所有事件破坏了对象封装,这个例程需要对许多对象有密切的了解。使用“发布/订阅”协议。

Model View Controller:模型和视图分离,控制器进行数据操作。

30 黑板

黑板:数据到达的次序无关紧要;在收到某项事实时,它可以出发适当的规则,反馈也很容易处理;任何规则集的输出都可以张贴到黑板上,并出发更为适用的规则。

黑板模式是一种常用的架构模式,应用中的多种不同数据处理逻辑相互影响和协同来完成数据分析处理。就好像多位不同的专家在同一黑板上交流思想,每个专家都可以获得别的专家写在黑板上的信息,同时也可以用自己的分析去更新黑板上的信息,从而影响其它专家。黑板模式的应用场景是要解决的任务可以分为多个子任务

一个黑板模式的的例子, JavaSpaces 中的操作:

黑板模式消除了对太多接口的需要。


ch6 当你编码时

传统智慧认为,项目一旦进入编码状态,工作就是机械地把设计转换成可执行语句。这是错误的想法。

编码不是机械工作,程序员需要在编码过程中不断得到反馈,改善、修正设计。

注重实效的程序员批判地思考代码。

31 靠巧合编程

小心不要得出错误的结论。

没有记入文档的行为可能会随着库的下一次发布而变化。

不要假定,要证明。

深思熟虑地编程:意识到你在做什么,依靠可靠的事物,为你的假定建立文档,不要做历史的奴隶,如果不再适用,所有代码都可以被替换。

32 算法速率

学会估计算法使用的资源:时间、处理器、内存等。

测试你的估算。

最好的不一定总是最好的:最快的算法不一定是最合适的算法。

33 重构

重构:重写,重做,重新架构。

何时重构?当你觉得代码不再何时,不要对改动犹豫不决。

早重构,常重构。

不要试图在重构时增加功能。

重构前确保良好的测试。

采取短小、深思熟虑的步骤重构。

34 易于测试的代码

写单测。

针对合约进行测试:代码是否符合合约,合约的含义是否与我们认为的一样。

测试是技术,更是文化。

35 邪恶的向导

wizard,很多 IDE 提供了自动化工具。

缺点是你不知道 wizard 帮你做了什么。

不要使用你不理解的向导代码。


ch7 在项目开始之前

36 需求之坑

不要搜集需求,挖掘需求。

如何深入了解用户需求? -- 成为用户。

建立需求文档,使用用例(use case)。

看长远一些:Y2K 问题,没有超出当时的商业实践向前看。

抽象比细节活得更长久。

维护项目词汇表,统一描述。

37 解开不可能解开的谜题

在盒子外思考 -- 更重要的是找到盒子。确定真正的约束是什么。

要相信“一定有更容易的方法”。“它一定要以这种方式完成吗?”

38 等你准备好

知道何时开始,何时等待。

等待、准备不是拖延。避免“启动恐惧症”。

39 规范陷阱

编写程序规范就是把需求归约到程序员能够接管的程度的过程。

对有些事情“做”胜于“描述”。

注重实效的程序员,倾向于把需求搜集、设计、以及实现视为同一个过程 -- 它们都是交付高质量系统的不同方面。

40 圆圈与箭头

形式方法只是一种工具。

批判看待方法学。不要向方法的虚假权威屈服。


ch8 注重实效的项目

41 注重实效的团队

团队同样不要容忍破窗户。

团队开发者必须要交流。

围绕功能而不是职位进行人员组织。

42 无处不在的自动化

人工流程难以保证一致性。

自动化生成代码,自动化测试,自动化构建。

43 无情的测试

早测试、常测试、自动测试。

编一点,测一点。

任何产品代码一旦存在,就需要测试。

记录 bug,一个 bug 只抓一次。

44 全都是写

把文档当作开发流程的一部分。“把英语当作一种编程语言。”

45 极大的期望

项目的成功是由它在多大程度上满足了用户的期望来衡量的。

46 傲慢与偏见

不要逃避责任,“这段代码是我写的。”