程序员修炼之道-书摘(一)
我们,采集的只是石头,却必须始终展望着未来的大教堂
务实的哲学
- 人生是你自己的,是你在拥有、经营和创造。
- 提供选择,别找借口。如果磁盘挂起,你所有的源码都在里面–而你没有备份,这就是你的错,跟老板说“我的源码被猫吃了”解决不了问题
- 不要搁置“破窗”(糟糕的设计、错误的决定、低劣的代码)不去修理。每发现一个赶紧修一个。一定要告诉自己“不要打破窗户”
- 找出合理的请求,然后不断完善,一旦有成果产出,展示给人们看,让他们大吃一惊。
- 够好即可得软件就是最好的。让用户参与权衡,如果你早点给用户一点东西玩,他们的反馈常常能引领你做出更好的最终方案。
- 目标:
- 每年学习一门新语言,不同的语言以不同 的方式解决相同的问题,拓宽思维,避免陷入陈规
- 每月读一本技术书,扩展领域,学习一些和项目不相关的东西
- 还要读非技术书
- 上课,本地大学或网上有趣的课程
- 与时俱进
- 批判性思维,批判性地分析你读到的和听到的东西
- 问“五个为什么”
- 谁从中受益
- 有什么背景
- 什么时候在哪里可以工作起来
- 为什么这是个问题
- 交流,缺乏有效的交流,好点子就成了一个孤儿。
- 传递信息的过程才算是交流,为了把信息送达,你需要了解受众的需求、兴趣和能力。
- 明白自己想说什么,越是有效的交流,影响力越大。
- 想法很重要,但是听众还希望有个好看的包装。
务实的方法
优秀设计的精髓
- 优秀的设计比糟糕的设计更容易变更,要信奉ETC原则(Easier To Change)
- 为什么解耦很好?因为通过隔离关注点,可让每一部分都容易变更
- 为什么单一职责原则有用?因为一个需求变化仅体现为某个单一模块上的一个对于变化
- 为什么命名很重要?因为好的命名可以使代码更容易阅读,而你需要通过阅读来变更代码
- ETC是一种价值观念,不是一条规则
- ETC是个向导,它能帮助你在不同的路线中选出一条。
DRY–邪恶的重复
- 程序员一直处于维护模式下,从未间断。我们的理解每天都在变化。
- 想要可靠地开发软件,或让开发项目更容易理解和维护,唯一的方法是遵循DRY原则:在一个系统中,每一处知识都必须单一、明确、权威地表达。
- DRY–不要重复自己。
- DRY不限于编码,DRY针对的是你对知识和意图的复制,它强调的是,在两个地方表达的东西其实是相同的,只是表达方式有可能完全不同。
- 代码中的重复
- 文档中的重复
- 数据结构中重复
- 表征的重复
- 开发人员间的重复
正交性
正交性:在计算科学中,象征着独立性和解耦性。有助于构建易于设计、构造、测试和扩展的系统。
- 但凡编写正交的系统,就能获得两个主要收益:提高生产力和降低风险。
- 我们希望设计的组件自成一体:独立自主,有单一的清晰定义的意图。当组件彼此隔离时,你知道可以变更其中一个组件,而不必担心影响到其他组件。
正交性设计
- 系统应该由一组相互协作的模块构成,每个模块实现的功能应独立于其他模块
- 不要依赖那些你无法控制的东西(电话号码,邮政编码,身份证号等外部标志符)
- 在引入第三方工具包和程序库时,请注意保持系统的正交性
编码时保持正交性,
- 保持代码解耦
- 避免全局数据
- 避免相似的函数
- 养成不断质疑代码的习惯,只要有机会就重新组织,改善其结构和正交性。
基于正交性设计和实现的系统更容易测试
- 建议将测试作为常规构建过程的一部分自动执行
- 修Bug也是评估整个系统的正交性的好时机
正交性也适用于文档,涉及的两个独立维度是内容和呈现。
采用正交性原则并与DRY原则紧密结合,可以让系统变得更灵活、更容易理解,并且更容易调试、测试和维护。
可逆性
- 实现一件事情的方法往往不止一种,提供第三方产品的供应商通常也不止一个
- 错误在于认为任何决定都是板上钉钉的,而没有为可能出现的意外做好准备
- 不设最终决定,很多人会尽力保持代码的灵活性,但其实还要考虑在体系结构、部署和供应商集成方面保持灵活性
- 放弃追逐时尚,要让你的代码具备“摇滚”精神:顺境时摇摆滚动,逆境时直面困难。
曳光弹
曳光弹式开发,即在真实条件下针对移动目标进行即时反馈的必要性
寻找重要的需求,那些定义了系统的需求。寻找你有疑问的地方,那些你认为有重大风险的地方。然后对开发进行优先级排序,首先从这些地方开始编码
最初的曳光弹就是,创建一个简单的工程,加一行“Hello World!,并确保其编译和运行。然后我们再去找整个应用程序中不确定的部分,添加上让它们跑起来的骨架
曳光代码不是一次性的:编写它是为了持续使用
曳光弹式开发和项目不会结束这种理念是一致的:总有东西需要改,总有新功能需要加,这是一个逐步递增的方法
使用曳光代码的优势:
- 用户可以更早地获得能工作的东西
- 开发者构造了一个可以在其中工作的框架
- 你有了一个集成平台
- 你有可以演示的东西
- 你对进度有更好的感觉
曳光弹并不总能击中目标,去弄清楚如何改变你已经做好的东西,想办法让它靠近目标。能够以更快的速度、更小的成本,收集到针对应用程序的反馈,并生成一个新的更准确的版本
原型与便签
- 用原型来研究什么类型的东西?任何有风险的东西,任何之前没有尝试过或对最终系统来说很关键的东西,任何未经证实、实验性或可疑的东西以及任何让你不舒服的东西
- 当制作一个原型时,那些细节可以忽略?正确性,完整性,健壮性,格式
- 不要把原型用于产品
领域语言
语言之界限,即是一个人世界之界限。
- 我们总是试图使用应用领域的词汇表来编写代码,在一些案例中,务实的程序员能跨越到下一个层级,直接用该领域的语言编程,直接使用该领域的词汇、语法和语义。
- 领域语言的特征
- RSpec和Phoenix路由是用它们的宿主语言(Ruby和Elixir)编写的;Cucumber测试和Ansible的配置是用它们自己的专门语言编写的。我们将RSpec和Phoenix路由视为内部领域语言的范例,Cucumber和Ansible采用的是外部语言
- 内部语言和外部语言之间的权衡
- 内部领域语言可以利用其宿主语言的特性,创建出来的领域语言更为强大,缺点是会受到宿主语言的语法和语义的限制
- 外部语言没有这样的限制,只需编写一个解析器
估算
- 通过学习估算,可以发展为对事物的数量级产生直觉,可以判别事情的可行性
- 所有的估算都是基于对问题的建模,一旦得到了模型,就可以将其分解为组件,每个组件都有一些参数,这些参数会影响组件对整个模型的贡献
- 一旦参数被分离,就可以为每个参数分配一个值
- 估算项目进度
- 计划评审技术(Program Evaluation Review Techningue,PERT),每个PERT任务都有一个乐观的、一个最有可能的和一个悲观的估算。任务被排列进一个相互依赖的网络中,然后使用一些简单的统计方法来确定整个项目的最佳和最差时间
- 确定一个项目的时间表的唯一方法,通常是来自于在这个项目上获得的经验
- 根据代码不断迭代的进度表,进度是由团队、团队的生产力和环境综合决定的,把提炼进度表作为每次迭代的一部分。
基础工具
纯文本的威力
- 我们把需求以知识的形式收集起来,然后在设计、实现、测试和文档中表达这些知识。
- 纯文本是将知识持久地存储下来的最佳格式,纯文本赋予了我们操作知识的能力,既可以用手工的方式,也可以用编程的方式操作
- 文本的威力
- 为防设备老化而加保险
- 利用杠杆效应让已有工具发挥最大优势
- 易于测试
Shell游戏
- 在Shell中,你可以调用所有能用的的工具,或通过管道用各种方式把工具组合起来
- 图形工具的好处在于WYSIWYG–所见即所得;弱势之处是WYSIAYG–所见即全部
加强编辑能力
- 操作编辑器游刃有余,最主要的收益来自于变得“顺畅”,不必再把心思花在编辑技巧上面。思考变流畅,编程就会受益
- 逐步游刃有余:只学习那些让你过的更舒服的命令
- 编辑时要自省,每次发现自己又在重复做某件事情时,要习惯性地想到“或许有更好的方法”
- 一旦发掘出一个新的有用的特性,需要尽快把它内化为一种肌肉记忆
版本控制
进步,远非寓于改变之中,而是依赖于保持。那些不能铭记过去的人,注定要重蹈覆辙。
- 版本控制系统跟踪你在源码和文档中所做的每个更改,使用正确配置的源码控制系统,总是可以将软件回退到以前的某个版本
- 版本控制系统对跟踪Bug、审核、性能以及质量这些目的,意义重大
- 永远使用版本控制,确保所有内容都在版本控制之下,文档,电话号码列表,供应商备忘录,Makefile文件,构建和发布过程,整理日志文件和小Shell脚本
- 分支出去,提供了隔离,团队项目的工作流核心通常围绕着分支来开展
- 把版本控制视为项目中枢
调试
- 调试只是在解决问题并为此攻关
- 去解决问题,而不是责备。Bug是你的错还是别人的错并不重要。无论是谁的错,问题仍然要你来面对
- 调试心态:不要恐慌
- 你在看到Bug或Bug报告时的第一反应是“这不可能”,那你就大错特错了。不要在“但那不可能发生”的思路上浪费哪怕一个神经元,因为很明显它会发生,而且已经发生了
- 永远要去发掘问题的根本原因,而不仅仅停留在问题的表面现象
- 在开始查Bug之前,请确保正在处理的代码可以干净构建–没有警告
- 开始动手修Bug时,最好是使其重现,修代码前先让代码在测试中失败
- 读一下那些该死的出错信息
- 错误的结果
- 输入值的敏感度
- 二分法
- 输出日志或跟踪信息
- 找个橡皮鸭:向其他人解释该问题,可以用来找到问题的原因
- 排除法
- 出错时受到惊吓的程度,与对正在运行的代码的信任程度成正比
文本处理
- 运用文本处理语言,可以快速地做出工具,或是为想法建立原型
- 工程日志
4 务实的偏执
在一个系统不完善、时间安排荒谬、工具可笑、需求不可能实现的世界里,让我们安全行事吧。就像伍迪·艾伦说的:“当所有人都真的在给你找麻烦的时候,偏执就是一个好主意。”
契约式设计
契约规定了你的权利和责任,同时也规定了对方的权利和责任。另外如果任何一方不遵守契约,还会面对一个针对后果的协议
伯特兰·迈耶在《面向对象软件构造》发明了契约式设计的概念。这是一种简单但功能强大的技术,侧重于文档化(并约定)软件模块的权利和责任,以确保程序的正确性。什么是正确的程序?不多也不少,正好完成它主张要做事情的程序。
- 文档化及对主张进行检验是契约式设计(BDC)的核心
软件系统中每一个函数和方法都力争有所作为。
- 前置条件,传递良好的数据是调用者的责任
- 后置条件,例程完成时世界的状态,不允许无限循环
- 类的不变式,从调用者的角度看,类会确保该条件始终为真
如果调用者满足了例程的所有前置条件,则例程应保证在完成时所有后置条件和不变式为真。
通过契约进行设计,在编写代码前,简单地列出输入域的范围,边界条件是什么,例程承若要交付什么–或更重要的是,没有承若要交付什么–这些对编写更好的软件来说,是巨大的飞跃
可以用断言部分模拟契约式设计,断言是一种对逻辑条件的运行时检查
通过使用断言或DBC机制来验证前置条件,后置条件和不变式,可以尽早崩溃并报告有关问题更准确的信息;在问题发生的地方尽早崩溃,能让找到问题和诊断问题更加容易
使用语义不变式来表达不可违背的需求,它必须是事物意义的核心,而不受策略的影响(策略用于更动态的业务规则)
在自主代理的视野下,代理有拒绝自己不想接受的请求的自由,它们可以自由地对契约重现协商。
死掉的程序不会说谎
- 尽快检测问题的好处之一是,可以更早崩溃,而崩溃通常是你能做的最好的事情
- 一个死掉的程序,通常比一个瘫痪的程序,造成的损害要小得多
断言式编程
自责中往往有种奢侈。我们自责时,总觉得别人无权在责备我们。
- 使用断言去预防不可能的事情,不要使用断言来代替真正的错误处理
- 断言与副作用,如果对条件做评估本身有副作用,就可能发生这样的事情。属于海森堡Bug–调试本身改变了被调试的系统的行为
- 保持断言常开
如何保持资源的平衡
- 有始有终,分配资源的函数或对象,对释放资源应负有责任
- 嵌套的分配
- 释放资源的顺序与分配资源的顺序相反
- 在代码的不同位置,如果都会分配同一组资源,就始终以相同的顺序分配它们,减少死锁的可能性。
- 保持平衡与异常,如果抛出异常,保证之前分配给异常的所有内容依次释放,变量的作用域或try catch代码块中的finally句子
- 检测平衡,写一些代码去实际检查资源有没有被恰当地释放
不要冲出前灯范围
- 小步前进–由始至终。总是采取经过深思熟虑的小步骤,同时检查反馈,并在推进前不断调整。把反馈的频率当作速度限制,永远不要进行“太大”的步骤或任务
宁弯不折
解耦
当我们试着单独挑出一个事物的时候,总会发现它与宇宙中其他一切都有关联。
- 解耦代码让改变更容易
- 只管命令不要询问,这个原则是说,不应该根据对象的内部状态做出决策,然后更新该对象
- 不要链式调用方法,当你访问某样东西时,尽量不要超过一个“.”
- 邪恶的全局化,全局可访问的数据是应用程序组件之间耦合的潜在来源
- 全局数据包括单件,外部资源
- 如果全局唯一非常重要,那么将它包装到API中
- 继承增加了耦合
- 让代码害羞一点:让它只处理直接知道的事情,这将有助于保持应用程序解耦,使其更易于变更
在现实世界中抛球杂耍
- 事件表达出信息的可用性,它可能来自外部世界:用户点击了按钮,也可能来自于内部:搜索完成了。
- 四个策略:有限状态机,观察者模式,发布/订阅,响应式编程与流
- 有限状态机(FSM)
- 状态机基本上就是怎样处理事件的一份规范,它由一组状态组成,其中一个是当前状态,对于每个状态,我们列出对该状态有意义的事件
- 添加动作,可以在某些转换上添加触发动作来增强它
- 观察者模式
- 在观察者模式中,我们有一个事件源,被称为被观察对象,而客户列表,也即观察者,会对其中的事件感兴趣
- 观察者模式有一个问题:每个观察者必须与观察对象注册在一起,所以引入了耦合。此外,回调是由观察对象以同步的方式内联处理的,会导致性能瓶颈
- 发布/订阅
- 发布/订阅推广了观察者模式,同时解决了耦合和性能问题
- 与观察者模式不同,发布者和订阅者之间的通信是在代码之外处理的,并且可能是异步的
- 响应式编程、流与事件
- 流让我们把事件当作数据集合来对待
变换式编程
- 所有程序其实都是对数据的一种变换——将输入转换成输出,然后当我们在构思设计时,很少考虑创建变换过程。相反,我们操心的是类和模块、数据结构和算法、语言和框架
- 编程讲的是代码,而程序谈的是数据
- 寻找变换的最简单方法是:从需求开始并确定它的输入和输出,这是一个自顶向下的方法
- 不要囤积状态,传递下去。不要把数据看作是遍布整个系统的小数据池,而要把数据看作是一条浩浩荡荡的河流。数据成为与功能对等的恭喜:管道是一系列的代码—>数据—>代码—>数据……数据不再和特定的函数组一起绑定在类定义中。
- 错误处理怎么做?可以在变换的内部或外部做错误检查
继承税
- 通过继承共享代码的问题
- 继承就是耦合,不仅子类耦合到父类,以及父类的父类等,而且使用子类的代码也耦合到所有祖先类
- 通过继承构建类型的问题,为了表示类之间的细微差别而逐层增加,增加复杂性,使程序更加脆弱
- 不要付继承税,更好的替代方案:接口与协议,委托,mixin与特征
- 尽量用接口来表达多态,接口与协议给了我们一种不使用继承的多态性
- 用委托提供服务:“有一个”胜过“是一个”
- mixin基本思想很简单:希望能够为类和对象扩展新的功能,但不用继承。讲现有事物和新事物的功能合并在一起
配置
- 使用外部配置参数化应用程序
- 静态配置
- 配置服务化,存储到API中