《程序员修炼之道》读书笔记

前言

  1. 编程不存在某种最佳解决方案,我们应该注重失效,在拥有足够广博的背景和经验基础上,以保证能在特定情况下选择好的解决方案。
  2. 背景源自对计算机科学的基本原理理解,经验来自广泛的实际项目。

第1章 注重实效的哲学 1

1 我的源码让猫给吃了 2
  • 诚实面对我们的无知和错误
  • 在做某件事情时除了尽你所能外,必须分析风险是否超过你的控制。对于不可能做到的事情或者风险太大,你有权不去为之负责。
    但是一旦承诺某件事完成,同意为某个结果负责就必须承担其责任。
  • 当自己犯错误的时候,诚实承认它,并设法提供各种选择。不要责备别人或东西,或是拼凑借口。
    在跟别人说做不到之前请先把自己的辩解说给猫听,看看是否合理还是愚蠢。你的老板听来又是怎样?
2 软件的熵 3
  • “不能容忍破窗户”
  • “破窗户”:低劣的设计,错误的决策或者糟糕的代码
  • 没时间修理的对策:用木板把它钉起来-加入注释 加入TODO 用虚设的数据加以替代。
3 石头汤与煮青蛙 5
  • 知道某件事情是对的,但是涉及到其他人,为了对抗漠然和拖延,就需要欺骗,让同事们开始在路上,那么我们离成功就不远了。
  • 煮青蛙:避免拖延和偏离设计,这会导致如青蛙一样被煮熟。
4 足够好的软件 8
  • 足够好的软件时满足用户的需求,所以必须让用户参与权衡
  • 知道何时止步:不要在过度修饰和过于求精中损坏完好的程序,迷失其中。
5 你的知识资产 10
  • 知识资产:所知的关于计算机技术和所工作的应用领域的全部事实以及他们的所有经验视为他们的知识资产。

  • 经营资产

    • 定期投资:具体见示范目标
    • 多元化:你知道的不同的事情越多,你就越有价值。由于计算机技术变化很快,所以掌握的技术越多,越能更好的进行调整,赶上变化
    • 管理风险:高风险高回报 低风险低回报
    • 低买高卖:靠自己的判断了
    • 重新评估和品和平衡:评估当前用户的技术,为适应市场变化或者职位需求是否需要学习新的东西。
  • 示范目标

    • 每年至少学习一种新语言
    • 每季度阅读一本技术书籍
    • 也要阅读非技术书籍。记住计算机是由人使用的。
    • 上课
    • 参加本地用户组织:主动参与了解公司以外的人在做什么,不要与世隔绝
    • 试验不同的环境
    • 跟上潮流
    • 上网
  • 学习的机会:

    • 不懂可以问人
  • 批判的思考:警惕唯一的答案,批判的分析听到的和读到的。

6 交流 14
  • 知道自己想要说什么:规划你想要说的东西-》写出大纲-》问自己“这是否讲清了我想要说的内容”-》提炼-》直到确实如此
  • 了解自己的听众:
  • 面对不同的听众,讲出他们感兴趣的东西。
    • 你想要听众知道什么
    • 他们对你讲的什么感兴趣
    • 他们的知识背景是什么
    • 你想要谁拥有这些信息
    • 他们想要多少细节(程度)
    • 你如何促使他们听你的话。
  • 选择时机:合适的时机事半功倍
  • 文档除了内容形式上的美观也至关重要。

第2章 注重实效的途径 19

7 重复的危害 20

DRY:don't repeat yourself

强加的重复:

  • 信息的多种表示:在编码一级,信息需要在不同平台(客户端和服务端)上表示,即使是在客户端不同语言的表示也会带来重复。解决方法:编写代码生成器,针对文本生成不同语言平台的代码。 尽量让低级的知识放在代码中,将注释保留给其他高级说明,否则一旦代码修改,注释就得一并修改。
  • 文档与代码:有些东西改变就得修改文档和代码,所以有能力的话建立文档到代码的生成机制。
  • 语言问题

无意的重复:原因来自信息结构的不规范,一旦发现多个相互依赖的数据元素时,就需要考虑去重复的问题

无耐性的重复:

开发者之间的重复:通过高层设计避免

