程序员修炼之道

本篇为云风翻译的《程序员修炼之道——通往务实的最高境界》第二版的读书笔记

利用晚上和周末的时间,用了大约一个月,断断续续地读完了,收获颇丰。期待第二次翻开这本书时,那个已经变化了的世界和我,有不一样的感悟。下面是完整的笔记:

第1章,务实的哲学

人生是你的

  1. 别等待技能过时,寻求提升和突破、不断尝试有趣的东西、扩充边界

  2. 无论是职业、岗位、工作和团队,都有权选择

主动改变

我的源码被猫吃了

  1. 赢得团队信任,小心维护,别破坏它

  2. 承担责任,提供选择,别找借口

责任、坦诚

软件的熵

破窗效应:一扇破损的窗户,只要一段时间不修理,建筑中的居民就会潜移默化地产生一种被遗弃的感觉——当权者不关心这幢建筑的感觉。然后其他的窗户也开始损坏,居民开始乱丢废物,墙上开始出现涂鸦,建筑开始出现严重的结构性损坏。心理学家的研究表明,绝望是会传染的,就像狭窄空间中的流感病毒。无视一个明显损坏的东西,会强化这样一种观念:看来没有什么是能修好的,也没人在乎,一切都命中决定了。所有的负面情绪会在团队成员间蔓延,变成恶性循环。

  1. 不要放任破窗,遇到问题(糟糕的设计、错误的决定、低劣的代码)不要回避,及早面对。不要让熵赢得胜利。

  2. 不要只是因为一些东西非常危机,就去造成附带损害。破窗一扇都嫌太多。

  3. “不要打破窗户”

软件规模带来的后增的重量

石头做的汤和煮熟的青蛙

  1. 找出合理的请求,不断完善。一旦有成果产出,展示给人们看。引导推进,让大家能窥视未来。

  2. 牢记全景,审视大局,而不要只专注个人在做的事情。

展示前景,做推动变革的催化剂

够好即可的软件

  1. 完成用户需求,达到基本的性能、隐私和安全标准。

  2. 与构想中的明天那个完美的软件相比,今天就还不错的软件通常更讨人喜欢。(人们不愿意等待、用户需求也有时效问题)

  3. 不要让过度的修饰和精炼侵蚀掉一个完好的程序。

知道何时止步

知识组合

  1. 知识和经验是最重要的专业资产,但是也是一种时效资产。

  2. 构建知识组合

    1. 定期投资

      1. 每年学习一门新语言

      2. 每月读一本技术书

      3. 读非技术书

      4. 上课

      5. 加入社群

      6. 尝试不同的环境(开发环境)

      7. 与时俱进

    2. 多样化:知道的越多,价值越大。熟悉的技能越多,越能适应变化。

    3. 风险管理:不要把所有的技术鸡蛋都放在一个篮子里。

    4. 低买高卖:在一项新兴技术变得流行之前就开始学习。

    5. 重新评估调整:不断尝试,温故而知新

  3. 学习的机会(与人交流、碎片化阅读)

  4. 批评性思维:知识的精确性容易受到商业主义的影响

    1. 多问“为什么“

    2. (世俗的怀疑)谁从中受益

    3. 有什么背景:每件事都发生在它自己的背景下

    4. 什么时候在哪里可以工作起来:当它结束后还会发生什么(二阶思考)

    5. 为什么这是个问题

知识与想法的交叉传授

交流!

  1. 多层次交流

    1. 开会

    2. 与终端用户合作

    3. 编写代码

    4. 编写文档

    5. 写建议和备忘录

  2. 英语:当成一门编程语言来使用(DRY、ETC、自动化)

  3. 了解听众:收集反馈,不要只是等待问题的出现,把问题问出来。

  4. 明白自己想说什么

  5. 想法很重要,但还有一个好看的包装(布局、样式、一致性)

  6. 让听众参与

  7. 做倾听者

  8. 回应别人

  9. 说什么和怎么说同样重要

  10. 把代码和文档绑在一起

第2章,务实的方法

优秀设计的精髓

优秀的设计比糟糕的设计更容易变更(Easy to Change)

DRY——邪恶的重复

