在《程序员修炼之道》一书中,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 不要容忍破窗户
- Be a Catalyst for Change 做变化的催化剂
- Remember the Big Picture 记住大图景
我们没有做过这个,但有人说,如果你抓一只青蛙放进沸水里,它会一下子跳出来。 但是,如果你把青蛙放进冷水里,然后慢慢加热,青蛙不会注意到温度的缓慢变化,会呆在锅里, 直到被煮熟。
- Make Quality a Requirements Issue 使质量成为需求问题
- 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 总是使用源码控制
- Fix the Problem, Not the Blame 要修正问题,而不是发出指责
- Don't Panic 不要恐慌
- "Select" Isn't Broken "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 要配置,不要集成
- Put Abstractions in Code, Details in Metadata 将抽象放进代码,细节放进元数据
- Analyze Workflow to Improve Concurrency 分析工作流,以改善并发性
- Design Using Services 用服务进行设计
- 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 与用户一同工作,以像用户一样思考。
- Abstractions Live Longer than Details 抽象比细节活得更长久
- Use a Project Glossary 使用项目词汇表
- 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 对有些事情"做"胜于"描述"
- Don't Be a Slave to Formal Methods 不要做形式方法的奴隶
- Expensive Tools Do Not Produce Better Designs 昂贵的工具不一定制作出更好的设计。
Pragmatic Projects
- Organize Around functionality, Not Job Functions 围绕功能、而不是工作职务进行组织
我们喜欢按照功能划分团队。把你的人划分成小团队,分别负责最终系统的特定方面的功能。 让各团队按照各人的能力,在内部自行进行组织。每个团队都按照他们约定的承诺,对项目中的其他团队负有责任。承诺的确切内容随项目而变化,团队间的人员分配也是如此。 这里的功能并不必然意味着最终用户的用例。数据库访问层是功能,帮助子系统也是功能。 我们是在寻求内聚的、在很大程度上自足的团队——和我们在使代码模块化时应该使用的标准完全一样。如果团队的组织是错误的,会有一些警告性迹象 —— 一个经典的例子是有两个子团队在做同 一个程序模块或类。
- Don't Use Manual Procedures 不要使用手工流程
Test Early. Test Often. Test Automatically. 早测试,常测试,自动测试
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 只抓一次
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 ,这两个进程就会永远等待下去。) - 无论是谁分配的资源,它都应该负责解除该资源的分配
函数的得墨忒耳法则
使用得墨忒耳法则将使你的代码适应性更好、更健壮,但也有代价
:作为“总承包人”,你的模块必须直接委托并管理全部子承包人,而不牵涉你的模块的客户。在实践中,这意味着你将
会编写大量包装方法,它们只是把请求转发给被委托者。这些包装方法既会带来运行时代价,也
会带来空间开销,在有些应用中,这可能会有重大影响—
甚至会让你无法承受。
与任何技术一样,你必须平衡你的特定应用的各种正面因素和负面因素。在数据库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活动图捕捉工作流,而且有时要为手边的事务建模,概念层类图很有用。但真
正的用例是具有层次结构和交叉链接的文字描述。用例可以含有指向其他用例的超链接,它们也
可以互相嵌套。
制作需求文档时的一大危险是太过具体。好的需求文档会保持抽象。在涉及需求的地方,最简单的、能够准确地反映商业需要的陈述是最好的。这并非意味着你可以含糊不清—你必须把底层的语义不变项当作需求进行捕捉,并把具体的或当前的工作实践当作政策记入文档。
需求不是架构。需求不是设计,也不是用户界面。需求是需要。
许多项目的失败都被归咎于项目范围的增大——也称为特性膨胀(feature
bloat)、蔓延特性论、或是需求蔓延。
管理需求增长的关键是向项目出资人指出每项新特性对项目进度的影响。当项目已经拖后了
一年,各种责难开始纷飞时,能够准确、完整地了解需求增长是怎样及何时发生的,会很有帮助。
我们很容易被吸进“只是再增加一个特性”
的大漩涡,但通过追踪需求,你可以更清楚地看
到,“只是再增加一个特性”,其实已经是本月新增的第15个新特性。
一定有更容易的方法!
有时你会发现,自己在处理的问题似乎比你以为的要难得多。感觉上好像是你走错了路 一定有比这更更容易的方法!或许现在你已落在了进度表后面,甚或失去了让系统工作起来的信 心,因为这个特定的问题是“不可能解决的”。 这正是你退回一步,问问自己以下问题的时候: - 有更容易的方法吗? - 你是在设法解决真正的问题,还是被外围的技术问题转移了注意力? - 这件事情为什么是一个问题? - 是什么使他如此难以解决? - 他必须以这种方式完成么? - 他真的必须完成么? 很多时候,当你设法回答这些问题时,你会有让自己吃惊的发现。很多时候,对需求的重新 诠释能让整个问题全都消失—就像是戈尔迪斯结。 你所需要的只是真正的约束、令人误解的的约束、还有区分它们的智慧。
是良好的判断还是拖延
每个人都害怕空白的纸页。启动新项目(甚或是已有项目中的新模块)可能会是让人身心交瘁的经验。我们许多人更愿意延缓做出最初的启动承诺。那么,你怎样才能知道,你什么时候是在拖延,而不是在负责地等待所有工作准备就绪? 在这样的情形下,我们采用的一种行之有效的技术是开始构建原型。选择一个你觉得会有困难的地方,开始进行某种“概念验证” (proof of concept )。在典型情况下,可能会发生两种情况。 一种情况是,开始后不久,你可能就觉得自己是在浪费时间。这种厌烦可能很好地表明,你最初的勉强只是希望推迟启动。放弃原型,回到真正的开发中。 另一种情况是,随着原型取得进展,你可能会在某个时刻得到启示,突然意识到有些基本的 前提错了。不仅如此,你还将清楚地看到可以怎样纠正错误。你将会愉快地放弃原型,投人正常的项目。你的直觉是对的,你为你自己和你的团队节省了可观的、本来会浪费的努力。 当你做出决定,把构建原型当作调查你的不适的一种方法时,一定要记住你为何这样做。你最不想看到的事情就是,你花了几个星期认真地进行开发,然后才想起你一开始只是要写一个原型。
无处不在的自动化
确保一致和准确的一种很好的方式是使团队所做的每件事情自动化。如果你的编辑器能够自动在你输入时安排代码的布局,为什么要手工进行呢?如果构建能够自动运行各种测试,为什么要手工完成测试表单呢? 自动化是每个项目团队的必要组成部分——为了确保事情得以自动化,制定一个或多个团队成员担任工具构建员,构造和部署使项目中的苦差事自动化的工具。让他们制作makefile、shell脚本、编辑器模板、实用程序等。
批准流程 有些项目具有各种必须遵循的管理工作流。例如,需要安排代码或设计复查,需要批准,等等。我们可以使用自动化——特别是网站——帮助减轻书面工作负担。 假定你想要使代码复查安排和批准自动化,你可以在在每个源文件里放置一个特殊标记:
/* Status: needs_review */
可以用一个简单的脚本检查所有的源码,并查找具有 needs_review状态的所有文件——这表明它们已准备好接受复查。随后你可以把这些文件的列表作为网页发布出去、或是自动发送e-mail给适当人员、甚或是使用某种日程软件自动安排一次会议。 你可以在网页上设置一个表单,用于让复查者登记文件是否通过了复查。在复查之后,状态可自动变为reviewed。是否与所有参与者一起进行检查取决于你。你仍然可以自动完成书面工作。 > 让计算机去做重复、庸常的事情——它会做得比我们更好。我们有更重要、更困难的事情要做。