首页 > 代码库 > 精益之美甚于帕斯雀(摘自《代码之道》第2章)
精益之美甚于帕斯雀(摘自《代码之道》第2章)
(译者注:帕斯雀(Pastrami)是这一种肉的专有名称,熏牛肉的一种,一般取牛肩部的肉来制作。国内的批发商普遍称之为胡椒熏牛肉火腿。)
曾经走在一个公共场所,比如机场的出入口通道或公园,突然一群狂徒冲你而来,想要说服你、或者威胁你、或者攻击你的无知。跟他们当中的任何一个人谈话,逻辑和推理都失去意义。在他们眼里,只有疯狂的信仰和不容辩驳的真理。即使你完全赞同他们,你也仍然没有机会去提问或分析。你必须深信不疑,无法提问,哪怕只提一个小小的问题。
这让我很不爽!我的意思是说,我真的全身心的不爽!我有自己的头脑,我想完全使用自己的头脑。不是只在聚会或者社交场合,而是包括我关心的所有主题和行为。我热衷于问一下“为什么”,并搞清楚事务的运作方式,这样的我才是真正的我。
你可能会认为,我这样的敏感性是给软件开发者的标准,因为他们无法去调试他们所不理解的东西。但一些致力于宗教、政治斗争和环境保护的人,他们在一些开发者的指引下也采纳时髦的开发实践,比如极限编程(XP,eXtreameProgramming)、敏捷方法和团队软件过程(TSP,Team Software Process)。他们拥有一样的热情!
任何事情都要适度
我非常喜欢这些开发范例提出来的很多思想和方法。但面对一个虔诚的追随者,如果我问为什么要做某个特定的事情,或者当我建议对规则或实践做一个很小的改动,以使它更适用于我的实际工作的时候,那就要当心了!这好比把一只魔戒摆在哈比人的面前——他顿时尖牙露出、头发根根竖起。对于一些开发者来说,极限编程和敏捷宣言已经成为一种祭仪。而对于另外一些开发者来说,团队软件过程是一种忠诚度的度量——你跟我们非友即敌。
请原谅我的务实。原谅我使用自己的头脑。原谅我不迷信魔法,而坚持去做有用的事情。我不会因为“你必须得这么做”而去做。我的行为原则是,要对为什么这样做会成功,而用其他方法就会失败,两方面都能得到相当充分的理由。
俭则不匮
这引起了我对精益的兴趣。是的,请看本文的标题。虽然在极限编程、敏捷方法和团队软件过程中有很多奇妙的东西,但至少有一个概念它们是共同的:减少工作浪费。这恰恰是精益设计和制造的焦点,而精益是源自于丰田汽车公司的一个概念,它比极限编程、敏捷方法和团队软件过程要早30多年。当极限编程、敏捷方法和团队软件过程以不同的方式试图解决这个问题的时候,我们可以通过使用精益模型来更好地理解它们各自采取了什么措施。
因此,冒着惹恼一些敏感狂热者的危险,让我们展开来看一看。精益聚焦在以最少的工作浪费来向客户传递尽可能多的价值。要做到这一点,它采用了拉动模型和持续的改进。拉动模型说起来很简单,“没有需求,工作就不开始。”这就减少了无用的、不必要的、无价值的工作。持续改进聚焦在减少浪费和建立顺畅的客户价值流上。
精益定义了7种类型的破坏客户价值流的浪费:
- 过量生产
- 运输
- 多余动作
- 等待
- 过程不当
- 库存
- 缺陷
这些显然是制造业的术语,是吗?它们不可能跟软件有关联,是吗?显然你太年轻无知了。所有这7种浪费都直接跟软件开发有关。它们是软件开发的7宗罪,下面我会逐一“宣判”,同时指出极限编程、敏捷方法、团队软件过程以及一般的常识是如何来规避它们的。
过量生产
第一个极度浪费是生产得太多,超过了真正需要的那个数量。但愿这永远也不要发生。有没有一个产品在出货之前没有被削减任何已经定义并实现好的功能?有没有一个产品在出货之前没有保留客户永远都不会使用到的功能?产品太复杂、太全面、太注重扩展性、太花哨、太累赘、太费解……过量生产太恐怖了,它会导致难以置信的浪费。
极限编程通过短而紧凑的迭代来解决这个问题。它坚持与客户的持续交流,以及和开发者之间的持续沟通。这保证了所有人都知道其他人在干什么,并且客户总是有较高的认可度。结果,几乎所有完成的工作都是对客户有价值的。
敏捷方法是精益实践的一个集合,它包括极限编程。因为敏捷更多的是一种联盟,它不是一种具体的方法,而是给开发提供了很多种有趣的方法。其中有一种就是叫作“Scrum”的项目管理实践(“Scrum”是根据橄榄球术语来命名的)。开发团队经常与客户代表碰头,通常是每30天,以展示工作进展、重新安排工作的优先级以及进行过程改进。跟极限编程一样,团队成员也每天开会更新各自的进度和讨论阻碍工作的问题。
通过每月对工作优先级的重新安排和每日对工作的重新组织,一个Scrum团队把自身的关注点锁定在了对客户重要的东西上面。几乎没有任何工作是浪费的。通过聚焦在定期进行的过程改进上,价值流可以经常性地被优化。
走向深处
当然,你也可能会不得当地使用Scrum和极限编程:你首先工作在“基础设施”上,而让客户苦苦等候着他们想要的价值。为了得到客户经常性的反馈,快速迭代有个基本前提:开发应该“深度优先”,而不是“广度优先”。
广度优先极端情况下意味着对每一个功能进行定义,然后对每个功能进行设计,接着对每个功能进行编码,最后才对所有功能一起进行测试。而深度优先极端情况下意味着对单个功能完整地进行定义、设计、编码和测试,而只有当这个功能完成了之后,你才能去做下一个功能。当然,两个极端都是不好的,但深度优先要好得多。对于大部分团队来说,应该做一个高级的广度设计,然后马上转到深度优先的底层设计和实现上面去。
这正是微软的Office功能小组的工作方式。首先,团队对他们需要哪些功能以及如何把这些功能放在一起做出计划。然后大家被分成多个小型的多工种团队,每个团队自始至终一次只负责一个功能。结果是,一个完整实现并且稳定的价值交付给客户演示的周期要短得多。
深度优先通过把注意力放在将被使用到的工作上,而不是可能永远不会被客户关注或者几乎不会得到稳定化的“基础设施”上,这样就能减少过量生产。另一个出色的深度优先开发方法是“测试驱动开发”(TDD,Test-Driven Development),但我想在“过程不当”那一节中再展开讨论。
运输
第二个极度浪费是等待一些尚未到达的东西。在制造业中,这通常是指部件的运输问题。对于软件来说,这个“运输”指的是团队之间的可交付成果的传递。这里有3个令人厌恶的运输问题之源:构建、分支和E-mail。
* 构建(build):构建花费的时间越长,浪费的时间就越多。我需要告诉你的是:极限编程和敏捷方法都坚持每天构建。但对于巨型团队来说,每天构建越来越不现实。这确实是个大问题。不容忽视!
* 分支(branch):微软使用Source Depot。它对整个公司的价值是巨大的。但它也像一只宠物象一样让人爱恨交加:当它还小的时候非常可爱,但几年之后,你要经常给它喂食并伺候它,你的活动自由受到了限制。建立代码分支是个很好的主意,因此很多大的团队都这么做了。现在假设你在A2.B3.C1分支上工作,而你的伙伴在A3.B1.C2上实现了一个关键功能或者修复了一个重大Bug,他需要把他的代码改动反向从C2到B1、再从B1到A3进行集成,之后你还需要把他的改动从A3到A2、再到B3、最后到C1进行集成。天哪!!!等所有这些集成都做完了,草都快长出来了。推荐的解决方案是,为你当前的产品线在发布期内建立同一级分支。
* E-mail:最后一个运输噩梦是E-mail通知:项目经理告诉开发和测试人员规范书准备好了;开发告诉测试说编码完成了;测试告诉开发说他们的工作被某个Bug阻碍了;开发告诉项目经理说他们的工作被某个设计变更阻碍了;还有我的个人偏好:在客户和依赖方或者卖主之间的任何沟通,特别是越洋沟通。极限编程和敏捷方法通过废除角色和建立每天会议制度来解决这个E-mail通知问题。但对于远程的卖主和依赖方,这行不通。眼下我们必须尽可能依赖自动通知,必要的话使用Live Meeting;如果使用E-mail,也要在E-mail中清清楚楚地给出对方预期得到的答复,减少E-mail的恣肆蔓延。
多余动作
第三个极度浪费是花时间去找东西。在制造业中,这是对机械和人的动作的一种浪费。在软件世界中,是指花在搞清楚要做什么、走向哪里和怎么去解决的时间。糟糕的搜索技术是动作浪费的一个典型例子。不可测试、无法维护、无法管理的代码同样是这种浪费。
使用断言和验证输入参数有助于快速发现Bug,并减少动作浪费。设计复审、代码复审、代码分析和单元测试都能达到减少浪费的目的。极限编程甚至建议“结对编程”(Pair Programming),但我个人认为,这样做是一种对资源的浪费(除非开发人员是在一起学习一个新的代码库)。团队软件过程度量你的所有活动和缺陷,你可以清楚地了解到你的时间是怎样被花掉的,因而你也能大大地削减你的多余动作。
一个特别讨厌但能避免的多余动作是为代码注释、Source Depot、Product Studio和代码签入邮件等复制Bug修复信息。每个人都浪费动作去管理Bug和项目进度数据的多个副本。有一些工具可以让这类事情变得简单,信息只需输入一次,便能自动被传播到其他所有的地方。这类工具应该用来减少动作的极度浪费。
等待
第四个极度浪费是工作上面的等待。前面谈到的运输问题只覆盖了在构建、分支集成和及时沟通方面的一大部分等待。但等待远远不止这些地方。最常见的死区是,团队在功能的优先级顺序上达不成一致,或者即使达成了一致但没按照既定的顺序去实施。也就是说,产品经理如果胡乱地写规范书,开发人员就不得不等待;开发人员不按顺序去实现功能,则测试人员只能等待;测试人员不按顺序去测试,那么所有人都必须等待。
极限编程、敏捷方法和团队软件过程都强调让团队决定优先顺序,并且得到客户或者他们的代表的签字认可,然后再按照那个顺序依次开展工作,直到他们决定重新审核优先级为止。团队软件过程在这方面尤为严格,但在没有灵活的领导者的情况下,计划也会缺少迭代性。
此外,不稳定的代码也会导致等待。只要代码不稳定,测试团队就不得不等待,其他用于得到客户反馈的机制也得等待。极限编程和敏捷方法特别关注可验证的稳定代码,这是深度优先策略的又一个要点。
过程不当
第五个极度浪费是工程过度。你可以看到它们通常表现为如下形式:开发极度复杂的功能,在已经表现足够好或者不是真正瓶颈的地方继续提高性能,还有增加不必要的泛化或者扩展性等。这种浪费跟过量生产有关,但这里更强调在具体功能的实现上面。
药方:测试驱动开发(TDD,Test-Driven Development)。这是一种极限编程和敏捷方法为实现设计提供的方法。作为一个副产品,这种方法同时也产生了完全覆盖代码的单元测试。其过程相当简单:
1. 定义你的应用程序接口或者公共类方法。
作者注:这是一个我和敏捷社区的一些成员起争执的地方:你是在写单元测试之前还是之后写应用程序接口或者公共类方法?偏执狂说之后写,但我认为要之前写。两者不同的是:前期设计的程度,以及你跟依赖你代码的外部组织之间的关系。我相信,在10万代码行级别的项目中适度的前期设计是成功的关键要素。
2. 根据一个应用程序接口或者类的需求写一个单元测试。
3. 编译并构建你的程序,然后运行单元测试并确认它失败。(如果成功了,则跳过第4步。)
4. 只写足够的代码以使单元测试通过。(同时要保证以前所有的单元测试仍然能够通过。)
5. 重复第2步至第4步,直到应用程序接口或者类的所有需求都测试通过。
很自然,当你掌握了这种方法的窍门之后,你可以一次为多个需求写多个单元测试。但当你刚刚起步的时候,最好还是一次只做一个。这样能够养成良好的习惯。
当你使用测试驱动开发方法时,你不会比绝对需要的再多写一点代码。你也自然而然地得到了很容易测试的代码,并且这些代码通常还是强内聚、松耦合、少冗余的——所有这些都是真正的好东西。哦,我曾提到你也能得到完全覆盖代码的单元测试了吗?你还有什么不满意的吗?
库存
第六个极度浪费是没有交付的工作和产品。这跟削减功能有关,但它也包括那些正在进展中的工作。当你采取宽度优先的开发方法时,你所有的工作同时开展,直到代码编写完成并完成稳定化。所有完成的规范书、设计和等待通过测试的代码都属于库存。它们的价值都尚未实现。
尚未实现的价值是一种浪费,因为你不能把价值演示给客户和合作伙伴看。你不能得到他们的反馈。你不能改进和优化客户的价值流。当然,如果产品计划改变了,这些尚未实现的库存通常就变成了巨大的工作浪费。
精益拉动模型强调只做需要做的事情,因此它的结果就是低库存,这在Scrum和测试驱动开发方法中得到了很好的验证。Scrum特别关注正在进展中的工作,时时跟踪并努力减少它。Scrum同时利用定期的机会去改进和优化你传递价值的方式。测试驱动开发方法要求你只实现满足需求的代码,多实现一点点都不要。
缺陷
第七个极度浪费是返工。这是最明显的一个,也是我过去批判得最多的一个。极限编程和敏捷方法通过各种方法来减少Bug和返工,这些方法包括测试驱动开发、每日构建、持续的代码复审和设计复审等等。
然而,极限编程和敏捷方法也以一种更为微妙的方法来减少Bug——通过建立一个能让你边走边学的框架。使用深度优先开发方法,在你为整个产品完成设计和编码之前,你一点一点地理解了项目的各个部分。这避免了严重的架构问题,而且通常这种架构问题隐藏得很深,等到被发现的时候已经太晚,不容许再调整了。听起来很熟悉吧?
减少缺陷是团队软件过程的专长。使用这种方法的团队能够使他们的Bug率下降到行业平均水平的千分之一。虽然团队软件过程本质上来说不是精益,但它也并不排斥深度优先的开发方法。
组合应用
是时候让我不得不激怒极限编程、敏捷方法和团队软件过程的虔诚追随者了。因为没有理由说你不能把这些方法组合起来达到更为出色的效果。使用Scrum来驱动一个精益、深度优先、灵活的、优化的开发计划。使用测试驱动开发方法来创建一个精益实现。使用团队软件过程来分析你的缺陷和工作,这将导致Bug和工作浪费的大量减少。这些对某些人听起来可能会怪怪的,但在我看来却是再合理不过的了。
我现在的感觉好像是刚刚找到了一块美味的帕斯雀牛肉。
精益之美甚于帕斯雀(摘自《代码之道》第2章)