赞
踩
虽然不记得阅读本书用了多久,但是整理本书的读书笔记用了两个小时的时间,因为本书的大部分内容对于笔者来说都是新知识,很难进行归纳总结
本书所讲的是程序员应具有的工作态度和在团队中作为开发者和领导者具备的各种”敏捷的”习惯。虽然本书对于程序员的硬实力(本书讲解的编程语言是面向对象类语言,但是讲解的代码非常少)帮助不大,但是对于程序员应该具备的软实力的培养和提高有极大的帮助,是每位程序员都应该反复阅读的书籍。
2001年2月,17位志愿者(包括作者之一Andy在内)聚集在美国犹他州雪鸟度假胜地,讨论一个新的软件开发趋势,这个趋势被不严格地称为“轻量型软件开发过程”。
我们都见过了因为开发过程的冗余、笨重、繁杂而失败的项目。世上应该有一种更好的软件开发方法——只关注真正重要的事情,少关注那些占用大量时间而无甚裨益的不重要的事情。
这些志愿者们给这个方法学取名为敏捷。他们审视了这种新的软件开发方法,并且发布了敏捷开发宣言:一种把以人为本、团队合作、快速响应变化和可工作的软件作为宗旨的开发方法。
要求团队中的每一个人(包括与团队合作的人)都具备职业精神,并积极地期望项目能够获得成功。它并不要求所有人都是有经验的专业人员,但必须具有专业的工作态度——每个人都希望尽最大可能做好自己的工作。
这意味着你不会在项目结束的时候才开始测试,不会在月底才进行一次系统集成,也不会在一开始编码的时候就停止收集需求和反馈。越早发现问题,就越容易修复问题,所以应该就在此时此刻把问题修复。
在敏捷的团队中,大家的重点是做事。你应该把重点放到解决问题上,而不是在指责犯错者上面纠缠。应该把精力放在解决问题上,而不是抱怨。
世上最糟糕的工作就是和一群爱搬弄是非的人共事。他们对解决问题并没有兴趣,相反,他们爱在别人背后议论是非。他们挖空心思指手画脚,议论谁应该受到指责。这样一个团队的生产力是极其低下的。如果你发现自己是在这样的团队中工作,不要从团队中走开——应该跑开。
一个重大的错误应该被当作是一次学习而不是指责他人的机会。团队成员们在一起工作,应互相帮助,而不是互相指责。
拙劣的代码工人会这样不假思索地改完代码,然后快速转向下一个问题。
而优秀的程序员会挖掘更深一层,尽力去理解为什么这里必须要加1,更重要的是,他会想明白会产生什么其他影响。
在工作压力之下,不去深入了解真正的问题以及可能的后果,就快速修改代码,这样只是解决表面问题,最终会引发大问题。
所有的大型系统都非常复杂,因此没有一个人能完全明白所有的代码。除了深入了解你正在开发的那部分代码之外,你还需要从更高的层面来了解大部分代码的功能,这样就可以理解系统各个功能块之间是如何交互的。
对事不对人,让我们骄傲的应该是解决了问题,而不是比较出谁的主意更好。
在一个需要紧密合作的开发团队中,如果能稍加注意礼貌对待他人,将会有益于整个团队关注真正有价值的问题,而不是勾心斗角,误入歧途。
即使你的建议不被全盘接受,也能对最终解决问题有所帮助。不要害怕受到批评。记住,任何一个专家都是从这里开始的。用Les Brown的一句话说就是:“你不需要很出色才能起步,但是你必须起步才能变得很出色。
能容纳自己并不接受的想法,表明你的头脑足够有学识。-亚里士多德
设立仲裁人:
在会议的开始,选择一个仲裁人作为本次会议的决策者。每个人都要有机会针对问题畅所欲言。仲裁人的责任就是确保每个人都有发言的机会,并维持会议的正常进行。仲裁人可以防止明星员工操纵会议,并及时打断假大空式发言。仲裁人应该专注于调停,而不是发表自己的观点(理想情况下不应在整个项目中有既得利益)。
不带个人情绪并不是要盲目地接受所有的观点。用合适的词和理由去解释为什么你不赞同这个观点或方案,并提出明确的问题。
也许你会跳起来告诉周围的人,那些代码是多么糟糕,但那只是抱怨和发泄,并不能解决问题。相反,你应该重写这些代码,并比较重写前后的优缺点。动手证明(不要只是嚷嚷)最有效的方式,是把糟糕的代码放到一边,立刻重写。列出重写的理由,会有助于你的老板(以及同事)认清当前形势,帮助他们得到正确的解决方案。
你应深知怎样做才是正确的,或者至少知道目前的做法是错误的。要有勇气向其他的项目成员、老板或者客户解释你的不同观点。
即使你已经在正确的轨道上,但如果只是停止不前,也仍然会被淘汰出局。
——Will Rogers(美国著名演员)
唯有变化是永恒的-赫拉克利特
跟踪技术变化。你不需要精通所有技术,但需要清楚知道行业的动向,从而规划你的项目和职业生涯。
如何才能跟上技术变化的步伐呢?
迭代和增量式的学习。每天计划用一段时间来学习新技术,它不需要很长时间,但需要经常进行。记下那些你想学习的东西——当你听到一些不熟悉的术语或者短语时,简要地把它记录下来。然后在计划的时间中深入研究它。
了解最新行情。互联网上有大量关于学习新技术的资源。阅读社区讨论和邮件列表,可以了解其他人遇到的问题,以及他们发现的很酷的解决方案。选择一些公认的优秀技术博客,经常去读一读,以了解那些顶尖的博客作者们正在关注什么。
参加本地的用户组活动。
参加研讨会议。计算机大会在世界各地举行,许多知名的顾问或作者主持研讨会或者课程。这些聚会是向专家学习的最直接的好机会。
如饥似渴地阅读。找一些关于软件开发和非技术主题的好书,也可以是一些专业的期刊和商业杂志,甚至是一些大众媒体新闻。
“在一个团队中,如果只是你个人技术很好还远远不够。如果其他团队成员的知识不够,团队也无法发挥其应有的作用:一个学习型的团队才是较好的团队。
比如你参加了一个课程或者研讨班之后,所学的知识如果不用,往往就会忘记。所以,你需要和其他团队成员分享所学的知识,把这些知识引入团队中。
“午餐会议”是在团队中分享知识非常好的方式。在一周之中挑选一天,例如星期三(一般来说任何一天都可以,但最好不要是星期一和星期五)。事先计划午餐时聚集在一起,这样就不会担心和其他会议冲突,也不需要特别的申请。为了降低成本,就让大家自带午餐。
优秀的管理者会重用那些能提高其他团队成员价值的人,因此这些活动也直接有助于你的职业生涯。
在学习一门新技术的时候,多问问自己,是否把太多旧的态度和方法用在了新技术上。
学习一门新的编程语言时,应使用推荐的集成开发环境,而不是你过去开发时用的工具插件。
新技术会让人感到有一点恐惧。你确实需要学习很多东西。已有的技能和习惯为你打下了很好的基础,但不能依赖它们。
为了解决问题,你需要知道许多可能的影响因素。当找人询问任何相关的问题时,让他们耐心地回答你的问题,这是你的职责。
或者,假设你和资深的开发者一起工作。他们可能比你更了解这个系统。但他们也是人,有时他们也会忘记一些东西。你的问题甚至会帮助他们理清思路。你从一个新人角度提出的问题,给他们提供了一个新的视角,也许就帮助他们解决了一直令人困扰的问题。
不停地问为什么。不能只满足于别人告诉你的表面现象。要不停地提问直到你明白问题的根源。
这就好比是从矿石中采掘贵重的珠宝。你不停地筛选掉无关的物质,一次比一次深入,直到找到发光的宝石。你要能感觉到真正地理解了问题,而不是只知道表面的症状。
在许多不成功的项目中,基本上都是随意安排工作计划,没有任何的规律。那样的随机安排很难处理。你根本不知道明天将会发生什么,也不知道什么时候开始下一轮的全体“消防演习”。
你可以做到在每天下班离开公司前运行测试,并提交一天完成的代码。
许多的敏捷技巧来源于时间盒——设定一个短时的期限,为任务设定不能延长的最终期限。你可以选择放弃其他方面的任务,但是最终期限是不变的。你可能不知道完成所有的任务需要多少个时间盒,但每个时间盒必须是短期的、有限的,并且要完成具体的目标。
“项目开发需要有一致和稳定的节奏。编辑,运行测试,代码复审,一致的迭代,然后发布。如果知道什么时候开始下一个节拍,跳舞就会更加容易。
我们的敌人不是客户,不是用户,不是队友,也不是管理者。真正的敌人是变化。软件开发如战争,形势的变化快速而又剧烈。固守昨天的计划而无视环境的变化会带来灾难。你不可能“战胜”变化——无论它是设计、架构还是你对需求的理解。
敏捷:成功的软件开发方法——取决于你识别和适应变化的能力。只有这样才有可能在预算之内及时完成开发,创建真正符合用户需求的系统。
在设计方面,做决定的时候必须有开发者参与。可是,在一个项目中,他们不应该做所有的决定,特别是业务方面的决定。
开发者(及项目经理)能做的一个最重要的决定就是:判断哪些是自己决定不了的,应该让企业主做决定。你不需要自己给业务上的关键问题做决定。
当你和客户讨论问题的时候,准备好几种可选择的方案。不是从技术的角度,而是从业务的角度,介绍每种方案的优缺点,以及潜在的成本和利益。和他们讨论每个选择对时间和预算的影响,以及如何权衡。无论他们做出了什么决定,他们必须接受它,所以最好让他们了解一切之后再做这些决定。如果事后他们又想要其他的东西,可以公正地就成本和时间重新谈判。
设计满足实现即可,不必过于详细
Design should be only as detailed as needed to implement
良好的战略设计应该扮演地图的角色,指引你向正确的方向前进。好的设计应该是正确的,而不是精确的。也就是说,它描述的一切必须是正确的。
设计可以分为两层:战略和战术。前期的设计属于战略,通常只有在没有深入理解需求的时候需要这样的设计。更确切地说,它应该只描述总体战略,不应深入到具体的细节。
如果需求有了小的变化,它仍然容易去实现,那么它就是好的设计。而如果小的需求变化就带来一大批基础代码的破坏,那么设计就需要改进。
盲目地为项目选择技术框架,就好比是为了少交税而生孩子
Blindly picking a framework is like having kids to save taxes.
在考虑引入新技术或框架之前,先要把你需要解决的问题找出来。你的表述方式不同,会让结果有很大差异。如果你说“我们需要xyzzy技术,是因为……”,那么就不太靠谱。你应该这样说:“……太难了”或者是“……花的时间太长了”,或者类似的句子。找到了需要解决的问题,接下来就要考虑:
这个技术框架真能解决这个问题吗?是的,也许这是显而易见的。但是,这个技术真能解决你面临的那个问题吗?或者,更尖锐一点说,你是如何评估这个技术的?是通过市场宣传还是道听途说?要确保它能解决你的问题,并没有任何的毒副作用。如果需要,先做一个小的原型。
你将会被它拴住吗?一些技术是贼船,一旦你“你使用了它,就会被它套牢,再也不可能回头了。它缺乏可取消性,当条件发生变化时,这可能对项目有致命打击。我们要考虑它是开放技术还是专利技术,如果是开放的技术,那又开放到什么程度?
维护成本是多少?会不会随着时间的推移,它的维护成本会非常昂贵?
已提交的代码应该随时可以行动
Checked-in code is always ready for action
你的项目进入不可发布状态的频率是多少?你的源代码服务器中的代码,是不是像圣安那在那个决定性的黄昏一样——没有进行编队,遇到紧急情况无法立即启动。
在团队里工作,修改一些东西的时候必须很谨慎。你要时刻警惕,每次改动都会影响系统的状态和整个团队的工作效率。在办公室的厨房里,你不能容忍任何人乱丢垃圾,为什么就可以容忍一些人给项目带来垃圾代码呢?
你会觉得,不管什么时候,你的老板、董事长、质量保障人“员、客户或者你的配偶来公司参观项目的时候,你都能很自信并毫不犹豫地给他们演示最新构建的软件。你的项目一直处于可以运行的稳定状态。
如果你不得不让系统长期不可以发布,那就做一个(代码和架构的)分支版本,你可以继续进行自己的实验,如果不行,还可以撤销,从头再来。千万不能让系统既不可以发布,又不可以撤销。
敏捷的一个主要特点就是持续开发,而不是三天打鱼两天晒网似地工作。
尽可能早地集成也更容易发现风险,这样风险及相关的代价就会相当低。而等的时间越长,你也就会越痛苦。
软件集成就像这一样。如果你不断地独立开发,忽然有一天跳到集成这一步,千万不要为受到打击而吃惊。你可以使用mock对象,编写独立的单元测试,而不需要立刻就集成和测试其他系统,只有当你自信它能工作的时候,才可以开始集成。
当早期就进行集成的时候,你会看到子系统之间的交互和影响,你就可以估算它们之间通信和共享的信息数据。你越早弄清楚这些问题,越早解决它们,工作量就越小。
决不要做大爆炸式的集成
Never accept big-bang integration
提早集成,频繁集成。代码集成是主要的风险来源。要想规避这个风险,只有提早集成,持续而有规律地进行集成。
如果你真正做对了,集成就不再会是一个繁重的任务。它只是编写代码周期中的一部分。集成时产生的问题,都会是小问题并且容易解决。
你要能用一种可重复和可靠的方式,在目标机器上部署你的应用。不幸的是,大部分开发者只会在项目的尾期才开始考虑部署问题。
如果现在你还是手工帮助质量保证人员安装应用,花一些时间,考虑如何将安装过程自动化
有了自动化部署系统后,在项目开发的整个过程中,会更容易适应互相依赖的变化。有了自动化部署系统后,在项目开发的整个过程中,会更容易适应互相依赖的变化。
一开始就实现自动化部署应用。使用部署系统安装你的应用,在不同的机器上用不同的配置文件测试依赖的问题。质量保证人员要像测试应用一样测试部署。
你无法冻结需求,正如你无法冻结市场、竞争、知识、进化或者成长一样。就算你真的冻结了,也很可能是冻结了错的东西。如果你期望用户在项目开始之前,就能给你可靠和明确的需求,那就大错特错了,赶快醒醒吧!
需求就像是流动着的油墨
Requirements are as fluid as ink
没有人的思想和观点可以及时冻结,特别是项目的客户。就算是他们已经告诉你想要的东西了,他们的期望和想法还是在不停地进化——特别是当他们在使用新系统的部分功能时,他们才开始意识到它的影响和可能发生的问题。这就是人的本性。
我们经常看到,给客户演示所完成功能的时间与得到客户需求的时间间隔越长,那么你就会离最初需求越来越远。应该定期地,每隔一段时间,例如一个迭代的结束,就与客户会晤,并且演示你已经完成的功能特性。
要频繁地获得反馈。如果你的迭代周期是一个季节或者一年(那就太长了),就应把周期缩短到一周或者两周。完成了一些功能和特征之后,去积极获得客户的反馈。每隔一周或者两周,邀请所有的客户,给他们演示最新完成的功能,积极获得他们的反馈。
要尊重客户的时间。如果客户只可以接受一个月一次会议,那么就定一个月。而且频繁的会议很难应付,而且还要开发代码让他们看。缩减次数,只有在你做完一些东西可以给他们演示的时候,大家才碰面。
给我一份详细的长期计划,我就会给你一个注定完蛋的项目
Show me a detailed long-term plan, and I’ll show you a project that’s doomed
迭代开发是:在小且重复的周期里,你完成各种开发任务:分析、设计、实现、测试和获得反馈,所以叫作迭代。
软件开发不是精细的制造业,而是创新活动。规划几年之后客户才能真正使用的项目注定是行不通的。
对付大项目,最理想的办法就是小步前进,这也是敏捷方法的核心。大步跳跃大大地增加了风险,小步前进才可以帮助你很好地把握平衡。
询问用户,哪些是使产品可用且不可缺少的核心功能。不要为所有可能需要的华丽功能而分心,不要沉迷于你的想象,去做那些华而不实的用户界面。从用户那里得到的反馈,会让我们进一步理解什么是用户真正想要的,以及下一步该实现哪些功能。
使用短迭代和增量开发,可以让开发者更加专注于自己的工作。
每个增量开发周期里,应该使用短的迭代(不应该超过两周)。每个迭代都要有演示,选择可能提供反馈的用户,给他们每人一份最新的产品副本。
在每4周的迭代中间安排一周的维护任务。没有规定说迭代必须要紧挨着下一个迭代。
如果发布的功能背离了用户的需要,那么多半是因为迭代的周期太长了。
增量的发布必须是可用的,并且能为用户提供价值。你怎么知道用户会觉得有价值呢?这当然要去问用户。
软件项目天生就是变化无常的,不可重复。如果要提前给出一个固定的价格,就几乎肯定不能遵守开发上的承诺。那么我们有什么可行的办法呢?我们能做更精确的评估吗?或者商量出另外一种约定。
大部分项目都是业务应用,一个用户和另一个用户都有着巨大的差别。项目的发掘和创造需要很多配合工作。或许你可以提供稍有不同的安排,试试下面的办法。
对客户来说,这种方式的好处是项目不可能会死亡。他们可以很早地看到工作的进度(或者不足之处)。他们总是可以控制项目,可以随时停止项目,不需要缴纳任何的违约金。他们可以控制先完成哪些功能,并能精确地知道需要花费多少资金。总而言之,客户会承担更低的风险。
而你所做的就是在进行迭代和增量开发。
基于真实工作的评估。让团队和客户一起,真正地在当前项目中工作,做具体实际的评估。由客户控制他们要的功能和预算。
在敏捷项目中,我们小步前进,不停地收集反馈,时刻矫正自己。但是,这些反馈都是从何而来呢?
在上一章中,我们讨论了与用户一起紧密工作——从他们那里获得反馈,并且采取实际的行动。本章中,我们主要讨论如何从其他渠道获得反馈。
为了应对代码的变化,你需要持续获得代码健康状态的反馈:它是在做你期望的事情吗?最近一次修改有没有无意中破坏了什么功能?这时,你就带上守护天使,确保所有功能都能正常工作。要做到这样,就需要自动化单元测试。
编写能产生反馈的代码
Coding feedback
在过去大部分开发者是如何工作的:你写了一小块代码,然后嵌入一些输出语句,来看一些关键变量的值。你也许是在调试器中或者基于一些桩(stub)程序来运行代码。你手工查看所有的运行结果,来修复发现的所有问题,然后扔掉那些桩代码,或者从调试器中退出,再去解决下一个问题。
敏捷式的单元测试正是采取了相同、相似的过程,并且还让其更上一层楼。不用扔掉桩程序,你把它保存下来,还要让其可以自动化地持续运行。你编写代码来检查具体值,而不是手工检查那些感兴趣的变量。
用代码来测试变量的具体值(以及跟踪运行了多少个测试),已经是非常普遍的做法。你可以选择一个标准的测试框架,来帮助你完成简单的编写和组织测试的工作,如Java的JUnit、C#或.NET的NUnit、测试Web Service的HttpUnit,等等。实际上,对任何你可以想象到的环境和语言都有对应的单元测试框架,其中的大部分都可以从http://xprogramming.com/software.htm上的列表中找到。
应该注意:
确保测试是可重复的。使用当前的日期或者时间作为参数,会让测试依赖运行时间,使用你自己机器上的IP地址同样会让它依赖运行时的机器,等等。
测试你的边界条件。11:59:59和0:00:00都是不错的日期测试边界条件。
不要放过任何一个失败的测试。在前面的案例中,一个测试一直失败了,但是因为一段时间内每天都会有几十个测试失败,没有人会注意到这个伪随机失败。
只要有了单元测试,就要让它们自动运行。也就是每次编译或者构建代码的时候,就运行一次测试。把单元测试的结果看作是和编译器一样——如果测试没有通过(或者没有测试),那就像编译没有通过一样糟糕。
接下来就是在后台架设一个构建机器,不断获得最新版本的源代码,然后编译代码,并运行单元测试,如果有任何错误它会让你及时知道。
结合本地单元测试,运行每个编译,构建机器不断编译和运行单元测试,这样你就拥有了一个守护天使。如果出现了问题,你会立刻知道,并且这是最容易修复(也是成本最低)的时候。
一旦单元测试到位,采用这样的回归测试,你就可以随意重构代码。可以根据需要进行实验、重新设计或者重写代码:单元测试会确保你不会意外地破坏任何功能。“你心情舒畅,你不用每次写代码的时候都如履薄冰。
单元测试让你的代码更加健壮。测试帮助你全面思考代码的行为,帮你练习正面、反面以及异常情况。
单元测试是学习工具。在你开始学习新API的时候,可以为这个API写个单元测试,从而加深自己的理解。这些学习用的测试,不仅能帮助你理解API的行为,还能帮助你快速找到以后可能引入的、无法与现有代码兼容的变化。
不是测试越多质量就会越高,测试必须要有效。如果测试无法发现任何问题,也许它们就是没有测试对路。
很多成功的公司都是靠着“吃自己的狗食”活着。也就是说,如果要让你的产品尽可能地好,自己先要积极地使用它。
我们的业务是要创造出能调用的API和可以使用的接口。这就是说,你在说服其他人使用它之前,先得让自己切实地使用这些接口。事实上,在你刚做完设计但还没有完成后面的实现的时候,应该使用它。这个可行吗?
使用被称为TDD(Test Driven Development,测试驱动开发)的技术,你总是在有一个失败的单元测试后才开始编码。测试总是先编写。通常,测试失败要么是因为测试的方法不存在,要么是因为方法的逻辑还不足以让测试通过。
编程之前,先写测试
Write tests before writing“code
先写测试,你就会站在代码用户的角度来思考,而不仅仅是一个单纯的实现者。这样做是有很大区别的,你会发现因为你自己要使用它们,所以能设计一个更有用、更一致的接口。
除此之外,先写测试有助于消除过度复杂的设计,让你可以专注于真正需要完成的工作。看看下面编程的例子,这是一个可以两人玩的“井字棋游戏”。
重点是:什么是成功地实现特定功能的最低成本。总之,程序员很容易走向另一个极端——一些不必要的过于复杂的事情——测试优先会帮助我们,防止我们走偏。
好的设计并不意味着需要更多的类
Good design doesn’t mean more classes
设计不是在开始编码的时候就结束了。你需要在它的生命周期中持续地添加测试,添加代码,并重新设计代码。
不要把测试优先和提交代码之前的测试等同起来。测试先行可以帮助你改进设计,但是你还是需要在提交代码之前做测试。
平台的不同,造成了结果的不一样
你已经编写了单元测试,测试你的代码。每次在修改或者重构代码的时候,在提交代码之前,你会运行测试用例。那么现在所要做的就是在各种支持的平台和环境中运行这些测试用例。
要在多个平台上测试,你只要为每个平台设置持续集成系统就行了。当你或者同事提交了代码,测试会在每个平台上自动运行。这样,提交代码之后的几分钟,你就可以知道它是否可以在不同的平台上运行!这是多么英明的办法呀!
不同环境,就有不同问题。使用持续集成工具,在每一种支持的平台和环境中运行单元测试。要积极地寻找问题,而不是等问题来找你。
关键业务逻辑必须要独立进行严格的测试,并且最后需要通过用户的审批。
但你也不可能拉着用户,逐一检查每个单元测试的运行结果。实际上,你需要能自动比较用户期望和实际完成的工作。
有一个办法可以使验收测试不同于单元测试。你应该让用户在不必学习编码的情况下,根据自己的需要进行添加、更新和修改数据。你有很多方法来实现它。
FIT,即集成测试框架,它很实用,可以更容易地使用HTML表格定义测试用例,并比较测试结果数据。
使用FIT,客户可以定义带有新功能的使用样本。客户、测试人员和开发人员(根据样本)都可以创建表格,为代码描述可能的输入和输出值。开发人员会参照带有正开发的代码结果的FIT表格中的。
样本编写测试代码。测试结果成功或者失败,都会显示在HTML页面中,用户可以很方便地查阅。
如果你工作了60个小时,也许你的老板会让你在时间表上只填写40个小时,这是公司会计想看到的。
我们不应该去计算工作量完成的百分比,而应该测定还剩下多少工作量没有完成。
一周工作40个小时,不是说你就有40个小时的编码时间。你需要减去会议、电话、电子邮件以及其他相关活动的时间。
当出了错误,你要尽可能地提供详细信息。黑屏和含义不明的“退出”按钮是很不友好的行为。更糟糕的是,在得到用户反馈的时候,还嘲笑用户愚蠢,而不去真正地解决问题。不管它是否是产品的bug,还是文档的bug,或者是对用户社区理解的bug,它都是团队的问题,而不是用户的问题。
没有愚蠢的用户,只有愚蠢、自大的开发人员。
“它就是这样的。”这不是一个好的答案。
如果代码问题解决不了,也许可以考虑通过修改文档或者培训来弥补。
你的用户有可能会阅读所有的文档,记住其中的所有内容。但也可能不会。
开发人员在完成任务时,可能会难以抵挡诱惑为节省时间而走“捷径”。然而,这些“捷径”往往只会推迟问题的爆发时间,而不是把它彻底解决掉。
最简单的方式,就是在开发过程中便细心“照看”代码。在编写代码时,每天付出一点小的努力,可以避免代码“腐烂”,并且保证应用程序不至变得难以理解和维护。、
代码要清晰地表达意图。这样的代码清晰易懂,仅凭小聪明写出的程序很难维护。注释可以帮助理解,也可能导致不好的干扰,应该总是用代码沟通。
项目是以增量式方式进行开发的,写程序时也应该进行增量式编程。在编写代码的时候,要想保持简单很难做到——实际上,想写出简单的代码要远比写出令人厌恶的、过分复杂的代码难得多。
开发代码时,应该更注重可读性,而不是只图自己方便。代码阅读的次数要远远超过编写的次数,所以在编写的时候值得花点功夫让它读起来更加简单。实际上,从衡量标准上来看,代码清晰程度的优先级应该排在执行效率之前。
例如,如果默认参数或可选参数会影响代码可读性,使其更难以理解和调试,那最好明确地指明参数,而不是在以后让人觉得迷惑。
明白地告诉阅读程序的人,代码都做了什么,这是让其便于理解的一种方式。让我们看一些例子。
PIE(PIE=Program Intently and Expressively,即意图清楚而且表达明确地编程)原则:
代码必须明确说出你的意图,而且必须富有表达力。这样可以让代码更易于被别人阅读和理解。代码不让人迷惑,也就减少了发生潜在错误的可能。一言以蔽之,代码应意图清晰,表达明确。
不要表现得好像很聪明似的,要遵循PIE原则:代码要清晰地表达意图。
用方法名来传达意向,对方法参数的命名要帮助读者理解背后的想法
要编写清晰的而不是讨巧的代码。向代码阅读者明确表明你的意图。可读性差的代码一点都不聪明。
使用符合当时情形的耦合。例如,通过散列表进行松耦合,这种方式适用于在实际状况中就是松耦合的组件。不要使用散列表存储紧密耦合的组件,因为这样没有明确表示出你的意图。
“应该让自己或团队的其他任何人,可以读懂自己一年前写的代码,而且只读一遍就知道它的运行机制。
建立代码文档无外乎两种方式:利用代码本身;利用注释来沟通代码之外的问题。
不要用注释来包裹你的代码
Don’t comment to cover up
源代码可以被读懂,不是因为其中的注释,而应该是由于它本身优雅而清晰——变量名运用正确、空格使用得当、逻辑分离清晰,以及表达式非常简洁。
如何界定一个良好的命名?良好的命名可以向读者传递大量的正确信息。不好的命名不会传递任何信息,糟糕的命名则会传递错误的信息。
要尽量避免使用神秘的变量名。不是说命名短小就等同于神秘:在许多编程语言中通常使用i来表示循环索引变量,s常被用来表示一个字符串。
不必费尽心机去用繁复冗长的名字替换大家已习惯的名称。
它不过是为代码添加了“噪音”。最坏的状况下,随着时间推进,这些注释则会过时,变得不再正确。
代码被阅读的次数要远超过被编写的次数,所以在编程时多付出一点努力来做好文档,会让你在将来受益匪浅。
可以先阅读注释,然后快速浏览代码,从而完全理解它做了什么,以及为什么这样做。
在代码可以传递意图的地方不要使用注释。
解释代码做了什么的注释用处不那么大。相反,注释要说明为什么会这样写代码。
与其花费时间去提升千分之一的性能表现,也许减少开发投入,降低成本,并尽快让应用程序上市销售更有价值。
但是谁来最终判定性能表现已经足够好,或是应用的展现已经足够“炫”了呢?客户或是利益相关者必须进行评估,并做出相关决定。如果团队认为性能上还有提升的空间,或者觉得可以让某些界面看起来更吸引人,那么就去咨询一下利益相关者,让他们决定应将重点放在哪里。
没有适宜所有状况的最佳解决方案。你必须对手上的问题进行评估,并选出最合适的解决方案。每个设计都是针对特定问题的——只有明确地进行评估和权衡,才能得出更好的解决方案。
没有最佳解决方案
No best solution
动态评估权衡。考虑性能、便利性、生产力、成本和上市时间。如果性能表现足够了,就将注意力放在其他因素上。不要为了感觉上的性能提升或者设计的优雅,而将设计复杂化。
真正的高性能系统,从一开始设计时就在向这个方向努力。
即使不能面面俱到,你也应该觉得已经得到了最重要的东西——客户认为有价值的特性。
增量式编程可以精炼并结构化你的代码。代码被复杂化、变成一团乱麻的几率减少了。所开发的代码基于即时的反馈,这些反馈来自以小步幅方式编写代码和测试的过程。
采取增量式编程和测试,会倾向于创建更小的方法和更具内聚性的类。你不是在埋头盲目地一次性编写一大堆代码。相反,你会经常评估代码质量,并不时地进行许多小调整,而不是一次修改许多东西。
在编写代码的时候,要经常留心可以改进的微小方面。这可能会改善代码的可读性。也许你会发现可以把一个方法拆成几个更小的方法,使其变得更易于测试。
在很短的编辑/构建/测试循环中编写代码。这要比花费长时间仅仅做编写代码的工作好得多。可以创建更加清晰、简单、易于维护的代码。
也许你看过这样一篇文章,其中提到了一个设计想法,表示为一个带有花哨名称的模式。放下杂志,眼前的代码似乎马上就可以用到这种模式。这时要扪心自问,是不是真的需要用它,以及它将如何帮你解决眼前的问题。问问自己,是不是特定的问题强迫你使用这个解决方案。不要让自己被迫进行过分设计,也不要将代码过分复杂化。
许多开发人员以自己程序的复杂性为荣,如果能听到说:“Wow,这很难,一定是花了很多时间和精力才做出来的吧。”他们就会面带自豪的微笑了。其实应当恰恰相反,开发人员更应该为自己能够创建出一个简单并且可用的设计而骄傲。
“简单性”这个词汇被人们大大误解了(在软件开发工作以及人们的日常生活中,皆是如此)。它并不意味着简陋、业余或是能力不足。恰恰相反,相比一个过分复杂、拙劣的解决方案,简单的方案通常更难以获得。
简单不是简陋
Simple is not simplistic
怎样才算优雅?
优雅的代码第一眼看上去,就知道它的用处,而且很简洁。但是这样的解决方案不是那么容易想出来的。这就是说,优雅是易于理解和辨识的,但是要想创建出来就困难得多了。”
当你觉得所编写的代码中没有一行是多余的,并且仍能交付全部的功能时,这种感觉就对了。这样的代码容易理解和改正。
当然,简单的解决方案必须要满足功能需求。为了简单而在功能上妥协,这就是过分简化了。
内聚性用来评估一个组件(包、模块或配件)中成员的功能相关性。内聚程度高,表明各个成员共同完成了一个功能特性或是一组功能特性。内聚程度低的话,表明各个成员提供的功能是互不相干的。
在决定创建一个类的时候,问问自己,这个类的功能是不是与组件中其他某个类的功能类似,而且功能紧密相关。这就是组件级的内聚性。
类也要遵循内聚性。如果一个类的方法和属性共同完成了一个功能(或是一系列紧密相关的功能),这个类就是内聚的。
低内聚性的代码会造成很严重的后果。假设有这样一个类,实现了五种完全不相干的功能。如果这5个功能的需求或细节发生了变化,这个类也必须跟着改变。如果一个类(或者一个组件)变化得过于频繁,这样的改变会对整个系统形成“涟漪效应”,并导致更多的维护和成本的发生。
一个更具内聚性的组件不会有太多导致其变化的原因,也因此而更加稳定。根据单一职责原则(查看《敏捷软件开发:原则、模式与实践》),一个模块应该只有一个发生变化的原因。
内聚性会影响一个组件的可重用性。组件粒度是在设计时要考虑的一个重要因素。根据重用发布等价原则:重用的粒度与发布的粒度相同。这就是说,程序库用户所需要的,是完整的程序库,而不只是其中的一部分。如果不能遵循这个原则,组件用户就会被强迫只能使用所发布组件的一部分。很不幸的是,他们仍然会被不关心的那一部分的更新所影响。软件包越大,可重用性就越差。
让类的功能尽量集中,让组件尽量小。要避免创建很大的类或组件,也不要创建无所不包的大杂烩类。
bug很容易跟踪,代码也易于修改,因为类和组件的责任都很清晰。
作为某段代码的调用者,开发人员绝对不应该基于被调用对象的状态来做出任何决策,更不能去改变该对象的状态。这样的逻辑应该是被调用对象的责任,而不是你的。在该对象之外替它做决策,就违反了它的封装原则,而且为bug提供了滋生的土壤。
与告知,不要询问相关的一个很有用的技术是:命令与查询相分离模式(command-query separation)。就是要将功能和方法分为“命令”和“查询”两类,并在源码中记录下来(这样做可以帮助将所有的“命令”代码放在一起,并将所有的“查询”代码放在一起)。
这就是说,从外部看来,“查询”不应该有任何副作用。
任何继承后得到的派生类对象,必须可以替换任何被使用的基类对象,而且使用者不必知道任何差异。换句话说,某段代码如果使用了基类中的方法,就必须能够使用派生类的对象,并且自己不必进行任何修改。
当使用继承时,要想想派生类是否可以替换基类。如果答案是不能,就要问问自己为什么要使用继承。
如果新类只是使用已有的类,并且两者之间的关系可以描述为has-a或是uses-a,就使用委托吧。
相对继承来说,委托更加灵活,适应力也更强。
在调试时面对的真正问题,是无法用固定的时间来限制。可以规定设计会议的持续时间,并在时间截止时决定采用最佳的方案。但是调试所耗费的时间,可能是一个小时、一天,甚至一周过去了,还是没有办法找到并解决问题。
对于一个项目来说,这种没有准确把握的时间消耗是不可接受的。不过,我们可以使用一些辅助技术,涵盖的范围包括:保留以前的问题解决方案,以及提供发生问题时的更多有用细节。
要想更加有效地重用你的知识和努力,记录问题解决日志是很有用的,我们会在下一页看到如何具体操作。当编译器警告有问题的时候,要假定警告就是错误,并且马上把它们解决掉。
如果一个熟悉的问题再次发生,我们会希望记起第一次是如何解决的,而且希望下次能够更快地把它搞定。然而,有时一个问题看起来跟以前遇到的完全一样,但是我们却不记得是如何修复的了。这种状况时常发生。
要想得到更好的效果,不妨维护一个保存曾遇到的问题以及对应解决方案的日志。
要共享日志给其他人,而不仅仅是靠一个人维护。把它放到共享的网络驱动器中,这样其他人也可以使用。或者创建一个Wiki,并鼓励其他开发人员使用和更新其内容。
当程序中出现一个编译错误时,编译器或是构建工具会拒绝产生可执行文件。我们别无选择——必须要先修正错误,再继续前行。
然而,警告却是另外一种状况。即使代码编译时产生了警告,我们还是可以运行程序。那么忽略警告信息继续开发代码,会导致什么状况呢?这样做等于是坐在了一个嘀嗒作响的定时炸弹上,而且它很有可能在最糟糕的时刻爆炸。
把级别调到最高,让任何警告不能被忽略。例如,GCC编译器支持-Werror参数,在Visual Studio中,开发人员可以改变项目设置,将警告视为错误。
在修改设置的时候,要记得在构建服务器上使用的持续集成工具中,修改同样的设置选项。(要详细了解持续集成,查看第87页习惯21。)这个小小的设置,可以大大提升团队签入到源码控制系统中的代码质量。
在开始一个项目的时候,要把相关的设置都准备好。在项目进行到一半的时候,突然改变警告设置,有可能会带来颠覆性的后果,导致难以控制。
单元测试带来的积极效应之一,是它会强迫形成代码的分层。要保证代码可测试,就必须把它从周边代码中解脱出来。
不要试图马上了解系统的所有细节。要想认真调试,就必须将有问题的组件或模块与其他代码库分离开来。
用原型进行分离
Prototype to isolate”
面对必须要隔离的问题时,感觉就像在一个茶杯中寻找一根针,而不是大海捞针。
如果将代码从其运行环境中分离后,问题消失不见了,这有助于隔离问题,另一方面,如果将代码从其运行环境中分离后,问题还在,这也有助于隔离问题。
以二分查找的方式来定位问题是很有用的。也就是说,将问题空间分为两半,看看哪一半包含问题。再将包含问题的一半进行二分,并不断重复这个过程。
在向问题发起攻击之前,先查找你的问题解决日志。
从事任何编程工作,都要考虑事物正常状况下是如何运作的。不过更应该想一想,当出现问题——也就是事情没有按计划进行时,会发生什么。
在调用别人的代码时,它也许会抛异常,这时我们可以试着对其处理,并从失败中恢复。当然,要是在用户没有意识到的情况下,可以恢复并继续正常处理流程,这就最好不过了。要是不能恢复,应该让调用代码的用户知道,到底是哪里出现了问题。
必须要处理所有的异常,倘若可以,从失败中恢复再好不过。如果不能处理,就要把异常传播到方法的调用者,这样调用者就可以尝试对其进行处理了。
决定由谁来负责处理异常是设计工作的一部分。
显示通用的信息,告诉用户发生了问题,要好过由于系统崩溃造成应用执行错误的动作,或者直接关闭(用户会因此感到困惑,并希望知道问题所在)。然而,类似“出错了”这样的消息,无法帮助团队针对问题做出诊断。用户在给支持团队打电话报告问题时,我们希望他们提供足够多且好的信息,以帮助尽快识别问题所在。遗憾的是,用很通用的错误消息,是无法提供足够的数据的。
解决方案是记录日志:当发生问题时,让应用详细记录错误的相关数据。错误日志最起码应该以文本文件的形式维护。不过也许可以发布到一个系统级别的事件日志中。可以使用工具来浏览日志,产生所有日志信息的RSS Feed,以及诸如此类的辅助方式。
不妨在显示给用户的信息中提供更多细节。
“错误信息有助于问题的解决。当问题发生时,可以详细研究问题的细节描述和发生上下文。
项目的成功与否,依赖于团队中的成员如何一起有效地工作,如何互动,如何管理他们的活动。全体成员的行动必须要与项目相关,反过来每个人的行为又会影响项目的环境。
也许你个人很讨厌开会,但是沟通是项目成功的关键。我们不只要跟客户谈话,还应该与开发人员进行良好的沟通。要知道其他人在做什么
要保证会议议题不会发散,每个人都应该只回答下述三个问题:
在大家到公司之后的半个小时到一个小时之内举行,是个不错的选择。
参加会议的人要遵守一些规则,以保证彼此不会分神,而且会议也不会跑题。这些规则有:只有团队成员——开发人员、产品所有者和协调者可以发言(查看上面对“猪”和“鸡”的描述)。他们必须回答上面的3个问题,而且不能展开深入讨论
要注意报告的细节。在会议中要给出具体的进度。
大家都盼望着立会。希望彼此了解各自的进度和手上的工作,而且不怕把各自遇到的问题拿出来公开讨论。
架构师应该负责设计和指导,但是许多名片上印着“架构师”的人配不上这个称号。作为架构师,不应该只是画一些看起来很漂亮的设计图,说一些像“黑话”一样的词汇,使用一大堆设计模式——这样的设计通常不会有效的。
不可能在PowerPoint幻灯片中进行编程
You can’t code in PowerPoint
这些架构师通常在项目开始时介入,绘制各种各样的设计图,然后在重要的代码实现开始之前离开。有太多这种“PowerPoint架构师”了,由于得不到反馈,他们的架构设计工作也不会有很好的收效。
一个设计要解决的是眼前面临的特定问题,随着设计的实现,对问题的理解也会发生改变。想在开始实现之前,就做出一个很有效的详细设计是非常困难的。
作为设计人员,如果不能理解系统的具体细节,就不可能做出有效的设计。只通过一些高度概括的、粗略的设计图无法很好地理解系统。
可逆性:
《程序员修炼之道》中指出不存在所谓的最终决策。没有哪个决策做出之后就是板上钉钉了。实际上,就时间性来看,不妨把每个重要的决策,都看作沙上堆砌的城堡,它们都是在变化之前所做出的预先规划。
好的设计者必须能够卷起袖子,加入开发队伍,毫不犹豫地参与实际编程。真正的架构师,如果不允许参与编码的话,他们会提出强烈的抗议。
应该根据设计开发出原型,经过测试,当然还有验证——它是要演化的。实现可用的设计,这是设计者或者说架构师的责任。
通过找到移除软件设计不可逆性的方式,从而去除所谓架构的概念。”增强可逆性是注重实效的软件实现方式的关键构成部分。
程序员在拒绝设计的同时,也就放弃了思考。
任何一位团队成员,只要理解某段代码的来龙去脉,就应该可以对其进行处理。如果某一段代码只有一位开发人员能够处理,项目的风险无形中也就增加了。
相比找出谁的主意最好、谁的代码实现很烂而言,解决问题,并让应用满足用户的期望要更为重要。
在团队中实行任务轮换制,让每个成员都可以接触到不同部分的代码,可以提升团队整体的知识和专业技能。
要强调代码的集体所有制。让开发人员轮换完成系统不同领域中不同模块的不同任务。
不要无意间丧失了团队的专家技能。如果某个开发人员在某个领域中极其精通,不妨让他作为这方面的驻留专家,而且系统的其他部分代码也对他开放,这样对团队和项目都很有帮助。
我们有时会发现自己在某些方面,比其他团队成员知道得更多。那要怎么对待这种新发现的“权威地位”呢?当然,可以用它来质疑别人,取笑他人做出的决策和开发的代码——有些人就是这样做的。不过,我们可以共享自己的知识,让身边的人变得更好。
通过详细解释自己知道的东西,可以使自己的理解更深入。当别人提出问题时,也可以发现不同的角度。也许可以发现一些新技巧——听到一个声音这样告诉自己:“我以前还没有这样思考过这个问题。
好的指导者在为他人提供建议时会做笔记。如果遇到需要花时间进一步观察和思考的问题,不妨先草草记录下来。此后将这些笔记加入到每日日志中”。
成为指导者意味着要分享——而不是固守——自己的知识、经验和体会。意味着要对别人的所学和工作感兴趣,同时愿意为团队增加价值。一切都是为了提高队友和你的能力与水平,而不是为了毁掉团队。
如果一直在就同一个主题向不同的人反复阐述,不妨记录笔记,此后就此主题写一篇文章,甚至是一本书。
授人以鱼,三餐之需;授人以渔,终生之用。”告诉团队成员解决问题的方法,也要让他们知道如何解决问题的思路,这也是成为指导者的一部分。
作为指导者,应该鼓励、引领大家思考如何解决问题。前面提“到过亚里士多德的话:“接纳别人的想法,而不是盲目接受,这是受过教育的头脑的标志。”应该接纳别人的想法和看问题的角度,在这个过程中,自己的头脑也得到了拓展。
如果整个团队都能够采纳这样的态度,可以发现团队的知识资本在快速提升,而且将会完成一些极其出色的工作成果。
给别人解决问题的机会。指给他们正确的方向,而不是直接提供解决方案。每个人都能从中学到不少东西。
用问题来回答问题,可以引导提问的人走上正确的道路。
使用版本控制系统的方式,会影响生产力、产品稳定性、产品质量和开发日程。
完成一项任务后,应该马上提交代码,不应该让代码在开发机器上多停留一分钟。如果代码不能被别人集成使用,那又有什么用处呢?应该赶紧发布出去,并开始收集反馈。
如果在任务完成之前就提交代码又会如何?也许你正在开发一些至关重要的代码,而且你想在下班回家晚饭之后再继续开发。要想在家里得到代码,最简单的方式就是将其提交到源代码控制系统,到家之后再把代码签出。
向代码库中提交仍在开发的代码,会带来很多风险。这些代码可能还有编译错误,或者对其所做的某些变化与系统其他部分的代码不兼容。当其他开发者获取最新版本的代码时,也会受到这些代码的影响。
有些源代码控制系统会区分“提交”和“可公开访问”两种代码权限。此时,可以进行临时的提交操作(比如在工作地点和家之间来回奔波时),不会因为完全提交未完成的代码,而让团队的其他“成员感到郁闷。
感觉好像整个团队就在源代码控制系统的另一端盯着你。要知道一旦提交代码,别人就都可以访问了。
代码复查或许是找到并解决问题的最佳方式。然而,有时很难说服管理层和开发人员使用它来完成开发工作。
代码复查不只针对初级开发者编写的代码——团队中每个开发人员的代码都应该进行复查,无论其经验丰富与否。
结对编程:
在极限编程中,不存在一个人独立进行编码的情况。编程总是成对进行的:一个人在键盘旁边(担任司机的角色),另一个人坐在后面担任导航员。他们会不时变换角色。有第二双眼睛在旁边盯着,就像是在进行持续的代码复查活动,也就不必安排单独的特定复查时间了。
在代码复查中要看什么呢?你可能会制订出要检查的一些特定问题列表(所有的异常处理程序不允许空,所有的数据库调用都要在包的事务中进行,等等),不过这里是一个可供启动的最基本的检查列表。
复查所有的代码。对于提升代码质量和降低错误率来说,代码复查是无价之宝。如果以正确的方式进行,复查可以产生非常实用而高效的成果。要让不同的开发人员在每个任务完成后复查代码。
不进行思考、类似于橡皮图章一样的代码复查没有任何价值。
代码复查需要积极评估代码的设计和清晰程度,而不只是考量变量名和代码格式是否符合组织的标准。
同样的功能,不同开发人员的代码实现可能不同。差异并不意味着不好。除非你可以让某段代码明确变得更好,否则不要随意批评别人的代码。
假定现在你手上有一个进行了一半的任务,由于技术上的难题,看起来不能准时完成了。如果这时积极通知其他相关各方,就等于给机会让他们提前找出解决问题的方案。
发送电子邮件,用即时贴传递信息,或快速电话通知,这都是通报大家的传统方式。还可以使用Alistair Cockburn提出的“信息辐射器”。信息辐射器类似于墙上的海报,提供变更的信息。路人可以很方便地了解其中的内容。以推送的方式传递信息,他们就不必再来问问题了。信息辐射器中可以展示目前的任务进度,和团队、管理层或客户可能会感兴趣的其他内容。
也可以使用海报、网站、Wiki、博客或者RSS Feed。只要让人们可以有规律地查看到需要的信息,这就可以了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。