作为程序员,我们做的就是收集、组织、维护以及治理知识。我们把知识文档化,写进规范;通过运行代码赋予知识以活力;在测试过程中,运用知识去知道应提供哪些检查。不幸的是,知识并不稳定。知识会改变——通常频率还很高。

在一个系统中,每一处知识都必须单一、明确、权威地表达

  1. DRY原则——不要重复自己

    1. 不限于编码

    2. 针对知识和意图的复制(并不是所有的代码重复都是知识的重复)

    3. 文档中的重复(代码注释:代码的意图被描述两次)

    4. 统一访问:一个模块提供的所有服务都应该通过统一的约定来提供,该约定不应表露出其内部实现是基于储存还是基于运算的。(来自《面向对象软件构造》)

    5. 表征重复:指两个事物(代码和外部实体)必须拥有接口的表征知识。一端发生改变,另一端就会坏掉。下面是一些减缓的策略

      1. 内部API间的重复:通过一个工具用来将API描述成一种中立的格式。保存起来,共享给不同的团队(如前后端)

      2. 外部API间的重复:制定正式的规范,可以方便你将API规范导入到本地API工具

      3. 数据源引起的重复:

        1. 数据源支持schema内省:不必手工编写代码来将要储存的数据包含其中,可以直接从schema生成容器代码

        2. 使用键值对数据结构,同时增加一层表驱动的校验组件。

    6. 开发人员间的重复

      1. 从高的层次看:建立一个强大的、紧密联系的、沟通良好的团队

      2. 模块间的重复:鼓励开发人员积极频繁的交流

        1. 开一个论坛(Slack频道),为每件说的事做永久记录

      3. 指派一名知识管理员,工作为促进知识的传播

  1. 含义:在计算机科学中,象征着独立性和解耦性

  2. 作用:消除不相关事物之间的影响(设计的组件自成一体:内聚)

  3. 好处

    1. 提高生产力

      1. 减少开发和测试时间

      2. 系统变得松散耦合

      3. 正交组件可组合使用

    2. 减少风险

      1. 代码中病变的部分被隔离开

      2. 变更与修复产生的任何问题都仅局限在特定区域

      3. 利于测试

      4. 与外部组件结合,不会被束缚的太紧密

  4. 设计

    1. 当一个功能需求被变更后,受到影响的模块应该只有一个

    2. 不要依赖那些你无法控制的东西

  5. 工具包和程序库:引入第三方工具包和程序库时,注意保持系统的正交性。

  6. 编码

    1. 保持代码解耦

    2. 避免全局数据(创建一个包含上下文的数据结构,并将结构传递出去)

    3. 避免相似的函数(策略模式)

  7. 测试:模块(单元)测试

  8. 文档:如markdown,关注内容,把样式呈现留给其他工具去处理

可逆性

  1. 不设最终决定

  2. 灵活的架构(除了代码,还要考虑体系结构、部署和供应商集成方面)

  3. 放弃追逐时尚

曳光弹

  1. 使用曳光弹找到目标

    1. 用户能够更早的获得能工作的东西

    2. 开发者构造了一个可以在其中工作的框架

    3. 你有了一个集成平台

    4. 你有演示的东西

    5. 你对进度有更好的感觉

  2. 曳光弹并不总能击中目标

原型与标签

  1. 需要做原型的东西

    1. 架构

    2. 已存在的系统中的新功能

    3. 数据结构或外部数据的内容

    4. 第三方工具或组件

    5. 性能问题

    6. 用户界面设计

  2. 怎样使用原型

    1. 可忽略的细节

      1. 正确性

      2. 完整性

      3. 健壮性

      4. 格式

    2. 使用高阶语言开山辟路(粘合剂、未来会丢弃原型)

  3. 制作架构原型

    1. 不必编写代码

    2. 使用白板贴标签和索引卡

    3. 尽量推迟思考细节

  4. 不要把原型用于产品

领域语言

  1. 靠近问题域编程

  2. 权衡内部语言和外部语言

