The Pragmatic Programmer
Ethan Yang Lv1

在《程序员修炼之道》一书中,Dave 和Andy将告诉我们怎样以一种我们能够遵循的方式编程。他们何以能这样聪明?他们不也是和其他序员一样,专注于各种细节而已吗?答案是他们在做某件事情时,会把注意力投注在他们在做的事情上——然后他们会试着把它做得更好。

设想你在参加 一个会议。或许你在想,这个会议没完没了,你还不如去写程序。而Dave 和 Andy 会想,他们为什么在开会,他们想知道是否可以通过另外的方式取代会议,并决定是否可 使某样事情自动化,以使开会的工作推后。然后他们就会这样去做。

这就是Dave 和Andy思考的方式。开会并非是某种使他们远离编程的事情。开会就是编程, 并且是能够加以改善的编程。我之所以知道他们以这样的方式思考,是因为这是书中的第 二条提示:思考你的工作。

那么再设想一下,他们这样思考了几年。很快他们就会拥有一堆解决方案。现在设想他们在工作中使用这些解决方案,又是几年;他们还放弃了其中太过困难、或者不能总是产生结果的解 决方案。噢,这样的途径几乎定义了“pragmatic” (注重实效)的含义。现在设想他们又用了一 两年来写下他们的解决方案。你也许会想,这些信息可真是金矿。你想对了。

Goals

  • 每年学习一门新语言

    不同的语言以不同的方式解决相同的问题。多学习几种不同的解决方法,能帮助自己拓宽思维,避免陷入陈规。此外,要感谢丰富的免费软件,让我们学习多种语言非常容易。

  • 每月读一本技术书

    虽然网络上有大量的短文和偶尔可靠的答案,但深入理解还是需要去读长篇的书。浏览书店页面后挑选和你当前项目主体相关的技术图书。一旦你养成习惯,就一个月读一本。在你掌握了当前正在使用的所有技术后,扩展你的领域,学习一些和你的项目不相关的东西。

  • 还要读非技术书

    记住,计算机是由人来使用的,你做的事情是为了满足人的需要,这非常重要。和你一起工作的是人,雇佣你的也是人,黑你的也是人。不要忘记方程式中的人的那一面。他需要完全不同的技能集(我们称这些为软技能,听起来很容易,但实际上他们很硬核,难以掌握)

  • 上课

    在本地大学或是网上找一些有趣的课程,或许也能在下一场商业会展或是技术会议上找到。

  • 加入本地的用户组和交流群

    不要只是去当听众,要主动参与。独来独往对你的职业生涯是致命的;了解一下公司之外的人们都在做什么。

  • 尝试不同的环境

    如果你只在Windows下工作,那么就花点时间在Linux上。如果你只使用简单的编辑器和Makefile,那就试试最新的炫酷复杂的IDE,反之亦然。

  • 与时俱进

    关心一下和你当前项目不同的技术,阅读相关的新闻和技术贴。这是一种很好的方式,可以了解用到那些不同技术的人的经验及他们所用的特殊术语,等等。

你是否在项目中使用过这些技术并不重要,甚至要不要把他们放在你的简历中也不重要。学习的过程将会扩展你的思维,为你打开全新可能性的大门,让你领悟新的做事方式。想法的交叉传授是很重要的;试着把你领悟到的东西应用到你当前的项目中。即使项目没有用到某项技术,你也可以借鉴一些方法。例如,熟悉面向对象,你就可以用不同的方式来编写朴素的C程序,理解函数式编程范式,就能用不同的方式来写Java等等。

批判性思维

  • 谁从中受益? 虽然听起来有点世俗,不过追踪钱的流动更容易理清脉络。其他人或其他组织的利益可能和你自己的一致,也可能不一致。

  • 有什么背景 每件事都发生在他自己的背景下,这也是为何“能解决所有问题”的方案通常不存在,而那些兜售“最佳实践”的书或文章实际上经不起推敲。“最适合谁”是一个值得考虑的好问题,类似的还有先决条件是什么、后果是什么,以及是短期的还是长期的。

  • 什么时候在哪里可以工作起来 在什么情况下?太晚了么?太早了么?不要停留在一阶思维下(接下来会发生什么),要进行二阶思考:当它结束后还会发生什么?

  • 为什么这是个问题? 是否存在一个基础模型?这个基础模型是怎么工作的?

Tips

A pragmatic philosophy

  • Provide Options, Don't Make Lame Excuses 提供各种选择,不要找蹩脚的借口

如果你确实同意要为某个结果负责,你就应切实负起责任。当你犯错误(就如同我们所有人都 会犯错误一样)、或是判断失误时,诚实地承认它,并设法给出各种选择。不要责备别人或别的东西,或是拼凑借口。除了尽你所能以外,你必须分析风险是否超出了你的控制。

  • Don’t Live with Broken Windows 不要容忍破窗户
不要留着“破窗户” (低劣的设计、错误决策、或是糟糕的代码)不修。发现一个就修一个。 如果没有足够的时间进行适当的修理,就用木板把它钉起来。或许你可以把出问题的代码放人注 释 (comment out), 或是显示“未实现”消息 ,或是用虚设的数据(dummy data) 加以替代 。 采取某种行动防止进一步的损坏,并说明情势处在你的控制之下。
  • Be a Catalyst for Change 做变化的催化剂
这正是拿出石头的时候。设计出你可以合理要求的东西,好好开发它。一旦完成,就拿给大家看,让他们大吃一惊。然后说:“要是我们增加...可能就会更好。”假装那并不重要。坐回椅子上,等着他们开始要你增加你本来就想要的功能。人们发现,参与正在发生的成功要更容易。 让他们瞥见未来,你就能让他们聚集在你周围’。
  • Remember the Big Picture 记住大图景

我们没有做过这个,但有人说,如果你抓一只青蛙放进沸水里,它会一下子跳出来。 但是,如果你把青蛙放进冷水里,然后慢慢加热,青蛙不会注意到温度的缓慢变化,会呆在锅里, 直到被煮熟。
  • Make Quality a Requirements Issue 使质量成为需求问题
你常常会处在须要进行权衡的情形中。让人惊奇的是 ,许多用户宁愿在今天用上有一些 “ 毛 边” 的软件,也不愿等待 一年后的多媒体版本。许多预算吃紧的T部门都会同意这样的说法。 今天的了不起的软件常常比明天的完美软件更可取。如果你给用户某样东西,让他们及早使用, 他们的反馈常常会把你引向更好的最终解决方案
  • Invest Regularly in Your Knowledge Portfolio 定期为你的知识资产投资

