不能记住过去的人,被判重复过去。 --《程序员修炼之道》
这句引言,一直被我用作座右铭,当在书中读到这句的时候,感触颇深,也是我打算开始写博客记录生活的开始。跟这本书的机缘巧合,来自于之前公司的一个学长,他看完了,我便借来看了。
在序章中看到无数褒奖,我很担心这本书又是一些把技术当做禅宗佛学讲道的废话,看了一些的时候,了解到这本书涵盖程序员成长过程中和软件开发中需要注意的地方,从程序员的个体哲学到编码过程的各个环节,再到团队的项目管理,从程序员如何扩充知识,如何思考问题,如何利用有效工具打造个人环境,到项目启动之前如何建立一些基本准则,如何分析、设计、编写、测试、重构,如何实现自动化,甚至是项目团队中提高实效的原则,编程是一门手艺,这样的工匠精神更是一次一次感化着我幼小的心灵。
注重实效的程序员的两个特点
Care About Your Craft
关心你的技艺
编程技术就是程序员的手艺,你的程序就是你的艺术品。时刻关注自己的技艺,保持热情、保持好奇,争取做到富有专长而又多才多艺。
关于程序员这个职业,引用@左耳朵耗子的一段微博:没哪个行业能像计算机行业这么活跃、刺激和有趣了。不仅是新兴工业革命的主力,又渗入到所有的行业中,干一辈子值了。//@_你亲爱的偏执狂: 程序员首先是工程师,Professional,就跟律师,医生一样,给大家解决问题;但是另一面呢,又是艺术家,创造新奇好玩的东西。这样的职业做一辈子有什么问题?
Think! About Your Work
思考!你的工作
虽然软件开发是工程学,但每个程序员并不是螺丝钉,而是活跃的造血细胞。我们要思考需求,推敲设计,展望愿景,打磨细节;我们要思考如果提高工作效率,如何成长;在对权威产生疑惑时,我们又要批判的思考而不茫然接受。除去工程技术以外,逻辑思维能力才是程序员的核心竞争力,保持活跃、勤奋的思维。
我的源码让猫给吃了
依据你的职业发展、你的项目和你每天的工作,为你自己和你的行为负责这样一种观念,是注重实效的哲学的一块基石。注重实效的程序员对他或她自己的职业生涯负责,并且不害怕承认无知或错误。这肯定并非是编程最令人愉悦的方面,但它肯定会发生——即使是在最好的项目中。尽管有彻底的测试、良好的文档以及足够的自动化,事情还是会出错。交付晚了,出现了未曾预见到的技术问题。
发生这样的事情,我们要设法尽可能职业地处理它们。这意味着诚实和坦率。我们可以为我们的能力自豪,但对于我们的缺点——还有我们的无知和我们的错误——我们必须诚实。
Provide Options, Don’t Make Lame Excuses
提供各种选择,不要找蹩脚的借口
这段对责任的描述并不只适用于程序员,但程序员可能会有自己的理解。面对历史遗留问题,是主动解决还是视而不见?问题发生时,是坦然担当还是去blame是猫吃了你的代码?
Sign Your Work
在你的作品上签名
过去时代的手艺人为能在他们的作品上签名而自豪。你也应该如此。“这是我编写的,我对自己的工作负责。”你的签名应该被视为质量的保证。当人们在一段代码上看到你的名字时,应该期望它是可靠的、用心编写的、测试过的和有文档的,一个真正的专业作品,由真正的专业人员编写。
关于签名我们曾经在代码规范中推行过,在类的头文件中加入类似下面的注释。有签名在对自己是鞭策,其它工友也容易找到你咨询问题
//------------------------------------------------------------------------------
//
// 版权所有(C)被猫吃了技术有限公司保留所有权利
//
// 创建者: 被猫吃了
// 创建日期: 2013-9-11
// 功能描述: 被猫吃了
//
//------------------------------------------------------------------------------
软件的熵
熵是一个来自物理学的概念,指的是某个系统中的“无序”的总量。当软件中的无序增长时,程序员们称之为“软件腐烂”(software rot)。有许多因素可以促生软件腐烂。其中最重要的一个似乎是开发项目时的心理(或文化)。
Don’t Live with Broken Windows
不要容忍破窗户
不要留着程序中的“破窗户”不修,低劣的设计,临时的糟糕的方案等等。而往往我们又面对着诸多的“现实”,没时间重构,重构风险大没资源测试。可是我们会永远生活在“现实”里面,不可能有某一天万事具备、良辰吉日等着让你开始着手去修理这些“破窗户”。我们可以借助自动测试等手段来帮助我们降低风险。如果真的没办法立即修复,请一定要做到:把发现的“破窗户”记入TODO List,并且定期Review它。
灭火的故事:
作为对照,让我们讲述Andy的一个熟人的故事。他是一个富得让人讨厌的富翁,拥有一所完美、漂亮的房子,里面满是无价的古董、艺术品,以及诸如此类的东西。有一天,一幅挂毯挂得离他的卧室壁炉太近了一点,着了火。消防人员冲进来救火——和他的房子。但他们拖着粗大、肮脏的消防水管冲到房间门口却停住了——火在咆哮——他们要在前门和着火处之间铺上垫子。
他们不想弄脏地毯。
这的确是一个极端的事例,但我们必须以这样的方式对待软件。如果你发现你所在团队和项目的代码十分漂亮——编写整洁、设计良好,并且很优雅——你就很可能会格外注意不去把它弄脏,就和那些消防员一样。即使有火在咆哮(最后期限、发布日期、会展演示,等等),你也不会想成为第一个弄脏东西的人。
重复的危害
给予计算机两项自相矛盾的知识,是James T. Kirk舰长(出自Star Trek,“星际迷航”——译注)喜欢用来使四处劫掠的人工智能生命失效的方法。遗憾的是,同样的原则也能有效地使你的代码失效。
我们觉得,可靠地开发软件、并让我们的开发更易于理解和维护的惟一途径,是遵循我们称之为DRY的原则:系统中的每一项知识都必须具有单一、无歧义、权威的表示。
DRY – Don’t Repeat Yourself
不要重复你自己
重复是代码中最坏的味道,大家可以回想一下,有多少Bug是因为重复代码漏改引起的,修改重复代码又浪费了多少时间。这么坏的东西一定要深恶痛绝!书中归纳了几种常见的重复类型:
强加的重复(imposed duplication)。开发者觉得他们无可选择——环境似乎要求重复。强加的重复细分为四类:
信息的多种表示
。举个例子,QT的语言源文件是(.ts文件),会由QT工具编译为.qm文件提供给应用程序使用。现在PC千牛把这两个文件都提交到了SVN,而不是只提交.ts文件然后动态生成.qm文件。因为漏提交.qm文件已经出过几次文案显示异常的Bug。解决这类重复很简单,保证单一数据源,其它的表示方式都通过根据这个数据源自动生成。办法是有了,但真能保证做到吗?
Write Code That WritesCode
编写能编写代码的代码
代码中的文档
。DRY法则告诉我们,要把低级的知识放在代码中,它属于那里;把注释保留给其他的高级说明。否则,我们就是在重复知识,而每一次改变都意味着既要改变代码,也要改变注释。注释将不可避免地变得过时,而不可信任的注释比完全没有注释更糟。逻辑清楚的代码自身就是最好的注释,除非是诡异的商业需求、不得已的临时解决方案抑或是在困难问题前屈服后使用的非常规方案。所以只有糟糕的代码才需要许多注释。文档与代码
。程序员们通常都有乖乖写文档的经历,但往往很难坚持,总有一天代码更新了,因为各种各样的理由,文档没有同步。所以在准备提供文档时请下定决心与做出承诺:保证要与代码进行同步的更新。语言问题
。就像C++的.h和.cpp文件,声明与实现就在重复着相同的内容。为了达到模块实现与接口分离的目的,就会出现这类重复。没有简单的技术手段避免,好在信息不一致编译期间会有错误。理想的做法是接口文件能通过实现文件自动生成。
无意的重复(inadvertent duplication)。开发者没有意识到他们在重复信息。
有时,重复来自设计中的错误。
struct Line
{
Point start;
Point end;
double length;
};
第一眼看上去,这个类似乎是合理的。线段显然有起点和终点,并总是有长度(即使长度为零)。但这里有重复。长度是由起点和终点决定的:改变其中一个,长度就会变化。最好是让长度成为计算字段。在以后的开发过程中,你可以因为性能原因而选择违反DRY原则。这经常会发生在你需要缓存数据,以避免重复昂贵的操作时。其诀窍是使影响局部化。对DRY原则的违反没有暴露给外界:只有类中的方法需要注意“保持行为良好”。
把DRY原则
真正的消化,在设计时就会对这类无意的重复敏感,从源头上减少重复发生的可能。
无耐性的重复(impatient duplication)。开发者偷懒,他们重复,因为那样似乎更容易。每个项目都有时间压力,你会受到诱惑去拷贝代码来实现相似的功能,总是没有时间去抽象出组件或者公用函数。如果你觉得受到诱惑,想一想古老的格言:“欲速则不达”,“磨刀不误砍柴功”。“想一想围绕着Y2K惨败的种种问题。其中许多问题是由开发者的懒惰造成的:他们没有参数化日期字段的尺寸,或是实现集中的日期服务库。”
开发者之间的重复(interdeveloper duplication)。同一团队(或不同团队)的几个人重复了同样的信息。在高层,可以通过清晰的设计、强有力的技术项目领导(参见288页“注重实效的团队”一节中的内容)、以及在设计中进行得到了充分理解的责任划分,对这个问题加以处理。我们觉得,处理这个问题的最佳方式是鼓励开发者相互进行主动的交流。想想散落在外的,数不清的旺旺版本,这何尝不是团队之间的重复呢?组件化的思考方式能缓解这个问题,在推进业务的同时,沉淀一些基础库与组件服务。之前在B2B积累的各种客户端组件,现在不就帮助PC千牛迅速变得强壮了吗?
Make It Easy to Reuse
让复用变得容易
你所要做的是营造一种环境,在其中要找到并复用已有的东西,比自己编写更容易。如果不容易,大家就不会去复用。而如果不进行复用,你们就会有重复知识的风险。
时间耦合
时间是软件架构的一个常常被忽视的方面,吸引我们的时间只是进度表上的时间。作为软件自身的一种设计要素,时间有两个方面对我们很重要:并发和次序。我们在编程时,通常并没有把这两个方面放在心上。当人们最初坐下来开始设计架构、或是编写程序时,事情往往是线性的,那是大多数人的思考方式——总是先做这个,然后再做那个。但这样思考会带来时间耦合:在时间上的耦合,方法A必须总在方法B之前调用,“嘀”必须在“嗒”之前发生。
程序在时序性上的依赖是客观存在的,我们需要做的是
:
1. 尽量减少不必要的时序依赖以提高并发能力;
2. 保证真正需要的时序依赖不存在被破坏的可能。人们通常会通过文档说明时序的依赖,就像MSDN中会写明使用COM之前必须调用CoInitialize()一样。但实际开发中时序上依赖通常会成为潜规则,只有当初开发的人自己知道,对后面维护的人来讲这就会是定时炸弹。对不得已的时序依赖一定要写入文档或者标明注释。
正交性
正交性”是从几何学中借来的术语。如果两条直线相交成直角,它们就是正交的。在计算技术中,该术语用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。
Eliminate Effects BetweenUnrelated Things
消除无关事物之间的影响
如果你编写正交的系统,你得到两个主要好处:提高生产率与降低风险。贯彻正交性原则可以促进组件化与复用;可以有效缩小错误代码影响的范围;更有利于单元测试。你也可以对项目团队的正交性进行衡量:只要看一看,在讨论每个所需改动时需要涉及多少人。人数越多,团队的正交性就越差。显然,正交的团队效率也更高(尽管如此,我们也鼓励子团队不断地相互交流)。
正交性与DRY原则紧密相关
。运用DRY原则,你是在寻求使系统中的重复降至最小;运用正交性原则,你可降低系统的各组件间的相互依赖。这样说也许有点笨拙,但如果你紧密结合DRY原则、运用正交性原则,你将会发现你开发的系统会变得更为灵活、更易于理解、并且更易于调试、测试和维护。
这本书花了很大的篇幅讲述DRY原则和正交性(也就是解耦),也提供了很多有实践意义的方法。回想一下设计模式,很多模式也正是为了解决这两个问题。这两个原则大家一定都耳熟能详,这里引用序言书评中的一句话:“能不能让正确的原则指导正确的行为本身,其实就是区分是否是高手的一个显著标志”。知道很容易,尝试在日常开发中去实践从而真正内化,最终达到运用自如。
我们认为违反这两个原则的设计和实现就是“破窗户“
。在确保自己不产生的同时,也要留意现有代码,发现问题抛出来,大家一起讨论如何优化何时优化(优化有风险,重构需谨慎)。最终要么消灭,要么确保一定被记录在案(把破窗口先用木板暂时封起来)。千万不要看到糟糕的代码皱皱眉、抱怨两句就结束了,把它放到TODO List里面!
重构
随着程序的演化,我们有必要重新思考早先的决策,并重写部分代码。这一过程非常自然。代码需要演化;它不是静态的事物。
无论代码具有下面的哪些特征,你都应该考虑重构代码:重复;非正交的设计;过时的知识(最典型的就是需求已经下线、方案已经改变,但过时代码却还残留甚至运行);性能问题。
人们通常用肿瘤来比喻重构的必要性,在现实的时间压力面前,需要做出正确的选择。追踪需要重构的事物。如果你不能立刻重构某样东西,就一定要把它列入计划。确保受到影响的代码使用者知道该代码计划要重构,以及这可能会怎样影响他们。
Refactor Early, Refactor Often
早重构,常重构
书中给出了几点重构实践上的指导:
- 不要试图在重构的同时增加功能。
- 在开始重构前,确保你拥有良好的测试。
- 采取短小,深思熟虑的步骤。把整体重构工作认真的分解为独立、轻量的几个步骤,每个步骤完成都可以进行测试,这将有助于快速定位问题。
无处不在的自动化
让计算机去做重复、庸常的事情——它会做得比我们更好。我们有更重要、更困难的事情要做。
Don’t Use Manual Procedures
不要使用手工流程
自动化为我们带来两个显而易见的好处:避免重复劳动提高效率;保持可靠的一致性与可重复性,排除人工作操作可能产生的错误。可以自动化的项目包括但不限于:项目编译,回归测试,构建与发布,通过单一数据源生成数据的其它表示。
“鞋匠的孩子没鞋穿”。我们是程序员,是否在的日常工作中经常制作自动化工具?至少掌握一门高级脚本语言用于快速开发自制工具。
可撤销性
我们让本书的许多话题相互配合,以制作灵活、有适应能力的软件。通过遵循它们的建议——特别是DRY原则(26页)、解耦(138页)以及元数据的使用(144页)——我们不必做出许多关键的、不可逆转的决策。这是一件好事情,因为我们并非总能在一开始就做出最好的决策。我们采用了某种技术,却发现我们雇不到足够的具有必需技能的人。我们刚刚选定某个第三方供应商,他们就被竞争者收购了。与我们开发软件的速度相比,需求、用户以及硬件变得更快。
There Are No FinalDecisions
不存在最终决策
没有人知道未来会悄怎样,尤其是我们!所以要让你的代码学会“摇滚”:可以“摇”就“摇”,必须“滚”就“滚”。
需求变更,是永恒的话题
。变更往往又总是不可避免、总是迫在眉睫。在设计与编码时尽可能的注意并应用以上几个原则,会让我们面对变化从容不迫,甚至可以达到“中流换马(change horses in midstream)”的灵活性。
元程序设计
细节会弄乱我们整洁的代码——特别是如果它们经常变化
。每当我们必须去改动代码,以适应商业逻辑、法律或管理人员个人一时的口味的某种变化时,我们都有破坏系统或引入新bug的危险。所以我们说“把细节赶出去!”把它们赶出代码。当我们在与它作斗争时,我们可以让我们的代码变得高度可配置和“柔软”——就就是,容易适应变化。
要用元数据(metadata)描述应用的配置选项:调谐参数、用户偏好、安装目录等等。元数据是数据的数据,最为常见的例子可能是数据库schema或数据词典。
Configure,Don’t Integrate
要配置,不要集成
但我们不只是想把元数据用于简单的偏好。我们想要尽可能多地通过元数据配置和驱动应用:为一般情况编写程序,把具体情况放在别处——在编译的代码库之外。
Put Abstractions in Code,Details in Metadata
将抽象放进代码,细节放进元数据
曳(yè)光弹
译著中对曳光弹的描述有点难懂,百科中的解释:曳光弹是一种装有能发光的化学药剂的炮弹或枪弹,用于指示弹道和目标。曳光弹在光源不足或黑暗中可显示出弹道,协助射手进行弹道修正,甚至作为指引以及联络友军攻击方向与位置的方式与工具。
这个类比也许有点暴力,但它适用于新的项目,特别是当你构建从未构建过的东西时。与枪手一样,你也设法在黑暗中击中目标。因为你的用户从未见过这样的系统,他们的需求可能会含糊不清。因为你在使用不熟悉的算法、技术、语言或库,你面对着大量未知的事物。同时,因为完成项目需要时间,在很大程度上你能够确知,你的工作环境将在你完成之前发生变化。
经典的做法是把系统定死。制作大量文档,逐一列出每项需求、确定所有未知因素、并限定环境。根据死的计算射击。预先进行一次大量计算,然后射击并企望击中目标。
然而,注重实效的程序员往往更喜欢使用曳光弹。
Use Tracer Bullets toFind the Target
用曳光弹找到目标
曳光代码并非用过就扔的代码:你编写它,是为了保留它。它含有任何一段产品代码都拥有的完整的错误检查、结构、文档、以及自查。它只不过功能不全而已。但是,一旦你在系统的各组件间实现了端到端(end-to-end)的连接,你就可以检查你离目标还有多远,并在必要的情况下进行调整。一旦你完全瞄准,增加功能将是一件容易的事情。
曳光开发与项目永不会结束的理念是一致的:总有改动需要完成,总有功能需要增加。这是一个渐进的过程。
曳光开发其实大家或多或少都在参与。新项目创建时搭建框架代码,逐渐为框架添加功能正是这样一个过程。我们会在框架中让关键流程能够运行,以检验新技术在真实环境中的表现与预研的结果是否一致;检验整体设计是否有明显的性能问题;让用户尽早看到可工作的产品以提供反馈;为整个团队提供可以工作的结构与集成平台,大家只需要关心增加功能代码让框架更丰满。
曳光开发和原型模式有明显区别。原型中的代码是用过就扔的,寻求以最快的速度展示产品,甚至会采用更高级的语言。曳光代码虽然简约,但却是完成的,它拥有完整的错误检查与异常处理,只不过是功能不全而已。
Bug与Debug
自从14世纪以来,bug一词就一直被用于描述“恐怖的东西”。COBOL的发明者,海军少将Grace Hopper博士据信观察到了第一只计算机bug——真的是一只虫子,一只在早期计算机系统的继电器里抓到的蛾子。在被要求解释机器为何未按期望运转时,有一位技术人员报告说,“有一只虫子在系统里”,并且负责地把它——翅膀及其他所有部分——粘在了日志簿里。
调试的心理学
发现了他人的bug之后,你可以花费时间和精力去指责让人厌恶的肇事者。但bug是你的过错还是别人的过错,并不是真的很有关系。它仍然是你的问题。
Fix the Problem, Not theBlame
要修正问题,而不是发出指责
人很容易恐慌,特别是如果你正面临最后期限的到来、或是正在设法找出bug的原因,有一个神经质的老板或客户在你的脖子后面喘气。但非常重要的事情是,要后退一步,实际思考什么可能造成你认为表征了bug的那些症状。
Don’t Panic
不要恐慌
bug有可能存在于OS、编译器、或是第三方产品中——但这不应该是你的第一想法。有大得多的可能性的是,bug存在于正在开发的应用代码中。记住,如果你看到马蹄印,要想到马,而不是斑马(这个比喻太棒了!)。OS很可能没有问题。数据库也很可能情况良好。
我们参加过一个项目的开发,有位高级工程师确信select系统调用在Solaris上有问题。再多的劝说或逻辑也无法改变他的想法(这台机器上的所有其他网络应用都工作良好这一事实也一样无济于事)。他花了数周时间编写绕开这一问题的代码,因为某种奇怪的原因,却好像并没有解决问题。当最后被迫坐下来、阅读关于select的文档时,他在几分钟之内就发现并纠正了问题。现在每当有人开始因为很可能是我们自己的故障而抱怨系统时,我们就会使用“select没有问题”作为温和的提醒。
Select” Isn’t Broken
“Select”没有问题
基于越是新添加的代码越可能引起问题的怀疑,书中推荐了二分查找的方法不断缩小范围,最终定位问题。这方法看起来很老土,但实践中往往很有效,在毫无头绪时不妨试一试。
在发现某个bug让你吃惊时(也许你在用我们听不到的声音咕哝说:“那不可能。”),你必须重新评估你确信不疑的“事实”。某样东西出错时,你感到吃惊的程度与你对正在运行的代码的信任及信心成正比。这就是为什么,在面对“让人吃惊”的故障时,你必须意识到你的一个或更多的假设是错的。不要因为你“知道”它能工作而轻易放过与bug有牵连的例程或代码。证明它。用这些数据、这些边界条件、在这个语境中证明它。
说到让人惊讶的bug,最近刚好经历了一次。关于PC千牛插件最大化行为的bug,我和杯酒电话中如何讨论都无法理解对方,最后到现场看了才明白。这个问题只会发作在低分辨率的电脑上,他是便携笔记本分辨率低,而我是高分屏的开发机。如果你目睹bug或见到bug报告时的第一反应是“那不可能”,你就完全错了。一个脑细胞都不要浪费在以“但那不可能发生”起头的思路上,因为很明显,那不仅可能,而且已经发生了。
Don’t Assume it– Prove It
不要假定,要证明
断言式编程
在自责中有一种满足感。当我们责备自己时,会觉得再没人有权责备我们。
——奥斯卡·王尔德:《多里安·格雷的画像》
每一个程序员似乎都必须在其职业生涯的早期记住一段曼特罗(mantra)。它是计算技术的基本原则,是我们学着应用于需求、设计、代码、注释——也就是我们所做的每一件事情——的核心信仰。那就是:这决不会发生……
“这些代码不会被用上30年,所以用两位数字表示日期没问题。”“这个应用决不会在国外使用,那么为什么要使其国际化?”“count不可能为负。”“这个printf不可能失败。”我们不要这样自我欺骗,特别是在编码时。
If It Can’t Happen, Use Assertions to Ensure That It Won’t
如果它不可能发生,用断言确保它不会发生
断言可能会引起副作用,因为断言可能会在编译时被关闭——决不要把必须执行的代码放在assert中。这个问题就是一种“海森堡虫子”(Heisenbug)——调试改变了被调试系统的行为。
断言的好处不言而喻,可以提高调试的效率,可以尽早的发现问题。调试的时候应该保持对断言敏感,如果自己没有时间去调查断言发生的原因,也应该把问题抛出来及时解决。如果对断言视而不见,也就失去了断言的意义。可以考虑在输出错误日志的方法中直接加入断言,往往需要记录错误的问题也是我们认为不应该发生或者需要引起关注的问题。到现在我还清晰的记得以前的一个Bug就是因为断言副作用引起的,因为我写了这样的代码:ASSERT(SUCCEEDED(Initialize()));,调试时一切正常,当以release编译发布测试包时就出现了问题。
靠巧合编程
你有没有看过老式的黑白战争片?一个疲惫的士兵警觉地从灌木丛里钻出来,前面有一片空旷地:那里有地雷吗?还是可以安全通过?没有任何迹象表明那是雷区——没有标记、没有带刺的铁丝网、也没有弹坑。士兵用他的刺刀戳了戳前方的地面,又赶紧缩回来,以为会发生爆炸。没有,于是他紧张地向前走了一会儿,刺刺这里,戳戳那里。最后,他确信这个地方是安全的,于是直起身来,骄傲地正步向前走去,结果却被炸成了碎片。士兵起初的探测没有发现地雷,但这不过是侥幸。他由此得出了错误的结论——结果是灾难的。
作为开发者,我们也工作在雷区里,每天都有成百的陷阱在等着抓住我们。记住士兵的故事,我们应该警惕,不要得出错误的结论。我们应该避免靠巧合编程——依靠运气和偶然的成功——而要深思熟虑地编程。
Don’t Program by Coincidence
不要靠巧合编程
书中提到两种依靠巧合编程的典型:实现的偶然与隐含的假定。实现的偶然就是在使用新技术、三方库或者其它人写的模块时,拼凑的代码碰巧工作了,那么我们就宣告胜利结束编码。当这些代码出问题时,通常会一头雾水,因为当初根本不明白它为什么会工作。隐含的假定是开发者使用自以为的前提,而实际上没有任何文档或者现实数据可以依靠。我曾经遇到过这样让人哭笑不得的经历:代码依赖了某个存在已久的bug的错误表现,当这个bug最终被修复时,原本运行良好的代码反而出现了问题。我们常说“踩坑”,这些坑可能是前人用巧合编程留下的,也可能是因为我们依靠了巧合编程而引起的。
避免实现的偶然,要求我们谨慎对待不熟悉的依赖,仔细阅读文档,代码虽然可以工作,但并不一定正确。避免隐含的假定,要求我们只依靠可靠的事物,针对暂时无法获知的可能,代码要以最坏的假定来对待,不能给自己盲目的乐观的条件。下次有什么东西看起来能工作,而你却不知道为什么,要确定它不是巧合。
书中另一个主题“邪恶的向导”,适合在这里提一下。向导产生的代码往往和我们编写的代码交织在一起,这要求我们去理解它,否则我们怎么敢去依靠它来让代码工作呢?
Don’t Use Wizard Code You Don’t Understand
不要使用你不理解的向导代码
需求之坑
Don't Gather Requirements- Dig for Them
不要搜集需求——挖掘它们
用户的需求描述可能是:只有员工的上级和人事部门才可以查看员工的档案。经过挖掘的需求:只有指定的人员才能查看员工档案。前者把规则硬性的写入了需求,但规则经常会改变。后者的优点是需求描述为一般性陈述,规则独立描述,这样规则可以成为应用中的元数据。在实现时可以把需求理解为:只有得到授权的用户可以访问员工档案,开发者就可能会实现某种访问控制系统。规则改变时,只有系统的元数据需求更新,以这样的角度去实现需求,得到的自然就是支持元数据、得到了良好分解的系统。但也要注意避免过度设计,需求也许就是那么简单。
Abstractions Live Longerthan Details
抽象比细节活得更长久
“投资”于抽象,而不是实现。抽象能在来自不同的实现和新技术的变化的“攻击”之下存活下去。书中反复举了Y2K问题的例子,认为其发生有两个主要原因:没有超出当时的商业实践往前看,以及对DRY原则的违反。即使需求要求把两个数字的年份用于数据输入、报表、以及存储,本来也应该设计一种DATE抽象,“知道”两个数据的年份只是真实日期的一种缩略形式。
极大的期望
如果你和用户紧密协作,分享他们的期望,工同他们交流你正在做的事情,那么当项目交付时,就不会发生多少让人吃惊的事情了。这是一件糟糕的事情。要设法让你的用户惊讶。请注意,不是惊吓他们,而是要让他们产高兴。给他们的东西要比他们期望的多一点。
Gently Exceed Your Users’ Expectations
温和地超出用户的期望
做到这一点的前提是要理解用户的期望。可以借助“曳光弹”和“原型”与用户交流。永远不要把我们认为好的东西当成是用户想要的。
足够好的软件
欲求更好,常把好事变糟。
——李尔王 1.4
有一个老的笑话,说一家美国公司向一家日本制造商订购100 000片集成电路。规格说明中有次品率:10 000片中只能有1片。几周过后订货到了:一个大盒子,里面装有数千片IC,还有一个小盒子,里面只装有10片IC。在小盒子上有一个标签,上面写着:“这些是次品”。要是我们真的能这样控制质量就好了。但现实世界不会让我们制作出十分完美的产品,特别是不会有无错的软件。时间、技术和急躁都在合谋反对我们。
软件何时“足够好”?客户会比开发人员更有发言权。他们可能尽早需要一个还可以的版本,但不想为了一个完美的版本再等上一年。虽然这里倡导我们不要追求极端的完美,但也不意味着我们可以交付充满瑕疵的半成品。引用耗子兄在《Rework》摘录及感想中的一段话:平衡Done和Perfect的方式正好就是这句话——“与其做个半成品,不好做好半个产品”,因为,一个半成品会让人绝望,而半个好产品会让人有所期望,这就是其中的不同
。