估算

  1. 通过估算来避免意外

  2. 多精确才够:挑选答案的单位来反映想要传达的精确性

  3. 估算从何而来

    1. 理解在问什么:掌握问题域的范围

    2. 对系统建模:对于一个项目,可能是开发组织在开发期间需要的每个步骤,以及关于系统可能如何实现的粗略图景

    3. 把模型分解成组件

    4. 确定每个参数的值

    5. 计算答案

    6. 记录你的估算能力

  4. 估算项目进度

    1. PERT(计划评审技术):每个任务都有一个乐观的、一个最有可能的和一个悲观的估算

    2. 项目时间表(怎样吃掉大象?一次咬一口)

      1. 检查需求

      2. 分析风险

      3. 设计、实现、集成

      4. 和用户一起验证

      5. 记录迭代的结束点,回到最初(检查需求),不断迭代进度表

    3. 放慢节奏:被要求做一个估算时,应该说“我等一下答复你”

第3章,基础工具

  1. 工具会放大你的才能

  2. 定期给自己的工具箱添加工具

  3. 让业务需求来驱动自己选择不同(新的)工具

纯文本的威力

  1. 将知识用纯文本保存

    1. 类型: HTML/JSON/YAML/HTTP..

    2. 优势

      1. 为防备老化而加保险

      2. 利用杠杆效应让已有工具发挥最大优势:版本控制、编辑器、命令行工具都会用到

      3. 易于测试

      4. 最小公分母:纯文本可作为各方相互沟通的公共标准

Shell游戏

  1. GUI

    1. 优势:操作简单、所见即所得

    2. 缺点:无法使用工具的全部能力、自动化能力弱、所见即全部

  2. 配置自己的专属Shell

    1. 主题

    2. 提示信息

    3. 别名和Shell函数

    4. 命令补全

加强编辑能力

  1. 游刃有余地使用编辑器:不用鼠标、触摸板完成日常编辑工作

  2. 逐步游刃有余

    1. 自省:发现自己又在重复做某件事情,要习惯性地想到,还有更好的办法,然后找到这个办法

    2. 内化:重复使用好的方法,将其内化为肌肉记忆

版本控制

  1. 共享目录绝非版本控制

  2. 从源码开始,逐步将所有的内容(文档、电话号码列表、shell脚本..)都提交到版本控制系统

  3. 使用分支

    1. 隔离特性

    2. 团队工作流

  4. 将版本控制视为项目中枢,需要支持以下功能

    1. 有良好的安全和访问权限控制

    2. 有符合直觉的界面

    3. 支持命令行来实现操作(自动化)

    4. 自动化构建和测试

    5. Pull Request

    6. 问题管理

    7. 漂亮的报告

    8. 团队交流(发邮件等通知方式)

调试

  1. 调试心理学

    1. 去解决问题,而不是责备

    2. 不要恐慌bug,永远去发掘问题的根本原因,而不仅仅停留在问题的表面现象

  2. 调试策略

    1. 拜访报告bug的用户

    2. 测试所有边界,复原实际的最终用户使用模式(重现bug)

    3. 输入值的敏感度(特定数据集)

    4. 二分法(分割数据集、分割调用栈、分割版本)

    5. 输出日志及跟踪信息

    6. 像他人解释问题

    7. 排除法

文本处理

学习一门文本处理语言

  1. shell

  2. python/ruby类的语言

工程日记

试着拥有一本工程日记

  1. 比记忆更可靠

  2. 提供了一个地方,保存与当前任务无关的想法

  3. 当你停下来,把东西写上去的时候,大脑可能会换挡,几乎就像在和某人说话一样——这是一个反思的好机会。

  4. 就是日记,以便回想起曾经的你在做些什么

第4章,务实的偏执

契约式设计(DBC)

  1. 文档化及对主张进行检验是契约式设计的核心

    1. 前置条件:传递良好的数据

    2. 后置条件:不允许无限循环

    3. 类的不变式

    4. 契约:如果调用者满足了例程的所有前置条件,则例程应保证在完成时所有后置条件和不变式都为真。

    5. 与正交性相比,契约强调编写“懒惰”的代码

  2. 断言是一种对逻辑条件的运行时检查,但不能完全实现DBC

  3. 尽早崩溃:在问题发生的地方尽早崩溃,能让找到问题和诊断问题更加容易

  4. 语义不变式:表达不可违背的需求

死掉的程序不会说谎

  1. 新的异常会自动传播

  2. 崩溃,不要制造垃圾

  3. “防御性编程是在浪费时间,让它崩溃!”——《Erlang程序设计》

  4. 一个死掉的程序,通常比一个瘫痪的程序,造成的损害要小得多