8 正交性 25

正交性:两条直线相交成直角,两条直线互不依赖, 沿着某条直线移动,投影到另外一条直线上的 位置不变
正交的好处:

  • 提高效率
    • 修改得以局部化,开发和测试时间得以降低
    • 促进复用,当组件有明确具体的职责,就可以与设计时未想到的新组件在一起。
  • 降低风险
    • 问题代码区被隔离开来,不会 扩散到其他部分
    • 所得系统更健壮
    • 测试更容易
      怎么做
    • 项目团队管理
    • 设计:设计时思考如果我显著改变某个特定功能背后,有多少模块会受影响
    • AOP:面向方面编程:可以让我再一个地方表达本来会分散到源码各处的某种行为
    • 编码:
      *让代码保持解耦 羞怯代码,Law of Demeter
      • 避免使用全局数据
      • 避免编写相似的函数(《重构》《设计模式》中提到策略模式)
      • 构建单元测试不仅因为它比集成测试更容易规定和进行,还因为其 本身也是对正交性的一项测试
    • 认同正交性,就要时刻记住运用DRY原则,寻求系统中重复降至最小;
9 可撤消性 33

不存在最终决策
为了达到可撤销性:需要灵活的架构,使用项CORBA技术能够将项目某些部分与语言分割开来。

10 曳光弹 36

先动手,然后观察各种反馈,立即改进
曳光开发与项目永不会结束的理念是一致的:总有改动需要完成,总有功能需要增加。这是一个渐进的过程。
曳光开发其实大家或多或少都在参与。新项目创建时搭建框架代码,逐渐为框架添加功能正是这样一个过程。我们会在框架中让关键流程能够运行,以检验新技术

曳光开发和原型模式有明显区别。原型中的代码是用过就扔的,寻求以最快的速度展示产品,甚至会采用更高级的语言。曳光代码虽然简约,但却是完成的,它拥有完整的错误检查与异常处理,只不过是功能不全而已。

优点:

  • 用户能够及早看到能工作的东西
  • 开发者构建了一个他们能再其中工作的结构
  • 有了一个集成环境
  • 有了用于演示的东西
  • 感知工作的进展。

曳光代码vs 原型制作

  • 原型开发

    • 有一个非常重要的前提:你写的这段代码,将一定会被遗弃!如果不满足这个条件,请不要使用这种方法,否则会出现很悲惨的事情。既然是一个原型,那么我们可以考虑使用更加简单的方法来实现他。比如C#或者java?或者使用一些项目不建议使用的库来简化开发?甚至直接用黑板,将你的想法画出来?这样你不但能很快让大家看到一个基本的效果,还能减少不少的开发量。在原型中也不要担心效率问题,实现的优不优雅等等因素,因为请谨记:你的这段代码将被丢弃。所以请放心大胆的在上面实验你的各种想法吧。
  • 曳光弹

    • 一旦使用原型找到合适的方向了之后,我们就可以将原型抛弃而转入曳光弹的开发了,这也是这两种开发模式本质的区别。在曳光弹开发时,你可以为产品或特性想一个较为合适的实现了,搭建一个足已支撑项目的框架,然后开始慢慢进入正常的产品开发迭代:完成一定的功能,交付使用,根据反馈再继续修改这部分功能……直到最终成型并交付。
11 原型与便笺 40

原型制作与完全的制作对比成本低很多,举例轿车制作商可以为某种新车设计不同的原型,目的是测试轿车的具体方面-空气动力学,结构特征等。同样道理,软件产品也可以使用原型制作,利用不同材料制作原型,可以是白板上的图形、绘图工具绘制的产品图等等。但是,如果发现自己处于不能放弃细节的环境中,需要问自己是否采用该方法而转而使用曳光弹方法。

应制作原型的事物:

  • 架构
  • 已有系统的新功能
  • 外部数据的结构或内容
  • 第三方工具或组件
  • 性能问题
  • 用户界面设计
    原型制作中可忽略的细节:
    • 正确性:虚设数据
    • 完整性:提高有意义的事物
    • 健壮性:错误检查
    • 风格