管理知识资产与管理金融资产非常相似:

  • 严肃的投资者定期投资——作为习惯
    • 就像金融投资一样,你必须定期为你的知识资产投资。即使投资量很小,习惯自身也和总量一样重要
  • 多元化是长期成功的关键
    • 你知道的不同的事情越多,你就越有价值。作为底线,你需要知道你目前所用的特 定技术的各种特性。但不要就此止步。计算技术的面貌变化很快— —今天的热门技术明天就可能变得近平无用(或至少是不再抢手)。你掌握的技术越多,你就越能更好地进行调整, 赶上变化 。
  • 聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的资产
    • 从高风险、可能有高回报,到低风险、低回报,技术存在于这样一条谱带上。把 你所有的金钱都投入可能突然崩盘的高风险股票并不是 一个好主意;你也不应太保守,错过可能的机会。不要把你所有的技术鸡蛋放在一个篮子里
  • 投资者设法低买高卖,以获取最大回报
    • 在新兴的技术流行之前学习它可能就和找到被低估的股票—样困难,但所得到的 就和那样的股票带来的收益一样。在Java 刚出现时学习它可能有风险,但对于现在已步入该领域的顶尖行列的早期采用者,这样做得到了非常大的回报。
  • 应周期性的重新评估和平衡资产
    • 这是一个非常动荡的行业。你上个月开始研究的热门技术现在也许已像石头一样冰冷。也许你需要重温你有一阵子没有使用的数据库技术。又或许,如果你之前试用过另一种语言,你就会更有可能获得那个新职位...…
  • Critically Analyze What You Read and Hear 批判地分析你读到的和听到的
  • It’s Both What You Say and the Way You Say It 你说什么和你怎么说同样重要

A pragmatic approach

  • Make It Easy to Reuse 让复用变得容易

你所要做的是营造 一种环境,在其中要找到并复用已有的东西,比自己编写更容易。如果不容易,大家就不会去复用。而如果不进行复用,你们就会有重复知识的风险。

  • Eliminate Effects Between Unrelated Things 消除无关事物之间的影响
如果你编写正交的系统,你得到两个主要好处:提高生产率与降低风险
  • There Are No Final Decisions 不存在最终决策
错误在于假定决策是浇铸在石头上的— 同时还在于没有为可能出现的意外事件做准备。 要把决策视为是写在沙滩上的,而不要把它们刻在石头上。大浪随时可能到来,把它们抹去。
  • Use Tracer Bullets to Find the Target 用曳光弹找到目标
曳光弹行之有效,是因为它们与真正的子弹在相同的环境、相同的约束下工作。它们快速飞 向目标,所以枪手可以得到即时的反馈。同时,从实践的角度看,这样的解决方案也更便宜。 为了在代码中获得同样的效果,我们要找到某种东西,让我们能快速、直观和可重复地从需求出发,满足最终系统的某个方面要求
  • Prototype to Learn 为了学习而制作原型
在构建原型时,你可以忽略哪些细节? - 正确性:你也许可以在适当的地方使用虚设的数据 - 完整性:原型也许只能在非常有限的意义上工作,也许只有一项预先选择的输人数据和一个菜单项。 - 健壮性。错误检查很可能不完整,或是完全没有。如果你偏离预定路径,原型就可能崩溃, 并在“烟火般的灿烂显示中焚毁”。这没有关系。 - 风格。在纸上承认这一点让人痛苦,但原型代码可能没有多少注释或文档。根据使用原型的经验,你也许会撰写出大量文档,但关于原型系统自身的内容相对而言却非常少。
  • Program Close to the Problem domain 靠近问题领域编程
无论是用于配置和控制应用程序的简单语言,还是用于指定规则或过程的更为复杂的语言, 我们认为,你都应该考虑让你的项目更靠近问题领域。通过在更高的抽象层面上编码,你获得了 专心解决领域问题的自由,并且可以忽略琐碎的实现细节。 记住,应用有许多用户。有最终用户,他们了解商业规则和所需输出;也有次级用户:操作人员、配置与测试管理人员、支持与维护程序员,还有将来的开发者。他们都有各自的问题领域, 而你可以为他们所有人生成小型环境和语言。
  • Estimate to Avoid Surprises 估算,以避免发生意外
  • Iterate the Schedule with the Code 通过代码对进度表进行迭代
一开始,你对需要多少次迭代、或是需要多少时间,也许只有模糊的概念。有些方法要求你 把这个作为初始计划的一部分定下来,但除了最微不足道的项目,这是 一个错误。除非你在开发与前一个应用类似的应用,拥有同样的团队和同样的技术,否则,你就只不过是在猜想。 于是你完成了初始功能的编码与测试,并将此标记为第 一轮增量开发的结束。基于这样的经 验,你可以提炼你原来对迭代次数,以及在每次选代中可以包含的内容的猜想。提炼会变得一次 比 一次好,对进度表的信心也将随之增长。

The Basic Tools

  • Keep Knowledge in Plain Text 用纯文本保存知识
  • Use the Power of Command Shells 利用命令 shell 的力量
  • Use a Single Editor Well 用好一种编辑器

选一种编辑器,彻底了解它 ,并将其用于所有的编辑任务。如果你用一种编辑器(或一组键 绑定)进行所有的文本编辑活动,你就不必停下来思考怎样完成文本操纵:必需的键击将成为本能反应。编辑器将成为你双手的延伸;键会在滑过文本和思想时歌唱起来。这就是我们的目标。

  • Always Use Source Code Control 总是使用源码控制
总是。即使你的团队只有你一个人,你的项目只需一周时间;即使那是“用过就扔” 的原型; 即使你的工作对象并非源码;确保每样东西都处在源码控制之下——文档、电话号码表、给供应商的备忘录、makefie、构建与发布流程、烧制CD母盘的shell小脚本— 每样东西。我们例行 公事地对我们敲人的每一样东西进行源码控制(包括本书的文本)。即使我们不是在开发项目, 我们的日常工作也被安全地保存在仓库中。
  • Fix the Problem, Not the Blame 要修正问题,而不是发出指责
发现了他人的bug之后,你可以花费时间和精力去指责让人厌恶的肇事者。在有些工作环境 中,这是文化的一部分,并且可能是“疏通剂”。但是,在技术竞技场上,你应该专注于修正问题,而不是发出指责。
  • Don't Panic 不要恐慌
人很容易恐慌,特别是如果你正面临最后期限的到来、或是正在设法找出bug的原因,有一个神经质的老板或客户在你的脖子后面喘气。但非常重要的事情是,要后退一步,实际思考什么可能造成你认为表征了bug的那些症状。
  • "Select" Isn't Broken "Select 没有问题"
我们参加过一个项目的开发,有位高级工程师确信select 系统调用在Solaris 上有问题。再多 的劝说或逻辑也无法改变他的想法(这台机器上的所有其他网络应用都工作良好这一事实也一样无济于事)。他花了数周时间编写绕开这一问题的代码,因为某种奇怪的原因,却好像并没有解决问题。当最后被迫坐下来、阅读关于select 的文档时,他在几分钟之内就发现并纠正了问题。现在每当有人开始因为很可能是我们自己的故障而抱怨系统时,我们就会使用“select 没有问题” 作为温和的提醒。