断言式编程

  1. 不要使用断言来代替真正的错误处理,断言检查的是不可能发生的事情

  2. 断言的误解

    1. 断言是一种调试措施,只需要在测试环境中保留

    2. 测试能完全覆盖

  3. 在生产环境中,如果确实存在性能问题,只需要关闭那些真正有影响的断言,其余的要保留

如何保持资源的平衡

  1. 资源使用模式:分配、使用、释放

  2. 有始有终原则:分配资源的例程也应该负责释放它

  3. 将资源的生命周期限定在某个封闭的代码块内(Ruby的文件open的变种方法,Python的with open语句)

  4. 对于一次需要不止一个资源的例程

    1. 释放资源的顺序与分配资源的顺序相反

    2. 在代码的不同位置,如果都会分配同一组资源,就始终以相同的顺序分配它们

  5. 支持异常的语言如何释放资源

    1. 利用变量的作用域(C++/Rust中的栈变量)

    2. 使用try..catch..finally

      1. 反模式:申请资源的代码放在try语句块中。如果申请资源失败也会触发异常,此时finally会释放一个从未申请的资源。因此,申请资源的代码应该放在try语句块之前

不要冲出前灯范围

  1. 小步前进 —— 由始至终:总是采取经过深思熟虑的小步骤,同时检查反馈,并在推进前不断调整。

  2. 别超过你能看见的范围,越是必须预测未来会怎样,就越有可能犯错。

第5章,宁弯不折

解耦

  1. 耦合的“症状”

    1. 不相关的模块和库之间古怪的依赖关系

    2. 对一个模块进行的“简单”修改,会传播到系统中不相关的模块里,或是破坏了系统中的其他部分

    3. 开发人员害怕修改代码,因为他们不确定会造成什么影响

    4. 会议要求每个人都必须参加,因为没有人能确定谁会受到变化的影响

  2. TDA(只管命令不要询问)原则:不应该根据对象的内部状态做出决策,然后更新该对象

  3. LoD(得墨忒耳法则):定义在C类中的函数只应该调用

    1. C类其他实例的方法

    2. 它的参数

    3. 它所创建出来的对象的方法,包括在栈上或堆上的对象

    4. 全局变量

  4. 链式调用和管道

    1. 调用的东西容易改变时,应该避免使用链式调用

    2. 把函数组合成管道,可以不依赖隐藏的实现细节,但是也引入了耦合 —— 管道中的函数所返回的数据格式必须与下一个函数所接受的格式兼容

  5. 避免全局数据

    1. 全局数据

      1. 全局变量

      2. 全局单件/模块

      3. 外部资源(数据库、数据存储、文件系统和服务API)

    2. 解决方案:确保始终将这些资源包装在你所控制的代码之后(包装到API中)

  6. 继承增加了耦合:子类化误用(继承税)

先现实世界中抛球杂耍

  1. 事件:表达出信息的可用性

    1. 编写响应事件的应用程序策略

      1. 有限状态机(FSM):一个纯粹的FSM就是一个事件解析器。它唯一的输出是最终的状态,可以通过在某些转换上添加触发动作来增强它

      2. 观察者模式:有一个事件源,被称为被观察对象,而客户列表,也即观察者,会对其中的事件感兴趣

        1. 引入耦合:每个观察者都必须与对象注册在一起

        2. 性能瓶颈:回调是由观察对象以同步的方式内联处理的

      3. 发布/订阅

        1. 解耦:发布者和订阅者通过(共享接口)信道连接,信道可以在单独的代码块中实现:库、进程或分布式基础设施

        2. 异步事件处理:允许应用程序运行时添加和替换代码,而无须更改现有代码。

        3. 缺点:

          1. 无法在查看发布者的同时立即看到有哪些订阅者涉及特定的消息

          2. 本质上只是一个消息传递系统

      4. 响应式编程与流

        1. 流让我们把事件当作数据集合来对待

        2. 流可以是异步的,这意味着你的代码有机会按事件到来的方式去回应事件

        3. 不再需要将事件视为必须管理的东西。事件流将同步和异步处理统一到一个通用的、方便的API之后

变换式编程