制作架构原型:

  • 主要组件的责任是否得到良好定义?是否适当?
  • 主要组件间协作是否得到良好的定义
  • 耦合是否得以最小化
  • 你能否确定重复的潜在来源
  • 接口的定义和各项约束是否可接受
  • 每个模块再执行中能否访问其所需的数据?是否在需要时进行访问?
12 领域语言 43

靠近问题领域编程:旨在脱离具体的编程语言和平台,在需解决的实际问题领域中去尝试给出解决方案(伪代码)。
实现小型语言,并通过解析生成器生成不同编程语言的代码。如使用BNF(Backus-Naur Form 可用于递归地规定上下文无关的语法)

13 估算 48

学习估算,并将此技能发展到你对事物的 数量级有直觉的程度,你就能展现出一种魔法般的能力。

估算单位

  • 时长1-15天 天
  • 时长3-8周 周
  • 时长8-30周 月
  • 时长30+周 努力思考一下

估算来自问题提供的信息

  • 第一步对提问内容的理解。
  • 第二步是建立系统模型
  • 第三部是把模型分解为组件,再一一估算
  • 最后一步是计算答案

第3章 基本工具 55

14 纯文本的威力 56

持久存储知识的最佳格式是纯文本

纯文本:由可打印字符组成,人可以直接阅读和理解其形式。纯文本也可以有结构,可以是xml,sgml,HTML和json。借助纯文本可以获得自描述(self-describing)

纯文本的缺点:

  • 比压缩的二进制文件占用空间大;
  • 要解释和处理纯文本文件需要的计算代价大。

纯文本的优点:

  • 保证不过时:文本的描述方式永远比其他的数据形式和应用存活得更久。
  • 杠杆作用:世界上每一样工具从源码管理系统到编译器环境,再到编辑器及独立的过滤器,都能够纯文本上进行。
  • 更易于测试。如果纯文本用于驱动系统测试的合成数据,那么增加、修改、删除测试数据就是一件简单的事情,无须为此创建任何特殊工具
15 shell游戏 60

虽然很多操作用GUI界面操作更直观方便,但是不能依赖它,因为GUI环境受限于设计者想要提供的能力。

需要投入精力熟悉shell命令,利用shell命令灵活地组织命令序列,高效地完成要做的事情。

16 强力编辑 63

最好精通一种编辑器,并将其用于所有编辑任务:代码、文档、备忘录、系统管理等等。
坚持使用一款编辑器,避免在不同编辑环境中切换,重新熟悉相应的编辑约定和命令带来的成本。
确保编辑器能在不同平台下使用:Emacs,vi、Crisp等

编辑器特性:

  • 可配置
  • 可扩展
  • 可编程
17 源码控制 67

无须多言

18 调试 69
  • 接受事实,调试就是解决问题。
  • 要修正问题,而不是发出指责,不管bug是你的过错还是他人不是很有关系,它任然是你的问题
  • 调试的思维
    • 不要恐慌:人在面对deadline的时候或者是老板或客户的催赶会变得恐慌
19 文本操纵 77

这里的文本操纵语言是轻量级的,类似脚本语言,像perl、python等脚本语言能够进行网络访问,测试数据的生成、文本的解析操纵,生成web文档。

建议:学习一门文本操纵语言

20 代码生成器 80

如果有写代码重复了,那么就需要 编写能编写代码的代码

代码生成器的类型:

  • 被动代码生成器:运行一次生成结果,之后结果与代码生成器无关了。本质是 参数化模板

    • 用途:
      • 创建新文件
      • 在变成语言之间进行一次性转换
      • 生成查找表及其他在运行时计算很昂贵的资源
  • 主动代码生成器:当发现自己在设法让两种完全不同的环境一起工作,那么就应该考虑主动代码生成器

代码生成器并不是要很复杂,勇于尝试。

代码生成器不一定要生成代码

第4章 注重实效的偏执 85

21 按合约设计 86

DBC:契约式设计

  • 规定调用者和被调用者双方的权利与责任
  • 前条件precondition
  • 后条件postcondition:完成时世界的状态
  • 类不变项class invariant:在例程退出后不变项必须为真

断言:断言不可能发生的事情

程序不一定非得一个出口

22 死程序不说谎 95

早崩溃。不要破坏(trash),写入错误的数据

23 断言式编程 97

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

