靠谱的程序员都是相似的 —— 《程序员修炼之道》读书笔记
第 1 章 注重实效的哲学
Provide Options, Dont Make Lame Excuses 提供各种选择,不要找蹩脚的借口
例如数据丢失,即使不是你的错,但是你事先没有备份,却责任连带上了你。
Don't Live with Broken Windows 不要容忍破窗户
及时修复有隐患的 bug。防止熵的增长,让代码变的腐烂。
《石头汤的故事》:
三个士兵从战场返回家乡,在路上俄了。他们看见前面有村庄,就来了精神一一他们相信村民会给他们一顿饭吃。但当他们到达那里,却发现门锁着,窗户也关着。经历了多年战乱,村民们粮食匮乏,并把他们有的一点粮食藏了起来。
士兵们并未气後,他们煮开一锅水,小心地把三块石头放进去。吃惊的村民们走出来望着他们。
“这是石头汤。”士兵们解释说。“就放石头吗?”村民们问。“一点没错一但有人说加一些胡萝卜味道更好个村民跑开了,又很快带着他储藏的一篮胡萝卜跑回来。
几分钟之后,村民们又问:“就是这些了吗?”
哦,”士兵们说:“凡个土豆会让汤更实在。”又一个村民跑开了接下来的一小时,士兵们列举了更多让汤更蛘美的配料:牛肉、韭某盐,还有香菜。毎次都会有一个不同的村民跑回去搜寻自己的私人储藏品。
最后他们煮出了一大锅热气騰腾的汤。士兵们拿掉石头,和所有村民起享用了一顿美餐,这是几个月以来他们所有人第一次吃饱饭。
人们发现,参与正在发生的成功要更容易。让他们警见未来,你就能让他们聚集在你周围。
很多商业的玩法也是如此。
也要防止石头汤
变成温水煮青蛙
,要时常考虑大局,看事情有利还是有害于你。
机遇与风险并存。
今天的了不起的软件常常比明天的完美软件更可取。
软件的优化永无尽头。
管理知识资产(Knowledge Portfolios)
与管理金融资产非常相似:
-
严肃的投资者把定期投资作为习惯。
-
多元化是长期成功的关键。
-
管理风险。聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的资产。
不要把你所有的技术鸡蛋放在一个篮子里。
-
投资者设法低买高卖,以获取最大回报。
在 Java 刚出现时学习它可能有风险,但对于现在已步入该领域的顶尖行列的早期采用者,这样做得到了非常大的回报。
-
应周期性地重新评估和平衡资产。
目标:
-
每年至少学习一种新语言。
一种不影响编程方式的语言,不值得知道。
学习至少六种编程语言。包括一种强调类抽象的语言(如Java或C ++),一种强调函数抽象的语言(如Lisp或ML或Haskell),一种支持语法抽象的语言(如Lisp),一种支持声明性规范(如Prolog或C ++模板) ,以及强调并行性的(如Clojure或Go)。
-
每季度阅读一本技术书籍。
设法把你学到的东西应用到你当前的项目中。
-
也要阅读非技术书籍。
-
上课。
-
参加本地用户组织。
-
试验不同的环境。
例如你只在 Windows 上工作,就在家玩一玩 Unix。
-
跟上潮流。
思想的“异花授粉“(cros-pollination)十分重要。
Critically Analyze What You Read and Hear 批判地分析你读到的和听到的
例如:不要低估商业主义的力量。Web 搜索引擎把某个页面列在最前面,并不意味着那就是最佳选择;内容供应商可以付钱让自己排在前面。书店在显著位置展示某一本书,也并不意味着那就是本好书,甚至也不说明那是一本受欢迎的书;它们可能是付了钱才放在那里的。
我相信,被打量比被忽略要好。
问题不只是你有什么,还要看你怎样包装它。除非你能够与他人交流,否则就算你拥有最好的主意、最漂亮的代码、或是最注重实效的想法,最终也会毫无结果。没有有效的交流
,一个好想法就只是一个无人关心的孤儿。
只有当你是在传达信息时,你才是在交流。为此,你需要了解你的听众的需要、兴趣、能力。
我们都曾出席过这样的会议:一个做开发的滑稽人物在发表长篇独白,讲述某种神秘技术的各种优点,把市场部副总裁弄得目光呆滞。这不是交流,而只是空谈,让人厌烦的(annoying3) 空谈。
It's Both What You Say and the Way You Say It 你说什么和你怎么说同样重要
调整你的交流风格,让其适应你的听众。
每个人习惯的交流风格确实不一样。
第 2 章 注重实效的途径
1、正交与内聚
什么是正交性
?
在计算技术中,该术语用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。
我们想要设计自足(self- contained)的组件:独立,具有单一、良好定义的目的(Yourdon 和 Constantine 称之为内聚
(cohesion)。
而相反,当任何系统的各组件互相高度依赖时,就不再有局部修正(local fix)
这样的事情。
比如著名的 MVC 架构,M、V、C 互相正交。
正交的好处:
1、提高生产率:
-
改动得以局部化,所以开发时间和测试时间得以降低。
-
正交的途径还能够促进复用。
-
如果你对正交的组件进行组合,生产率会有相当微妙的提高。假定某个组件做 M 件事情,而另一个组件做 N 件事情。如果它们是正交的,而你把它们组合在一起,结果就能做 M * N 件事情。
2、降低风险
如果某个想法是你唯一的想法,再没有什么比这更危险的事情了。
要实现某种东西,总有不止一种方式。
工程是找最优解,而不是找唯一解。没有可供选择就没有权衡利弊。
要把决策视为是写在沙滩上的,而不要把它们刻在石头上。大浪随时可能到来,把它们抹去。
无论你使用的是何种机制,让它可撤消
。
不存在最终决策。
2、曳光代码 vs 原型制作
曳光代码
:你在系统的各组件间实现了端到端(end-to-end) 的连接。
原型
的设计目的就是回答一些问题,所以与投入使用的产品应用相比,它们的开发要便宜得多、快捷得多。
区别:原型制作生成用过就扔的代码。曳光代码虽然简约,但却是完整的,并且构成了最终系统的骨架的一部分。
3、小型语言
无论是用于配置和控制应用程序的简单语言,还是用于指定规则或过程的更为复杂的语言,我们认为,你都应该考虑让你的项目更靠近问题领域。通过在更高的抽象层面上编码,你获得了专心解决领域问题的自由,并且可以怱路琐碎的实现细节。
这也是高级语言存在的好处。
在最简单的情况下,小型语言可以采用面向行的、易于解析的格式。 在实践中,与其他任何格式相比,我们很可能会更多地使用这样的格式。只要使用 switch 语句、或是使用像 Perl 这样的脚本语言中的正则表达式,就能够对其进行解析。
你还可以用更为正式的语法,实现更为复杂的语言。 这里的诀窍是首先使用像 BNF2 这样的表示法定义语法。一旦规定了文法,要将其转换为解析器生成器(parser generator) 的输人语法通常就非常简单了。C 和 C 程序员多年来一直在使用 yac(或其可自由获取的实现,bison )。在 Lex and YACCILMB92] 一书中详细地讲述了这些程序。Java 程序员可以选用 javacc。如其所示,旦你了解了语法,编写简单的小型语言实在没有多少工作要做。
要实现小型语言还有另一种途径:扩展已有的语言。
可以通过两种不同的方式使用你实现的语言。
数据语言
产生某种形式的数据结构给应用使用。这些语言常用于表示配置信息。
命令语言
更进了一步。在这种情况下,语言被实际执行,所以可以包含语句、控制结构、以及类似的东西。
在某种程度上,所有的解答都是估算。
在不同情境下,需要估算的精度不一样。
关于估算,一件有趣的事情是,你使用的单位会对结果的解读造成影响。如果你说,某事需要 130 个工作日,那么大家会期望它在相当接近的时间里完成。但是,如果你说“哦,大概要六个月”,那么大家知道它会在从现在开始的五到七个月内完成。这两个数字表示相同的时长,但“130 天”却可能暗含了比你的感觉更高的精确程度。
这属于心理学的范畴了。
第 3 章 基本工具
Keep Knowledge in Plain Text 用纯文本保存知识
通过纯文本,你可以获得自描述(self-describing)
的、不依赖于创建它的应用的数据流。
在现在普遍使用纯文本保存信息的年代,大家已经习以为常了。
GUI 的好处是 WYSIWYG 所见即所得(what you see is what you get)
。缺点是 WYSIAYG 所见即全部所得(what you see is all you get)。
不然你还想咋样……
在 Windows 下使用 Unix 工具:
Cygnus Solutions 公司有一个叫做 Cygwin
的软件包。除了为 Windows 提供 Unix 兼容层以外,Cygwin 还带有 120 多个 Unix 实用程序,包括像 ls、grep 和 find 这样的很受欢迎的程序。
在大学时代还没换 mac 的时,用过Cygwin。
Use a Single Editor Well 用好一种编辑器
选一种编辑器,彻底了解它,并将其用于所有的编辑任务。
但如果你发现自己在“羡慕”别人的编辑器,你可能就需要重新评估自己的位置了。
IDE 还是一个好。
进步远非由变化组成,而是取决于好记性。不能记住过去的人,被判重复过去。
个人做到相对容易,民族来说难。
Don't Assume it- Prove It 不要假定,要证明
不要惧怕证明。
第 4 章 注重实效的偏执
当每个人都确实要对你不利时,偏执就是一个好主意—— Woody Allen
也是一种无可奈何吧。
1、DBC
DBC(Design By Contract)按合约设计
。这是一种简单而强大的技术,它关注的是用文档记载(并约定)软件模块的权利与责任,以确保程序正确性。什么是正确的程序?不多不少,做它声明要做的事情的程序。用文档记载这样的声明,并进行校验,是DBC的核心所在。
就像与人打交道也需要合约。
前条件(precondition)
。为了调用例程,必须为真的条件;例程的需求。在其前条件被违反时,例程决不应被调用。传递好数据是调用者的责任。
后条件(postcondition)
。例程保证会做的事情,例程完成时世界的状态。例程有后条件这一事实意味着它会结束:不允许有无限循环。
类不变项(class invariant)
。类确保从调用者的视角来看,该条件总是为真。在例程的内部处理过程中,不变项不一定会保持,但在例程退出、控制返回到调用者时,不变项必须为真(注意,类不能给出无限制的对参与不变项的任何数据成员的写访问)。
前置条件必须满足,后置条件必须实现,对象状态必须可被断言。
如果任何一方没有履行合约的条款,(先前约定的)某种补偿措施就会启用例如,引发异常或是终止程序。不管发生什么,不要误以为没能履行合约是 bug。
继承和多态是面向对象语言的基石,是合约可以真正闪耀的领域。
例子:实现 DBC 的前条件和后条件
/**
* @pre anitem != null // Require real data
* @post pop() == anitem // Verify that it's on the stack
*/
public void push(final String anitem)
如果语言不在代码中支持 DBC。即使没有自动检查,你也可以把合约作为注释放在代码中,并仍然能够得到非常实际的好处。
对于 Java,可以使用 icontract。它读取( Javadoc 形式的)注释,生成新的包含了断言逻辑的源文件。
动态合约与代理:我有不想接受的请求的自由——“我无法提供那个,但如果你给我这个,那么我可以提供另外的某样东西。“
2、断言式编程
在自责中有一种满足感。当我们责备自己时,会觉得再没人有权责备我们
—— 奥斯卡・王尔德:《多里安·格雷的画像》
不要用断言代替真正的错误处理。断言检查的是决不应该发生的事情。
还要记住断言可能会在编译时被关闭一一决不要把必须执行的代码放在 assert 中。
在一个更低、但用处并非更少的层面上,你可以投资购买能检査运行中的程序的内存泄漏情况(及其他情况)的工具。Purify 和 Insure++ 是两种流行的选择。
第 5 章 弯曲,或折断
1、耦合
得墨忒耳定律
(Law of Demeter,缩写LoD)亦被称作“最少知识原则(Principle of Least Knowledge)”,是一种软件开发的设计指导原则,特别是面向对象的程序设计。得墨忒耳定律是松耦合的一种具体案例。
- 每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;
- 每个单元只能和它的朋友交谈:不能和陌生单元交谈;
- 只和自己直接的朋友交谈。
得墨忒耳法则
试图使任何给定程序中的模块之间的耦合减至最少。
Configure, Don't Integrate 要配置,不要集成。
Put Abstractions in Code. Details in Metadata 将抽象放进代码,细节放进元数据。
元数据
到底是什么?严格地说,元数据是关于数据的数据。最为常见的例子可能是数据库 schema 或数据词典。
时间耦合(temporal coupling)
。时间有两个方面对我们很重要:并发(事情在同一时间发生)和次序(事情在时间中的相对位置)。
使用像 UML 活动图(UML activity diagram)
这样的表示法来捕捉他们对工作流
的描述。
分析工作流,以改善时间耦合。
UML 的缺点:只能描述 OO。
2、扩展 —— UML 的争议
UML
(Unified Modeling Language,统一建模语言)
常用:流程图、用例图、类图、时序图……
特点(数字越高代表越有用):
- 代码自动生成:0(很多时候不支持或者支持不完备,所以会导致 UML 和代码的耦合性太差)
- 代码高层设计和交流: 4
- 验证正确性:2 (但要做精确的验证,必须用专门的工具)
- 当成文档、留作凭证:3(甲方乙方之间扯皮甩锅时特别有用)
缺点:
-
学习成本高(体系很庞大,建议只做简单了解就够用了)
-
灵活性不高(必须遵守复杂的限制条件,最后反被工具束缚)
-
表述领域有限(只能表述 OO)
-
不符合程序员思维(还不如直接写代码)
-
上面特点里提到的若干条……
参考资料:
1、UML 还有用吗?
https://www.zhihu.com/question/23569835
2、如何反驳 UML 无用论?
https://www.zhihu.com/question/41212231
第 6 章 当你编码时
他不知道代码为什么失败,因为他一开始就不知道它为什么能工作。
Don't Program by Coincidence 不要靠巧合编程
当你发现突然间解决了一个问题,这跟写了个 bug 差不多要引起警惕。
在所有层面上,人们都在头脑里带着许多假定工作但这些假定很少被记入文档,而且在不同的开发者之间常常是冲突的。并非以明确的事实为基础的假定是所有项目的祸害。
这就是文档的重要性。
1、估算算法
注重实效的程序员几乎每天都要使用:估计算法使用的资源、时间、处理器、内存,等等。
如果关系总是线性的(于是时间的增加与 n 的值成正比),这一节也就无关紧要了。但是,大多数重要的算法都不是线性的。
还好大多数算法都是亚线性的。例如,二分査找。
O0 表示法
是处理近似计算的一种数学途径。在度量的事物(时间、内存,等等)的值设置了上限。
O (n^2) 时,我们的意思是,在最坏的情况下,所需时间随 n 的平方变化。
最高阶的项将主宰函数的值,习惯做法是去掉所有低阶项,并且对任何常数系数都不予考虑。
这实际上是 O0 表示法的一个弱点,即某个 O (n^2)算法可能比另一个 O (n^2) 算法要快 1000 倍,但你从表示法上却看不出来。
例如,假定你有一个例程,处理 100 条记录需要 1 秒。处理 1,000 条记录需要多长时间?
如果你的代码是 O (1),那么它仍然需要 1 秒。如果是 O (lg (m)),那么你可能要等上 3 秒。O (n) 将线性增长到 10 秒,而 O (nlg (n)) 大约需要 33 秒。如果你的运气太差,有一个 O (n^2) 例程,那么事情很快就会开始失控。
你可以使用常识估算许多基本算法的阶。
简单循环。
如果某个简单循环从 1 运行到 n,那么算法很可能就是 O (n) 一时间随 n 线性增加。其例子有穷举査找、找到数组中的最大值、以及生成校验和。
嵌套循环。
如果你需要在循环中嵌套另外的循环,那么你的算法就变成了 O (m * n),这里的 m 和 n 是两个循环的界限。这通常发生在简单的排序算法中,比如冒泡排序:外循环依次扫描数组中的每个元素,内循环确定在排序结果的何处放置该元素。这样的排序算法往往是 O (n^2)。
二分法。
如果你的算法在每次循环时把事物集合一分为二,那么它很可能是对数型 O (lg (n)) 算法对有序列表的二分査找、遍历二权树、以及查找机器字中的第一个置位了的位,都可能是 O (ln (n)) 算法。
分而治之。
划分其输入,并独立地在两个部分上进行处理,然后再把结果组合起来的算法可能是 O (nlg (n))。经典例子是快速排序,其工作方式是:把数据划分为两半,并递归地对每一半进行排序。尽管在技术上是 O (n2),但因为其行为在馈入的是排过序的输入时会退化,快速排序的平均运行时间是 O (nlg (n)) 。
组合。
只要算法考虑事物的排列,其运行时间就有可能失去控制。这是因为排列涉及到阶乘(从数字 1 到 5 有 5! =5 x4 x3 x2 x1=120 种排列)得出 5 个元素的组合算法所需的时间:6 个元素需要 6 倍的时间,7 个元素则需要 42 倍的时间。其例子包括许多公认的难题的算法一旅行商问题、把东西最优地包装进容器中、划分一组数、使每一组都有相同的总和,等等。在特定问题领域中,常常用启发式方法(heuristics) 减少这些类型的算法的运行时间。
2、重构
遗憾的是,最为常见的软件开发的比喻是修建建筑(building construction)。
但不同的是,重写、重做和重新架构代码合起来,称为重构(refactoring)
。
你也许可以用一个医学上的比喻来向老板解释这一原则:把需要重构的代码当作是一种“脚瘤”。切除它需要进行“侵入性”的外科手术。你可以现在手术,趁它还小把它取出来。你也可以等它增大并扩散一但那时再切除它就会更昂贵、更危险。等得再久一点,“病人”就有可能会丧命。
Refactor Early, Refactor Often 早重构
,常重构
重构的注意点:
-
不要试图在重构的同时增加功能。
-
在开始重构之前,确保你拥有良好的测试。尽可能经常运行这些测试。
完美,不是在没有什么需要增加、而是在没有什么需要去掉时达到的。
—— Antoine de St Exupery, Wind, Sand, and Stars, 1939
契合现在的极简风格、或者包豪斯设计主义。
Dont Gather Requirements-dig for Them 不要搜集需求——挖掘它们
找出用户为何要做特定事情的原因、而不只是他们目前做这件事情的方式,这很重要。到最后,你的开发必须解决他们的商业问题,而不只是满足他们陈述的需求。
有一种能深人了解用户需求、却未得到足够利用的技术:成为用户。
Work with a User to Think Like a User
与用户一同工作,以像用户一样思考
很多时候客户因为不专业,根本不知道自己的核心需求是啥,所以导致的表述的偏差和失实。
要创建并维护项目词汇表(project glossary)
有些领域在没有专用词汇引用的空白时,就只有自己发明了。
通过把需求制作成超文本文档,我们可以更好地满足不同听众的需要。
超本文文档是个很有力的工具。
第 7 章 在项目开始之前
弗里吉亚的国王戈尔迪斯曾经系过一个没有人能解开的结。据说能解开戈尔迪斯结题的人将会统治整个亚洲。亚历山大大帝来了,用剑劈开了这个结。只是对要求做了小小的不同解释,就是这样…他后来的确统治了亚洲大部分。
跳出思维盒子。
问题并不在于你是在盒子里面思考,还是在盒子外面思考,而在于找到盒子一确定真正的约束。
你所需要的只是真正的约東、令人误解的的约束、还有区分它们的智慧。
很多时候,对需求的重新诠释能让整个问题全都消失一一就像是戈尔迪斯结。
约束决定了你怎样提出解决方案。艺术是带着镣铐跳舞,工程更是。
这里有一个挑战:写一份简短的描述,告诉别人怎样系鞋带。快,试一试。
传递信息的媒介,确实有不同的优势和劣势。在系鞋带来看,更适合的是视频而不是文字。
只是要知道,随着规范越来越详细,你得到的回报会递减。
过犹不及。太多的规范反而成了新的、没必要的约束。
规定无法构建的东西太容易了。
Some Things Are Better Done than Described 对有些事情“做”胜于“描述”。
你越是把规范当作安乐毯,不让开发者进人可怕的编码世界,进入编码阶段就越困难。把他们赶出来。考虑构建原型,或是考虑曳光弹开发。
试着做,可以让你的规范更好更精简。切勿纸上谈兵。
注重实效的程序员批判地看待方法学,并从各种方法学中提取精华,融合成每个月都在变得更好的一套工作习惯。这至关紧要。你应该不断努力提炼和改善你的开发过程。决不要把方法学的呆板限制当做你的世界的边界。
方法只配拿来我用,或组合或改造,而不是反过来限制我。
第 8 章 注重实效的项目
Organize Around Functionality, Not Job Functions. 围绕功能、而不是工作职务进行组织。
这样可以防止工作指派的重复。
1、测试
因为我们不可能编写出完美的软件,所以我们也不可能编写出完美的测试软件。我们需要对测试进行测试。
Use Saboteurs to Test Your Testing. 通过“蓄意破坏”测试你的测试。
是否需要测试测试的测试的测试?
Test State Coverage, Not Code Coverage. 测试状态覆盖,而不是代码覆盖。
即使具有良好的代码覆盖,你用于测试的数据仍然会有巨大的影响。
所以不要盲目追求高代码覆盖。
一旦测试人员找到了某个 bug,这应该是测试人员最后一次发现这个 bug。应该对自动化测试进行修改,从此每次都检査那个特定的 bug。
不要侥幸,发现 bug 就要及时补上针对这个 bug 的测试。
2、注释
也不管作者的角色是什么(开发者或技术文档撰写者),所有的文档都是代码的反映。如果有歧义,代码才最要紧一一无论好坏。
我觉得有时候代码也会服从文档,看实际情况吧。
根据源码中的注释和声明产生格式化文档相当直截了当。例如 Javadoc。
最好还支持导出为 api 文档。
Gently Exceed Your Users Expectations 温和地超出用户的期望。
步子迈的太大也会吓到用户。慢慢来。
匿名(尤其是在大型项目中)可能会为邋過、错误、懒惰和糟糕的代码提供繁殖地。
我们想要看到对所有权的自豪。“这是我编写的,我对自己的工作负责。”你的签名应该被视为质量的保证。当人们在一段代码上看到你的名字时,应该期望它是可靠的、用心编写的、测试过的和有文档的,一个真正的专业作品,由真正的专业人员编写。
希望早日有我署名的 public 好作品。
总结 & 梳理
1、正交 & 高内聚低耦合
内聚性,关注的是一个软件单位内部的关联紧密程度。
耦合性,则是强调两个或多个软件单位之间的关联紧密(依赖)程度。
具备正交关系的两个模块,可以做到一方的变化不会影响另外一方的变化。
我们要追求高内聚,低耦合。
得墨忒耳法则(最少知识原则):可以实现高内聚,低耦合。