所有程序其实都是对数据的一种变换——将输入转换成输出。然而,当我们在构思设计时,极少考虑创建变换过程。相反,我们操心的是类和模块、数据结构和算法、语言和框架。

  1. 寻找变换:将整个程序表示为函数,然后找到从输入到输出的步骤。这是一个自顶向下的方法

  2. 不要囤积状态,传递下去:把数据看作是一条河流,数据称为与功能对等的东西:代码->数据->代码->数据··

继承税

  1. 两种类型的继承

    1. Simula方法认为继承是一种将类型组合起来的方法(C++、Java等语言得到延续)

    2. Smalltalk学派认为,继承是一种动态的行为组织(Ruby、JavaScript等语言可以看到)

  2. 通过继承共享代码的问题

    1. 继承就是耦合:不仅子类耦合到父类,以及父类的父类等,而且使用子类的代码也耦合到所有祖先类

  3. 通过继承构建类型的问题

    1. 看待问题的方式为类别、层次划分,不断的增加层次,反而会增加复杂性,使程序更加脆弱

    2. 多重继承问题:无法为领域进行准确地建模

  4. 不要付继承税(更好的替代方案)

    1. 接口与协议:尽量用接口来表达多态

    2. 委托:通委托提供服务——“有一个”胜过“是一个”

    3. mixin与特征:将现有事物和新事物的功能合并在一起,利用mixin来共享功能

配置

  1. 使用外部配置参数化应用程序

    1. 静态配置

      1. 设计为全局

      2. 放在一个API中,解耦呈现细节

    2. 配置服务化

      1. 在身份认证和访问权限控制将多个应用程序的可见内容阻隔开的情况下,让多个应用程序可以共享配置信息

      2. 配置的变更可以在任何地方进行

      3. 配置数据可以通过专有UI维护

      4. 配置数据变得动态(程序运行时更改)

  2. 不要做的太过:配置太多也会造成维护灾难

  3. 不要因为懒,就放弃做决策,而把分歧做成配置

第6章,并发

  1. 概念

    1. 并发性:两个或更多哥代码段在执行过程中表现得像是在同时运行一样。通常基于纤程、线程、进程来实现

    2. 并行性:的确在同一时刻一起运行。通常是在同一CPU上的多个核心、同一机器上的多个CPU,或是连接在一起的多台计算机

打破时域耦合

  1. 时间

    1. 并发性(在同一时刻发生的多件事情)

    2. 次序(事情在时间轴上的相对位置)

  2. 通过分析工作流来提高并发性

    1. 借助UML活动图来寻找并发可能

  3. 并发的机会(找到那些耗时任务中,无须把时间花在代码上的活动)

    1. 查询数据库

    2. 访问外部服务

    3. 等待用户输入

    4. 。。。

  4. 并行的机会(如果有多个处理器,无论是本地还是远程的)

    1. 分割工作,让每一块子任务都能独立进行

    2. 合并结果

共享状态是不正确的状态

  1. 非原子更新

    1. 信号量和其他形式的互斥

      1. 缺陷:将保护资源访问权的责任委托给使用者

  2. 让资源具备事务性:将资源访问转移到一个中心位置

  3. 多个资源的事务:将多个资源组合为一个新的模块

  4. 非事务性更新:随机故障通常是并发问题

角色和进程

  1. 概念

    1. 角色:一个独立的虚拟处理单元,具有自己的本地(且私有的)状态,每个角色都有一个信箱。当消息出现在信箱中且角色处于空闲状态时,角色被激活并开始处理消息。处理完该条消息后,它将继续处理信箱中的其他信息,如果信箱是空的,则返回休眠状态

    2. 进程:通常代表一种更通用的虚拟处理机,它一般由操作系统实现,可以让并发处理更容易。进程也能(根据约定)被约束为以角色的形式运转,我们在这里说的就是这类进程

  2. 角色只会是并发的

    1. 没有一件事是可控的。接下来会发生什么,不会事先计划,信息从原始数据到最终输出的传输过程,也无法统筹安排

    2. 系统中唯一的状态,保存在消息和每个角色的本地状态中。消息除了接收方可以读取,没有其他途径检查,本地状态在角色之外无法被访问

    3. 所有的消息都是单向的 —— 没有回应这个概念。如果希望角色能返回一个回应消息,则需要在发送给角色的消息中包含自己的信箱地址,然后角色(最终)会将回应作为另一条消息发送到该信箱

    4. 角色会将每一条消息处理完,一次只会处理一条