断言时不要有副作用

24 何时使用异常 100

理解需求,异常是留给意外事件的

25 怎样配平资源 103

要有始有终:分配资源,使用它,释放它

嵌套的分配(一次性不只一个资源)

  • 以与资源分配的次序相反解除资源的分配,如果一个资源含有对另一个资源的引用,就不会造成资源被遗弃
  • 在代码不同的地方分配同一组资源,总是以相同的次序分配他们,这将降低死锁的可能性。

第5章 弯曲,或折断 111

26 解耦与得墨忒耳法则 112

遵循得墨忒耳法则虽然可以减少模块之间的依赖,但是会带来很多委托方法出现,不仅增加无关的代码,还影响代码的执行速度,所以需要根据不同的场景折衷,违反规范来赢取性能的改进。

27 元程序设计 117

???
元数据

  • 元数据(metadata):描述应用的配置选项:调协参数、用户偏好
  • 元数据严格意义上是数据的数据,宽泛意义上是对任何对应用进行描述的数据。(除了偏好外,还有资源等)
  • 元数据是在运行时被访问和使用,而不是在编译时。
    把抽象放在代码,把细节放在元程序
28 时间耦合 121

一开始编程都是按照时间的顺序去进行。但是一旦需要并发时,就出现了麻烦。

利用UML的活动图,分析工作流。

29 它只是视图 127

我们基于分而治之的理念将程序分成若干个模块,但是怎么管理组织不同模块(类)之间依赖确实一个难题。

我们从事件(event)这个概念出发,将新变化发送给感兴趣的对象。、

假如通过一个 例程(例程是某个系统对外提供的功能接口或服务的集合。比如操作系统的API、服务等就是例程)来自百度百科。那么例程就需要知道各个对象之间的交互有密切的了解。显然,我们可以采用订阅/发布的模式让某个订阅者只接受它感兴趣的事件。

CORBA Event Service 允许参与对象通过事件信道(公共总线)发送和接受通知。

MVC(模型-视图=控制器)模式有效地让模型与GUI分离,又与管理视图的控件分离。

模型:表示图标对象的抽象数据模型。模型对任何视图或控制器都没有直接的了解
视图:解释模型的方式。它定业模型中的变化和来自控制器的逻辑事件
控制器:控制视图。并向模型提供新数据的途径。它既向模型也向视图发布事件。

仍然有耦合,情看下一节 黑板

30 黑板 134

将警探办案将线索信息张贴在黑板的例子指出黑板方法的特性:

  • 没有对象需要知道另外其他对象的存在,他们从黑板中查看信息,并添加他们的发现。
  • 使用黑板的对象或者模块都有一个共同点就是围绕同一个目标或者功能,如在例子中是破案。

对于复杂多变的工作流,我们可以黑板来协调。

  • 数据到达的次序无关紧要
  • 在收到某项事实会触发适当的规则。

在分布式类黑板系统JavaSpaces中的接口设计如下:

名称 功能
read 在空间中查找并获取数据
write 把数据项放入空间
take 与read类似,但同时从空间中移除该数据项
notify 设定每当写入与模版匹配的对象时就发出通知

第6章 当你编码时 139

31 靠巧合编程 140

为什么不能靠巧合编程(看起能工作):

  • 它也许不是真的能工作
  • 依靠的边界条件知识偶然,在另一个条件下又不能工作了
  • 没有记入文档的行为可能随着库的下次发布而变化
  • 多余的调用让代码变慢
  • 多余的调用可能引入bug

深思熟虑地编程

  • 总是意识到自己在做什么
  • 不要盲目编程,构建不完全理解的应用和使用你不熟悉的技术
  • 按照计划行事,有条不紊!!!
  • 依靠可靠的事物,如可靠的库。
  • 为你的假定建立文档。
  • 为你工作划分优先级,时间花在重要和最难的方面。
  • 不做历史的奴隶,加入已有的代码不适用了,尽快替换。
32 算法速率 144

对于算法使用的资源。处理、内存进行估算。

O()表示法对我们度量的事物值设置了上限。