如果你“只改动了一样东西”,系统就停止了工作,那样东西很可能就需要对此负责— 直接地或间接地,不管那看起来有多牵强。有时被改动的东西在你的控制之外 :OS 的新版本、编译器、数据库或是其他第三方软件都可能会毁坏先前的正确代码 。可能会出现新的bug。你先前 已绕开的bug得到了修正,却破坏了用于绕开它的代码。API 变了,功能变了;简而言之,这是全新的局面,你必须在这些新的条件下重新测试系统。所以在考虑升级时要紧盯着进度表;你可能会想等到下一次发布之后再升级。

  • Don't Assume it —— Prove It 不要假定,要证明

某样东西出错时,你感到吃惊的程度与你对正在运行的代码的信任及信心成正比。 这就是为什么,在面对“让人吃惊” 的故障时,你必须意识到你的 一个或更多的假设是错的。不 要因为你“知道” 它能工作而轻易放过与bug有牵连的例程或代码。证明它。用这些数据、这些边界条件、在这个语境中证明它。
  • Learn a Text Manipulation Language 学习一种文本操纵语言
  • Write Code That Writes Code 编写能编写代码的代码
被动代码生成器只运行 一次来生成结果。然后结果就变成了独立的— 它与代码生成器分离 了。 主动代码生成器在每次需要其结果时被使用。结果是用过就扔的——它总是能由代码生成器重新生成。主动代码生成器为了生成其结果,常常要读取某种形式的脚本或控制文件。

Pragmatic Paranoia

  • You Can't Write Perfect Software 你不可能写出完美的软件
  • Design with Contracts 通过合约进行设计
  • Crash Early 早崩溃

我们很容易掉进“它不可能发生” 这样 —种心理状态。我们中的大多数人编写的代码都不检 查文件是否能成功关闭,或者某个跟踪语句是否已按照我们的预期写出。而如果所有的事情都能 如我们所愿,我们很可能就不需要那么做— 这些代码在任何正常的条件都不会失败。但我们是 在防卫性地编程,我们在程序的其他部分中查找破坏堆栈的“淘气指针”,我们在检查确实加载了共享库的正确版本。 所有的错误都能为你提供信息。你可以让自己相信错误不可能发生,并选择忽略它。但与此 相反,注重实效的程序员告诉自己,如果有 一个错误,就说明非常、非常糟糕的事情已经发生了。

  • If It Can't Happen, Use Assertions to Ensure That It Won't 如果他不可能发生,用断言确保他不会发生
无论何时你发现自己在思考“但那当然不可能发生”,增加代码检查它。最容易的办法是使 用断言。
  • Use Exceptions for Exceptional Problems 将异常用于异常的问题
关于异常的问题之一是知道何时使用它们。我们相信,异常很少应作为程序的正常流程的一 部分使用;异常应保留给意外事件。假定某个未被抓住的异常会终止你的程序,问问你自己:“如 果我移走所有的异常处理器,这些代码是否仍然能运行?” 如果答案是“否”,那么异常也许就 正在被用在非异常的情形中。
  • Finish What You Start 要有始有终
只要在编程,我们都要管理资源:内存、事务、线程、文件、定时器— 一所有数量有限的事 物。大多数时候,资源使用遵循一种可预测的模式:你分配资源、使用它,然后解除其分配。 但是,对于资源分配和解除分配的处理,许多开发者没有始终如 一的计划。

Bend, or Break

  • Minimize Coupling Between Modules 使模块之间的耦合减至最少

应该直接要求提供你所需的东西,而不是自行“挖通” 调用层次。函数的得墨忒耳法则试图使任何给定程序中的模块之间的耦合减至最少。它设法阻止 你为了获得对第三个对象的方法的访问而进入某个对象

  • Configure, Don't Integrate 要配置,不要集成
我们想要让我们的系统变得高度可配置。不仅是像屏幕颜色和提示文本这样的事物,而 且也包括诸如算法、数据库产品、中间件技术和用户界面风格之类更深层面的选择。这些选择应该 作为配置选项、而不是通过集成或工程(engineering)实现。要用元数据(meta data)描述应用的配置选项:调谐参数、用户偏好(user preference)、安装目录,等等。
  • Put Abstractions in Code, Details in Metadata 将抽象放进代码,细节放进元数据
- 它迫使你解除你的设计的耦合,从而带来更灵活、可适应性更好的程序。 - 它迫使你通过推迟细节处理,创建更健壮、更抽象的设计— 完全推迟到程序之外。 - 无需重新编译应用,你就可以对其进行定制。你还可以利用这 一层面的定制,轻松地绕开正在运行的产品系统中的重大bug。 - 与通用的编程语言的情况相比,可以通过 一种大为接近问题领域的方式表示元数据。 - 你甚至还可以用相同的应用引擎 —但是用不同的元数据— —实现若干不同的项目。
  • Analyze Workflow to Improve Concurrency 分析工作流,以改善并发性
我们需要容许并发,并考虑解除任何时间或次序上的依赖。这样做,我们可以获得灵活性, 并减少许多开发领域中的任何基于时间的依赖:工作流分析、架构、设计、还有部署。
  • Design Using Services 用服务进行设计
在饥饿的消费者模型中,你用一些独立的消费者任务和 一个集中式工作队列取代中央调度 〈 154] 器。各个消费者任务从工作队列中抓取一项,并对其进行处理。当各个任务完成其工作时,就回 到队列抓取下一项。这样,如果任何特定的任务陷入停顿,其他任务可以利用这 一空闲,并且各 个组件都可按自己的步伐前进。每个组件都在时间上解除了与其他组件的耦合。
  • Always Design for Concurrency 总是为并发进行设计
  • Separate Views from Models 使视图与模型分离
  • Use Blackboards to Coordinate Workflow 用黑板协调工作流

While You Are Coding

  • Don't Program by Coincidence 不要靠巧合编程

巧合可以在所有层面上让人误人歧途—从生成需求直到测试。特别是测试,充满了虚假的 因果关系和巧合的输出。很容易假定X是Y的原因,但正如我们之前所说的:不要假定,要证明。 在所有层面上,人们都在头脑里带着许多假定工作—一但这些假定很少被记人文档,而且在不同的开发者之间常常是冲突的。并非以明确的事实为基础的假定是所有项目的祸害。

  • Estimate the Order of Your algorithms 估算你的算法的阶
如果你不能确定代码需要多少时间,或是要使用多少内存,就试着运行它,变化输 人记录的数目,或可能影响运行时间的无论什么东西。随后把结果绘制成图。你应该很快就能了解到曲线的形状。随着输人量的增大,它是向上弯曲、是直线、还是保持平直?三个或四个点应该就能告诉你答案。
  • Test Your Estimates 测试你的估算
在整个理论当中,不要忘了一些实际的考虑。对于小输入集,运行时间看起来也许是在线性增长。但给代码馈人数百万条记录,随着系统开始颠簸,时间就会突然退化。如果你测试排序例程用的是随机的输入值,当它第一次遇到有序的输入时,你可能会很惊讶。注重实效的程序员会设法既考虑理论问题;又考虑实践问题。在进行所有这些估算之后,唯一作数的计时是你的代码 运行在实际工作环境中、处理真实数据时的速率。 如果要获得准确的计时很棘手,就用代码剖析器获得你的算法中的不同步骤的执行次数,并针对输入的规模绘制这些数字。
  • Refactor Early, Refactor Often 早重构,常重构