第7章,当你编码时

听从蜥蜴脑

本能就是我们的无意识大脑对模式的一种直接反应,有些是天生的,有些是通过不断重复学习到的

  1. 空白页

    1. 害怕空白页的原因

      1. 感知面下潜藏着某种形式的疑虑

      2. 担心自己会犯错

    2. 解决方案

      1. 倾听你内心的蜥蜴 —— 停止正在做的事情,给自己一点时间和空间,让大脑自我组织

      2. 把问题外化 —— 把正在写的代码涂鸦到纸上、向其他人解释一下怎么回事,把问题暴露给不同部分的大脑

      3. 做原型

  2. 阅读别人的代码

    1. 在看似重要的地方做笔记

    2. 有意识地寻找模式

  3. 学会在编码时听从直觉,不仅如此,还可延伸到更宽泛的领域(设计、需求)

巧合式编程

  1. 避免通过巧合编程,因为靠运气和意外来获得成功是行不通的,编程应该深思熟虑

  2. 不要假设,要证明(不以既定事实为基础的假设,是所有项目的祸根)

  3. 找到恰好能用的答案和找到正确的答案不是一回事

  4. 深思熟虑地编程

    1. 时刻注意你在做什么

    2. 能否向更初级的程序员解释代码?

    3. 不要在黑暗中编码(使用完全掌握的技术)

    4. 按计划推进

    5. 只依赖可靠的东西(如果不知道某件事是否可靠,就要做最坏的打算)

    6. 将假设文档化(参考契约式编程)

    7. 不要只测试代码,还要测试假设

    8. 为你的精力投放排一个优先级,把时间花在重要的方面

    9. 不要成为历史的奴隶,不要让现有的代码去支配未来的代码。

算法速度

  1. 借助大O符号进行算法评估(时间、空间)

  2. 通过常识判断算法的复杂度级别

  3. 在实际情况中,对估算做测试:不断改变数据量级和其他对运行时间造成很大影响的东西

  4. 不要过早优化,在投入宝贵的时间尝试改进算法之前,确保算法是否确实时瓶颈

重构

软件更像园艺而非建筑——它更像一个有机体而非砖石堆砌

  1. 马丁福勒将重构定义为一种:重组现有代码实体、改变其内部结构而不改变其外部行为的规范式技术

    1. 这项活动是由规范的,不应随意为之

    2. 外部行为不变:现在不是添加功能的时候

  2. 重构是一项日复一日的工作,需要采取低风险的小步骤进行。这是一种有针对性的、精确的方法,有助于保持代码易于更改,而不是对代码库进行自由的、大规模的重写

  3. 何时该重构

    1. 重复

    2. 非正交设计

    3. 过时的知识

    4. 使用(在真实的环境被真实的人使用,容易看出哪些特性是重点、而哪些无关紧要)

    5. 性能

    6. 通过了测试

  4. 尽早重构,经常重构

  5. 怎样重构

    1. 不要试图让重构和添加功能同时进行

    2. 在开始重构之前,确保有良好的测试。尽可能多地运行测试

    3. 采取简短而慎重的步骤,保持小步骤,并在每个步骤之后进行测试