|一些常见的O()表示法 |
|---------------- |--- ---- |
|O(1)|常量型:数组访问|
|O(lg(n))|对数型:二分查找|
|O(n)|线性型 顺序查找|
|O(nlg(n))| 比线性差,对于 分而治之的算法(划分其输入,并独立在两个部分进行处理) 但不会差很多(快速排序、堆排序的平均运行时间)|
|O(n^2)|平方律型(选择和插入排序)|
|O(n^3)|立方型2nxn矩阵相乘|
|O(C^n)|指数型 旅行商问题|

虽然我们不用去设计编写排序等算法,但是 估算算法的阶 有利我们对自己编写的程序的运行情况有一定了解

33 重构 149

重构=重写+重做+重新架构

何时进行重构:

  • 重复(DRY)
  • 非正交设计
  • 过时的知识
  • 性能

早重构,常重构

怎样重构:

  • 不要在重构同时增加功能
  • 在开始重构之前确保拥有良好的测试,尽可能经常运行这些测试,这样能及早发现问题。
  • 采取短小的步骤,并在每个步骤后进行测试,避免长时间的调试。
34 易于测试的代码 153

单元测试:针对合约进行测试。

为测试而设计:

在设计模块甚至是单个例程时既设计其合约也设计测试该合约的代码。

35 邪恶的向导 160

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

#### 第7章 在项目开始之前 163

36 需求之坑 163

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

应该用明了的陈述句表达需求。有时候在陈述需求中会夹带着商业政策,而 商业政策是经常改变的。所以我们需要将商业政策与需求做区分。

在讨论用户界面时,需求、政策和实现之间区别可能会变的模糊不清。

重点
找出用户 为何做特定事情的原因,而不是他们目前做这件事情的方式。

建立需求文档:用use case(用例)来描述系统的特定用法。

相应的用例模板

抽象比细节活得更长久

维护项目的词汇表,利于沟通。

37 解开不可能解开的谜题 172

当遇到一个迷途难以解开的时候,解决的方法可能不在你现在思考的范围内。所以需要重新确定方法的约束,有些约束是绝对约束,而有些约束是先入为主。

不要在盒子外面思考-要找到盒子:我们需要确定问题的自由度,也就是约束
想想特洛伊木马

一定有更容易的方法

  • 有更容易的方法?
  • 你是在设法解决问题还是被外部的技术问题转移注意力
  • 这件事情为什么是一个问题
  • 是什么使它难以解决
  • 它必须以这种方式完成吗?
  • 它真的必须完成吗?
38 等你准备好 174

倾听反复出现的疑虑-等你准备好再开始

是良好的判断还是拖延

  • 拖延:对于概念的验证出现“浪费时间”的厌烦
  • 良好的判断,随着原型进展,肯呢个在某个时刻得到启示,突然意思到某个前提是错误的。
39 规范陷阱 176

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

40 圆圈与箭头 178

不做形式方法的奴隶

工具只是工具,要为我所用。

第8章 注重实效的项目 181

41 注重实效的团队 181
  • 不留破窗户
  • 不做温水青蛙,时刻注意到外部的变化如需求、商业政策等
  • 不要重复自己
  • 正交性:围绕功能而不是工作职务进行组织
42 无处不在的自动化 186

不要手工

自动化:

  • 测试
  • 构建
  • 生成代码
  • 批准流程
43 无情的测试 191

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

要到通过全部测试,编码才算结束。

测试什么

  • 单元测试:对于某个模块演练的代码
  • 集成测试:说明组成项目的主要子系统能工作且能很好的协同
  • 验证和校验(validation和verification):程序相应正常但是结果不是用户需要的,是错误的
  • 资源耗尽,错误及恢复:错误发生时会得体的失败,保存相应的信息吗?
  • 性能测试:压力测试
  • 可用性测试:真正的用户进行测试

怎样测试:

  • 回归测试:把当前测试输出与已知值进行比较
  • 测试数据:大量数据、边界数据
  • 演练GUI系统
  • 对测试进行测试:通过“蓄意破坏”测试你的测试
  • 彻底测试:尽可能频繁地测试,避免最后时限才进行测试
44 全都是写 200

把闻到那股建里卖年,不要拴在外面

45 极大的期望 205

温和超出用户的需求

46 傲慢与偏见 208

在你的作品签名:为自己负责

推荐阅读更多精彩内容