高效程序员的45个习惯-敏捷开发修炼之道
一些误解一直在坊间流行。一般误认为敏捷就是快。岂不知它本来要以 “lightweight processes” (轻量级过程)命名,只不过有些参会者不喜欢被看做是在拳台上跳来跳去的轻量级 拳手,所以采用了 "敏捷" 这个词。
还有其他一些误解是:
- 敏捷就是只写代码不写文档
- 敏捷需要重构而无需设计
- 敏捷迭代就是尽量做到最小
- 敏捷需要天才的程序员才能应用
迭代开发,价值优先
分解任务,真实进度
站立会议,交流畅通
用户参与,调整方向
结对编程,代码质量
测试驱动,安全可靠
持续集成,尽早反馈
自动部署,一键安装
定期回顾,持续改进
不断学习,提高能力
一. 敏捷-高效软件开发之道
不管路走了多远,错了就要重新返回。
软件开发像是在冲浪:一直处于动态、不断变化的环境中。大海本身无法预知,充满风险,并且海里还可能有鲨鱼出没。
冲浪之所以如此有挑战性,是因为波浪各不相同。在冲浪现场,每次波浪都是独一无二的,冲浪的动作也会各不相同。例如,沙滩边的波浪和峭壁下的波浪就有很大的区别。
在软件开发领域里,在项目研发过程出现的需求变化和挑战就是你在冲浪时要应对的海浪:它们从不停止并且永远变化,像波浪一样。在不同的业务领域和应用下,软件项目具有不同的形式,带来了不同的挑战。甚至还有鲨鱼以各种伪装出没。
敏捷开发宣言:
- 个体和交互胜过过程和工具
- 可工作的软件胜过面面俱到的文档
- 客户协作胜过合同谈判
- 响应变化胜过遵循计划
以人为本、团队合作、快速响应变化和可工作的软件
敏捷开发就是在一个高度协作的环境中,不断地使用反馈进行自我调整和完善。
平衡的艺术:一个习惯很可能会做的过火或者做得不够以及被误用。我们会给出一些建议,帮你掌握平衡,并告诉你一些技巧,能使习惯真正为你所用。
二. 态度决定一切
选定了要走的路,就是选定了它通往的目的地。
专业的态度应该着眼于项目和团队的积极结果,关注个人和团队的成长,围绕最后的成功开展工作。
1. 做事
指责不能修复bug
在敏捷的团队中,大家的重点是做事。你应该把重点放到解决问题上,而不是在指责犯错者上面纠缠。
你可以从自己先做起。如果一个开发者带着抱怨或问题来找你,你要了解具体的问题,询问他你能提供什么样的帮助。这样简单的一个行为就清晰地表明你的目的是解决问题,而不是追究责任,这样就会消除他的顾虑。你是给他们帮忙的。这样,他们会知道每次走进你的时候,你会真心帮助他们解决问题。他们可以来找你把问题解决了,当然还可以继续去别处求助。
勇于承认自己不知道答案,这会让人感觉放心。一个重大的错误应该被当做是一次学习而不是指责他人的机会。团队成员们在一起工作,应互相帮助,而不是互相指责。
平衡的艺术
- "这不是我的错",这句话不对。"这都是你的错",这句话更不对。
- 如果你没有犯过任何错误,就说明你可能没有努力去工作
- 如果一个团队成员误解了一个需求、一个API调用,或者最近一次会议做的决策,那么,也许就意味着团队的其他成员也有相同的误解。要确保整个团队尽快消除误解。
- 如果一个团队成员的行为一再伤害了团队,则他表现得很不职业。那么,他就不是在帮助团队向解决问题的方向前进。这种情况下,我们必须要求他离开这个团队。
2. 欲速则不达
我们经常会遇到这种情况,出现了一个bug,并且时间紧迫。快速修复确实可以解决它:只要新加一行代码或者忽略那个列表上的最后一个条目,它就可以工作了。但接下来的做法才能说明,谁是优秀的程序员,谁是拙劣的代码工人。
拙劣的代码工人会这样不假思索地改完代码,然后快速转向下一个问题。
优秀的程序员会挖掘更深一层,尽力去理解为什么这里必须要加1,更重要的是,他会想明白会产生什么其他影响。
在工作压力之下,不去深入了解真正的问题以及可能的后果,就快速修改代码,这样只是解决表面问题,最终会引发大问题。快速修复的诱惑,很容易令人把持不住,坠入其中。短期看,它似乎是有效的。但从长远来看,它无异于穿越一片流沙,你也许侥幸走过了一半的路程(甚至更远),一切似乎都正常。但是转眼间悲剧就发生了....
尽管我们在谈论理解代码,特别是在修改代码之前一定要很好地理解它,然而同样道理,你也需要了解团队的开发方法或者开发过程。
你必须要理解团队采用的开发方法,你必须理解如何恰如其分地使用这种方法,为何它们是这样的,以及如何成为这样的。
只有理解了这些问题,你才能进行有效的改变
孤立非常危险,不要让开发人员完全孤立地编写代码。如果团队成员花些时间阅读其他同事写的代码,他们就能确保代码是可读的和可理解的,并且不会随意的代码。阅读代码的频率越高越好。实行代码复审,不仅有而且是发现bug最有效的方法之一。
在项目中,代码应该是很亮堂的,不应该有黑暗死角。你也许不知道每块代码的每个细节,或者每个算法的每个步骤,但是你对整体的相关知识有很高的了解。没有任何一块代码被警戒线或者 "切勿入内" 的标志隔离开。
平衡的艺术
- 如果有一位团队成员宣布,有一块代码其他人都很难看懂,这就意味着任何人(包括原作者)都很难维护它。请让它变得简单些
- 不要急于修复一段没能真正理解的代码。这种+1/-1的病症始于无形,但是很快就会让代码一团糟。要解决真正的问题,不要治标不治本。
- 所有的大型系统都非常复杂,因此没有一个人能完全明白所有的代码。除了深入了解你正在开发的那部分代码之外,你还需要从更高的层面来了解大部分代码的功能,这样就可以理解系统各个功能块之间是如何交互的。
3. 对事不对人
你很可能见过,对方案设计的讨论失控变成了情绪化的指责-做决定是基于谁提出了这个观点,而不是权衡观点本身的利弊。
但是,这也很正常。当Lee先生在做一个新方案介绍的时候,下面有人会说:"那样很蠢!" (这也就暗示着Lee先生也很蠢。) 如果把这句话推敲一下,也许会好一点:"那样很蠢,你忘记考虑它要线程安全。" 事实上最适合并且最有效的表达方式应该是: "谢谢,Lee先生。但是我想知道,如果两个用户同时登录会发生什么情况?"
看出其中的不同了吧!下面来看看对一个明显的错误有哪些常见的反应。
- 否定个人能力
- 指出明显的缺点,并否定其观点
- 询问你的队友,并提出你的顾虑
第一种方法是不可能成功的。那样提出问题根本不会对他的水平有任何提高,反而会导致他以后再也不会提出自己的任何想法了。
第二种方法至少观点明确,但也不能给对方太多的帮助,甚至可能会让你自己惹火上身。因为它只在xx模块的环境中使用,现在该是你觉得自己愚蠢了。
第三种方法。没有谴责,没有评判,只是简单地表达自己的观点。让对方自己意识到这个问题,而不是扫他的面子。由此可以开始一次交谈,而不是争辩。
在一个需要紧密合作的开发团队中,如果能稍加注意礼貌对待他人,将会有益于整个团队关注真正有价值的问题,而不是勾心斗角,误入歧途。我们每个人都能有一些极好的创新想法,同样也会萌生一些很愚蠢的想法。
如果你准备提出一个想法,却担心有可能被嘲笑,或者你要提出一个建议,却担心自己丢面子,那么你就不会主动提出自己的建议了。然而,好的软件开发作品和好的软件设计,都需要大量的创造力和洞察力,分享并融合各种不同的想法和观点,远远胜于单个想法为项目带来的价值。
能容纳自己并不接受的想法,表明你的头脑足够有学识。
- 设定最终期限
- 逆向思维:先积极地看到它的正面,然后再努力地从反面去认识它。目的是要找出优点最多缺点最少的那个方案,而这个好办法可以尽可能地发现其优缺点。也有助于少带个人感情。
- 设立仲裁人:在会议的开始,选择一个仲裁人作为本次会议的决策者。每个人都要有机会针对问题畅所欲言。仲裁人的责任就是确保每个人都有发言的机会,并维持会议的正常进行。仲裁人可以防止明星员工操作会议,并及时打断假大空式发言。
- 支持已经做出的决定:一旦方案被确定了(不管是什么样的方案),每个团队成员都必须通力合作,努力实现这个方案。每个人都要时刻记住,我们的目标是让项目成功满足用户需求。客户并不关心这是谁的主意-他们关心的是,这个软件是否可以工作,并且是否符合他们的期望。结果最重要。
- 支持已经做出的决定。
4. 排除万难,奋勇前进
你必须有勇气向前冲锋,做你认为对的事情
三. 学无止境
即使你已经在正确的轨道上,但如果只是停止不前,也仍然会被淘汰出局。
软件开发行业是一个不停发展和永远变化的领域。虽然有一些概念一直有用,但还有很多知识很快就会过时。从事软件开发行业就像在跑步机上,你必须一直跟上步伐稳步前进,否则就会摔倒出局。
许多新技术都基于现有的技术和思想。它们会加入一些新的东西,这些新东西是逐步加入的量。如果你跟踪技术变化,那么学习这些新东西对你来说就是了解这些增量变化。如果你不跟踪变化,技术变化就会显得很突然并且难以应付。
学习新的技术和新的开发方法很重要,同时你也要摒弃陈旧和过时的开发方法。换句话说,你需要 懂得丢弃。
5. 跟踪变化
唯有变化是永恒的
- 增量和迭代式的学习。每天计划用一段时间来学习新技术,它不需要很长时间,但需要经常进行。记下那些你想学习的东西-当你听到一些不熟悉的术语或者短语时,简要地把它记录下来。然后在计划的时间中深入研究它。
- 了解最新行情。
- 参加本地的用户组活动
- 参加研讨会议
- 如饥似渴地阅读
平衡的艺术
- 许多新想法从未变得羽翼丰满,成为有用的技术。即使是大型、热门和资金充裕的项目也会有同样的下场。你要正确把握自己投入的精力。
- 你不可能精通每一项技术,没有必要去做这样的尝试。只要你在某些方面成为专家,就能使用同样的方法,很容易地成为新领域的专家。
- 你要明白为什么需要这项新技术:它试图解决什么样的问题?它可以被用在什么地方?
- 避免在一时冲动的情况,只是因为想学习将应用转换到新的技术、框架或开发语言。在做决策之前,你必须评估新技术的优势。开发一个小的原型系统,是对付技术狂热者的一剂良药。
6. 对团队投资
午餐会议
每周讲座,介绍一些概念,演示工具,或者做团队感兴趣的任何一件事情。你可以挑一本书,给大家说说其中一些特别内容、项目或者实践。无论什么主题都可以。
主持人先讲15分钟,然后,进行开放式讨论,这样每个人都可以发表自己的意见,讨论这个主题对于项目的意义,讨论应该包括所能带来的益处,提供来自自己应用程序的示例,并准备好听取进一步的信息。
平衡的艺术
- 坚持有计划有规律地举行讲座。持续、小步前进才是敏捷。
- 不要局限于纯技术的图书和主题,相关的非技术主题(项目估算、沟通技巧等)也会对团队有帮助
7. 懂得丢弃
打破旧习惯很难,更难的是自己还没有意识到这个问题。丢弃的第一步,就是要意识到你还在使用过时的方法,这也是最难的部分。另一个难点就是要做到真正地丢弃旧习惯。思维定式是经过多年摸爬滚打才构建成型的,已经根深蒂固,没有人可以很容易就丢弃它们。
平衡的艺术
沉舟侧畔千帆过,病树前头万木春
8. 打破砂锅问到底
不能只满足于别人告诉你的表面现象,要不停地提问直到你明白问题的根源
平衡的艺术
- 你可能会跑题,问一些与主题无关的问题。问 "为什么",但是要问到点子上
- 当你问 "为什么" 的时候,也许你会被反问:"为什么你问这个问题?" 在提问之前,想好你提问的理由,这会有助于你问出恰当的问题。
- "这个,我不知道" 是一个好的起点,应该由此进行更进一步的调查,而不应在此戛然结束。
9. 把握开发节奏
在很多不成功的项目中,基本上都是随意安排工作计划,没有任何的规律。那样的随机安排很难处理。你根本不知道明天将会发生什么。
但是,敏捷项目会有一个节奏和循环,让开发更加轻松。例如,Scrum约定了30天之内不应发生需求变化,这样确保团队有一个良性的开发节奏。这有助于防止一次计划太多的工作和一些过大的需求变更。
项目开发需要有一致和稳定的节奏。编辑,运行测试,代码复审,一致的迭代,然后发布。如果知道什么时候下一个节拍,跳舞就会更加容易。
四. 交付用户想要的软件
没有任何计划在遇敌后还能继续执行
设计师软件开发的基础,没有它很难做好开发,但你也不能被它牵制(设计指导而不是操纵开发)
10. 让客户做决定
决定什么不该决定
让你的客户做决定。开发者、经理或业务分析师不应该做业务方面的决定。用业务负责人能够理解的语言,向他们详细解释遇到的问题,并让他们做决定。
平衡的艺术
- 记录客户做出的决定,并注明原因。
- 不要用低级别和没价值的问题打扰繁忙的业务人员。
- 不要随意假设低级别的问题不会影响他们的业务。如果能影响他们的业务,就是有价值的问题
- 如果业务负责人回答 "我不知道",这也是一个称心如意的答案。也许是他们还没有想到那么远,也许是他们只有看到运行的实物才能评估出结果。尽你所能为他们提供建议,实现代码的时候也要考虑可能出现的变化。
11. 让设计指导而不是操纵开发
敏捷方法建议你早在开发初期就开始编码。是否那就意味着没有设计呢?不,绝对不是,好的设计仍然十分重要。画关键工作图(例如:用uml)是必不可少的,因为要使用类及其交互关系来描绘系统是如何组织的。在做设计的时候,你需要花时间去思考(讨论)各种不同选择的缺陷和益处,以及如何做权衡。
然后,下一步才考虑是否需要开始编码。如果你在前期没有考虑清楚这些问题,就草草地开始编码,很可能会被很多意料之外的问题搞晕。
但是,即使之前已经提交了设计文档,也还会有一些意料之外的情况出现。时刻谨记,词阶段提出的设计只是基于你目前对需求的理解而已。一旦开始了编码,一切都会改变。设计及其代码实现会不停地发展和变化。
如果你自己都不清楚所谈论的东西,就根本不可能精确地描述它 -冯诺依曼
CRC(类-职责-协作)
- 类名
- 职责:它应该做什么?
- 协作者:要完成工作它要与其他什么对象一起工作?
代码很自然地为设计的好坏提供了最好的反馈。如果需求有了小的变化,它仍然容易去实现,那么它就是好的设计。
好的设计应该是正确的,而不是精确的。也就是说,它描述的一切必须是正确的,不应该涉及不确定或者可能会发生变化的细节。它是目标,不是具体的处方。
平衡的艺术
- "不要在前期做大量的设计" 并不是说不要设计。只是说在没有经过真正的代码验证之前,不要陷入太多的设计任务。当对设计一无所知的时候,投入编码也是一件危险的事。如果深入编码只是为了学习或创造原型,只要你随后能把这些代码扔掉,那也是一个不错的办法。
- 即使初试的设计到后面不再管用,你仍需设计:设计行为是无价的。正如美国总统艾森豪威尔所说:“计划是没有价值的,但计划的过程是必不可少的。” 在设计过程中学习是有价值的,但设计本身也许没有太大的用处。
- 白板、草图、便利贴都是非常好的设计工具。复杂的建模工具只会让你分散精力,而不是启发你的工作。
12. 合理地使用技术
- 这个技术框架真能解决这个问题吗?如何评估这个技术的?
- 你将会被它拴住吗?有些技术是贼船,它缺乏可取消性
- 维护成本是多少?
新技术应该像是新的工具,可以帮助你更好地工作,它自己不应该成为你的工作。
每一门技术都会有优点和缺点,无论它是开源的还是商业商品、框架、工具或者语言,一定要清楚它的利弊
不要开发那些你容易下载到的东西。虽然有时需要从最基础开发所有你需要的东西,但那是相当危险和昂贵的
13. 保持可以发布
已提交的代码应该随时可以行动
14. 提早集成,频繁集成
在团队里合作,修改一些东西的时候必须很谨慎。你要时刻警惕,每次改动都会影响系统的状态和整个团队的工作效率。在办公室的厨房里,你不能容忍任何人乱丢垃圾,为什么就可以容忍一些人给项目带来垃圾代码呢?
下面是一个简单的工作流程,可以防止你提交破坏系统的代码:
- 在本地运行测试。先保证你完成的代码可以编译,并且能通过所有的单元测试。接着确保系统中的其他测试都可以通过。
- 检出最新的代码。从版本控制系统中更新代码到最新的版本,再编译和运行测试。这样往往会发现让你吃惊的事情:其他人提交的新代码和你的代码发生了冲突
- 提交代码。现在是最新的代码了,并且通过了编译和测试,你可以提交它们了。
在做上面事情的时候,也许你会遇到这样一个问题-其他人提交了一些代码,但是没有通过编译或者测试。如果发生了这样的事情,要立即让他们知道,如果有需要,可以同时警告其他的同事。当然,最好的办法是,你有一个持续集成系统,可以自动集成并报告集成结果。
千万不能让系统即不可以发布,又不可以撤销。
代码集成式主要的风险来源,要想规避这个风险,只有提高集成,持续而有规律地进行集成。
15. 提早实现自动化部署
能用一种可重复和可靠的方式,在目标机器上部署你的应用。不行的是,大部分开发者只会在项目的尾期才开始考虑部署问题。结果经常出现部署失败,要么是少了依赖的组件,要么是少了一些图片,要么就是目录结构有误。
从第一天起就开始交付
平衡的艺术
- 一般产品在安装的时候,都需要有相应的软、硬件环境。比如,Java或Ruby的某个版本、外部数据库或者操作系统。这些环境的不同很可能会导致很多技术支持的电话。所以检查这些依赖关系,也是安装过程的一部分
- 在没有询问并征得用户的同意之前,安装程序绝对不能删除用户的数据
- 部署一个紧急修复的bug应该很简单,特别是在生产服务器的环境中。你知道这会发生,而且你不想在压力之下,在凌晨3点半,你还在手工部署系统
- 用户应该可以安全并且完整地卸载安装程序,特别是在质量保证人员的机器环境中。
- 如果维护安装脚本变得很困难,那很可能是一个早期警告,预示着:很高的维护成本(或不好的设计决策)
16. 使用演示获得频繁反馈
需求就像是流动着的油墨
你时常会听到一些人想要冻结需求。但是,现实世界中的需求就像是流动着的油墨。你无法冻结需求,正如你无法冻结市场、竞争、知识、进化或者成长一样。就算你真的冻结了,也很可能是冻结了错的东西。如果你期望用户在项目开始之前,就能给你可靠的和明确的需求,那就大错特错了,赶快醒醒吧!
没有人的思想和观点可以及时冻结,特别是项目的客户。就算是他们已经告诉你想要的东西了,他们的期望和想法还是在不停地进化-特别是当他们在使用新系统的部分功能时,他们才开始意识到它的影响和可能发生的问题。这就是人的本性。
作为人类,不管是什么事情,我们都能越做越好,不过是以缓慢而逐步的方式。你的客户也一样。在给了你需求之后,他们会不停地研究这些功能,如何才能让它们变得更好使用。如果,你觉得自己要做的所有工作就是按照用户最初的需求,并实现了它们,但是在交付的时候,需求已经发生了变化,你的软件可能不会令他们满意。在软件开发过程中,你将自己置于最大的风险中:你生产出了他们曾经要求过的软件,但却不是他们现在真正想要的。那最后的结果就是:惊讶、震惊和失望,而不是满意。
你的客户的期望就像宇宙飞船的实际位置。软件开发的成功就在于最后你离客户的期望有多近。你计算的每个精确位置,就是一个给客户演示目前已经完成功能的机会,也正是得到用户反馈的时候。在你动身进入下一段旅程的时候,这些反馈可以用来纠正你的方向。
我们经常看到,给客户演示所完成功能的时间与得到客户需求的时间间隔越长,那么你就会离最初需求越来越远。
应该定期地,每隔一段时间,例如一个迭代的结束,就与客户会晤,并且演示你已经完成的功能特性。
如果你能与客户频繁协商,根据他们的反馈开发,每个人都可以从中受益。客户会清楚你的工作进度。反过来,他们也会提炼需求,然后趁热反馈到你的团队中。这样,他们就会基于自己进化的期望和理解为你导航,你编写的程序也就越来越接近他们的真实需求。客户也会基于可用的预算和时间,根据你们真实的工作进度,排列任务的优先级。
维护项目术语表
不一致的术语是导致需求误解的一个主要原因。企业喜欢用看似普遍浅显的词语来表达非常具体、深刻的意义。
我经常看到这样的事情:团队中的程序员们,使用了和用户或者业务人员不同的术语,最后因为 "阻抗失调" 导致bug和设计错误。
为了避免这类问题,需维护一份项目术语表。人们应该可以公开访问它,一般是在企业内部网或者wiki上,这听起来似乎是一件小事情-只是一个术语列表及其定义。但是,它可以帮助你,确保你真正地和用户进行沟通。
在项目的开发过程中,从术语表中为程序结构-类、方法、模型、变量等选择合适的名字,并且要检查和确保这些定义一直符合用户的期望。
演示是用来让客户提出反馈的,有助于驾驭项目的方向。如果缺少功能或者稳定性的时候,不应该拿来演示,那只能让人生气。可以及早说明期望的功能,让客户知道,他们看到的是一个正在开发中的应用,而不是一个最终已经完成的产品。
17. 使用短迭代,增量发布
统一过程和敏捷方法都使用迭代和增量开发。使用增量开发一次开发应用功能的几个小组。每一轮的开发都是基于前一次的功能,增加为产品增值的新功能。这时,你就可以发布或者演示产品。
迭代开发是,在小且重复的周期里,你完成各种开发任务:分析、设计、实现。测试和获得反馈,所以叫做迭代。
迭代的结束就标记一个里程碑。这时,产品也许可用,也许不可用。在迭代结束时,新的功能全部完成,你就可以发布,让用户真正地使用,同时提供技术支持、培训和维护方面的资源。每次增加的新功能都会包含多次迭代。
对付大项目,最理想的办法就是小步前进,这也是敏捷方法的核心。大步跳跃大大地增加了风险,小步前进才可以帮助你很好地把握平衡。
18. 固定的价格就意味着背叛承诺
固定价格的合同会是敏捷团队的一大难题。我们一直在谈论如何用持续、迭代和增量的方式工作。但是现在却有些人跑过来,想提早知道它会花费多少时间及多少成本。
从客户方来看,这完全是理所应当的。客户觉得做软件就好比是盖一栋楼房,或者是铺设一个停车场,等等。为什么软件不能像建筑业等其他传统的行业一样呢?
也许它真的与建筑有很多相似之处:真正的建筑行业,但不是我们想象中的建筑业。根据英国1998年的一个研究,由于错误而返工的成本大约占整个项目成本的30%。这不是因为客户的需求变化,也不是物理定律的变化,而是一些简单错误。比如,横梁太短,窗户洞太大等等。这些都是简单并且为人熟悉的错误。
软件项目会遭遇各种各样的小错误,还要加上基础需求的变化(不,我要的不是一个工棚,而是一栋摩天大楼),不同个体和团队的能力差别非常巨大(20倍,甚至更多),当然,还不停地会有新技术出现。
软件项目天生就是变化无常的,不可重复。那么我们有什么可行的办法呢?我们能做更精确的评估吗?或者商量出另外一种约定。
项目的挖掘和创造需要很多配合工作。试试下面的办法:
- 主动提议先构建系统最初的、小的和有用的部分。挑选一系列小的功能,这样完成第一次交付应该不多于6-8周。向客户解释,这时候还不是要完成所有的功能,而是要足够一次交付,并能让用户真正使用
- 第一个迭代结束时客户有两个选择:可以选择一系列新的功能,继续进入下一个迭代;或者可以取消合同,仅需支付第一个迭代的几周费用,他们要么把现在的成果扔掉,要么找其他的团队来完成它。
- 如果他们选择继续前进。那么这时候,应该就能很好地预测下一个迭代工作。在下一个迭代结束的时候,用户仍然有同样的选择机会“要么现在停止,要么继续下一个迭代。”
对客户来说,这种方式的好处是项目不可能会死亡。他们可以很早地看到工作的进度。他们总是可以控制项目,可以随时停止项目,不需要缴纳任何的违约金。他们可以控制先完成哪些功能,并能精确地知道需要花费多少资金。总而言之,客户会承担更低的风险。
五. 敏捷反馈
19. 守护天使
- 确保测试时可重复的,使用当前的日期或者时间作为参数,会让测试以来运行时间,使用你自己机器上的IP地址同样会让它以来运行时的机器,等等
- 测试你的边界条件。11:59:59和0:00:00都是不错的日期测试边界条件
- 不要放过任何一个失败的测试。在前面的案例中,一个测试一直失败了,但是因为一段时间内每天都会有几十个测试失败,没有人会注意到这个伪随机失败
20. 先用它再实现它
什么是成功地实现特定功能的最低成本
不要把测试优先和提交代码之前的测试等同起来。测试先行可以帮助你改进设计,但是你还是需要在提交代码之前做测试。
21. 不同环境,就有不同问题
22. 自动验收测试
23. 度量真实的进度
几乎所有公司的时间表都是为工资会计准备的,不是用来度量软件项目的开发进度的。
我们不应该去计算工作量完成的百分比,而应该测定还剩下多少工作量没有完成。如果你最初估计这个任务需要40个小时,在开发了35个小时之后,你认为还需要另外30个小时的工作。那就得到了很重要的度量结果(这里诚实非常重要,隐瞒真相毫无意义)
如果能一直让下一步工作是可见的,会助于进度度量。最好的做法就是使用待办事项(backlog)
在Scrum开发方法中,每个迭代被称作sprint,通常为30天时间,sprint的待办事项列表是当前迭代任务列表,它会评估剩下的工作量,显示每个任务还需要多少小时可以完成。
每个工作日,每个团队成员会重新评估完成一个任务还需要多少小时,不管怎么样,只要所有任务的评估总和超过了一个迭代剩余的时间,那么任务就必须移到下一个迭代中开发。
24. 倾听用户的声音
倒霉的客户必须要配置那些包含了一些魔术数字的模糊系统文件,否则系统根本不会运行。系统既没有错误提示消息,也不会崩溃,只是显示大黑屏和一个斗大的 "退出" 按钮。事实上,安装说明书中有一行提到了这样的问题,但显然 80% 的用户忽略了这个信息,因此只能求助公司的技术支持部门,并遭到他们的嘲笑。
每一个抱怨的背后都隐藏了一个事实。找出真相,修复真正的问题。
没有愚蠢的用户,只要愚蠢、自大的开发人员。
六. 敏捷编码
开始看起来非常正常的项目,是什么让它最终变得难以掌控?开发人员在完成任务时,可能会难以抵挡诱惑为节省时间而走 "捷径"。然而,这些 "捷径" 往往只会推迟问题的爆发时间,而不是把它彻底解决掉。当项目时间上的压力增加时,问题最终还是会在项目团队面前出现,让大家心烦意乱。
开始看起来非常正常的项目,是什么让它最终变得难以掌控?开发人员在完成任务时,可能会难以抵挡诱惑为节省时间而走 "捷径"。然而,这些 "捷径" 往往只会推迟问题的爆发时间,而不是把它彻底解决掉。当项目时间上的压力增加时,问题还是会在项目团队面前出现,让大家心烦意乱。
如何保证项目开发过程中压力正常,而不是在后期面对过多的压力,以致噩梦连连呢?最简单的方式,就是在开发过程中便细心 "照看" 代码。在编写代码时,每天付出一点小的努力,可以避免代码 "腐烂",并且保证应用程序不至于变得难以理解和维护。
25. 代码要清晰地表达意图
设计软件有两种方式,一种是设计的尽量简单,并且明显没有缺陷。另一种方法是设计得尽量复杂,并且没有明显的缺陷
开发代码时,应该更注重可读性,而不是只图自己方便。代码阅读的次数要远远超过编写的次数,所以在编写的时候值得花点功夫让它读起来更加简单。实际上,从衡量标准上来看,代码清晰程度的优先级应该排在执行效率之前。
例如,如果默认参数或可选参数会影响代码可读性,使其更难以理解和调试,那最好明确地指明参数,而不是在以后让人迷惑。
在改动代码以修复bug或添加新功能时,应该有条不紊地进行。首先,应该理解代码做了什么,它是如何做的。接下来,搞清楚将要改变哪些部分,然着手修改并进行测试。作为第一步的理解代码,往往是最难的。如果别人给你的代码很容易理解,接下来的工作就省心多了。要敬重这个黄金法则,你欠他们一份情,因此也要让你自己的代码简单、便于阅读。
// v1:2是什么意思
coffeeShop.placeOrder(2);
// v2:添加注释
coffeeShop.placeOrder(2 /* large cup */);
// v3
public enum CoffeeCupSize{
Small,
Medium,
Large
}
coffeeShop.placeOrder(CoffeeCupSize.Large);
代码必须明确说出你的意图,而且必须富有表达力。这样可以让代码更易于被别人阅读和理解。代码不让人迷惑,也就减少了发生潜在错误的可能。一言以蔽之,代码应意图清晰,表达明确。(PIE原则 program intently and expressively 意图清楚且表达明确地编程)
在编写代码时,应该使用语言特性来提升表现力。使用方法名来传达意向,对方法参数的命名要帮助读者理解背后的想法。异常传达的信息是哪些可能会出问题,以及如何进行防御式编程,要正确地使用和命名异常。好的编码规范可以让代码变得易于理解,同时减少不必要的注释和文档。
防御式编程:在防御式驾驶中要建立这样一种思维,那就是你永远也不能确定另一位司机要做什么。这样才能确保在其他人做出危险动作时你也不会受到伤害。你要承担起保护自己的责任,哪怕是其他司机犯的错误。
核心思想:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。这种思想是将可能出现的错误造成的影响控制在有限的范围内。
26. 用代码沟通
通常程序员都很讨厌写文档。这是因为大部分文档都与代码没有关系,并且越来越难以保证其符合目前的最新状况。这不只违反了DRY原则(不要重复你自己,don't repeat yourself),还会产生使人误解的文档,这还不如没有文档。
建立代码文档无外乎两种方式:利用代码本身;利用注释来沟通代码之外的问题
如果必须通读一个方法的代码才能了解它做了什么,那么开发人员先要投入大量的时间和精力才能使用它。反过来讲,只需短短几行注释说明方法行为,就可以让生活变得轻松许多。开发人员可以很快了解到它的意图、它的期待结果,以及应该注意之处。
如何命名很重要。程序元素的命名是代码读者必读的部分。通过使用细心挑选的名称,可以向读者传递大量的意图和信息。反过来讲,使用人造的命名范式会让代码难以阅读和理解。这些范式中包括的底层数据类型信息,会硬编码在变量名和方法名中,形成脆弱的、僵化的代码。
使用细心挑选的名称和清晰的执行路径,代码几乎不需要注释。
对于类中的每个方法:
目的:为什么需要这个方法?
需求(前置条件):方法需要什么样的输入,对象必须处于何种状态,才能让这个方法工作?
承诺(后置条件):方法成功执行后,对象现在处于什么状态,有哪些返回值?
异常:可能会发生什么样的问题?会抛出什么样的异常?
注释不能替代优秀的代码。
解释代码做了什么的注释用处不那么大。相反,注释要说明为什么会这样写代码。
当重写方法时,保留描述原有方法意图和约束的注释。
27. 动态评估取舍
- 如果现在投入额外的资源和精力,是为了将来可能得到的好处,要确认投入一定要得到回报(大部分情况下,是不会有回报的)
- 真正的高性能系统,从一开始设计时就在向这个方向努力
- 过早的优化是万恶之源
- 过去用过的解决方案对当前的问题可能适用,也可能不使用。不要事先预设结论,先看看现在是什么状况。
28. 增量式编程
当你开车进行长途旅行时,两手把住方向盘,固定在一个位置,两眼直盯前方,油门一踩到底几个小时,这样可能吗?当然不行了,你必须掌握方向,必须经常注意交通状况,必须检查油量表,必须停车加油、吃饭,准备其他必需品,以及诸如此类的活动。
关键在于持续做一些细小而有用的事情,而不是做一段长时间的编程或重构。
29. 保持简单
许多开发人员倾向于将投入的努力与程序复杂性混同起来。如果你看到别人给出的解决方案,并评价说 "非常简单且易于理解",很有可能你会让设计者不高兴。许多开发人员以自己程序的复杂性为荣。
除非有不可辩驳的原因,否则不要使用模式、原则和高难度技术之类的东西。
太过简洁不等于简单,那样无法达到沟通的目的。
一个人认为简单的东西,可能对另一个人就意味着复杂。
30. 编写内聚的代码
内聚性用来评估一个组件(包、模块或配件)中成员的功能相关性。内聚程度高,表明各个成员共同完成了一个功能特性或是一组功能特性。内聚程度低的话,表明各个成员提供的功能是互不相干的。
31. 告知,不要询问
"面向过程的代码取得信息,然后做出决策。面向对象的代码让别的对象去做事情。" alec sharp通过观察后,一针见血地指出了这个关键点。但是这种说法并不仅限于面向对象的开发,任何敏捷的代码都应该遵循这个方式。
David Bock使用 "送报男孩和钱包的故事" 很好地诠释了这一点。假定送报男孩来到你的门前,要求付给他本周的报酬。你转过身去,让送报男孩从你的后屁股兜里掏出钱包,并且从中拿走两美元(你希望是这么多),再把钱包放回去。然后,送报男孩就会开着他崭新的美洲豹汽车扬长而去了。
将命令与查询分离开来(command-query separation)
在这个过程中,送报男孩作为 "调用者" , 应该告诉客户付他2美元。他不能探寻客户的财务状况,或是钱包的薄厚,他也不能代替客户做任何决策。这都是客户的责任,而不属于送报男孩。敏捷代码也应该以同样的方式公工作。
一个常规的 "命令" 可能会改变对象的状态,而且有可能返回一些有用的值,以方便使用。一个 "查询" 仅仅提供给开发人员对象的状态,并不会对其外部的可见状态进行修改。
这就是说,从外部看来,"查询" 不应该有任何副作用
像 "命令" 这种会产生内部影响的方法,强化了告知,不要询问的建议
32. 根据契约进行替换
(Liskov)里氏替换原则:任何继承后得到的派生类对象,必须可以替换任何被使用的基类对象,而且使用者不必知道任何差异。
当使用继承时,要想想派生类是否可以替换基类。如果答案是不能,就要问问自己为什么要使用继承。如果答案是希望在编写新类的时候,还要重用基类中的代码,也需要考虑转而使用聚合。聚合是指在类中包含一个对象,并且该对象是其他类的实例,开发人员将责任委托给所包含的对象来完成(该技术同样被称为委托)
针对 is-a 关系使用继承;针对 has-a 或 uses-a 关系使用委托
那么继承和委托分别在什么时候使用呢?
- 如果新类可以替换没有的类,标签它们之间的关系可以通过is-a来描述们就要使用继承
- 如果新类只是使用已有的累,并且两者之间的关系可以描述为has-a或是uses-a,就使用委托。
七. 敏捷调试
即使是运作得最好的敏捷项目,也会发生错误。bug、错误、缺陷-不管被称作什么,它们总会发生。
在调试时面对的真正问题,是无法用固定的时间来限制。
33. 记录问题解决日志
问题发生日期
问题描述
解决方案详细描述
引用文章或网址,以提供更多细节或相关信息
任何代码片段、设置或对话框的截屏,只要它们是解决方案的一部分,或者可以帮助更深入地理解相关细节
-
记录问题的时间不能超过在解决问题上花费的时间。要保持轻量级和简单,不必达到对外发布式的质量
-
找到以前的解决方法非常关键。使用足够的关键字,可以帮助你在需要的时候发现需要的条目
-
如果通过搜索web,发现没人曾经遇到同样的问题,也许搜索的方式有问题
-
要记录发生问题时应用程序、应用框架或平台的特定版本。同样的问题在不同的平台或版本上可能表现得不同
-
要记录团队做出一个重要决策的原因。否则,在6-9个月之后,想再重新回顾决策过程的时候,这些细节就很难再记得了,很容易发生互相指责的情形。
34. 警告就是错误
有些警告是过于挑剔的编译器的良性副产品,有些则不是。
签入构建工具中的代码不应该产生任何警告信息
- 弃用的方法被弃用是有原因的。不要再使用它们了。至少,安排一个迭代来将它们安全地移除掉
- 如果将过去开发完成的方法标记为弃用方法,要记录当前用户应该采取何种变通之策,以及被弃用的方法将会在何时一起移除
35. 对问题各个击破
大型系统非常复杂-在执行过程中会有很多因素起作用。从整个系统的角度来解决问题,就很难区分开,哪些细节对要定位的特定问题产生影响,而哪些细节没有。
答案很清晰:不要试图马上了解系统的所有细节。要想认真调试,就必须将有问题的组件或模块与其他代码库分离开来。如果有单元测试,这个目的就已经达到了。否则,你就得开动脑筋了。
比如,在一个时间紧急的项目中,发现一个严重的数据损毁问题。要花很多精力才能知道哪里出了问题,因为开发团队没有将数据库相关的代码与其他的应用代码分离开。他们无法将问题报告给软件厂商,当然不能把整个代码库用电子邮件发给人家!
于是,开发了一个小型的原型系统,并展示了类似的症状;然后将其发送给厂商作为实例,并询问他们的专家意见,使用原型帮助他们对问题理解得更清晰。
而且,如果他们无法在原型中再现问题的话,原型也能告诉他们可以工作的代码示例,这也有助于分离和发现问题。
识别复杂问题的第一步,是将它们分离出来。既然不可能在半空中试图修复飞机引擎,为什么还要试图在整个应用中,诊断其中某个组成部分的复杂问题呢?当引擎被从飞机中取出来,而且放在工作台上之后,就更容易修复了。
用原型进行分离
隔离问题不应该只在交付软件之后才着手。在构建系统原型、调试和测试时,各个击破的策略都可以起到帮助作用。
用二分查找的方式来定位问题是很有用的
36. 报告所有的异常
- 决定由谁来负责处理异常是设计工作的一部分
- 不是所有的问题都应该抛出异常
- 报告的异常应该在代码的上下文中有实际意义。在上述的例子中,抛出一个NullPointterException看起来也许不错,不够这就像抛出一个null对象一样,起不到任何帮助作用
- 如果代码中会记录运行时调试日志,当捕获或是抛出异常时,都要记录日志信息;这样做对以后的跟踪工作很有帮助
- 检查异常处理起来很麻烦。没人愿意调用抛出31种不同检查异常的方法。这是设计师的问题:要把它解决掉,而不是随便打个补丁就算了
- 要传播不能处理的异常
37. 提供有用的错误信息
实际上,不妨在显示给用户的信息中心提供更多细节。好比说,可以看到是哪条SQL查询或存放过程发生了错误;这样可以很快找到问题并且修正,而不是浪费大把的时间去盲目的碰运气。不过另一方面,在生产系统中,向用户显示数据连接问题的特定信息,不会对他们有多大帮助。而且有可能吓他们一跳。
一方面要提供给用户清晰、易于理解的问题描述和解释,使他们有可能寻求变通之法。另一方面,还有提供具备关于错误的信息技术细节给用户,这样方便开发人员寻找代码中真正的问题所在。
不过,当应用进入生产系统后,就不能把这些底层细节直接暴露给用户了,而要提供链接,或是某些访问错误日志的入口。支持团队可以请用户点击错误信息,并读出错误日志入口的相关信息,这样支持团队可以很快找到错误日志中的特定细节。对于独立系统来说,点击链接,有可能会将错误信息通过电子邮件发送到支持部门。
区分错误类型
程序缺陷
环境问题
用户错误
平衡的艺术
- 像 "无法找到文件" 这样的错误信息,就其本身而言无助于问题的解决。"无法打开 /andy/project/main.yaml以供读取 " 这样的信息更有效
- 没有必要等待抛出异常来发现问题。在代码关键点使用断言以保证一切正常。当断言失败时,要提供与异常报告同样详细的信息
- 在提供更多信息的同时,不要泄漏安全信息、个人隐私、商业机密,或其他敏感信息
- 提供给用户的信息可以包含一个主键,以便在日志文件或审核记录中定位相关内容
八. 敏捷协作
我不仅发挥了自己的全部能力,还将我所仰仗的人的能力发挥到极致。
38. 定期安排会面时间
要保证会议议题不会发散,每个人都应该只回答下述三个问题。
- 昨天有什么收获?
- 今天计划要做哪些工作?
- 面临着哪些障碍?
只能给予每个参与者很少的时间发言(大约两分钟)。如果要详细讨论某些问题,可以在立会结束之后,再召集相关人员。
一般来说,在大家到公司之后的半个小时到1个小时之内举行,是个不错的选择。
参加会议的人要遵守一些规则,以保证彼此不会分神,而且会议也不会跑题。他们必须回答上面的3个问题,而且不能展开深入讨论。
只有团队成员-开发人员、产品所有者和协调者可以发言
立会的时间最长不超过30分钟,10-15分钟比较理想
如果要使用需提前预订的会议室,就把预订的时间设定为一个小时吧。这样就有机会在15分钟的立会结束后,马上召开更小规模的会议。
要注意报告的细节。在会议中要给出具体的进度,但是不要陷入细节之中。例如,"我在开发登录页面" 就不够详细。"登录页面目前接受guest/guest作为登录用户名和密码,我明天会连接数据库来做登录验证",这样的详细程度才行
39. 架构师必须写代码
一个设计要解决的是眼前面临的特定问题,随着设计的实现,对问题的理解也会发生改变。想在开始实现之前,就做出一个很有效的详细设计是非常困难的。因为没有足够的上下文,能得到的反馈也很少,甚至没有。设计会随着时间而演进,如果忽略了应用的现状(它的具体实现),要想设计一个新的功能,或者完成某个功能的提升是不可能的。
作为设计人员,如果不能理解系统的具体细节,就不可能做出有效的设计。只通过一些高度概括的、粗略的设计图无法很好地理解系统。
<程序员修炼之道>中指出不存在所谓的最终决策。没有哪个决策做出之后就是板上钉钉了。实际上,就时间性来看,不妨把每个重要的决策,都看作沙上堆砌的城堡,它们都是在变化之前所做出的预先规划。
martin fowler在题为 "who needs an architect?" 的文章中提到:一个真正的架构师.....应该指导开发团队,提升他们的水平,以解决更为复杂的问题。他接着说:“我认为架构师最重要的任务是:通过找到移除软件设计不可逆性的方式,从而去除所谓架构的概念。” 增强可逆性是注重实效的软件实现方式的关键构成部分。
- 不要允许任何人单独进行设计,特别是你自己
40. 实行代码集体所有制
- 不要无意间丧失了团队的专家技能。如果某个开发人员在某个领域中及其精通,不妨让他作为这方面的驻留专家,而且系统的其他部分代码也对他开放,这样对团队和项目都很有帮助
- 开发人员不必了解项目每一部分的每个细节,但是也不能因为要处理这个模块的代码而感到惊恐
- 有些场合是不能采用代码集体所有制的。也许代码需要某些特定的知识、对特定问题域的了解,比如一个高难度的实施控制系统
41. 成为指导者
与团队其他人一起共事是很好的学习机会。知识有一些很独特的属性:假设你给别人钱的话,最后你的钱会变少,而他们的财富会增多。但如果是去教育别人,那双方都可以得到更多的知识。
通过详细解释自己知道的东西,可以使自己的理解更深入。当别人提出问题时,也可以发现不同的角度。
42. 允许大家自己想办法
用问题回答问题,可以引导提问的人走上正确的道路
如果有人真的陷入胶着状态,就不要折磨他们了。告诉他们答案,再解释为什么是这样
43. 准备好后再共享代码
44. 做代码复查
- 代码能否被读懂和理解?
- 是否有明细的错误?
- 代码是否会对应用的其他部分产生不良影响?
- 是否存在重复的代码(在复查的这部分代码中,或是在系统的其他部分代码)?
- 是否存在可以改进或重构的部分?
平衡的艺术
- 不进行思考、类似于橡皮图章一样的代码复查没有任何价值
- 代码复查需要积极评估代码的设计和清晰程度,而不只是考量变量名和代码格式是否符合组织的标准
- 同样的功能,不同开发人员的代码实现可能不同。差异并不意味着不好。除非你可以让某段代码明确变得更好,否则不要随意批评别人的代码
- 如果不及时跟进讨论中给出的建议,代码复查是没有实际价值的。可以安排跟进会议,或者使用代码标机系统,来标识需要完成的工作,跟踪已经处理完的部分
- 要确保代码复查人员得到每次复查活动的反馈。作为结果,要让每个人知道复查完成后所采取的行动
45. 及时通报进展与问题
九. 尾声:走向敏捷
只要一个新的习惯
拯救濒临失败的项目
引入敏捷:管理者指南
引入敏捷:程序员指南
结束了吗