程序员修炼之道
第二章 务实的方法
曳光弹
提示20 使用曳光弹找到目标
页码 110
曳光代码不是一次性的:编写它是为了持续使用。代码中需要包含所有的错误检查、结构、文档、自检查这些任何生产代码都应具备的东西。它只是功能还不完整。但是,只要在各个组件间,从一头到另一头全部打通,就可以检查出离目标有多么接近,并在必要时做出调整。一旦抵达目标,再添加功能就很容易了。
页码 112
曳光弹会告诉你击中了什么。可能它并不能每次都击中目标,你需要不断地调整瞄准,直到击中目标。这正是问题的关键。 曳光代码也一样。当你不能100%确定要做什么的时候,就用这个技术。如果你一开始的一系列尝试出错了:用户说“我不是这个意思”,或是数据在你需要的时候还没到位,或是有一些性能问题,请不要诧异,去弄清楚如何改变你已经做好的东西,想办法让它靠近目标。你会受益于这种精益的开发方式。小块代码惯性也小——容易快速地变更。你能够以更快的速度、更小的成本,收集到针对应用程序的反馈,并生成一个新的更准确的版本。而且,应用程序的每个主要组件都已经出现在曳光代码中,用户可以确信他们看到的东西基于现实,而不仅仅是一个纸面规范
页码 113
曳光代码方法解决的是一个不同的问题。你需要知道,应用程序作为一个整体,是如何整合在一起的,需要向用户展示实践中交互如何工作,而且你想要给开发人员一个框架,用来挂接代码。在这种情况下,就可以用曳光代码构建出一个程序,它包含普通的装箱算法(比如来一个东西就装一个)和一个简单但能工作的用户界面。一旦将应用程序中的所有组件都放置在一起,就有了一个框架,可以展示给用户和开发人员。随着时间的推移,你会把新的功能添加到这个框架中,把留白的程序补完。但是框架保持不变,并且你知道系统将继续这样运作,一直和一开始完成曳光代码时一致。
页码 113
这一区别非常重要,值得我们反复强调。原型生成的是一次性代码;曳光代码虽然简单但是完整,它是最终系统框架的组成部分。可以将原型制作看作是在发射一颗曳光弹之前进行的侦察和情报收集工作。
第三章 基础工具
提示28 永远使用版本控制
页码 158
一个思想实验 把一整杯茶(一种英式早茶,可以加点牛奶)倒在笔记本上;然后把机器拿到天才吧,让他们折腾去;再买一台新的带回家。 你需要多久,才能把机器恢复到当初的状态?就是举起那只致命的杯子时的状态,包括所有的 SSH 热键、编辑器配置、Shell 设置、安装的软件,等等。我们今年就碰到一次这样的事情。 好在,原来机器所有影响配置和使用的数据,都保存在版本控制系统中,包括:
- 所有的用户参数以及点开头的文件
- 编辑器配置
- 用 Homebrew 安装的软件列表
- 用来配置软件的 Ansible 脚本
- 所有当前项目
当天下午机器就复原了
提示32 读一下那些该死的出错信息
页码 168
因此,对规模较大的问题,二分法是一种非常快捷的方法。让我们来看看在调试中如何运用。 当你面对一个大型的堆栈跟踪,试图找出到底是哪个函数把值搞坏了时,可以在调用栈中间选一帧来进行切分,看看错误是否在那里出现。如果是,那么就知道需要关注之前的帧,否则问题就出在之后的帧上。然后继续切分。即使堆栈跟踪信息有64帧,这种方法也会在最多6次尝试之后给出一个答案。 如果你发现在某个数据集上出现了 Bug,可能也会这样做。将数据集一分为二,看看向应用程序提供其中一半数据时是否会出现问题。不断分割数据,直到得到能显露问题的最小集。
第四章 务实的偏执
提示36 你无法写出完美的软件
页码 181
我们不断地与他人的代码交互,代码可能不符合我们的高标准,或需要处理可能有效也可能无效的输入。所以我们被教导要防御式编程——有任何疑问,都要去验证我们得到的一切信息;使用断言来检测错误数据,不信任任何可能来自潜在攻击者或有恶意者的数据;做一致性检查,对数据库的列设置约束——做完这些,我们通常就会自我感觉良好
契约式设计
页码 183
伯特兰·迈耶(《面向对象软件构造》[Mey97])在 Eiffel 语言中发明了契约式设计的概念。[1]这是一种简单但功能强大的技术,侧重于文档化(并约定)软件模块的权利和责任,以确保程序的正确性。什么是正确的程序?不多也不少,正好完成它主张要做的事情的程序。文档化及对主张进行检验是契约式设计(缩写为 DBC)的核心。 软件系统中的每一个函数和方法都力争有所作为。在开始做事之前,这个函数可能对世界的状态有一些期望;当结束时,或许它也能够对世界的状态做出一个陈述。迈耶将这些期望和主张描述如下。 例程和任何潜在调用者之间的契约因此可以被解读为
- 如果调用者满足了例程的所有前置条件,则例程应保证在完成时所有后置条件和不变式都为真。
提示37 通过契约进行设计
页码 190
谁的责任 检查前置条件是谁的责任?是调用者还是被调用的例程?当它被当作语言的一部分实现出来时,答案是两者都不是:前置条件是在幕后被测试的,测试发生在调用者调用例程之后,但在进入例程之前。因此,如果需要显式地检查参数,则必须由调用者执行,因为例程本身永远不会看到违反其前置条件的参数。(对于没有内建支持的语言,需要在被调用的例程中加上一个用于检查这些断言的前置片段及/或后置片段。)
死掉的程序不会说谎
提示38 尽早崩溃
页码 197
Erlang 和 Elixir语言信奉这种哲学。乔·阿姆斯特朗,Erlang 的发明者,《Erlang 程序设计》[Arm07]的作者,有一句反复被引用的话:“防御式编程是在浪费时间,让它崩溃!”在这些环境中,程序被设计成允许出故障,但是故障会由监管程序掌控。监管程序负责运行代码,并知道在代码出故障时该做什么,这可能包括在代码出错后做清理工作、重新启动等。当监管程序本身出错时会发生什么?它自己还有一个监管程序来管理这些事件,从而形成一种由监管程序树构成的设计。该技术非常有效,有助于解释这些语言在高可用性、容错性系统中的用法。
25 断言式编程
页码 198
似乎每个程序员在职业生涯早期,都一定会记住一句魔咒。我们学着将这句咒语作为计算领域的基本原则,一种基础信念,运用到需求、设计、代码、注释及所做的任何事情中。它念起来像是这样的 这件事绝不会发生…… “这个应用程序绝不会在国外使用,所以为什么要做国际化?”“count绝不会为负数”“写日志绝不会失败。” 不要再这样自我欺骗下去了,特别是在编程的时候。
提示39 使用断言去预防不可能的事情
页码 199
不要使用断言来代替真正的错误处理。断言检查的是不可能发生的事情
页码 200
如果我们为检测错误而添加的代码最终“滋生”了新的错误,那就很尴尬。如果对条件做评估本身有副作用,就可能发生这样的事情
页码 201
这个问题属于一种海森堡 Bug[3]——调试本身改变了被调试的系统的行为
页码 202
在生产环境中使用断言能赚到大钱 Andy 有一个老邻居,开了一家小型创业公司做网络设备。他们有一个成功的秘诀,即决定在最终产品中保留断言。这些断言经过精心设计,可以报告导致失败的所有相关数据,并通过美观的 UI 呈现给最终用户。这种级别的反馈,来自实际情况下的真实用户,能使开发人员填补漏洞并修复这些模糊的、难以复制的 Bug,从而得到非常稳定的防弹级软件。 这家小而不知名的公司拥有如此坚固的产品,很快就以数亿美元的价格被收购了。 姑妄听之。
提示42 小步前进——由始至终
页码 215
提示42 小步前进——由始至终 总是采取经过深思熟虑的小步骤,同时检查反馈,并在推进前不断调整。把反馈的频率当作速度限制,永远不要进行“太大”的步骤或任务。 这里说的反馈到底是什么意思?是任何能独立证实或否定你行为的东西。例如: · 你对API和算法的理解,REPL[7]得到的结果能给出反馈 · 单元测试能给出最近变更的反馈 · 用户演示和对话能给出关于功能和可用性的反馈 怎样的任务才算太大?任何大到需要“占卜”的任务。就像汽车前灯的投射距离有限一样,我们只能看到未来的一两步,也许只有几个小时,最多两三天。大于这个范围后,就会很快超出理性猜测的范畴,进入疯狂的猜想。
第5章 宁弯不折
页码 219
在前面的可逆性中,我们谈到了不可逆转的决定的危险。在本章中,我们将告诉你如何做出可逆的决策,这样代码在面对不确定的世界时可以保持灵活性和适应性。
30 变换式编程
页码 247
所有程序其实都是对数据的一种变换——将输入转换成输出。然而,当我们在构思设计时,很少考虑创建变换过程。相反,我们操心的是类和模块、数据结构和算法、语言和框架
提示49 编程讲的是代码,而程序谈的是数据
页码 250
提示49 编程讲的是代码,而程序谈的是数据 寻找变换 有时候,找到变换的最简单方法是,从需求开始并确定它的输入和输出。现在你已经将整个程序表示为函数。然后就可以找到从输入到输出的步骤。这是一个自顶向下的方法
页码 253
你可能认为这只是一个语法糖。但管道运算符以一种非常真实的方式,给思考的转变带来了革命性的机遇。使用管道意味着,你会自动地从数据变换的角度去思考;每次你看到 |>时 ,实际上看到的是数据在一次变换和下一次变换之间流动的地方。 许多语言都有类似
提示50 不要囤积状态,传递下去
页码 258
到目前为止,我们的变换已经在一个不出错的世界中工作起来了。但是,又如何在现实世界中使用它们呢?如果我们只能建立线性链,那么怎样添加错误检查所需的所有条件逻辑? 有许多方法可以做到这一点,但是所有方法都依赖于一个基础约定:永远不在变换之间传递原始值。取而代之的是,将值封装在一个数据结构(或类型)中,该结构可以告知我们所包含的值是否有效。例如,在 Haskell中,这个包装器被称为 Maybe。在 F#和 Scala中是 Option。
31 继承税
页码 267
继承就是耦合。不仅子类耦合到父类,以及父类的父类等,而且使用子类的代码也耦合到所有祖先类
页码 269
可以预料到,API的变化将破坏 Vehicle类的客户。但顶层对此并不关心,它只关心是否在使用 Car。Car类在实现层面做的事情并不为顶层代码所关心,但是Car类的确被破坏了。 类似地,实例变量的名称纯粹是内部实现细节,但是当 Vehicle的名称改变时,这样的变更也会(无声地)破坏 Car。 太多耦合了。
页码 269
有些人认为继承是定义新类型的一种方式。他们最喜欢的设计图表,会展示出类的层次结构。他们看待问题的方式,与维多利亚时代的绅士科学家们看待自然的方式是一样的,即将自然视为须分解到不同类别的综合体。
页码 269
不幸的是,这些图表很快就会为了表示类之间的细微差别而逐层添加,最终可怕地爬满墙壁。由此增加的复杂性,可能使应用程序更加脆弱,因为变更可能在许多层之间上下波动。
提示51 不要付继承税
页码 270
更好的替代方案 让我们推荐三种技术,它们意味着你永远不需要再使用继承: · 接口与协议 · 委托 · mixin 与特征
提示52 尽量用接口来表达多态
页码 273
委托 继承鼓励开发人员创建这样的对象,其类拥有大量的方法。如果父类有 20 个方法,而子类只想使用其中的两个,那么其对象还是会将其他 18 个方法放在那里,并使其能被调用。类失去了对其接口的控制。这是一个常见的问题——许多持久化框架和 UI框架坚持让应用程序的组件继承一些框架所提供的基类:
提示53 用委托提供服务:“有一个”胜过“是一个”
页码 276
class Account
# nothing but account stuff
end
class AccountRecord
# wraps an account with the ability
# to be fetched and stored
end
作为一个例子,让我们来回头看一看AccountRecord
。在我们之前的讨论中,AccountRecord
既需要了解账户,又需要了解持久化框架。它还必须对所有持久化层需要暴露给外部世界的方法做委托。
mixin CommonFinders
def find(id){ ... }
def findAll(){ ... }
end
class AccountRecord extends BasicRecord with CommonFinders
class OrderRecord extends BasicRecord with CommonFinders
mixin给了我们一个替代方案。首先,我们可以写一个 mixin,让它实现(例如)三个标准查询方法中的两个。然后我们就可以以 mixin的形式把它们加到 AccountRecord
里。接下来,当我们写一个新的类来做一些持久化的事情时,就可以也将这个 mixin加上去。
配置
提示55 使用外部配置参数化应用程序
页码 280
虽然静态配置很常见,但目前我们倾向于另一种做法。我们仍然希望配置数据保持在应用程序外部,但不直接放在文件中,也不放在数据库里;而是储存在一个服务 API之后。
页码 281
配置应该是动态的,这在我们转向高可用性应用程序时至关重要。为了改变单个参数就必须停下来重启应用程序,这样的想法已完全脱离当前的现实。使用配置服务,应用程序的组件可以注册所使用参数的更新通知,服务可以在配置被更改时发送包含了新值的消息。 无论采用何种形式,配置数据都会驱动应用程序的运行时行为。当配置值更改时,不需要重新构建代码。
并发
33 打破时域耦合
页码 287
搜寻并发性
在许多项目中,我们需要对应用程序的工作流建模并加以分析,这是设计工作的一部分。我们想知道哪些事情可以同时进行,哪些必须按照严格的顺序发生。有一种方法可以做到,那就是使用类似活动图之类的符记来理清工作流。[2]
提示56 通过分析工作流来提高并发性
页码 287
例如,我们可能正在为一个自动化椰林飘香鸡尾酒制造机编写软件。被告知步骤如下: 1.打开搅拌机 2.打开椰林飘香预拌粉 3.将预拌粉放入搅拌机 4.量半杯白朗姆酒 5.倒入朗姆酒 6.加两倍冰 7.关闭搅拌机 8.液化一分钟 9.打开搅拌机 10.拿出玻璃杯 11.拿出粉红小伞 12.端出
然而,如果酒保真的以这样的步骤一个接一个地按序操作,肯定会丢掉工作。尽管操作是依次描述下来的,但其中许多步骤可以并行执行。可使用下面的活动图来挖掘和推理出其中的并发性。
页码 290
流水线中的许多处理工序,必须访问外部信息(读取文件、写入文件,以及通过管道调用外部程序)。所有速度相对较慢的工作,都为我们提供了利用并发性的机会:实际上,流水线中的每个步骤都是并发执行的——从上一个步骤读取的同时写入下一个步骤。
页码 291
并行的机会 记住两者的区别:并发性是一种软件机制,而并行性则和硬件相关。如果我们有多个处理器,无论是本地的还是远程的,只要能把工作分割开,并在处理器之间分配,就可以减少总的时间。 在这样分割时,最理想的是,将工作分成相对独立的一块一块——每一块工作都能独立进行,而不需要为其他部分做出任何等待。一种常见的模式是,将一大块工作分解成独立的小块,并行处理每一块,然后合并结果。
角色与进程
提示59 用角色实现并发性时不必共享状态
页码 310
在角色模型中,不需要为处理并发性编写任何特定代码,因为没有共享状态。对于业务从头到尾的逻辑,也没有必要以“做这个,做那个”的方式,将其显式地写在代码里,因为角色会基于收到的消息自己处理这些事情。 这里也没有提及底层架构该是怎样的。这样的一组组件,在单处理器、多处理器甚至网络上的多台机器上,都能很好地工作。
一个例子:
让我们把角色概念套进上面的餐厅案例中。在这种情况下,我们将有三个角色(顾客、服务员和装派的柜子)。
整个消息流如下所示:
- 我们(看作是某种外部的神一般的存在)让顾客感觉到饥饿
- 作为回应,他们会向服务员点派
- 服务员会向柜子要一些派给顾客
- 只要有一块派,就会送到顾客那里,并通知服务员加到账单中
- 如果派已经没有了,柜子会告知服务员,让服务员去给顾客道歉
// concurrency/actors/index.js
// 顾客
const customerActor = {
'hungry for pie': (msg, ctx, state) => {
return dispatch(state.waiter, { type: "order", customer: ctx.self, wants: 'pie' });
},
'put on table': (msg, ctx, state) => {
console.log(`${ctx.self.name} sees "${msg.food}" appear on the table`);
},
'no pie left': (msg, ctx, state) => {
console.log(`${ctx.self.name} sulks...`);
}
};
// 服务员
const waiterActor = {
"order": (msg, ctx, state) => {
if (msg.wants === "pie") {
dispatch(state.pieCase, { type: "get slice", customer: msg.customer, waiter: ctx.self });
} else {
console.dir(`Don't know how to order ${msg.wants}`);
}
},
"add to order": (msg, ctx) => {
console.log(`Waiter adds ${msg.food} to ${msg.customer.name}'s order`);
},
"error": (msg, ctx) => {
dispatch(msg.customer, { type: 'no pie left', msg: msg.msg });
console.log(`The waiter apologizes to ${msg.customer.name}: ${msg.msg}`);
}
};
// 装派的柜子
const pieCaseActor = {
'get slice': (msg, context, state) => {
if (state.slices.length == 0) {
dispatch(msg.waiter, { type: 'error', msg: "no pie left", customer: msg.customer });
return state;
} else {
var slice = state.slices.shift() + " pie slice";
dispatch(msg.customer, { type: 'put on table', food: slice });
dispatch(msg.waiter, { type: 'add to order', food: slice, customer: msg.customer });
return state;
}
}
};
// 启动系统
const actorSystem = start();
let pieCase = start_actor(
actorSystem,
'pie-case',
pieCaseActor,
{ slices: ["apple", "peach", "cherry"] }
);
let waiter = start_actor(
actorSystem,
'waiter',
waiterActor,
{ pieCase: pieCase }
);
let cl = start_actor(
actorSystem,
'customer1',
customerActor,
{ waiter: waiter }
);
let c2 = start_actor(
actorSystem,
'customer2',
customerActor,
{ waiter: waiter }
);
dispatch(c1, { type: 'hungry for pie', waiter: waiter });
dispatch(c2, { type: 'hungry for pie', waiter: waiter });
dispatch(c1, { type: 'hungry for pie', waiter: waiter });
dispatch(c2, { type: 'hungry for pie', waiter: waiter });
dispatch(c1, { type: 'hungry for pie', waiter: waiter });
sleep(500)
.then(() => {
stop(actorSystem);
});
输出:
customer1 sees "apple pie slice" appear on the table
Waiter adds apple pie slice to customer1's order
customer2 sees "peach pie slice" appear on the table
Waiter adds peach pie slice to customer2's order
customer1 sees "cherry pie slice" appear on the table
Waiter adds cherry pie slice to customer1's order
The waiter apologizes to customer1: no pie left
customer1 sulks...
The waiter apologizes to customer2: no pie left
customer2 sulks...
页码 310
Erlang 语言及其运行时是一个角色模型实现的绝佳例子(虽然 Erlang的发明者并没有阅读关于角色的原始论文)。Erlang 把角色称为进程,但其不是常规的操作系统进程。相反,就和我们之前讨论过的角色一样,Erlang的进程是轻量级的(你可以在一台机器上运行数百万个进程),其通过发送消息进行通信。每一个进程都与其他进程相互隔离,进程之间没有状态共享。
36 黑板
页码 314
假设我们正在编写一个程序来接受和处理抵押或贷款的申请。管理这一领域的法律极其复杂,联邦、州和地方政府都各有说法。贷款人必须证明自己已经披露了某些信息,还必须询问某些特定信息——但另一些特定问题又是一定不能问的,大致如此。 除去相关法律的泥沼,我们还面临以下问题:
- 回应可能以任何次序到达。例如,对信用核查或产权调查的查询可能会花费大量时间,而名字和地址等项可能立即可以获知。
- 数据收集可能由不同的人完成,分布在不同的办公室和不同的时区。
页码 315
采用黑板,再结合一个封装有法律需求的规则引擎,是解决这些困难的一种优雅的方案。数据到达的顺序无关紧要:当发布一个事实时,可以触发适当的规则。反馈也很容易处理:任何一组规则的输出都可以发布到黑板上,从而触发更多适用的规则
第7章 当你编码时
页码 320
测试不是关于找Bug的工作,而是一个从代码中获取反馈的过程,涉及设计的方方面面,以及API、耦合度等。这意味着,测试的主要收益来自于你思考和编写测试期间,而不是运行测试那一刻。我们将在为编码测试中探索这个思想。
页码 320
命名是软件开发中最困难的事情之一。我们不得不给很多东西起名字,而我们选择的名字在很多方面决定了所创造的最终是什么。在编写代码时,需要注意任何潜在的语义偏移。
37 听从蜥蜴脑
页码 323
有时,代码只是从你的大脑飞进编辑器:想法变成比特似乎毫不费力。 其他时候,编程感觉就像在泥泞中爬山。每走一步都需要付出巨大的努力,走上三步就会后退两步。 但是,作为一名专业人士,你会坚持下去,一步一个脚印:这是你的工作。不幸的是,你真正应该做的事情可能恰恰相反。 你的代码试图告诉你一些事情。它说这超过了本应具有的难度——也许结构或设计是错误的,也许你解决了错误的问题,也许你只是创造了一个只会招来Bug的蚂蚁农场。不管是什么原因,你的蜥蜴脑正在感知来自代码的反馈,它拼命地尝试让你倾听。
提示61 倾听你内心的蜥蜴
页码 326
根据我们的经验,在第一个原型的某个时间点上,你会惊讶地发现自己正在随着音乐哼唱,享受着编写代码的愉快体验。紧张感会消失,取而代之的是一种紧迫感:让我们把这件事做好
页码 326
学会在编码时听从直觉是一项需要培养的重要技能。但它同样适用于更宽泛的领域。有时候,如果一个设计让你感觉不妥,或是一些需求让你觉得不爽,就停下来去分析这些感觉。如果你身处一个支持性环境,那就大声说出这些感觉。探索下去,很可能在某个黑暗的门口潜伏着什么东西。听从你的直觉,在问题跳出来之前加以避免。
提示63 评估算法的级别
页码 342
还要考虑一下你写的代码本身。当 n较小时,一个简单的 O(n²)循环比一个复杂的O(n lgn)算法表现得要好得多,在 O(n lgn)的内层循环非常昂贵时尤为如此。
40 重构
页码 345
使用构造一词作为指导性隐喻意味着以下步骤:
1.建筑师绘制蓝图。
2.承包商挖地基,修建上层建筑,铺设电线和管道,并进行最后的装修。
3.租客们搬进来,从此过上了幸福的生活,遇到任何问题只需叫建筑维护人员来解决。
可是软件不是这样工作的。软件更像是园艺而非建筑——它更像一个有机体而非砖石堆砌。你根据最初的计划和条件在花园里种植很多花木。有些茁壮成长,另一些注定要成为堆肥。你会改变植物相对的位置,利用光和影、风和雨的相互作用。过度生长的植物会被分栽或修剪,那些不协调的颜色可能会转移到更美观的地方。你拔除杂草,给需要额外帮助的植物施肥。你不断地监测花园的健康状况,并根据需要(对土壤、植物、布局)做出调整。
页码 346
重构并不是一种特殊的、隆重的、偶尔进行的活动。为了重新种植而在整个花园中翻耕,重构不是这样的活动。重构是一项日复一日的工作,需要采取低风险的小步骤进行,它更像是耙松和除草这类活动。这是一种有针对性的、精确的方法,有助于保持代码易于更改,而不是对代码库进行自由的、大规模的重写。 为了保证外部行为没有改变,你需要良好的自动化单元测试来验证代码的行为。
提示65 尽早重构,经常重构
页码 349
重构是一项需要慢慢地、有意地、仔细地进行的活动。马丁·福勒提供了一些简单技巧,可以用来确保进行重构不至于弊大于利: 1.不要试图让重构和添加功能同时进行。
2.在开始重构之前,确保有良好的测试。尽可能多地运行测试。这样,如果变更破坏了任何东西,都将很快得知。
3.采取简短而慎重的步骤:将字段从一个类移动到另一个类,拆分方法,重命名变量。重构通常涉及对许多局部进行的修改,这些局部修改最终会导致更大范围的修改。如果保持小步骤,并在每个步骤之后进行测试,就能避免冗长的调试。
41 为编码测试
提示66 测试与找 Bug 无关
页码 354
为方法写一个测试的考虑过程,使我们得以从外部看待这个方法,这让我们看起来是代码的客户,而不是代码的作者。
提示67 测试是代码的第一个用户
页码 335
有一个发生于 2006 年的有趣案例,当时敏捷运动的领军人物罗恩·杰弗里斯开始写一个博客的系列文章。这个系列,记录了他尝试用测试驱动的方法,来开发解数独程序的过程。[11]在写了五篇文章之后,他改进了底层棋盘的呈现方式,进行了多次重构,直到对对象模型满意为止。但之后他放弃了这个项目。按顺序阅读这些博客文章是很有趣的,可以看到一个聪明的人是如何被通过测试的喜悦套牢,开始被琐事分心。
提示68 既非自上而下,也不自下而上,基于端对端构建
页码 357
我们坚信,构建软件的唯一方法是增量式的。构建端到端功能的小块,一边工作一边了解问题。应用学到的知识持续充实代码,让客户参与每一个步骤并让他们指导这个过程。
页码 340
如何测试这些模块的组合呢?
假设我们有一个模块A,使用了DataFeed
和LinearRegression
。那么就可以依次测试:
DataFeed
的完整契约LinearRegression
的完整契约- A 的契约。这份契约依赖其他契约,但并没有暴露所依赖的契约
这种风格的测试,要求首先测试模块的子组件。一旦验证了子组件,就可以继续测试模块本身。
如果 DataFeed
和 LinearRegression
通过了测试,但 A 没有通过测试,我们就可以确信问题出在 A 上面,或是出在 A 对那些子组件的用法上面。这种技巧是减少调试工作的好方法:我们可以快速地集中于模块 A 中问题的可能来源,而不会浪费时间反复检查它的子组件。
提示69 为测试做设计
页码 341
这意味着经常需要对软件做测试——一旦软件被部署下去,真实世界的数据就会流经它的血管。与电路板或芯片不同,软件中没有测试引脚,但我们可以提供模块内部状态的各种视图,而不必用调试器去查看(在生产环境中可能不方便或不可能使用调试器)。 包含跟踪消息的日志文件就是这样一种机制。日志消息应该采用规范一致的格式;你可能希望自动解析日志以推断程序的处理时间或逻辑路径
页码 344
应该编写测试吗?要。但等你写了 30 年后,不妨从容地做些试验,看看它究竟给你带来什么好处。
43 出门在外注意安全
页码 356
在编写代码时,你可能反复经历着“可以跑了!”和“为什么不工作?”的循环,间或抱怨一句“不可能啊……”在爬坡的过程中经历了几次起伏之后,很容易对自己说:“唷,都跑起来了!”并宣告代码已经完成。当然,还没有完成。你已经完成了 90 %,现在只需考虑剩余路程,但行百里者半九十,这相当于又是一个90%。
接下来要做的是分析代码中那些可能出错的路径,并将其添加到测试套件中。你要考虑传入错误的参数、泄漏的资源或资源不存在等此类事情。
页码 358
输入数据是一种攻击载体 永远不要信任来自外部实体的数据,在将其传递到数据库、呈现视图或其他处理过程之前,一定要对其进行消毒。
44 事物命名
页码 366
我们认为,事物应该根据它们在代码中扮演的角色来命名。这意味着,无论何时,只要你有所创造,就需要停下来思考“我这一创造的动机是什么?”
这是一个强有力的问题,因为它把你从立即解决问题的心态中带出来,让你看到更大的图景。当我们考虑一个变量或函数的作用时,所考虑的是它的特别之处,它能做什么,它与什么相互作用。在很多时候,对于正要去做的事情,一旦我们怎么都想不出一个适合它的名字,往往就会幡然醒悟,意识到这件事情其实毫无意义。
第八章 项目启动之前
需求之坑
提示75 无人确切知道自己想要什么
页码 376
需求神话
在软件开发的早期,计算机的价值(按每小时的平摊成本计算)要高于与计算机打交道的人的价值。为了省钱,我们第一次就试着把事情做对。这个过程有一部分是在试图明确我们要让机器做什么。我们将从获得需求规范开始,将其转化为设计文档,然后变成流程图和伪代码,最后写成代码。还没完,在代码输入电脑之前,我们还需要花时间在办公桌前检查一遍。
这样做会花很多钱。这样的成本意味着,人们只有在知道自己真正想要什么的时候才会尝试自动化。
提示76 程序员帮助人们理解他们想要什么
页码 378
新手开发人员经常犯的错误是,把这种对需求的声明照单全收,然后实现对应方案。 根据我们的经验,最初对需求的声明,往往并非绝对化的要求。客户可能没有意识到这一点,但一定希望你能一起去探索。
页码 380
需求是一个过程 在前面的例子中,开发者获取需求并将结果反馈给客户。这开启了探索之旅。在探索过程中,随着客户尝试不同的解决方案,你可能会得到更多的反馈。这是所有需求采集过程的现实情况:
提示77 需求是从反馈循环中学到的
页码 381
有一个简单的方法可以让你深入客户的头脑,但这个方法并不常用:成为客户。你正在为帮助台编写一个系统吗?不妨花几天时间和有经验的客服一起接电话。你正在让手动库存控制系统自动化吗?试着去仓库工作一周。
提示79 策略即元数据
页码 384
我们相信,最好的需求文档,或许也是唯一的需求文档,就是可以工作的代码。
页码 386
需求不是架构;需求无关设计,也非用户界面;需求就是需要的东西
46 处理无法解决的难题
页码 396
偶尔也会出现一个真的非常困难的问题,你会发现自己顿时被卷入项目之中:一些工程问题你把握不了,或者某些代码比你想象的更难写,也许看起来根本做不到。但这些真的像看起来那么难吗?
解决方法藏在别处。解谜的奥妙在于确定真正的(而不是想象的)约束条件,在这个约束条件下找到解开的方法。有些约束条件是绝对的,有些其实是一些先入为主的观念。应该尊重那些绝对的约束条件,无论这些约束条件看起来多么令人反感或愚蠢。 但另一方面,正如亚历山大证实的,一些明显的约束条件并非真的约束条件——许多软件问题都很狡猾。
47 携手共建
页码 403
结对编程是极限编程(XP)的实践之一,它已经在 XP 之外流行开来。在结对编程中,一个开发人员操作键盘,另一个不操作。他们可以一起解决问题,还可以根据需要切换打字任务。
页码 403
结对编程有很多好处。不同的人有不同的背景和经验,有不同的解决问题的技巧和方法,对任何特定的问题有不同的关注点。充当打字员的开发者必须专注于语法和编码风格的底层细节,而另一个人可以自由地在更高层次的范畴考虑问题。虽然这听起来像是一个小小的区别,但请记住,我们人类的大脑带宽有限。天马行空地输入编译器勉强能接受的深奥单词和符号,就已占用了我们相当大的处理能力。在执行任务的过程中,有另一个开发人员的完整大脑可用,将带来更多的脑力供我们支配。
页码 404
事实上,在我们的第一个“不可能完成的”项目中,由一个人打字,而另一个人与业务专家讨论这个问题,是一个常见的场景——最后这样的组合成为一个三人的小团体。
敏捷的本质
提示83 敏捷不是一个名词;敏捷有关你如何做事
页码 407
我们觉得很多人已经忽视了敏捷的真正含义,因而希望看到人们回归到最基本的东西。 记住宣言中的价值观: 我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。由此我们建立了如下价值观:
- 个体和互动高于流程和工具
- 工作的软件高于详尽的文档
- 客户合作高于合同谈判
- 响应变化高于遵循计划
页码 409
所以下面是我们以敏捷方式工作的秘诀:
1.弄清楚你在哪里。
2.朝想去的方向迈出有意义的最小一步。
3.评估在哪里终结,把弄坏的东西修好。
页码 410
优秀设计的精髓中,我们曾断言,设计的衡量标准是对设计出的结果进行变更的容易程度:优秀的设计比糟糕的设计更容易变更。
第九章 务实的项目
务实的团队
提示84 维持小而稳定的团队
页码 416
在一些团队方法中,团队会设一个“质量官”——由这个人对交付产品的质量负责。这显然是荒谬的:质量只能来自团队每个成员的独立贡献。质量是内在的,无法额外保证。
提示85 排上日程以待其成
页码 420
如果要等一个星期才能在团队会议上提出问题或分享状态,那就会存在很多摩擦力。无摩擦力意味着,提出问题,分享进展、问题、见解和学到的东西,以及时刻关注队友正在做什么——都很容易,不需要什么仪式。
务实的入门套件
提示90 尽早测试,经常测试,自动测试
页码 432
事实上,好项目的测试代码可能会比产品代码更多。生成这些测试代码所花费的时间是值得的。从长远来看,最终的成本会低得多,而且你实际上有机会生产出几乎没有缺陷的产品。 另外,知道通过了测试,可以让你对代码已经“完成”产生高度信心。
提示91 直到所有的测试都已运行,编码才算完成
页码 433
集成测试可以表明,组成项目的主要子系统能够很好地相互协作。有了良好的契约并进行过完善的测试,任何集成问题都可以被很容易地检测到。否则,集成将成为滋生Bug 的肥沃土壤。事实上,这常常是系统中最大的 Bug 来源
提示94 每个 Bug 只找一次
页码 437
一个 Bug一旦被人类测试员发现,这就应该是它被该人类测试员发现的最后一次。要立即修改自动化测试,以便这个特定的 Bug,从此往后每次都被检查到——不能有任何例外,无论它多么琐碎,也无论开发者有多少抱怨,或是不停唠叨“哦,永远不会再发生了
52 取悦用户
页码 440
用户真正要的不是代码,他们只是遇到某个业务问题,需要在目标和预算范围内解决。他们的信念是,通过与你的团队合作,能够做到这一点。 他们的期望与软件无关,甚至并不隐含在提供给你的任何规范说明中(因为在你的团队对其进行多次迭代之前,该规范说明都是不完整的)。 那么,如何发掘他们的期望呢?问一个简单的问题: 这个项目在完成一个月(或是一年,不管多久)之后,你根据什么来判断自己已经取得成功? 你很可能会对最终答案感到惊讶。一个对产品推荐做改进的项目,实际上可能是根据客户留存率来判断的;合并两个数据库的项目,可能是根据数据质量来判断的,也可能是根据节省的成本来判断的。但是真正有意义的是这些对业务价值的期望,而不仅仅是软件项目本身。软件只是达到这些目的的一种手段
跋
提示98 先勿伤害
页码 498
有一个与恕道相关的拷问:我自己愿意成为这个软件的用户吗?我希望分享自己的详细信息吗?我希望自己的行踪被交给零售店吗?我愿意乘坐这辆自动驾驶汽车吗?做这件事我能心安吗?