改善协作
想象一个软件开发组织,它和市场紧密连接,可以随时交付完成的工作或调整方向,对市场做出准确的反应,这样的组织必然会在市场竞争中占据优势。响应能力是所有软件开发组织所期望具备的,但真正能做到的却很少。究竟是什么影响了组织的响应能力。从内部看,主要是堆积的“在制品”,和技术及学习“债务”。从外部看主要是和市场的连接程度,也即研发团队和业务人员、用户的协作。以下将分别分析这三个方面对组织响应能力的影响。
“在制品”是响应外部变化的负担
所谓“在制品”(Work In Progress,WIP),是指已经开始但未完成(可以向最终用户交付)的工作。主要包括:未实施的决策、未实现的设计、未集成的编码和未验证的系统。
图1是一个典型的瀑布开发模型,需求阶段产出系统的全部需求规格说明;分析设计阶段产出整个系统的架构和详细设计;测试验证前完成所有的代码。这些都属于不可交付的“在制品”,在整个开发过程中,都无法交付软件以响应客户的需求。另外,在项目开发过程中,任何需求的变更,都意味着对“在制品”的返工,例如在编码结束时需求发生了变化,意味着前期的需求分析、架构和详细设计,以及编码都要进行返工,这引入了需求变更的额外成本。因为这些原因,团队无法在较低额外开销的情况下,随时向客户交付产品或改变方向,其响应能力必然是受限的。
如图2所示,理想的迭代模式下,“在制品”仅仅存在于迭代之内。团队在迭代开始时,计划一部分需求,迭代结束时产生可交付的软件,清空所有的“在制品”。这样的模式为提升组织的响应能力提供了可能,1)迭代结束时软件是可交付的,可以通过交付它们来响应市场的需要。2)每个迭代开始前,都可以对产品的功能需求和规划做出调整,响应市场、用户及相关各方的反馈。而且因为迭代结束时“在制品”的数量为零,不存在对“在制品”的返工成本。
即使是迭代开发,大部分团队并不能做到迭代产出物的直接可交付。如图3所示,在一个典型的迭代开发中,每个迭代产生可运行的软件。但可运行不等于可交付,由于技术手段等的限制,迭代结束时会有遗留未完成工作。随着迭代的进展,未完成工作不断累积,形成迭代开发模式下的“在制品”,它们损害了组织响应能力。首先,可运行的软件加上未完成的工作,才构成可交付的软件。也就是交付软件前必须完成这些工作,组织无法随时交付以响应客户的需求。其次,这些“在制品”也意味着风险,未完成工作的不确定性,使得交付更加不可控,损害响应能力。
技术和学习“债务”(debts)加大响应的难度
“债务”是指将来某个时刻要偿还的负担。如图4所示,如果没有适当技术实践的支持,随着迭代的进行,既有代码的单元测试工作增加;功能回归测试工作量变大;代码质量因频繁变更而变差;系统越来越复杂,团队成员却缺乏对系统的理解。这些构成了软件开发中的“债务”,它们加大了将来系统修改、测试的难度,因而降低了系统的响应能力。
“债务”又可以分为技术“债务”(tech. Debts)和学习“债务”(learning Debts)。技术“债务”是指因系统的不良设计、实现和测试,导致系统在需要变更时,难以被理解,难以改变;改变后,难以验证代码的正确性;难以对既有功能进行回归测试。学习“债务”是指,在开发过程中,团队成员未能及时分享和掌握业务及实现相关的知识,加大系统变难度,而且更容易引入质量问题,甚至新的技术“债务”。“债务”对系统的影响,并不会立刻显现。但长期来看,它会使系统响应变化时,耗费时间更长,成本更高,引入质量问题的可能性更大,严重损害组织的响应能力。
有效的协作决定响应及时性和准确
“在制品”数量、“债务”水平决定了软件开发组织的内部响应性,与之同样重要的是响应的准确性。它取决于组织与外部的连接程度,也就是团队与用户及业务人员有效协作,以及团队内部有效协作,即时获取、整合市场信息,并准确传递至开发团队,有效地落实到开发活动当中。
综上所述,只有降低“在制品”,维持低“负债”,并合理协作,才能保证准确、即时地响应,达成目标。敏捷开发的实践正是在构建这样一个系统。
实施敏捷
从一定程度上说,敏捷软件开发就是通过系统的实践,减少“在制品”数量,降低“债务”水平和改善协作来提高组织的响应能力。表1列举常见的敏捷实践,并分析了它们对于以上三个方面的贡献。我们将这些实践分成了三类—管理实践、团队技术实践和个人技术实践,下面将分别对这些实践加以总结。
管理实践
- 用户故事:用户故事是端到端的,足够小需求单元。用户故事应该是具备用户价值和可交付的。它作为敏捷开发中计划的单位,为减少“在制品”提供了可能。同时,它是从用户的角度出发,以用户的语言描述需求,是用户、业务人员和开发团队沟通的起点和工具,改善了他们之间的协作。
- 短迭代开发,小版本发布:短迭代开发模式下,每个迭代产生可交付的软件,“在制品”始终控制在迭代内,处于较低的水平。小版本发布,即时获取用户的反馈,为调整产品的方向提供了最可靠的输入,使开发团队与用户的协作变得具体和有效。
- 自组织的多功能团队:多功能的团队,避免了工作在各个功能团队间的传递和等待,极大降低了“在制品”产生的可能,和维持的时间。同时,多功能团队成员间的知识共享和学习也降低了学习的“债务”。团队的自组织,使决策可以在团队层面迅速做出,减少了等待和批准的时间,避免 “在制品”的累积。
团队技术实践
- 持续集成:持续集成作为一个开发实践,强调小步地增加功能,并随时集成、验证,始终维持可运行的软件系统,使“在制品”数量控制在一个低水平。同时,持续集成作为一个团队纪律,保证被集成的代码能达到定义的质量标准,如通过代码规则检查,自动测试等,屏蔽特定类型的技术“债务”。
- 统一语言:使用领域专家的语汇(领域语言)作为开发团队、业务人员以及用户之间的沟通语言,减少沟通中产生误解的可能,提高沟通效率。代码中出现的概念也应该与领域语言保持一致,这进一步确保了实现和领域之间的一致,既改善了合作,也降低了因系统理解难度而带来的技术“债务”。
接收测试驱动开发:在开发活动前,由业务、开发人员和测试人员共同参与,用实例对需求进行细化和澄清,确保大家的一致理解。这首先是一个良好的沟通工具,使三方的合作变得有序和有效。另一方面这些实例将成为功能测试自动化的起点,降低了测试相关的技术“债务”;这些实例同时还会成为理解系统的依据,降低了学习“债务”。
个人技术实践
- 整洁代码:代码仅实现功能是不够的,整洁的代码清晰的表达意图,易于理解和改变。整洁的代码降低了技术“债务”。
- 设计原则和实践:不管是Robert.C.Martin的S.O.L.I.D.原则,还是其它一些耳熟能详的设计原则,如消除重复、分离关注点和向稳定依赖等,其目的都是要产生一个高内聚、低耦合的系统,使系统易于变更和维护,减少技术“债务”。
- 持续重构:随着变更的引入,即使是好代码也有变坏的倾向,产生技术“债务”。持续重构正是要消除这些技术“债务”,维持代码的整洁,并使之符合基本的设计原则。持续重构应该是团队和个人的工程习惯,而非一次性的行为。
- 简单设计:过度设计带来的复杂性,本身就会成为未来变更的负担(债务)。另一方面,超越当下功能要求的设计无法得到充分的功能验证,会成为“在制品”。简单设计在实现功能、消除重复,以及清晰表达的前提下,力求用最少的元素实现系统。
- 自动单元测试, 测试驱动开发:单元测试是系统健壮性的基石,设计良好的自动化单元测试是系统最有效的防护网,它确保代码发生变更时,原有意图不被破坏。也只有有了单元测试的保护,重构才可能持续安全地进行。测试驱动开发则是产生良好设计的手段,同时它往往能生成最优和最及时的单元测试用例。
- 结对编程:结对编程有机综合了两个程序员的思维能力,更容易产生设计和实现良好的代码。同时,通过持续的代码检查,减少错误引入,并在开发人员之间有效传递了知识,降低了技术和学习“债务”。
以上,我们从响应能力出发,梳理了敏捷软件开发中的主要实践,这些实践并非为敏捷软件开发所专享。其实施,也非一蹴而就,需要以团队整体技术和管理水平的提高作为支撑。否则反而可能会带来新的“在制品”和“债务”,特别是“债务”。比如,引入单元测试自动化是为了减少技术“债务”,但如果单元测试自动化本身设计不良,测试用例过分依赖于实现细节。这样,对实现细节的改变,会影响到测试用例的正确运行,反而使单元测试成为负担,提高变更的成本,降低响应能力。
敏捷的实践之间是相互关联的,成为一个有机系统,才能发挥效益。管理实践之间是相互支持的。例如,离开多功能的团队,开发任务就必须在多个功能团队之间传递,无法做到短迭代开发。 技术实践之间是相互支持的。例如,没有单元测试自动化的保障,持续重构的风险将激增,在操作上变得不现实;有了持续重构保障,维持一个最简单的系统设计,需要时再通过重构,使设计适应新的变化要求,简单设计才更可能得以实施。技术实践和管理实践也是相互支持的。例如,短迭代开发中没有技术实践支持,就会不断积累技术“债务”,产品质量和开发效率不断下降;而,短迭代内全生命周期的反馈,也为各类技术实践的部署提供了便利。
综上,敏捷实践的应用和团队技术水平的提高,相辅相成,在应用中不断总结提高,构建和完善实践体系,加强客户、业务人员和团队的合作,以及团队内部的合作,持续、有效地减少“在制品”,以及技术和学习“债务”,提升组织的整体技术和管理水平,只有这样才能构建可持续的响应能力。