[美]David Scott Bernstein
这本书旨在帮助你降低构建与维护软件的成本。给出一些最佳实践的方法。我可以直接在摘要将这9条建议列出来,但是有一些很精妙的话、很切实的感受还是需要自己通过阅读书本去获得。读完这本书会让我对“软件开发工程师”这个职业产生一种自豪感和满足感,为自己是这个行业中的一员表示很庆幸。要知道在此之前我为自己只是单纯的“crud”有点被磨平了热情,但是现在又找回了初心,希望这本书也能为你带来一些不同以往的观点,刷新你的认知。
文中有两个观点讲的很不错,一个是测试驱动开发的红绿重构,一个是开发的工作是使软件持续产生价值,其实更重要是对于这本书题目的刻画–“构建易维护代码”。在谈构建易维护代码之前,我们应该问问自己,为什么要构建易维护的代码?做这件事的意义是什么?不做会有什么风险?其实我在读完之后对“软件价值”有了比较深的理解,软件也像生命一样,是有周期有寿命的,总有结束的那一天,我们开发人员就像医生一样,要做的事是尽量的延长“软件”的寿命,使其产生持续的价值。那怎样延长它的有效周期呢?就需要在coding的时候遵守这些实践准则,构建易维护、易拓展、可重用的高质量代码,使其发挥最大的功效,产生最大的价值。这就是保证代码质量的意义。同时书中有提到一个方法帮助你去多思考,就是多问问why,为什么要这么做?这也是我在不断被问的过程中逐渐意识到的,有这种why的意识很重要,这种业务逻辑是怎么考量的?技术选型为什么是这样的?换成别的行不行?其实我们做任何事的时候都是一个不断抉择的过程,为什么要选这个,多问问自己理由。为什么我要写读书总结?为什么我要读这本书?asking is the true word.answering is the key world。ß
(1)在问如何做之前先问做什么、为什么做、给谁做
(2)小批次构建
(3)持续集成
(4)协作
(5)编写整洁的代码
(6)测试先行
(7)用测试描述行为
(8)最后实现设计
(9)重构遗留代码
软件是纯粹的思想产物。它源自我们的大脑,通过我们的手指,输入到计算机中。而它掌控着一切。
如果软件有人用,那么它便需要被修改;所以必须编写出可修改的软件。
为了要对某些事物准确建模,我们必须先理解它。
成为一个出色开发者需要的所有条件都是可以学习的技能。
与其试图预计将来可能的修改,我们应该研究出一些工程实践帮助软件更好地应对修改。
软件开发面对着不同于其他学科的独有挑战,为了应对这些挑战,我们必须理解这些实践背后的原则。
工具越强大,也越容易误用。
用链锯伐木比手锯更快,同样也更容易伤到自己。这个物理世界的隐喻在虚拟世界同样适用。工具越强大,也就越容易误用,所以我们必须小心使用手中的工具,才能保证它正确发挥作用。
所以这行需要不停的学习,新技术层出不穷,每出一样又多了一个全新的知识点,可以探索一个新的领域,新的方向,岂不美哉?出现一个山头,征服一座山。哪个行业会像我们这行这么磅礴,这么生机勃勃啊?
软件一直处在未知世界之中,技术、原则和理论在不断进化,这种状态将会一直持续。软件开发是一个年轻的行业,即便我们发展迅速,也依旧前路漫漫。
对此没有简单的答案,但可以用我们的才智来解决这些问题。让我们开始公开讨论和共享标准,敞开心扉珍视那些重要的事情。
最终,构建一个健康的行业,就像构建一个健康的社会一样,需要每个人都参与其中。任何组织都是依靠其成员运作的,我们已经见到以前所未有的方式进行软件开发的新型组织正在崭露头角。开源、知识共享协议,以及诸如GitHub8 这样的工具,给各种工具和库提供了免费获取渠道。我们有了改变整个行业的基础。剩下的只是愿不愿意使用的问题。而且这种转变正在发生着。
从软件开发行业来说,需要分享的精神。正如医学界一样,科学界一样,技术需要分享,才能促进行业的进步,社会的进步。
一所医院的医生如果将能够救人一命的信息对其他医院的医生保密,那是不道德的。软件开发者也是一样。我们开发的一些产品真的会挽救生命,所以我们必须分享知识。如果我们提高了整个行业的专业性,就会得到正反馈。我们分享方法论、模式、原则、实践。我们并不需要分享商业机密和专有信息。
学习、实践、分享。
我们需要分享那些可学习且容易理解的原则和实践,这样才能为软件开发建立规范——为了让它成为真正的专业性行业。
这和其他工程领域的道理一样。经验丰富的水电工都遵循着久经考验的标准和实践,而确立这些标准和实践的人们不单是以最快、最高效地完成工作为目的。他们也在考量其他因素,比如公共安全。他们必须合理接入市政供水和排水系统以及电网,而且这些标准必须是通用且可执行的。
其实,程序员就是一个翻译家,把人需要去做的事情,翻译成语言,让计算机去做。那在翻译的过程中确实会存在着一些误差,这个误差,就是我们说的bug,bug是一定会存在的。没有完美的翻译家,也没有完美的程序员。问题是如何快速的定位bug修复bug下次避免再发生类似bug,才是职业程序员的腻害所在。在这个成长的过程中,有很多tips可以帮助我们成为更好的程序员。在意细节,追求产出价值的同时保持代码的优雅,岂不是很棒?
我们都是凡人,总会犯些小错。但是在严格按照指令执行的计算机中,一个小错误可能引发大问题。计算机不知你的真正意图是什么。它们不是传译或者翻译,又或者仅仅将代码当作建议或指导,而是盲目地执行特定的指令。所以,如果遵循特定的规范保证程序正确执行,然后持续进行测试,我们就可以(通常是非常快速地)修复任何bug然后继续工作。耽误不了多少时间。
其实实质是:程序员打代码是工作中的小小一部分而已,更多的是沟通、对接、写文档,更更重要的,是找bug,定位问题。找bug、捋顺代码逻辑,是很占工作时长的。如果你发现一个问题,随手有一个测试用例可以测该功能函数,不用找测试入参、不用看代码细节、不用找函数返回结构,一个test run起来,都明了了。
有人会告诉你实践TDD能减少缺陷,但是有成本。你会编写比产品代码两倍还多的测试代码,所以自然而然人们会认为这降低了开发速度,但这是个错误的假设。这种想法认为影响软件开发速度的瓶颈因素是打字。
但这不是真的。询问任何一个开发者,考察任何一个项目。开发者花费的大部分时间不是在编码上,而是在以下方面:阅读需求文档、编写文档、开会,还有最耗费时间的排查bug。
这句话总结了极限编程的要点。
极限编程中的开发实践,诸如测试先行、重构、结对编程、设计技巧和持续集成,是软件开发成功的关键,无论采用哪种开发方法论都是如此。它们对理解问题域和精确建模提供了环境。
如何选择是否需要重构
如果生产环境上的软件正常工作不需要扩展,则无需重构代码。重构代码有风险和成本,所以我们希望最后收益能够抵得上开销。
第二次做好
相反,试图一蹴而就会有很大的压力。对于所有人来说都一样。知道可以回过头去修改、随时清理(可以在任何时候重构),会让我们很自由。
重构的开闭原则
重构是在不改变外部行为的前提下调整设计。开闭原则是指软件实体应该“对扩展开放而对修改关闭”。换句话说,力求在添加新功能的时候做到添加新代码并将现有代码的修改最小化。避免修改现有代码是因为很可能会引发新的bug。
如果有天发现倒车档失灵,你会在这种无法倒车的情况下开多久再去检修变速器呢?
有些事情最好放到最后处理,有些则不能推后。知道这两者之间的差别绝对重要。代码也是一样。有缺陷的bug要及时补丁,不然就从一个点漏成一条缝、一个洞了。
他们没法倒车,所以必须调整他们的行为(驾驶习惯),绕弯路到达目的地,为的是不使用倒车。一个问题会导致更多的问题。越早处理技术债,花费的成本就越低,就像信用卡欠款一样。
为什么要做重构?程序员也需要学习说话的艺术之道。
他看着我问道:“那你为什么要重构?”
我应该如何回答?
软件开发者时常遇到这样的情况。有时候不知如何作答,是因为我们和管理层存在沟通障碍。我们使用的是开发者的语言。
我不能告诉经理重构代码是为了好玩,是因为它让我感觉良好,或者因为我想要学习Clojure或者其他新技术……这些对管理者来说都是不可接受的答案。我必须强调重构对于公司的重要意义,而且它确实意义重大。
开发者知道这些,但需要用恰当的词汇也就是商务用语来表达,其实就是收益和风险。
我们如何在降低风险的同时提高收益?
软件本身的特点决定了其高风险和多变性。重构能降低以下四个方面的成本:
·日后对代码的理解
·添加单元测试
·容纳新功能
·日后的重构
重构的定义
重构是指在不改变外部行为的前提下对代码的内部结构进行重组或重新包装。
软件开发工程师和基金经理一样,需要优秀的人才来促进最初的demo完成,但是不是这样保持下去,而是要一直操作一直管理维护的。市场在变,基金投资会变,代码当然也需要变。
代码的衰变是真实存在的,即使一开始编写良好的软件也常常难以预计将来会面临的变更。这实际上是好事!不需要变更的软件通常是没人使用的软件。我们希望自己构建的软件为人所用,为了软件能持续给人带来价值,它需要容易修改。
各个系统模块保持独立。
客户端代码应该简单地说“解压吧”,然后相应的解压软件就解压文件,如同魔法一般……但并非魔法。
这就是所谓的多态。我们可以构建相互独立的代码,所有代码各司其职。举例来说,如果出现了一种新的之前没有的压缩软件,现有的代码应该可以自动适用,因为选择压缩软件并不是它的职责。代码将职责委托给了压缩软件。只要压缩软件正常工作,所有代码就都能正常工作。这种技术让我们的代码和系统中其他的代码保持独立,允许我们安全高效地扩展现有系统。
不关系你具体的实现细节,我点菜鱼香肉丝,不管你怎么买的菜怎么开的火,我点什么的你上什么就完事了。
然而,面向对象中有一个被称作“多态”的技术。听起来复杂,但概念很简单:可以在不知道实现细节的情况下进行工作。
举例来说,我想将一个文件用压缩它的软件来解压缩。如果是zip文件,就用unzip。如果是pack文件,就用unpack。我不关心压缩方式的差异。我只知道我有个被压缩的文件,我想用任何可以做到的方式解压它。
圈复杂度是代码可能产生的途径,一个if对应两种行为,两个if对应4种,二的n次方。所以一个代码的测试数量应该等于圈复杂度。
只有一个条件(if 语句)的代码的圈复杂度是2,表示代码中有两种可能的路径,也就是说代码可以产生两种不同的行为。如果代码中没有 if 语句,没有条件逻辑,代码的圈复杂度则是1,代码只会产生一种可能的行为。但是这种增长是指数级的。两个 if 语句的圈复杂度是4,三个 if 语句的圈复杂度是8,以此类推。尽你所能降低代码的圈复杂度,因为一般来说,一个方法所需的最少测试数量等于其圈复杂度。
圈复杂度代表代码中的路径数量
1976年12月,Thomas J. McCabe在他的论文《复杂性度量》1中最早提出圈复杂度的概念。它代表代码中的路径数量。
代码就像新闻一样会被阅读,也许有些人会觉得这很奇怪。通常软件被阅读的次数是编写次数的十倍。
用测试描述行为,从而建立活的标准文档。
终于到了“红绿重构”!
测试驱动开发有三个独立的阶段。我们称之为红条、绿条、重构,因为这些是你可以从单元测试框架中得到的明显提示。
tdd本质是:为了写出可测试的代码。
实际上,我不关心你的开发者是不是进行测试先行开发。我关心的是他们编写出可测试的代码,而TDD是最有效的方式
梦幻联动:重构。
在Martin Fowler出版《重构:改善既有代码的设计》[FBBO99]之后,重构才真正引入软件开发理论体系中,在书中他将重构定义为“在不更改外在行为的前提下,对代码做出修改,以改进程序的内部结构”。
TDD可以提供迅速的反馈,可以帮助我们在开发过程中迅速发现问题,而不是时隔好久之后,再回过来看代码。就像你已经走远了,再回来看你当时踩的坑是需要消耗成本的。但是我觉得最好的解决方式是:尽快推动用户使用。工具必须要多用。脑子也是。
如果我犯了一个错误三个月后才发现,我没法把前因后果对接上,今后很可能会犯同样的错。到那时候,我已经深处于另一个项目,必须停下手中的工作回过头去处理一个根本想不起来的bug 。
这就是为什么事情得不到改善,因为开发者没有得到迅速的“刺激-响应”。测试驱动开发可以提供这样的迅速反馈。
单元的定义是行为。
单元是指一个行为的单元:一个独立的、可验证的行为。它必须对系统产生可观察的影响,而不和系统的其他行为耦合。
理解这点十分关键。
“单元”这一词语用来强调一个行为不依赖系统中其他行为单元。这并不是说每个类都需要有一个测试类,或者每个方法都要有一个测试方法。单元测试意味着每个可观察到的行为都应该有一个相对应的测试
关键词
从某种意义上说,测试是传感器,就像你车上的引擎灯一样。它们一直在那,如果新添加的代码让它由绿变红,就是告诉你有些地方出错了。
测试角度的由外到内和开发角度的由内到外。
实践测试驱动开发的方式有很多,我推荐“测试先行开发”:开发者先针对一个功能编写测试,然后实现那个功能让测试通过。从测试角度(由外而内)到代码角度(由内而外)反复切换,可以给我们所需的反馈,让开发进程变得更稳定。
测试驱动开发视为对行为的定义而不是对行为的验证
如果你开始将测试驱动开发视为对行为的定义而不是对行为的验证,你就会对所需的测试有更清楚的认识。这样进行测试驱动开发,是为了给你安全地清理代码提供简洁又有意义的基础测试,但它不能取代质量保证工作。
qa的各种测试形式
质量保证(Quality Assurance,QA)测试有许多种形式。
·“组件测试”体现各个组成单元之间的配合情况。
·“功能测试”体现所有组成单元在一起完成整个端到端的行为。
·“场景测试”体现用户和系统的交互行为。
·“性能测试”验证这些情形:“这个系统能承受很大的负载吗?我们进行过独立测试,但是如果百万级用户进行并发请求会怎么样?”
·“安全测试”验证代码的脆弱程度。
·其他测试 = 质量保证测试
·单元测试 = 开发者测试
·验收测试 = 客户测试
预先准备什么样的环境下,触发了什么条件,产生了什么样的结果。
环境、条件、结果。
在特定的场景下,如果触发了一些事件,我们会看到这样的结果。
“特定的场景”是你为测试做的前期准备,是搭建好测试需要的先决条件。“触发的事件”是开关,在此引入需要测试的行为。“结果”是你对实际执行结果和期望结果进行比较,然后就可以知道测试是否通过。
·高内聚的代码减少副作用。
·松散耦合的代码更容易测试。
·封装良好的代码容易扩展。
·自主的代码让软件更模块化。
·没有冗余的代码降低维护成本。
优质的代码有什么特点?优质的代码应该清晰、容易理解、容易扩展。
今天的代码质量提高会为将来带来速度的提升
提高日后开发速度的方式就是现在提高代码质量。关注代码质量会保证我们编写的软件清晰且容易阅读。
可测试性成了衡量设计或实现质量的标尺。
哈哈哈哈哈买噶
如果在英语词典中查找“cohesion”(内聚)这个词,你会发现它有近义词“adhesion”(附着),即描述事物如何纠结在一起。但是软件开发者的“内聚”意味着软件实体(类和方法)应该具有单一的职责。
Al Shalloway说过:“不要有上帝对象!”
我问Al:“你所谓的上帝对象是因为它试图处理所有的事情,对吗?”他回答说:“不是,我称之为上帝对象,是因为在试图修改它的时候会惊呼‘我的上帝啊’。”
首先,也是最重要的是,高质量的代码应该内聚——每个片段都只关注一件事情。
5个为什么
通常,表面上的问题都不是真正的问题:它仅仅是另外一个深层次问题的表象。一个查找问题根源的方法是“5个为什么”。当面对一个问题时,问为什么会发生,或者是什么东西造成了这个问题,然后针对那个答案接着问为什么会发生,直到你问了至少5次。差不多在第4次问为什么的时候,通常都会发现一些之前没察觉到的值得注意的问题。
找寻小的改进
很多组织都倾向于进行革命性的改变,或者试图一次做出许多改变。小幅度、小范围的改进执行起来更快捷而且更容易。如果每两周仅做出2%的改进,那么一年之后你就有了50%的改进。小的改进更容易接受,而且容易达成!
把工作当玩耍!
这还不够。结对编程很有趣。事实上,工作的感觉越像玩耍,我们越容易选择工作而不是玩耍。如果真的热爱自己的工作,其实永远算不上真正的“劳作”,我不是第一个产生这种想法的人。我来分享一个小秘密……
我最喜欢做的事情之一,就是参加一个会议然后引爆全场。
加强学习和分享
按旧时代的惯例,稳定的工作来自于特殊化。掌握一些让你变得无可替代的知识,会让你免于被裁,甚至升职加薪,而且可能成为公司内部博弈的筹码。那样的话,谁不想把那些知识据为己有不与他人分享呢?
但时至今日,稳定的工作——引申一下,稳定的职业——所需要的恰恰相反。我们觉得团队中那些乐于分享知识的人更有价值。结对,群战,围攻……这些都是帮助人们合作的技术,让我们做得更多、更高效、更优质。
代码审查重点是讲设计思路
设计和代码的审查应该首先并且着重指出设计思路并说明为何选择这种设计。理解设计中做了哪些取舍以及其是否方便从不同角度扩展,这才是代码审查中应该讨论的。
paper vs practice
纸上得来和亲身体验有很大差别。
结对编程怎么样找到合适的人呢?
1、根据性格匹配。外向的配内向的。
2、根据经验匹配。老手带新手。
3、随机匹配。不一样的组合搭配出意外的惊喜。
任意匹配到不同的人,也是一个不断磨合、互相学习的过程。我们很难从想法很相同的人那里学习到新的知识。
不知道为什么看到这一段我有一种相亲的赶脚,所以这也是个接触各种类型的人、学习过程啊。
第三种方式,也是我十分推荐的,是随机配对各位开发者。一次次的随机配对,让我们可以和所有人紧密合作进而更好地发挥,这样并不是忽视了人们编程方式的不同,而正是因为编程方式不同,才需要结对来磨合。我们很难从一个和你的想法完全一样的人那里学到新东西。
教学的本质
思维不是等待承载的器皿,而是等待点燃的火炬。”教学不是把我头脑里的东西灌输到你的头脑。教学是一个共同探索的过程,是让学生展现思考的过程,让他们自己发现答案。他们问的每一个问题都是一个帮助他们学习的机会,所以我通常都不会直接给出答案。我会试图帮助他们自己想出答案,因为思考过程是最具启发性的,也是最有价值的,对于一个问题往往没有统一的标准答案。
真正能扩展思维的最佳学习方式,就是去教授它。对其他人进行讲解,当你听到自己说了些什么的时候,你会对自己大吃一惊。
结对编程的两个角色:驾驶员和领航员。
提问:为什么你要这么做而不那么做?
参与、讨论、交流。5-20分钟后互换角色。驾驶员是键盘前的那个人,领航员坐在他旁边能够清楚地看到整个显示器。
但是领航员并不是干坐在那里看着驾驶员编码。领航员和驾驶员彼此交流。这应该是真正的交流,不仅仅是坐在旁边对你进行批评,指出你的错误。对你搭档的成就进行认可和说“哎呀,有个拼写错误”一样重要。更重要的是要提出“为什么你这么做而不那样做”,然后听他解释。答案可能会让你大吃一惊。
参与,讨论,交流。
然后转换角色。
驾驶员交出键盘鼠标成为领航员,领航员变成驾驶员。短期影响是代码难以阅读,长期影响是代码难以维护。影响代码的易读性、可维护性。代码集体所有权对于软件开发非常重要。在传统的瀑布模型环境中,每个人都有自己的一组代码,工作风格各不相同,短期影响是代码难以阅读,长期影响是代码难以维护。
1、结对可适用老手带新手,对于老手来说由于被旁观会很大的提升代码质量,对于新手来说则是学习的最直接、最高效的方式。
2、结对对设计、调试、重构等任务有很大帮助。
3、形成统一风格,不需要根据代码就能猜出是哪位同事编写的,团队内部形成一个统一的标准,不需多完美,只需内部统一。
结对编程可以帮助彼此提高速度,有助于资深开发者指导那些经验尚欠的开发者。
遵循优秀的开发实践可以让开发者互相学习、互相支持。
在有人旁观时我们更不容易偷懒或者编写出糟糕的代码,所以结对编程可以产出更容易维护的代码。对于设计、调试、重构等任务,结对工作也同样有帮助。
结对编程有助于形成统一代码风格和代码集体所有权。
结对编程的好处:
1、知识更容易在团队中传播。所谓听君一席话 胜读十年书。
2、避免成员专业狭隘,不是只了解自己手上的活,而是对整个团队的项目、业务都有所了解。
结对编程能帮助知识在团队中迅速传播,比我知道的任何其他方法都要有效,而且对于复合型的团队来说,所有成员都熟悉整个代码库很重要。结对编程可以防止团队成员过于专门化,并且帮助团队达成共识。
结对编程实质是两个头脑共同解决一个问题。
结对编程并非轮流使用计算机,而是让两个头脑来解决同一个问题,这样会比两人各自单独工作更迅速并且质量更好。
模仿和学习的过程。想我当年真的啥都不懂,完全是搬着电脑直接坐在别人旁边看着别人敲的,被打趣到手把手教学。其实有师傅带是件非常重要的事。尤其在新手期,很多习惯、认知就在那时养成了。现在我们都没有这种机会了。
极限编程中最有价值同时也是最被轻视和误解的一个实践就是结对编程,结对编程是两个开发者在一台计算机前执行同一项任务。
对于程序员来说,沟通和协作同样很重要。
软件开发是一项社会性活动,需要相当多的沟通和交互,需要不间断地学习和交流,对抽象事物进行处理和讨论,所以不同个体之间的协同极其重要。
处理痛苦的方式有两种:避免痛苦,或者学着承受。
当故事是高内聚低耦合的时候,系统便会更专精且容易构建。保证故事短小、专注、容易验证,有助于系统的清晰性和可维护性。
用户故事越短越好,拆分得越细越好。
用户故事越短越好。短故事容易预估、理解和实现。短故事有助于构建高内聚低耦合的代码。短故事更容易测试。
根据价值对功能进行重要性排序。
待办任务列表应该被排序,用以保证最高价值的功能优先构建,不重要的功能被推后甚至取消。这让更多的时间可以花在那些高价值的项目上。
薛定谔的猫
奥地利物理学家埃尔温·薛定谔提出了一系列的思想实验,用来解释量子力学。他假想(并没有真正建造)一个盒子里面装着一只猫,里面有一个装置,当猫有意或无意触发这个装置的时候,就会释放某种毒素。他指出,在不确定的状态下,无法知道毒素是否被释放,而且无法观察到猫的状态。我们必须接受,在未知的状态下,猫既是死的也是活的。唯一确定的方式是打开盒子。
百分之百利用率的高速公路等于停车场。
所以也不可能有全心全意投入工作八个小时的程序员。能打四个小时代码的程序员已经算是高效的。
归根到底,百分之百利用率的高速公路会是什么样子的?百分之百利用率的高速公路会跟停车场一样。员工也是一样。当你让一个员工以百分之百的负荷工作的时候,他通常会停摆。百分之五十左右是理想的负荷。如果你的开发者能够在百分之五十的时间里都产生实际价值,那将是非常惊人的。人毕竟不是机器。我们需要时间持续学习,我们需要伸展身体,我们需要吃饭,我们需要上洗手间。
在商业中,我们需要关注两件基础性的事物:价值和风险。
大的问题仅仅是许多小问题的集合,而小的问题比大的问题解决起来容易得多。所以本质上就是问题分解,这是一种技能(并不是唯一的技能),而且是软件开发从职业到专业需要发展的关键技能之一。
我们应该用对客户的价值来衡量自己。这是我赞同的为数不多的几个度量方法之一,因为它不鼓励局部优化。几乎所有其他的度量方法(代码行数、速度等)都是局部优化。如果客户价值以外的其他东西拖了你的后腿,那就是没有价值的,没有益处的。
小批次构建帮助我们更好的接受反馈,更容易发现问题从而完善系统。
所有的这一切都指向更小的反馈周期。得到的反馈越多,就越容易发现问题,而越早发现的问题就越容易处理。
通过小批次构建,我们寻求的是验证而非假设。
软件行业和制造行业不同。
制造行业通过加入更多人手能有效提高产量、产出。
而软件行业需要人与人之间的交互,引入更多的人手反而会加大沟通的成本,导致效率的降低。
有点意思。
如果你生产的是烤面包机,拿到了一个大订单,想要加倍生产,你可以雇用双倍的员工,加入一条新的生产线,然后双倍地产出……这样是行得通的!
但是如果你是在构建软件,有大量的需求,想要产量加倍,如果你投入双倍的人力……会发生什么?
事情会变得更缓慢,甚至完全停滞。
这是软件开发和制造业完全不同的又一佐证。
制造业有并行的规则——两组完全独立的生产线可以让产量加倍,但是在软件中则有许多人与人之间的交互,在增加更多人手的时候,交互变得更复杂而速度则变得缓慢。生产线的机械性工作可以完全独立,但是软件开发却不是机械性的工作。通常,当我们在项目中加入更多人手的时候,需要更多的沟通和协作,这会让项目开发速度变得更慢而不是更快。
Frederick Brooks在《人月神话》[Bro95]中详述了关键路径的重要性:“怀孕生子需要九个月,无论有多少妇女参与其中。”一个妇女九个月可以生产一个婴儿,九个妇女一个月不可能生产一个婴儿。
通过关注软件应该做什么而不是如何做,开发者可以自由地探索最好的实现方式。
·为了构建更优质的软件,你需要知道如何和你周围的人沟通。
·把定义功能的关键性对话从描述实现细节转变为描述做什么、为什么做、给谁做,这有助于建立探索式开发流程。
·优秀的产品负责人编写出高质量的用户故事和清晰定义的验收标准。
·消除需求文档的编写,在产品负责人和开发团队之间建立创造性的合作关系,构建功能将更高效,可以节省三分之一的开发时间。
先将各种路径划分清楚,再根据各种路径对应的情形,编辑测试用例,再开发。
用户故事陈述了快乐路径,但我们也需要考虑许多其他路径,包括次要路径、异常路径、错误路径。我通常把边界情况记录在用户故事上以便跟踪,之后我会根据这些编写测试,然后用测试驱动实现。
用户故事中“所以从句”通过陈述一个功能带来的收益定义了为什么会有一个功能需求。只要遵循这个功能的核心需求,就能给我们的开发过程带来更多选择。
快乐路径是开发的主线,但是情形是最少的,更需要全面考虑非快乐路径。
快乐路径假设什么事情都不可能出错,但开发者必然会需要对付次要路径、错误路径、异常路径。一个功能中只有一个或者几个快乐路径,但可能会有很多的异常路径,所以用系统性的方式处理这些异常对于简化软件非常有帮助。
故事=what +why+who
故事描述了做什么、为什么做、给谁做
故事是包含如下内容的一句话:
·某个东西是什么
·为什么会有某个东西
·这个东西是给谁做的
不同的开发者对同一需求可能有不同的实现方式,我们不应该关心你到底用的是哪一种实现方式,而是关心你在这些实现方式之间为什么做了这样的取舍。
我不关心某一位开发者是如何实现某一功能的,我关心的是所有开发者都能理解他们选择的方式和未选择的方式之间的利弊。
软件开发不再是告诉计算机去干这个干那个了,而是创建一个计算机根据一组相关对象的交互行为而执行的世界。
这听起来像电影《创:战纪》一样,我不想把软件拟人化——众所周知计算机没有意识——但是如果你创建了一个山坡对象和一个球对象,并且建模得当,这个球就会顺着山坡滚下去。
这9个实践有助于在保证软件开发流程顺利进行的同时创建可修改代码
但是和毕加索一样,在打破规则之前必须要理解他们。
9个实践如下:
(1)在问如何做之前先问做什么、为什么做、给谁做
(2)小批次构建
(3)持续集成
(4)协作
(5)编写整洁的代码
(6)测试先行
(7)用测试描述行为
(8)最后实现设计
(9)重构遗留代码
软件的价值包括现在的价值和将来的价值。生命周期管理对于软件行业同样适用。
软件是一种资产,而资产的价值不仅仅取决于我们现在能从中获取的价值,也取决于将来能从中获取的价值。“生命周期管理”在房屋建筑学和产品设计学中扮演着十分重要的角色,所以它在软件设计学中的地位也至关重要并不足为奇。
经常需要修改代码的更深层含义是:用户从软件中获取了更多的价值。而开发者可以满足这种价值。如果带着这种信念去面对各种需要修改的需求,应该会心情好一些。
很长时间内,开发者都以为软件不需要修改,觉得开发软件是一件一蹴而就的事情。但事实上如果软件有人使用,就很可能需要修改。这是件好事,它意味着用户找到了从软件中获取更多价值的方式。开发者希望通过让代码容易修改来满足用户。
内部质量and外部质量
软件的外部质量诸如用户体验——可用性、少有异常、更新及时等——是软件内部质量的表现。即使用户并不直接体验软件的内部质量,他们也会受到影响。内部质量低下的软件也让开发者难以维护。
消除压力可以让程序员有更多的想法、更能高效的编码。
想象一下,如果要求每个演员都是一条就过,那么他们拍摄电影时将会承受地狱般的压力。仅仅知道你不需要“一次就完美”而是“只需合格即可”,就会让事情简单很多。通过消除(或至少是大量减少)由于担心表现所产生的压力,开发者基本上可以一次就做好,或者发现新的想法,或者发现新的做事方式,并且敢于接受更多的挑战,因为现在他们知道了即使一开始失败他们依然能够挽回。
实践的标准
若要成为一个实践必须:
·在多数情况下产生价值;
·容易学习且容易传授;
·简单易行——简单到无需思考。
当一个实践符合这三个条件时,它就很容易在团队中推广并且使团队受益。只需要使用某个实践,然后自然而然就能够在日常工作中节约时间和精力。
我们可能对目标比较模糊,可以把这些原则当做指导我们做出正确事情的大智慧。
在软件中,有很多方式实现“开闭”。它将推动我们构建高内聚对象,进行抽象编程,保持行为之间解耦。值得注意的是,这些特性都是我们为了实现“开闭原则”过程中的副作用。
同样,也有许多实现“单一职责原则”的方式。遵循这一原则会让开发者从问题领域内发掘出更多实体、隔离行为、使系统更加模块化。所有这些都有助于构建更具弹性的系统。
首要原则之二:开闭原则,要尽量避免对原代码做编辑,而应该做扩展,毕竟修改旧代码的成本和风险,远远大于写新的代码。
开闭原则:“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。”
这意味着设计的系统应该允许在不修改原有代码的前提下轻易扩展。
单一职责原则,修改某个类的原因,有且只有一个。我们在软件开发中有类的概念,就是为了将每个功能职责给独立成一个个互不影响类,更易测试、扩展,更具分离性、模块性、独立性。
作为系统中对象的模板,意味着我们需要把它设计成代表单一事物。这意味着很多事情。这意味着我们的系统中会有许多小巧的类,每一个类都专注于实现单一的职责。
通过将类的职责变得单一,我们限制了这个职责和系统中其他类的交互。这让这个类变得更容易测试,更容易找到 bug,而且便于将来的扩展。“单一职责原则”引导开发者设计出分离性好的、模块化的系统。
对待别人就像你希望别人对待你一样。
首要原则最早是由马可·奥勒留在讨论“黄金法则”的时候提出的:“对待别人就像你希望别人对待你一样。”黄金法则之所以是首要原则,是因为我们的法律、我们的社会甚至我们的文化都构建在这短短一句之上。其他的原则都可以通过首要原则引申出来。
有时为了功能快速上线,代码质量没有得到很好的保证,这个时候你就要表露出你为业务快速上线做出了妥协,但同时你也需要一定的时间后续在重构整理这部分代码。
在虚拟世界中,对于质量的关注通常会节约长期开销,同时也会节省短期花费。这并不是说开发者不能时不时地做出些短期的妥协,而是当他们做出妥协时要清楚地知道,每次他们回头面对糟糕的代码的时候要付出的代价。如果代价过高,他们也许就会想要在进一步增强软件之前回去清理代码。
商业中会权衡成本收益,软件也应如此。和其他资产一样,软件也应该经常维护,以免成为累赘。
专家编码,速度更快的原因在于保持了代码的质量。
他们不会不顾代码质量而加快速度,反之,因为他们保持了代码的高质量才能保持快速编码。意识到这点会影响我们对软件开发的认知。
观念上面就不对,很多时候我们的目标就是代码可以成功运行,这个业务流程执行没大问题,根本就不去考虑这样子设计会导致后面要扩展新功能的时候会有多么麻烦。所以一直都在打补丁,一直都在重复造轮子。
从很多方面来说,遗留代码的产生是因为我们有着这样的概念:代码的质量并不重要,重要的是软件正常工作。
但这是个错误的认知。如果软件会被使用,那么它便会被修改,所以它必须被编写得可以被修改。大部分软件并不这样。许多代码纠缠不清,所以无法独立部署或扩展,维护起来成本很高。虽然正常工作,但是由于其编写的方式导致难以修改,所以人们只好蛮力修改,让它以后的修改成本变得更高。
大量练习+学习
软件构建相当复杂,也许是人类接触过的最复杂的活动。编写软件是一项需要大量技能和练习才能成功的学科。
左右脑齐用
编写代码需要客观性的技能,软件开发需要主观上的艺术创造性,软件开发者需要在这两者之间保持平衡。
技术发展的周期,需要五波人,你是哪一波
技术采用周期:可分为五波人,这五波人也能代表该技术发展的阶段。
·创新者首批采用新技术。
·受到创新者成功经验的启发,早期实践者是下一批采用新技术的。
·当出现了很多的操作指南、技术变得更加易用的时候,多数派先行者开始加入了。
·在多数派先行者的帮助下,技术变为主流,多数派后行者也加入了。
·最后,那些滞后者在别无选择的情况下才加入。
物理世界是有容错性的,任何生命非生命系统是有弹性的,而软件不允许失误,可能会产生灾难性的后果,所以需要以可验证的方式构建系统。
近乎所有物理世界的事物都是有容错性的。有生命的和无生命的系统都有着很大的弹性。但软件是世界上最脆弱的事物。一个错误的比特可以导致灾难性的系统故障。因为这一事实,我们必须用一种可验证的方式构建系统。
软件开发是非常反直觉的。引发了制造业革命的质量控制标准在应用到软件项目时遭遇了全面的失败。我们在工业革命时期学到的东西在软件上毫无意义。软件是完全不同的生物。
越小越好
在极限编程中,将大问题用时间盒子分解为小问题的方法叫作“迭代”,在Scrum里面叫作“冲刺”。但是我对这两个名称都不是很喜欢。它们容易给人以错误的印象。
敏捷和Scrum并不是急于求成而是循序渐进。敏捷本质上就是“范围控制”——限制我们工作的内容——我们用时间盒子来度量仅仅是为了让人们习惯于范围控制。
时间盒子是说,“我们要在固定的时间内做这个工作”,通常是一个非常短的时间段,在Scrum里面差不多是1~4周,比较常见的是为期两周的一个迭代或者冲刺。
关键是,我们应该以更小的范围或者工作单元来思考,而不是时间长度。我们将大任务分解为更小的工作单元,依然可以产出可观测的结果。工作单元越小越好——小的任务更容易预估、实现和验证。
小批次构建
当人们明白“小批次构建”是为了尽可能快地完成任务、限制半成品存在的时候,他们才能更有效地应用这些实践,进而看到更大的收益。
将一个任务细分成一个个小小的任务。类似于长跑运动员,他不会直接将20千米作为他的下一个目标,他会先将目标定为下一个红绿灯,当他到达到下一个红绿灯之后,他会讲他的目标定为下一个房子,一步步的实现,一步步的创建目标。小即是好。
举例来说,实践“小批次构建”的核心目的是为了将任务从开始到完成的周期缩到最短,更小的任务可以更快地完成。
敏捷的好处之一是创建一个发现式的过程,可以让团队得到及时的反馈。鼓励开发人员和产品负责人进行有意义的交流,而不是单纯的抛弃长且冗余的接口文档。
以我的经验来说,在敏捷环境中开发软件的诸多好处之一是,敏捷有助于建立一个发现式的过程,团队持续收到反馈并从中学习。敏捷需求(在敏捷中叫作故事)并不是取代了那些在瀑布模型中要求的长而详尽的说明文档。故事的目的是引发对话——它要求并且鼓励软件开发者和产品负责人进行有意义的交流。从这类交流中开发者能够了解到实现需求所需的东西。
敏捷开发的最终目的是使不断交付有价值的功能,实现途径包括测试驱动开发和结对编程,实质是使流程精简,使开发能够有效地投入到工程实践中。
“通过持续不断地及早交付有价值的软件使客户满意”,这一承诺是敏捷流程的核心。换句话说,敏捷理论摒弃了用增加流程来保证质量的方式,建议流程更加精简,好让开发者有更多的时间实施更切实际的工程实践。敏捷理论引入了一些技术性实践,如测试驱动开发和结对编程,这些实践有助于创建可修改的,更容易部署、维护和扩展的软件。
但什么是软件中的浪费呢,和汽车不一样,软件不需要钢铁、轮胎和其他供应,情况又如何呢?
“精益”理论认为,软件的浪费是所有已经开展但尚未完成的任务,是半成品(Work-In-Progress,WIP)。我可以进一步引申,任何不是软件的东西,或者没有给客户产生直接价值的东西,都可以视为浪费。
这种理念是包括极限编程、Scrum和精益在内的敏捷软件开发方法论所一致认同的。虽然本书不是关于某单一方法论的,但是许多后面要讨论的实践方法都是在2001年著名的“敏捷宣言”中提出的。这个宣言表示:“我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。”
类似于-在错误的道路上停止就是进步。
类似于-开源与节流,关注的是节流。
Deming关注的是浪费,以及如何消除这些浪费。在制造业中,库存占据了仓储空间,所以被视为浪费。这意味着库存不仅仅冻结了资产,而且每天都会产生持续的仓储花费。直到将产品发送给客户,你才能将库存产生的负收入变为盈利。
精益理念由此而来
Deming带来的观念让日本企业将关注点放在了持续质量改进这一概念上。这需要确立一组质量优先的标准以及各级组织专注于生产质量和用户价值的最大化的承诺。Deming的想法被丰田公司采纳,最终发展成“精益”理念,这个理念显然在丰田起到了很好的作用。
聪明人 新想法
在千禧年到来之际,一群成功的软件开发者意识到,对于软件开发流程来说,少即是多。从不同的角度出发,他们努力找到一个轻量级的软件开发流程。一开始,他们称其为“轻量级软件开发流程”,但是担心这样的名字不能引起足够的重视,于是改名为“敏捷软件开发流程”。
以后就将修bug当成打地鼠的游戏好啦~
bug常常仅仅是冰山一角,修复一个问题可能会导致更多的问题浮出水面。软件内部常常以盘根错节的方式编写,所以修复bug可能很快变成打地鼠一样的体验,一个看似简单只需要几分钟的修复过程可能变为系统中无穷无尽的修改。
作为开发者,我和bug打过很多年的交道,但是直到最近才开始意识到它们的本质:bug是软件开发流程中的缺陷。
和真正的昆虫一样,软件的bug需要合适的条件才能生存。
真实
新的功能引入新的bug,从而导致另外一个bug,然后一个接着一个,如此往复。在没有针对可扩展性做过设计的软件上进行扩展无异于雪上加霜。
软件结果的定义
- 成功
斯坦迪什咨询集团对成功的软件项目的定义是:“按时完成,没超过预算,所有的功能和性能与预先定义的一样。”
- 遇到困难
遇到困难的项目不太容易界定。这些项目虽然完成了,但是总会有些妥协,不是预算上的(成本过多),就是时间上的(延迟交付),或者交付较少的功能,或者性能不如预期。
- 失败的(有缺陷的)
失败的项目是那些被取消的、不曾见天日的项目。通常,项目由于开发者以外的原因被取消或者失败。一个项目可能因种种原因导致失败:资金不足、市场变化、公司策略变化,等等。
我们加入的流程越多情况越糟糕,因为流程无法支配创造力。我们必须认清软件开发本质上是一个创造过程。
让人们参与到某件事情中的最有效方法可以总结为一个词:尊重。
所以注释应该写代码缘由而不是代码解读?那我大部分的注释是写这一块的整体逻辑,传参试例。
看到大量的“流水账注释”——那些并非描述代码缘由而是描述代码行为的注释——的时候,我会意识到写出这样注释的开发者格外担心阅读代码的人是否理解其代码的所作所为。代码应该是自解释的,应该通过优秀的命名和通行的用法使得软件易于理解。
hhhhh考古 这比喻绝了,接手别人代码确实是这样,没有注释没有接口文档没有测试用例,真的很难搞。
当我面对一个多年前构建的、没有文档、变量命名糟糕的软件时,有时候会觉得,考古学家在从一块陶片中窥视一个失落文明的秘密的时候一定也是这种感觉。简直是无以为继。
原来软件和生物一样都是有一个生命周期的,虽然最终的那一天终将会到来,但是我们可以通过一些方式推迟那一天的到来,对于软件开发来说就是在构建的时候可以多多考虑代码的稳定性、可扩展性,那对于人来说,就是保持良好的健康的生活方式,最终目的都是为了使这个生命周期更长久一些。
我们知道软件是如何演变成遗留代码的。和其他事物一样,软件也有一定的生命周期。程序被创造、使用、修补,最终淘汰。软件像生物一样,如果它赖以生存的操作系统被淘汰,也会死亡。正如医生一样,软件开发者能做的最多是推迟大限的到来。如果病患术后的生活质量比之前得到了提升,则治疗是成功的。但我们都清楚,我们所有人都有大限将至的一天,软件也一样。
【软件开发是世上独一无二的工作】有点意思。
软件开发是世上独一无二的工作。当我们理解了它的本质以及它需要持续更新的特性后,就可以找到许多方式来增强所编写的代码的健壮性,进而降低维护和扩展的成本。
遗留代码就是可维护性、可读性、可扩展性、可复用性很差的代码。
不同的人对“遗留代码”有着不同的定义,但是简而言之,遗留代码就是指因为种种原因,格外难以修正、改进以及使用的代码。
代码=遗产,留下来影响世界,明明是软件,但是却像硬件一样不易修改。代码是思想表达的工具。
遗产是已经死亡的事物存留下的依旧影响着世界的那部分。能留下遗产的生命是优秀的,但是软件并非如此。我们用温和的词语“遗留”来形容那些已经失去活力但是依旧运行的代码,让那些过去的决策持续影响着那些深陷其中的人。软件和硬件要区分对待。我们称硬件为“硬”是因为它是固定的,没有工具是无法调整的。软件的“软”是指它由思想而生,通过代码来表达,加载到硬件中然后行使一些职责。开发者通过编程的逻辑表达想法和需求,赋予软件生命。
反应迅速称为敏捷。对于软件而言,敏捷意味着可以快速适应需求的变化。
面对威胁与机遇的时候,反应迅速的组织被称为“敏捷”组织。一个敏捷的组织会吸取经验教训,不会被那些短期内不会修改的软件束缚手脚,无法行动。一些思想者,包括我在内,选择“敏捷”这个词来形容我们都希望软件能拥有即刻适应需求变化的能力。
工作的价值并不是只是实现业务而已,而是你要让你的功能能够持续有价值。重点是持续。
受雇的开发者必须意识到,仅仅言听计从是不够的,他们有义务让交付的软件持续产生价值。