时间压力常常被用作不进行重构的借又。但这个借又并不成立:现在没能进行重构,沿途修 正问题将需要投人多得多的时间— 那时将需要考虑更多的依赖关系。我们会有更多的时间可用吗?根据我们的经验,没有。 追踪需要重构的事物。如果你不能立刻重构某样东西,就 一定要把它列入计划。确保受到影 响的代码的使用者知道该代码计划要重构,以及这可能会怎样影响他们。
  • Design to Test 为测试而设计
当你设计模块、甚或是单个例程时,你应该既设计其合约,也设计测试该合约的代码。通过 设计能够通过测试、并履行其合约的代码,你可以仔细地考虑边界条件和其他非如此便不会发现的问题。没有什么修正错误的方法比从一开始就避免发生错误更好。事实上,通过在你实现代码之前构建测试,你必须在你确定采用某个接口之前先对它进行试验。
  • Test Your Software, or Your Users Will 测试你的软件,否则你的用户就得测试
  • Don't Use Wizard Code You Don't Understand 不要使用你不理解的向导代码
没有人应该制作他们不完全理解的代码。

Before The Project

  • Don't Gather Requirements —— Dig for Them 不要搜集需求——挖掘他们

许多书籍和教程都把需求搜集当作项目的早期阶段。“ 搜集” 一词似乎在暗示,一群快乐的分析师,随着背景播放的温柔的《田园交响曲》,寻觅散布在四周地面上的智慧金块。“搜索” 暗示着需求已经在那里—你只需找到它们,把它们放进你的篮子,就可以愉快地上路了。 事情在很大程度上并非如此。需求很少存在于表面上。通常,它们深深地埋藏在层层假定、 误解和政治手段的下面。

  • Work with a User to Think Like a User 与用户一同工作,以像用户一样思考。
有一种能深入了解用户需求、却未得到足够利用的技术:成为用户。你在编写客户服务系统? 花几天时间与有经验的支持人员一起接听电话 。你正在使人工股票控制系统自动化?在交易所里 工作一周”。除了让你洞见系统实际上将如何被使用,你还会吃惊地发现,“我能否在你们工作时〈 204 在这里呆上一周?” 这个请求能怎样帮助你与用户建立信任和沟通的基础。只是要记住,不要妨碍他们的工作!
  • Abstractions Live Longer than Details 抽象比细节活得更长久
  • Use a Project Glossary 使用项目词汇表
要创建并维护项目词汇表(project glossary)——这是定义项目中使用的专用术语和词汇的 地方。项目的所有参与者,从最终用户到支持人员,都应该使用这个词汇表,以确保一致性。这就意味着,可以访问词汇表的人员范围应该很广泛——这是采用基于web 的文档的一个有效论据。
  • Don't Think Outside the Box —— Find the Box 不要在盒子外面思考——要找到盒子
在面对棘手的问题时,列出所有在你面前的可能途径。不要排除任何东西,不管它听起来有 多无用或愚蠢。现在,逐一检查列表中的每 一项,并解释为何不能采用某个特定的途径。你确定吗? 你能否证明? 对你的各种约束进行分类,并划定优先级。木匠开始做活路时,会首先锯出最长的木料,然后再从剩下的木头中锯出较小的木料。按照同样的方式,我们想先确定最为严格的约束,然后再在其中考虑其余的约束。
  • Listen to Nagging Doubts —— Start When You're Ready 倾听反复出现的疑虑——等你准备好再开始。
作为开发者,你一直在试验各种东西,看哪些可行, 哪些不可行。你一直在积累经验与智慧。当你面对一件任务时,如果你反复感觉到疑虑,或是体验到某种勉强,要注意它。你可能无法准确地指出问题所在,但给它时间,你的疑虑很可能就会结晶成某种更坚实的东西,某种你可以处理的东西。软件开发仍然不是科学。让你的直觉为你的表演做出贡献。
  • Some Things Are Better Done than Described 对有些事情"做"胜于"描述"
- 首先,认为规范将捕捉系统或其需求的每 一处细节和细微差别, 这很幼稚。在受限的问题领域中,有一些形式方法能够对系统进行描述,但他们仍然要求设计者向最终用户解释该表示方法的含义—— 人的解释仍然会搅乱事情。即使这样的解释中的固有问题并不存在, 一般用户也很可能无法准确地说出他们所需的系统。他们可能会说,他们已经理解了需求,他们可能会在你制作的200 页的文档上签字,但你可以确信, 一旦他们看到运行的系统,你就会被各种变更要求淹没。 - 其次,语言自身的表达能力存在着问题。所有的图示技术和形式方法都仍然依赖于用自然语言表达要进行的操作” 。而自然语言实在不能胜任这项工作。看一看任何合约的措词:为了进行精确的表达,律师不得不以最不自然的方式扭曲语言。
  • Don't Be a Slave to Formal Methods 不要做形式方法的奴隶
有些开发者,在有许多已沉没项目的大海里漂流,不断抓住最新的时尚,就像是遇到海难的人紧紧抓住漂来的木头一样。每当有新的木头飘过时,他们都会费力的游过去,希望这一块会更好。但到最后,不管漂浮物有多好,这些开发者仍然漫无目的地漂流着。 我们喜欢(有些)形式技术和方法。但我们相信,盲目的采用任何技术,而不把它放进你的开发实践和能力的语境中,这样的处方肯定会让你失望。
  • Expensive Tools Do Not Produce Better Designs 昂贵的工具不一定制作出更好的设计。
形式开发方法只是工具箱里的一种工具。如果在仔细分析之后, 你觉得需要使用形式方法,那就采用它— 但要记住谁是主人。不要变成方法学的奴隶: 圆圈与箭头会让你变成糟糕的主人。注重实效的程序员批判地看待方法学,并从各种方法学中提取精华,融合成每个月都在变得更好的一套工作习惯。 这至关紧要。你应该不断努力提炼和改善你的开发过程。决不要把方法学的呆板限制当做你的世界的边界。 不要向方法的虚假权威屈服。有人也许会带着大量类图和150个用例步入会议室,但所有那些纸张仍然只是他们对需求和设计的难免出错的解释。在考察工具的产出时,试着不要考虑它值多少钱。

Pragmatic Projects

  • Organize Around functionality, Not Job Functions 围绕功能、而不是工作职务进行组织

我们喜欢按照功能划分团队。把你的人划分成小团队,分别负责最终系统的特定方面的功能。 让各团队按照各人的能力,在内部自行进行组织。每个团队都按照他们约定的承诺,对项目中的其他团队负有责任。承诺的确切内容随项目而变化,团队间的人员分配也是如此。 这里的功能并不必然意味着最终用户的用例。数据库访问层是功能,帮助子系统也是功能。 我们是在寻求内聚的、在很大程度上自足的团队——和我们在使代码模块化时应该使用的标准完全一样。如果团队的组织是错误的,会有一些警告性迹象 —— 一个经典的例子是有两个子团队在做同 一个程序模块或类。

  • Don't Use Manual Procedures 不要使用手工流程