为编码测试

  1. 测试与找bug无关:测试获得的主要好处发生在你考虑测试及编写测试的时候,而不是运行测试的时候

  2. 测试驱动编码

    1. 为方法写一个测试的考虑过程,使我们得以从外部看待这个方法,这让我们看起来是代码的客户,而不是代码的作者

    2. 测试时代码的第一个用户,测试所提供的反馈至关重要,可以指导编码过程

  3. 测试驱动开发(TDD)

    1. 基本循环

      1. 决定要添加一小部分功能

      2. 编写一个测试。等相应功能实现后,该测试会通过

      3. 运行所有测试。验证一下,是否只有刚刚编写的那个测试失败了

      4. 尽量少写代码,秩序保证测试通过即可。验证一下,测试现在是否可以干净地运行

      5. 重构代码:看看是否有办法改进刚刚编写的代码(测试或函数)。确保完成时测试仍然通过

    2. 好处

      1. 保证代码始终都有测试,这意味着你会一直考虑测试的状态

    3. 缺点

      1. 花费过多的时间来确保总是有100%的测试覆盖率

      2. 做了很多冗余的测试

      3. 他们的设计倾向于从底层开始,然后逐步上升(不要忘记时不时停下来看看大局,是否偏离了解决方案)

    4. TDD动因

      1. 小步前进,一次一个测试。这是TDD的优点

      2. 鼓励人们不断优化简单的问题。这是TDD的缺点

    5. 构建软件的方法是增量式的

      1. 既非自上而下,也不自下而上,基于端对端构建

      2. 一边工作一边了解问题,应用学到的知识持续充实代码,让客户参与每一个步骤并让他们指导这个过程

  4. 针对契约测试:参考契约式设计

    1. 可以尽量避免下游的灾难

    2. 为测试做设计

  5. 临时测试(发生在我们通过手动运行来捣鼓代码的时候)

    1. 如果代码出现过问题,就有再出问题的可能性。调试完后,需要把这个临时测试正式化,加入到单元测试库中

  6. 测试文化

    1. 测试先行(测试驱动设计)

    2. 边做边测

    3. 永不测试(编写30年测试后再说 —— 意味着养成了测试驱动思考)

    4. 要对软件做测试,否则只能留给最终用户去做

    5. 测试是编程的一部分,不该留给其他部门的人去做

    6. 测试、设计、编码 —— 都是在编程

第8章,项目启动之前

需求之坑

  1. 无人确切知道自己想要什么:通常情况下,它们(需求)被埋在层层的假设、误解和政治之下,更糟糕的是,需求通常根本不存在

  2. 程序员的工作是帮助人们了解他们想要什么

    1. 来了需求不要照单全收

    2. 寻找边缘情况并不厌其烦地明确需求信息(探索)

    3. 解释客户的需求,并反馈需求所含的问题(反馈)

  3. 需求是从反馈循环中学到的

    1. 通过灵活易变的代码展示模型和产品原型

    2. 通过短期迭代过程和客户的直接反馈来做需求

  4. 和客户一起工作以便从用户角度思考

  5. 需求与策略:针对更普遍的情况做实现,至于系统需要支持的那种特定类型的东西,只是通用实现在加入策略信息后的示例

  6. 需求与现实:成功的工具会让用的人觉得称手,成功的需求采集也会将其考虑在内

  7. 需求的文档化

    1. 需求文档不是为客户准备的(客户从不阅读规范)

    2. 需求文档是为计划准备的

    3. 需求不是架构,需求无关设计,也非用户界面:需求就是需要的东西

  8. 如果防止需求蔓延(功能膨胀、特性泛滥或需求蠕变)?持续反馈

  9. 维护一张(在线)术语表:如果用户和开发者使用不同的名称来称呼相同的事物,或者更糟糕的是,使用相同的名称来指代不同的事物,那么项目就很难取得成功

处理无法解决的难题

解谜的奥妙在于确定真正的(而不是想象的)约束条件,在这个约束条件下找到解开的方法。有些约束条件是绝对的,有些其实是一些先入为主的观念。

  1. 自由度

    1. 解决谜题的关键是,认识到你所受到的约束和你所拥有的自由度,因为认识到这些就会找到答案(不要跳出框框思考 —— 找到框框)

    2. 对约束进行分类和排序

  2. 跳出自身的局限

    1. 让大脑休息,做点不同的事情

    2. 找个人解释一下这个问题

  3. 记录工程日志:将什么行得通什么行不通反馈给大脑

携手共建

不仅仅是提问、讨论、做笔记,还要在真正编码的同一时刻提问和讨论

  1. 结对编程

    1. 充当打字员的开发者必须专注于语法和编码风格的底层细节

    2. 另一个人可以自由地在更高层次的范畴考虑问题

  2. 群体编程:

    1. 一个编码人员,多个除了开发人员的小团体,如用户、项目赞助者和测试人员

    2. 可想像成基于现场编码的紧密协作

  3. 不要一个人埋头钻进代码中

    1. 打造代码,而非打造自我

    2. 从小规模做起(4-5人)

    3. 批评要针对代码,而不针对人。“让我们看看这一块”听起来比“你搞错了”好得多

    4. 倾听他人的观点并试着理解,观点不同不是错误

    5. 频繁进行回顾,为下一次做好准备