人的可重复性并不像计算机那么好。我们也不应期望他们能那样。shell脚本或批处理文件能以相同的次序、反复执行同样的指令。它们能被置于源码控制之下,你因而也可以检查流程的修改历史。

Test Early. Test Often. Test Automatically. 早测试,常测试,自动测试

- 寻找 bug有点像是用网捕鱼。我们用纤小的网(单元测试)捕捉小鱼,用粗大的网(集成测试)捕捉吃人的鲨鱼。有时鱼会设法逃跑,所以为了抓住在我们的项目池塘里游动的、越来越狡猾的缺陷,要补上我们发现的任何漏洞。 - 许多团队都会为他们的项目精心制订测试计划。有时他们甚至会使用这些计划。但我们发现,使用自动测试的团队成功的机会要大得多。与呆在架子上的测试计划相比,随每次构建运行的测试要有效得多。 - bug 被发现得越早,进行修补的成本就越低。“编一点,测一点”是Smalltalk 世界里流行的一句话,我们可以把这句话当作我们自己的曼特罗,在编写产品代码的同时(甚至更早)编写测试代码。 - 事实上,好的项目拥有的测试代码可能比产品代码还要多。编写这些测试代码所花的时间是值得的。长远来看,它最后会便宜得多,而你实际上有希望制作出接近零缺陷的产品。

Coding Ain't Done Til All the Tests Run 要到通过全部测试,编码才算完成

Use Saboteurs to Test Your Testing 通过“蓄意破坏”测试你的测试

因为我们不可能编写出完美的软件,所以我们也不可能编写出完美的测试软件。我们需要对测试进行测试。

Test State Coverage, Not Code Coverage

即使具有良好的代码覆盖,你用于测试的数据仍然会有巨大的影响,而且,更为重要的是,你遍历代码的次序的影响可能是最大的。

Find Bugs Once 一个bug 只抓一次

一旦测试人员找到了某个 bug,这应该是测试人员最后一次发现这个 bug。应该对自动化测试进行修改,从此每次都检查那个特定的 bug,没有例外,不管多琐碎,也不管开发者会怎样抱怨说:“哦,那决不会再发生了。” 因为它会再次发生。而我们完全没有时间去追踪自动化测试本可以为我们找到的 bug。我们必须把时间花在编写新的代码——以及新的 bug——上。

Treat English as Just Another Programming Language 把英语当作又一种编程语言

注重实效的程序员会把文档当作整个开发过程的完整组成部分加以接受。不进行重复劳动,不浪费时间,并且把文档放在手边——如果可能,就放在代码本身当中,文档的撰写就可以变得更容易。

Build Documentation In, Don't Bolt It On 把文档建在里面,不要拴在外面

为项目制作的文档基本上有两种:内部文档和外部文档。内部文档包括源码注释、设计与测试文档,等等。外部文档是发运或发布到外界的任何东西,比如用户手册。但不管目标受众是谁,也不管作者的角色是什么(开发者或技术文档撰写者),所有的文档都是代码的反映。如果有歧义,代码才最要紧——无论好坏。

Gently Exceed Your Users'Expectations 温和地超出用户的期望

在抽象的意义上,应用如果能正确实现其规范,就是成功的。遗憾的是,这只能付抽象的账。 在现实中,项目的成功是由它在多大程度上满足了用户的期望来衡量的。不符合用户预期的项目注定是失败的,不管交付的产品在绝对的意义上有多好。但是,像希望得到廉价洋娃娃的小孩的父母一样,你走得太远也会失败。

Sign Your Work 在你的作品上签名

注重实效的程序员不会逃避责任。相反,我们乐于接受挑战,乐于使我们的专业知识广为人知。如果我们在负责一项设计,或是一段代码,我们是在做可以引以自豪的工作。 匿名(尤其是在大型项目中)可能会为邋遢、错误、懒惰和糟糕的代码提供繁殖地。只把自己看作齿轮上的一个齿、在无休无止的状况报告中制造蹩脚的借口、而不去编写优良的代码,那太容易了。

调试

这是痛苦的事: 看着你自己的烦忧,并且知道 不是别人,而是你自己一人所致 ——索福克勒斯:《埃阿斯》

调试的心理学

要接受事实:调试就是解决问题,要据此发起进攻。 bug 是你的过错还是别人的过错,并不是真的很有关系。它仍然是你的问题。

调试的思维方式

最容易欺骗的人就是自己 —— Edward Bulwer-Lytton, The Disowned

在你开始调试之前,选择恰当的思维方式十分重要。你须要关闭每天用于保护自我(ego) 的许多防卫措施,忘掉你可能面临的任何项目压力,并让自己放松下来。

在调试时小心“近视”。要抵制只修正你看到的症状的急迫愿望:更有可能的情况是,实际 的故障离你正在观察的地方可能还有几步远,并且可能涉及许多其他的相关事物。要总是设法找 出问题的根源,而不只是问题的特定表现。

从何处开始

在开始查看bug之前,要确保你是在能够成功编译的代码上工作——没有警告。我们例行公 事地把编译器警告级设得尽可能高。把时间浪费在设法找出编译器能够为你找出的问题上没有意义!我们需要专注于手上更困难的问题。

  • 你也许需要与报告bug 的用户面谈,以搜集比最初给你的数据更多的数据
  • 人工合成的测试不能足够地演练 (exercise)应用 。 你必须既强硬地测试边界条件,又测试现实中的最终用户的使用模式。你需要系统地进行这样的测试。

造成惊讶的要素

当你遇到让人吃惊的bug时,除了只是修正它而外,你还需要确定先前为什么没有找出这个 故障。考虑你是否需要改进单元测试或其他测试,以让它们有能力找出这个故障。

还有,如果bug是一些坏数据的结果,这些数据在造成爆发之前传播通过了若千层面,看一 看在这些例程中进行更好的参数检查是否能更早地隔离它。

在你对其进行处理的同时,代码中是否有任何其他地方容易受这同 一个bug的影响?现在就是 找出并修正它们的时机。确保无论发生什么,你都知道它是否会再次发生。

如果修正这个bug需要很长时间,问问你自己为什么。你是否可以做点什么,让下一次修正 这个bug变得更容易?也许你可以内建更好的测试挂钩,或是编写日志文件分析器。

最后,如果bug 是某人的错误假定的结果,与整个团队一起讨论这个问题。如果 一个人有误 解,那么许多人可能也有。

去做所有这些事情,下一次你就将很有希望不再吃惊。

无情的测试

单元测试

单元测试是对某个模块进行演练的代码。单元测试是我们将在本节讨论的所有其他形式的测试的基础。如果各组成部分自身不能工作,它们结合在一起多半也不能工作。你使用的所有模块都必须通过它们自己的单元测试,然后你才能继续前进。

集成测试

集成测试说明组成项目的主要子系统能工作,并且能很好地协同。如果在适当的地方有好的合约,并且进行了良好的测试,我们就可以轻松地检测到任何集成问题。否则,集成就会变成肥沃的 bug 繁殖地。事实上,它常常是系统的 bug 来源中最大的一个。 集成测试实际上只是我们描述过的单元测试的一种扩展——只不过现在你在测试的是整个子系统遵守其合约的情况。

验证和校验

一旦你有了可执行的用户界面或原型,你需要回答一个最重要的问题:用户告诉了你他们需要什么,但那是他们需要的吗? 它满足系统的功能需求吗?这也需要进行测试。没有 bug、但回答的问题本身是错误的,这样的系统不会太有用。要注意用户的访问模式(access pattern),以及这些模式与开发者所用的测试数据的不同。

资源耗尽、错误及恢复

现在你已经很清楚,系统在理想条件下将会正确运行,你需要知道的是,它在现实世界的条件下将如何运行。在现实世界中,你的程序没有无限的资源;它们会把资源耗尽。你的代码可能遇到的一些限制包括: 内存空间、磁盘空间、CPU带宽、挂钟时间、磁盘带宽、网络带宽、调色板、视频分辨率

  • 你可能会实际检查磁盘空间或内存分配的失败,但是否经常检查其他各项呢?你的应用适用于有256种颜色的640×480 的屏幕吗?它能在有24位颜色的1600×1280的屏幕上运行,而不会看上去像一张邮票?批处理是否会在存档开始之前结束?
  • 你可以检测环境的限制,比如视频参数,并进行相应的调整。但是,不是所有失败都是可以恢复的。如果你的代码检测到内存已经耗尽,你的选择有限:除了失败,你也许没有足够的资源去做任何事情。
  • 当系统确实失败时,它会得体地失败吗?它会尽可能设法保存其状态、防止工作丢失吗?或是会当着用户的面造成“GPF”(General Protection Fault)或“core-dump”?

性能测试

性能测试、压力测试或负载测试也可能会是项目的一个重要方面。 问问你自己,软件是否能满足现实世界的条件下的性能需求——预期的用户数、连接数、或每秒事务数。它可以伸缩吗? 对于有些应用,你可能需要用专门的测试硬件或软件模拟现实情况下的负载。

可用性测试

可用性测试与到目前为止讨论过的其他测试类型不同。它是由真正的用户、在真实的环境条件下进行的。

根据人的因素考察可用性。需要处理需求分析过程中的任何误解吗?软件对于用户,就像是手的延伸吗?(我们不仅想让自己的工具顺手,也想让我们为用户创建的工具让他们觉得顺手。)

与验证与校验的情况一样,你需要尽早在还有时间更正时进行可用性测试。对于较大的项目,你可以引入人员因素(human factor)专家。(至少,玩一玩单向镜也很有意思。)

没能满足可用性标准就像是除零错误,是个大 bug。

代码中的注释

一般而言,注释应该讨论为何要做某事、它的目的和目标。代码已经说明了它是怎样完成的,所以再为此加上注释是多余的——而且违反了 DRY原则。 注释源码给你了完美的机会,让你去把项目的那些难以描述、容易忘记,却又不能记载在别的任何地方的东西记载下来:工程上的权衡、为何要做出某些决策、放弃了哪些替代方案,等等。 我们喜欢看到简单的模块级头注释、关于重要数据与类型声明的注释、以及给每个类和每个方法所加的简要头注释、用以描述函数的用法和任何不明了的事情。 你可以为参数建立文档,但问问你自己,这是否在所有情况下都真的有必要。JavaDoc 工具提倡的注释程度似乎是合适的。

下面是不应出现在源码注释中的一些内容: - 文件中的代码导出的函数的列表。有些程序可以为你分析源码。使用它们,列表就保证是最新的。 - 修订历史。这是源码控制系统的用途所在。但是,在注释中包括最后更改日期和更改人的信息可能是有用的。 - 该文件使用的其他文件的列表。使用自动工具可以更准确地确定这些信息。 - 文件名。如果在文件中必须出现文件名,不要手工进行维护。RCS 和类似的系统可以自动使这一信息保持最新。如果你移动文件或更改文件名,你不会希望必须记得编辑头注释。 在源文件里应该出现的最重要的信息之一是作者的姓名——不一定是最后编辑文件的人,而是文件的所有者。使责任和义务与源码联系起来,能够奇迹般地使人保持诚实。

Others

下面是一些你可以在架构原型中寻求解答的具体问题: - 主要组件的责任是否得到了良好定义?是否适当? - 主要组件间的协作是否得到了良好定义? - 耦合是否得以最小化? - 你能否确定重复的潜在来源? - 接又定义和各项约束是否可接受? - 每个模块在执行过程中是否能访问到其所需的数据?是否能在需要时进行访问? 根据我们制作原型的经验,最后一项往往会产生最让人惊讶和最有价值的结果。

如果这听起来像你... 那么考虑...
我使用许多不同的编辑器,但只使用其基本特性 选一种强大的编辑器,好好学习他
我有最喜欢的编辑器,但不使用其全部特性 学习他们。减少你需要敲击的键数
我有最喜欢的编辑器,只要可能就使用它 设法扩展它,并将其用于比现在更多的任务

代码生成器

无论何时你发现自己在设法让两种完全不同的环境 一起工作,你都应该考虑使用主动代码生 成器。

按合约设计

  • 前条件:为了调用例程,必须为真的条件;例程的需求。在其前条件被违反时 ,例程决不应被调用 。传递好数据是调用者的责任。
  • 后条件: 例程保证会做的事情,例程完成时世界的状态。例程有后条件这一事实意味着它会结束 :不允许有无限循环。
  • 类不变项 (class invariant)。类确保从调用者的视角来看,该条件总是为真。在例程的内部处理过程中,不变项不一定会保持,但在例程退出、控制返回到调用者时,不变项必须为真 (注意,类不能给出无限制的对参与不变项的任何数据成员的写访问)。

嵌套的分配

对于一次需要不只一个资源的例程,可以对资源分配的基本模式进行扩展。有两个另外的建议: - 以与资源分配的次序相反的次序解除资源的分配。这样,如果一个资源含有对另一个资源的引用,你就不会造成资源被遗弃。 - 在代码的不同地方分配同一组资源时,总是以相同的次序分配它们。这将降低发生死锁的可能性。(如果进程A申请了resource2 ,并正要申请resource2,而进程B申请了resource2, 并试图获得resource1 ,这两个进程就会永远等待下去。) - 无论是谁分配的资源,它都应该负责解除该资源的分配

函数的得墨忒耳法则

image 使用得墨忒耳法则将使你的代码适应性更好、更健壮,但也有代价 :作为“总承包人”,你的模块必须直接委托并管理全部子承包人,而不牵涉你的模块的客户。在实践中,这意味着你将 会编写大量包装方法,它们只是把请求转发给被委托者。这些包装方法既会带来运行时代价,也 会带来空间开销,在有些应用中,这可能会有重大影响— 甚至会让你无法承受。