敏捷的本质

敏捷不是一个名词,敏捷有关你如何做事

  1. 价值观

    1. 个体和互动高于流程和工具

    2. 工作的软件高于详尽的文档

    3. 客户合作高于合同谈判

    4. 响应变化高于遵循计划

  2. 敏捷工作方式的秘诀

    1. 弄清楚你在哪里

    2. 朝想去的地方迈出有意义的最小一步

    3. 评估在哪里终结,把弄坏的东西修好

    4. 在每件事的每个层面上递归地使用上面的步骤

第9章,务实的项目

务实的团队

  1. 维持小而稳定的团队

  2. 团队作为一个整体,不应该容忍破碎的窗户 —— 那些没人去修的小问题

  3. 鼓励每个人积极监控环境的变化。保持清醒,对项目范围扩大、时间缩短、额外特性、新的环境 —— 任何在最初的理解中没有的东西,都要留心。对新的需求要保持度量

  4. 为知识组合安排日程

    1. 旧系统的维护

    2. 流程的反思与精炼

    3. 实验新技术

    4. 学习和提升技能

  5. 团队整体的对外交流

    1. 在外人看来,最糟糕团队就是那些看起来闷闷不乐、沉默寡言的团队

    2. 优秀的团队有独特的个性

    3. 创建团队品牌

  6. 不要重复自己

    1. 知识共享:良好的沟通(即时、无摩擦力)

    2. 提出问题、分享进展、问题、见解和学到的东西

    3. 时刻关注队友正在做什么

  7. 组织全功能的团队:要构建团队,只有这样才可以端到端地、增量地、迭代地构建代码

  8. 确保团队拥有构建工具的技能,以便可以构建和部署工具,用其来将项目开发和生产部署自动化

  9. 团队是由个人组成的,赋予每个成员能力,让他们以自己的方式发光发热,要提供完善的架构来支持他们

椰子派不上用场

  1. 环境很重要:

    1. 特定的神奇,以及浮于表面的结构、策略、流程和方法是不够的

    2. 做能起作用的事,别赶时髦:多尝试,找到真正起作用的事

  2. 同一尺码无法适应所有人

    1. 不要照搬所谓的方法论(Scrum/精益/看板/XP/敏捷)

    2. 通过尝试,找到每种特定方法中最好的部分,并对其进行调整以供使用

  3. 真正的目的

    1. 目标不是使用某种方法论,而是交付可以工作的软件

    2. 对目标所需的交付周期进行评估,并试图不断缩短它

    3. 只有当这样做在业务上有意义时,才有必要在用户需要时即期交付

务实的入门套件

  1. 版本控制

    1. 持续交付:使用版本控制来驱动构建、测试和发布。

  2. 持续测试

    1. 寻找Bug有点像用网捕鱼。我们使用精细的小渔网(单元测试)来捕捉小鱼,使用大的粗渔网(集成测试)来捕捉食人鲨。

    2. 尽早测试,经常测试,自动测试

    3. 好项目的测试代码可能会比产品代码更多

    4. 直到所有的测试都已运行,编码才算完成

    5. 类型

      1. 单元测试:对模块测试

      2. 集成测试:对组成项目的子系统之间进行测试

      3. 确认和验证:对用户真正需求测试

      4. 性能测试:现实世界条件下的性能要求——包括预期的用户数、连接数或每秒事务数,是否可伸缩

    6. 对测试做检测:有目的的引入Bug,并验证测试是否能够捕获到

    7. 测试要彻底:测试状态覆盖率,而非代码覆盖率

    8. 基于特性测试

  3. 完全自动化:不要使用手动程序

取悦用户

作为开发者,我们的目标是取悦用户。用户真正要的不是代码,他们只是遇到某个业务问题,需要在目标和预算范围内解决。

傲慢与偏见

  1. 为自己的作品署名

  2. 恪守恕道(“己所不欲,勿施于人”),尊重他人的代码

  3. 代码所有权必须有所归属,但不必被个人拥有

  4. 对所有权引以为傲

Last updated