与任何技术一样,你必须平衡你的特定应用的各种正面因素和负面因素。在数据库schema 设 计中,常常会为了改善性能而对schema进行“反规范化”:违反规范化规则,以换取速度。在这 里也可进行类似的折衷。事实上,通过反转得墨忒耳法则,使若干模块紧密耦合,你可以获得重 大的性能改进。只要对于那些被耦合在 一起的模块而言,这是众所周知的和可以接受的,你的设计就没有问题。

为并发进行设计

首先,必须对任何全局或静态变量加以保护,使其免于并发访问。现在也许是问问你自己、 你最初为何需要全局变量的好时候。此外,不管调用的次序是什么,你都需要确保你给出的是一致的状态信息。例如,何时查询你的对象的状态才是有效的?如果你的对象在某些调用之间处在无效状态,你也许就是在依赖一个巧合:没有人会在那个时间点调用你的对象。 假定你有一个窗口子系统,其中的widget 是先创建,再显示在显示屏上,分两个步骤进行。在其显示出来之前,你不能设置 widget 中的状态。取决于代码的设置方式,你可能会依靠这样一个事实:在你将其显示在屏幕上之前,其他对象都不会使用已创建的widget。 但这在并发系统中可能并不为真。在被调用时,对象必须总是处在有效的状态中,而且它们可能会在最尴尬的时候被调用。你必须确保,在任何可能被调用的时刻,对象都处在有效的状态中。这 一问题常常出现在构造器与初始化例程分开定义的类中(构造器没有使对象进入已初始化状态)。

发布-订阅模式

尽管在典型情况下,MVC是在GUI 开发的语境中教授的,它其实是一种通用的编程技术。 视图是对模型(也许是其子集)的一种解释——它无需是图形化的。控制器更是 一种协调机制, 不一定要与任何种类的输入设备有关。 - 模型:表示目标对象的抽象数据模型。模型对任何视图或控制器都没有直接的了解。 - 视图:解释模型的方式。它订阅模型中的变化和来自控制器的逻辑事件。 - 控制器:控制视图、并向模型提供新数据的途径。它既向模型、也向视图发布事件。 黑板方法的 一些关键特性是: - 没有侦探需要知道其他任何侦探的存在—他们查看黑板,从中了解新的信息,并且加上他们的发现。 - 侦探可能接受过不同的训练 ,具有不同程度的教育背景和专业经验,甚至有可能不是在同一 管辖区工作。他们都渴望破案,但这就是全部共同点。 - 在这个过程中,不同的侦探可能会来来去去,并且工作班次也可能不同。 - 对放在黑板上的内容没有什么限制。可以是图片、判断、物证,等等。 黑板。数据到达的次序无关紧要:在收到某项事实时,它可以触发适当的规则。反馈也很容易处理:任何规则集的输出都可以张贴到黑板上,并触发更为适用的规则。

实现的偶然

  • 它也许不是真的能工作——它也许只是看起来能工作。
  • 你依靠的边界条件也许只是一个偶然。在不同的情形下(或许是不同的屏幕分辨率),它的表现可能就会不同。
  • 没有记入文档的行为可能会随着库的下一次发布而变化。
  • 多余的和不必要的调用会使你的代码变慢。
  • 多余的调用还会增加引人它们自己的新bug 的风险。 对于你编写给别人调用的代码,良好的模块化以及把实现隐藏在撰写了良好文档的小接口之后,这样 一些基本原则都能对你有帮助。

怎样深思熟虑的编程

我们想要让编写代码所花的时间更少,想要尽可能在开发周期的早期抓住并修正错误,想要 在一开始就少制造错误。如果我们能深思熟虑地编程,那对我们会有所帮助: - 总是意识到你在做什么。 - 不要盲目地编程。试图构建你不完全理解的应用,或是使用你不熟悉的技术,就是希望自己被巧合误导。 - 按照计划行事,不管计划是在你的头脑中,在鸡尾酒餐巾的背面,还是在某个CASE 工具生成的墙那么大的输出结果上。 - 依靠可靠的事物。不要依靠巧合或假定。如果你无法说出各种特定情形的区别,就假定是最坏的。 - 为你的假定建立文档。“按合约设计” 有助于澄清你头脑中的假定,并且有助于把它们传达给别人。 - 不要只是测试你的代码,还要测试你的假定。不要猜测;要实际尝试它。编写断言测试你的假设。如果你的断言是对的,你就改善了代码中的文档。如果你发现你的假定是错的,那么就为自己庆幸吧。 - 为你的工作划分优先级。把时间花在重要的方面;很有可能,它们是最难的部分。如果你的基本原则或基础设施不正确,再花哨的铃声和又哨也是没有用的。 - 不要做历史的奴隶。不要让已有的代码支配将来的代码。如果不再适用,所有的代码都可被替换。即使是在一个程序中,也不要让你已经做完的事情约束你下一步要做的事情 —— 准备好进行重构。这一决策可能会影响项目的进度。我们的假定是其影响将小于不进行改动造成的影响”。 所以下次有什么东西看起来能工作,而你却不知道为什么,要确定它不是巧合。

你应在何时进行重构

当你遇到绊脚石——代码不再合适,你注意到有两样东西其实应该合并或是其他任何对你来说是 “错误” 的东西——不要对改动犹豫不决。应该现在就做。无论代码具有下面的哪些特征, 你都应该考虑重构代码: - 重复:你发现了对DRY原则的违反 - 非正交的设计:你发现有些代码或设计可以变得更为正交 - 过时的知识:事情变了,需求转移了,你对问题的了解加深了。代码需要跟上这些变化。 - 性能:为了改善性能,你需要把功能从系统的一个区域移到另一个区域。

怎样进行重构

  • 不要试图在重构的同时增加功能
  • 在开始重构之前,确保你拥有良好的测试。尽可能经常运行这些测试。这样,如果你的改动破坏了任何东西,你就能很快知道。
  • 采取短小、深思熟虑的步骤:把某个字段从一个类移往另一个,把两个类似的方法融合进超类中。重构常常涉及到进行许多局部改动,继而产生更大规模的改动。如果你使你的步骤保持短小,并在每个步骤之后进行测试,你将能够避免长时间的调试。

确保对模块做出的剧烈改动——比如以一种不兼容的方式更改了其接又或功能— 会破坏 构建,这也很有帮助。也就是说,这些代码的老客户应该无法通过编译。于是你可以很快找到这些老客户,并做出必要的改动,让它们及时更新。 所以,下次你看到不怎么合理的代码时,既要修正它,也要修正依赖于它的每样东西。要管理痛苦:如果它现在有损害,但以后的损害会更大,你也许最好—-劳永逸地修正它。记住软件的熵中的教训:不要容忍破窗户。

编写单元测试

模块的单元测试不应被扔在源码树的某个遥远的角落里。它们须放置在方便的地方。对于小型项目,你可以把模块的单元测试嵌人在模块自身里。对于更大的项目,我们建议你把每个测试都放进 一个子目录。 通过使测试代码易于找到,你是在给使用你代码的开发者提供两样无价的资源: - 一些例子,说明怎样使用你的模块的所有功能。 - 用以构建回归测试、以验证未来对代码的任何改动是否正确的一种手段。

在C++中,通过使用#ifdef有选择地编译单元测试,在面向对象的语言和环境中,你可以创建一个提供这些常用操作的基类。各个测试可以对其进行继承,并增加专用的测试代码。你可以使用Java中的标准命名约定和反射(reflection ),动 态地构建测试列表。这一技术是遵循DRY原则的好方法——你无需维护可用测试的列表。

不管你决定采用的技术是什么,测试装备都应该具有以下功能: - 用以制定设置与清理的标准途径 - 用以选择个别或所有可用测试的方法 - 分析输出是否是预期(或意外)结果的手段 - 标准化的故障报告形式

在调试过程中,我们可以临时创建一些特定的测试。它们可以像print 语句这样简单, 也可以是在调试器或IDE环境中交互地输入的一段代码。 在调试会话的最后,你需要使即兴测试正式化。如果代码曾经出过问题,它很可能还会 再出问题。不要把你创建的测试随便扔掉;把它加到已有的单元测试中。

挖掘需求

完美,不是在没有什么需要增加、而是在没有什么需要去掉时达到的。 —— Antoine de St. Exupery, Wind, Sand, and Stars, 1939

需求是对需要完成的某件事情的陈述。 在讨论用户界面时,需求、政策和实现之间的区别可能会变得非常模糊。“系统必须能让你选择贷款期限” 是对需求的陈述。“我们需要一个列表框,以选择贷款期限” 可能是,也可能不是。如果用户一定要有列表框,那么它就是需求。相反,如果他们是在描述选择能力,但只是用列表框做例子,这个陈述就可能不是需求。 找出用户为何要做特定事情的原因、而不只是他们目前做这件事情的方式,这很重要。 到最 后,你的开发必须解决他们的商业问题,而不只是满足他们陈述的需求。用文档记载需求背后的 原因将在每天进行实现决策时给你的团队带来无价的信息。

建立需求文档

看待用例的一种方式是强调其目标驱动(goal-driven)的本质。 可以用UML活动图捕捉工作流,而且有时要为手边的事务建模,概念层类图很有用。但真 正的用例是具有层次结构和交叉链接的文字描述。用例可以含有指向其他用例的超链接,它们也 可以互相嵌套。 image 制作需求文档时的一大危险是太过具体。好的需求文档会保持抽象。在涉及需求的地方,最简单的、能够准确地反映商业需要的陈述是最好的。这并非意味着你可以含糊不清—你必须把底层的语义不变项当作需求进行捕捉,并把具体的或当前的工作实践当作政策记入文档。 需求不是架构。需求不是设计,也不是用户界面。需求是需要。 许多项目的失败都被归咎于项目范围的增大——也称为特性膨胀(feature bloat)、蔓延特性论、或是需求蔓延。 管理需求增长的关键是向项目出资人指出每项新特性对项目进度的影响。当项目已经拖后了 一年,各种责难开始纷飞时,能够准确、完整地了解需求增长是怎样及何时发生的,会很有帮助。 我们很容易被吸进“只是再增加一个特性” 的大漩涡,但通过追踪需求,你可以更清楚地看 到,“只是再增加一个特性”,其实已经是本月新增的第15个新特性。

一定有更容易的方法!

有时你会发现,自己在处理的问题似乎比你以为的要难得多。感觉上好像是你走错了路 一定有比这更更容易的方法!或许现在你已落在了进度表后面,甚或失去了让系统工作起来的信 心,因为这个特定的问题是“不可能解决的”。 这正是你退回一步,问问自己以下问题的时候: - 有更容易的方法吗? - 你是在设法解决真正的问题,还是被外围的技术问题转移了注意力? - 这件事情为什么是一个问题? - 是什么使他如此难以解决? - 他必须以这种方式完成么? - 他真的必须完成么? 很多时候,当你设法回答这些问题时,你会有让自己吃惊的发现。很多时候,对需求的重新 诠释能让整个问题全都消失—就像是戈尔迪斯结。 你所需要的只是真正的约束、令人误解的的约束、还有区分它们的智慧。

是良好的判断还是拖延

每个人都害怕空白的纸页。启动新项目(甚或是已有项目中的新模块)可能会是让人身心交瘁的经验。我们许多人更愿意延缓做出最初的启动承诺。那么,你怎样才能知道,你什么时候是在拖延,而不是在负责地等待所有工作准备就绪? 在这样的情形下,我们采用的一种行之有效的技术是开始构建原型。选择一个你觉得会有困难的地方,开始进行某种“概念验证” (proof of concept )。在典型情况下,可能会发生两种情况。 一种情况是,开始后不久,你可能就觉得自己是在浪费时间。这种厌烦可能很好地表明,你最初的勉强只是希望推迟启动。放弃原型,回到真正的开发中。 另一种情况是,随着原型取得进展,你可能会在某个时刻得到启示,突然意识到有些基本的 前提错了。不仅如此,你还将清楚地看到可以怎样纠正错误。你将会愉快地放弃原型,投人正常的项目。你的直觉是对的,你为你自己和你的团队节省了可观的、本来会浪费的努力。 当你做出决定,把构建原型当作调查你的不适的一种方法时,一定要记住你为何这样做。你最不想看到的事情就是,你花了几个星期认真地进行开发,然后才想起你一开始只是要写一个原型。

无处不在的自动化

确保一致和准确的一种很好的方式是使团队所做的每件事情自动化。如果你的编辑器能够自动在你输入时安排代码的布局,为什么要手工进行呢?如果构建能够自动运行各种测试,为什么要手工完成测试表单呢? 自动化是每个项目团队的必要组成部分——为了确保事情得以自动化,制定一个或多个团队成员担任工具构建员,构造和部署使项目中的苦差事自动化的工具。让他们制作makefile、shell脚本、编辑器模板、实用程序等。

批准流程 有些项目具有各种必须遵循的管理工作流。例如,需要安排代码或设计复查,需要批准,等等。我们可以使用自动化——特别是网站——帮助减轻书面工作负担。 假定你想要使代码复查安排和批准自动化,你可以在在每个源文件里放置一个特殊标记:

/* Status: needs_review */

可以用一个简单的脚本检查所有的源码,并查找具有 needs_review状态的所有文件——这表明它们已准备好接受复查。随后你可以把这些文件的列表作为网页发布出去、或是自动发送e-mail给适当人员、甚或是使用某种日程软件自动安排一次会议。 你可以在网页上设置一个表单,用于让复查者登记文件是否通过了复查。在复查之后,状态可自动变为reviewed。是否与所有参与者一起进行检查取决于你。你仍然可以自动完成书面工作。 > 让计算机去做重复、庸常的事情——它会做得比我们更好。我们有更重要、更困难的事情要做。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
This site is deployed on
Unique Visitor Page View