跳转到内容

软件工程方法

本章开启了本书最后一部分,也就是关于软件工程的部分。这一部分会讨论软件工程方法、代码效率、测试、调试、设计技术、设计模式,以及如何面向多个平台开发。

你刚开始学编程时,节奏大概率完全由自己掌控。你可以把事情拖到最后一刻,也可以在实现过程中大幅推翻原有设计。但在专业软件开发环境里,程序员通常没有这种自由。即便是最开明的工程经理,也会承认流程总得有一些。今天,理解软件工程流程的重要性,已经不亚于理解如何写代码本身。

本章会概览几种不同的软件工程方法。我不会对其中任何一种做特别深入的展开——因为关于软件工程流程,本来就已有大量优秀书籍。我的目标,是用比较宏观的方式介绍若干不同类型的流程,帮助你看清它们之间的差异与取舍。我会尽量避免直接推崇或否定任何某一种方法。相反,我更希望你在了解了多种方法各自的 tradeoff 之后,能够为你自己和团队组合出一套真正适用的流程。无论你是独自接项目的 contractor,还是身处一个横跨数个大洲、拥有数百名工程师的大团队,理解不同的软件开发方式,都会在日常工作中持续帮助到你。

本章最后还会讨论 version control system。它让你可以方便地管理 source code,并跟踪其历史记录。对任何公司来说,version control system 都是必需品,否则 source code 维护很快就会变成噩梦。即便是单人项目,我也强烈建议你使用这样的系统。

软件开发史几乎到处都是失败项目的故事。从预算超支、营销失败的 consumer application,到声势浩大却最终翻车的操作系统,似乎软件开发的任何领域都逃不开这种趋势。

即便软件最终真的交付给了用户,bug 也已经普遍到让终端用户不得不习惯不断接收更新与补丁。有时,软件根本做不到它本应完成的任务;有时,它虽然“能跑”,却并不按用户预期那样工作。所有这些问题都指向同一个事实:写软件真的很难。

你可能会好奇:为什么软件工程看起来比其他工程学科更容易失败?例如,虽然汽车也会有缺陷,但你很少看到它们因为 buffer overflow 突然熄火然后要求 reboot(当然,随着越来越多汽车部件变成 software-driven,这种玩笑也不再那么离谱)。你的电视可能不完美,但你也不用为了看第 6 频道,先升级到 2.3 版。

是不是其他工程学科只是比软件开发更成熟?土木工程师之所以能造出可靠桥梁,是不是因为桥梁建造已经有了悠久积累?化学工程师能成功制备化合物,是不是因为前几代早已把大多数 bug“调通”了?软件工程到底是“太新了”,还是说它本来就是一种性质不同的学科,而这种学科的内在属性就注定它更容易产生 bug、产出不好用的结果,甚至酿成注定失败的项目?

看起来,软件工程的确有些不同。首先,计算机技术变化极快,这给软件开发过程本身带来了很大不确定性。即使在你的项目期间没有发生什么颠覆性技术突破,仅仅 IT 行业自身的迭代速度,就足以制造各种问题。与此同时,软件常常还必须快速开发,因为竞争非常激烈。

软件开发的进度也往往难以预测。只要一个棘手 bug 就可能花掉几天、甚至几周,精确排期几乎不可能。更糟的是,即使一切看起来都在按计划推进,产品定义仍然经常变化——这通常被称为 feature creep 或 scope creep。如果不加控制,这种 scope creep 最终会直接导致软件膨胀。

软件通常也极其复杂。我们没有简单而精确的方法来证明一个程序完全没有 bug。如果一份混乱或有缺陷的代码在多个版本中被持续维护,它很可能在数年内都持续拖累软件本身。很多软件系统复杂到,当团队发生人员变动时,谁都不想靠近那些乱成一团的 legacy code。于是,项目便陷入无尽的 patch、hack 和 workaround 循环。

当然,普通商业风险在软件行业同样存在。市场压力、沟通不畅都会制造麻烦。很多程序员都想远离办公室政治,但开发团队与产品/市场团队之间出现紧张关系,其实一点也不罕见。

所有这些不利于软件工程的因素,都说明我们需要某种流程。软件项目体量大、复杂度高、节奏又快。如果想避免失败,工程团队就必须采用某种系统,来驾驭这个难以控制的过程。

设计优雅、代码干净、可维护性强的软件,当然是可以做出来的。我坚信这一点。但它需要每一位团队成员持续投入努力,也需要真正遵守合适的软件开发流程与实践。

软件复杂性并不是什么新问题。几十年前,人们就已经意识到:需要某种形式化流程。于是,围绕 software life cycle 出现了多种建模方式,它们试图通过定义“从最初想法到最终产品”的一系列步骤,为软件开发这件混乱的事带来一点秩序。这些模型经过多年演进,如今仍在深刻影响大量软件开发工作。

软件领域最经典的 life cycle model 之一,就是 Waterfall Model。它背后的基本思想是:软件的构建几乎可以像照着菜谱做菜一样。只要按顺序完成一组既定步骤,最终就能得到一个很棒的巧克力蛋糕——或者,在这里,是一个程序。每一个阶段都必须先完整结束,下一阶段才能开始,如图 28.1 所示。之所以叫 waterfall,是因为整个过程像瀑布一样,只能一层一层向下流。

一张流程图展示软件开发过程,由 7 个矩形框组成,并用向下箭头连接:1. Planning;2. Design;3. Implementation;4. Unit Testing;5. Subsystem Testing;6. Integration Testing;7. Evaluation。

[^FIGURE 28.1]

这个过程从正式 planning 开始,其中包括收集一份尽可能完整的 requirements 列表。这个列表定义了产品在 feature 上应达到怎样的完整度。requirements 越具体,项目成功的概率通常就越高。下一步是设计软件,并把设计详细规格化。和 requirements 阶段一样,design 阶段也越具体越好,因为越具体,就越有机会成功。所有设计决策通常都在此时做出,往往还会包括 pseudo-code,以及需要编写的具体 subsystem 定义。各 subsystem 的负责人会理清自己的代码如何协作,团队则就 architecture 的细节达成一致。接下来才进入 implementation。由于 design 已经被完整规定,代码就必须紧贴设计实现,否则各部分最终就无法拼合起来。最后四个阶段则留给 unit testing、subsystem testing、integration testing 和 evaluation。

Waterfall Model 的主要问题在于:在真实实践中,如果不提前探一探下一阶段,几乎不可能真正完成当前阶段。你不写出至少一点点代码,设计就不可能真的定下来。再者,一旦测试暴露出 bug,你就不得不沿着“瀑布”往回爬,重新写代码——不管你的模型愿不愿意承认这一点。

Waterfall Model 后来也被发展出多种不同变体。例如,有些流程会在正式收集 requirements 之前先加入一个 feasibility 阶段,通过实验先判断方案是否可行。

Waterfall Model 的价值在于它的简单。你自己,或者你的经理,也许曾在项目里采用过这种方式,只是没有正式给它命名而已。它的底层假设是:只要每个步骤都尽可能完整、准确地完成,那么后续步骤就会走得顺畅。只要第一步把所有 requirement 都仔细规定清楚,第二步又把所有设计决策和设计问题都处理妥当,那么第三步的 implementation 理应只是把设计翻译成代码这么简单。

正因为简单,基于 Waterfall Model 的项目计划看起来通常非常有条理,也比较容易管理。每个项目都会以同样的方式开始:详尽列出所有必须具备的 feature。例如,使用这种方法的 manager 可以要求:在 design 阶段结束时,每一个负责 subsystem 的工程师都必须提交正式的 design document 或 functional subsystem specification。对 manager 来说,这样做的好处在于:通过让工程师尽早把 requirement 和 design 都写明,风险(至少在理论上)会被压低。

从工程师视角看,Waterfall Model 强迫你把主要问题尽量前置解决。所有工程师在真正开始写大量代码之前,都必须先理解项目并完成 subsystem 设计。理想情况下,这意味着代码只需要写一次,而不是在最后发现模块拼不上时再仓促打补丁或大面积重写。

对 requirement 非常明确的小型项目而言,Waterfall Model 其实可以工作得相当不错。尤其是在 consulting 场景下,它还有一个明显优势:项目一开始就能定义清晰的成功指标。通过形式化 requirement,consultant 更容易准确交付客户真正想要的东西,而客户也会被迫更明确地说明项目目标。

在许多组织里,以及几乎所有现代软件工程教材中,Waterfall Model 都已经失宠。批评者认为,它的根本前提——即软件开发任务会以离散、线性的方式发生——本身就站不住脚。Waterfall Model 通常不允许向后回退。但不幸的是,在今天的许多项目中,新的 requirement 往往会在开发过程中不断冒出来。比如,某个潜在客户会临时提出一个对于成交至关重要的新 feature;又或者,竞争对手发布了一个新 feature,迫使你也必须跟上。

另一个缺点是:Waterfall Model 一边试图通过“尽早、尽正式地做决策”来降低风险,一边却又可能把真正的风险藏起来。例如,在 design 阶段,一个重大设计问题可能根本没被发现、被轻描淡写地带过、被遗忘,甚至被有意回避。等到 integration testing 最终把这种不匹配暴露出来时,团队就只能重新“逆流而上”,回头修补之前的问题。瀑布流程中任何一个环节犯的错误,都很可能在最后阶段引发延误。而早期发现问题,往往非常困难,也并不常见。

如果你真的要使用 Waterfall Model,通常需要借鉴其他方法的思路,让它变得更灵活一些。

Waterfall Model 的很多改进形式都已经被系统化,其中一种就叫 Sashimi Model。它最重要的改进,是引入了“阶段之间允许重叠”这一概念。之所以叫 sashimi model,是因为 sashimi 这道日本料理本身就是一片片鱼肉彼此覆盖、相互叠压。虽然该模型仍然强调严格的 planning、design、coding 和 testing 流程,但相邻阶段之间允许部分重叠。图 28.2 展示了 Sashimi Model 的一个例子,你可以看到阶段之间的重叠关系。重叠意味着:两个阶段中的活动可以同时发生。这实际上承认了一个事实——某个阶段往往不可能在完全不碰下一个阶段的前提下,就被彻底完成。

一张流程图展示软件开发过程,由 7 个矩形框组成,并用向下箭头连接:1. Planning;2. Design;3. Implementation;4. Unit Testing;5. Subsystem Testing;6. Integration Testing;7. Evaluation。

[^FIGURE 28.2]

Spiral Model 由 Barry W. Boehm 于 1986 年提出,是一种 risk-driven 的软件开发流程。后来又衍生出多种变体,统称为 spiral-like models。本节讨论的模型,属于一大类被称为 iterative processes 的技术。它背后的核心思想是:即使某件事出错了,也没关系,因为你可以在下一轮 iteration 中修正它。图 28.3 展示了沿着这种 spiral-like model 完成一次循环时会经历的内容。

这个模型的 phases 与 Waterfall Model 的步骤有些相似。discovery phase 会识别 requirement、确定目标、评估各种 alternative(设计备选方案、代码复用、购买第三方库等),并确定约束条件。evaluation phase 会对 implementation alternative 做评估、分析风险,并考虑 prototype 的选择。在 spiral-like model 中,evaluation phase 尤其强调对风险的识别和消解。那些被认为风险最高的任务,会优先在当前 spiral cycle 中落地实现。也就是说,development phase 要做什么,是由 evaluation phase 中识别出的风险来决定的。比如,如果 evaluation 发现某个 algorithm 风险极高,甚至可能根本无法实现,那么当前 cycle 中 development 的主要任务,就会变成建模、构建并测试这个 algorithm。第四个 phase 留给 analysis 与 planning。团队会根据当前 cycle 的结果,制定下一轮 cycle 的计划。

一张图把圆分成四个象限:1. Discovery;2. Evaluation;3. Development;4. Analysis。箭头表示各 phase 按顺时针方向推进。

[^FIGURE 28.3]

图 28.4 展示了在一个 operating system 开发过程中,沿 spiral 走过三个 cycle 的示例。第一个 cycle 产出的是包含主要 requirement 的计划;第二个 cycle 产出的是展示 user experience 的 prototype;第三个 cycle 则实现了一个被判定为高风险的 component。

一张图把圆分成四个象限:1. Discovery;2. Evaluation;3. Development;4. Analysis。每个象限中又有多层同心圈,对应多次循环。

[^FIGURE 28.4]

可以把 spiral-like model 看成是:把 Waterfall Model 中最有价值的那一部分,与迭代方法结合起来。图 28.5 展示了一种把 waterfall process 改造成允许迭代的 spiral-like process 的思路。Waterfall Model 的两大问题——隐藏风险,以及过于线性的开发路径——都可以通过迭代 cycle 得到缓解。

三张相似流程图并排展示,每张都包含 7 个矩形框:1. Planning;2. Design;3. Implementation;4. Unit Testing;5. Subsystem Testing;6. Integration Testing;7. Evaluation。箭头展示执行顺序。

[^FIGURE 28.5]

另一个重要优点,是优先处理风险最高的任务。通过把风险前置,并承认新情况随时都可能出现,spiral-like model 可以避免 Waterfall Model 中那种“把定时炸弹埋到最后才引爆”的局面。一旦出现意外问题,团队仍可以沿用同样的四阶段方法去处理它。

这种迭代方式也允许吸收 tester 的反馈。例如,可以先发布一个较早版本,供内部甚至外部进行评估。tester 可能会指出:某个 feature 缺失了,或者某个已有 feature 的行为不符合预期。spiral-like model 内部自带一种机制,可以对这类输入作出反应。

最后,由于每个 cycle 之后都会再次分析并重新形成设计,design-then-implement 这种方式在实践中的困难也大幅减轻。随着 cycle 不断推进,团队会对整个系统积累更多认知,而这些新认知又可以反过来影响后续 design。

spiral-like model 的主要缺点,是很难把每一轮 iteration 的范围控制得足够小,从而真正得到迭代的收益。在最坏情况下,如果 iteration 划得太长,spiral-like model 最终也可能退化成 Waterfall Model。不幸的是,spiral-like model 只能“描述” software life cycle,而不能直接规定“该如何把某个具体项目拆成一轮轮 cycle”,因为这种拆分高度依赖具体项目本身。

其他可能的缺点,还包括:每个 cycle 都重复四个 phase 所带来的流程开销,以及协调不同 cycle 的难度。实际执行时,想把所有团队成员恰好在合适时间聚到一起讨论 design,并不容易。如果不同团队同时在做产品的不同部分,它们很可能处在平行推进的不同 cycle 中,这就容易失去同步。例如,在开发一个 operating system 时,user interface 团队可能已经准备进入 Window Manager cycle 的 discovery phase,而 core OS 团队却仍然还停留在 memory subsystem 的 development phase。

另一个问题,是项目整体的时间规划。如果你不断重复这四个 phase,那么到底什么时候该停下、结束项目?现实里,这通常还是要由 management 来决定终止条件,并为其给出理由。

为了解决 Waterfall Model 的缺陷,agile model 于 2001 年被正式提出,其核心文本就是著名的 Agile Manifesto。

这份 manifesto 的全文(摘译自 http://agilemanifesto.org)如下:

我们一直在身体力行地开发软件,也在帮助他人如此实践,并由此不断发现更好的软件开发方式。在这个过程中,我们逐渐更加重视:

  • 个体与互动 高于流程与工具
  • 可工作的软件 高于面面俱到的文档
  • 客户协作 高于合同谈判
  • 响应变化 高于遵循计划

也就是说,右侧的事项并非没有价值,但与之相比,我们更重视左侧的事项。

从这份 manifesto 可以看出,agile 本身只是一个高层概念。它本质上是在告诉你:要让流程足够灵活,以便在开发过程中能方便地吸收客户的变化。Scrum 是最常见的 agile 软件开发 methodology 之一,下一节就会讨论它。

software life cycle model 能够较好回答“我们下一步做什么?”这个问题,但通常并不能很好回答紧随其后的另一个问题:“具体该怎么做?”为了给“how”提供一些更可操作的答案,人们发展出了许多 software engineering methodologies,为专业软件开发提供实践层面的经验法则。关于这些 methodology 的书和文章多到数不清,不过其中有几种尤其值得关注:Scrum、Unified Process、Rational Unified Process、Extreme Programming 和 Software Triage。

agile model 只是高层基础;它并没有明确规定,在现实里到底该怎样落地。而这正是 Scrum 发挥作用的地方。Scrum 是一种 agile methodology,对日常如何执行给出了相当明确的说明,也是业界最常见的软件工程 methodology 之一。

Scrum 是一个 iterative process。在 Scrum 中,每一轮迭代叫作一个 sprint cycle。sprint cycle 是整个 Scrum process 的核心。sprint 的长度应当在项目开始时确定,通常为两到四周。每个 sprint 开始之前,团队会决定 sprint goals,并承诺在 sprint 结束时交付这些目标。在每个 sprint 结束时,理想状态是:软件已有一个“真正可运行、已经测试、并代表客户需求一个子集”的可用版本。Scrum 认识到客户在开发过程中很可能不断改变想法,因此它允许每个 sprint 的结果都被交付给客户。这样,客户就能看到软件的迭代版本,并向开发团队反馈潜在问题。

Scrum 中有三种角色。第一种是 product owner,它是客户与团队之间的连接点。product owner 会根据客户输入编写高层 user stories,为每个 user story 分配优先级,并把它们放进 Scrum 的 product backlog。实际上,团队中任何人都可以往 product backlog 里写高层 user story,但 product owner 负责决定哪些保留、哪些移除。

第二种角色是 Scrum master。他负责让流程真正跑起来。Scrum master 可以属于团队的一部分,但不被视为团队 leader,因为在 Scrum 中,团队是自组织的。Scrum master 更像是团队的对外联系人,让其他成员可以专注在自己的任务上。他还要确保团队正确遵循 Scrum process,例如组织下一节会讲到的 daily Scrum meeting。Scrum master 和 product owner 应由两个人分别担任。

第三种也是最后一种角色,就是 team 本身。team 负责实际开发软件,而且团队规模通常应保持较小,最好少于 10 人。

每个 sprint cycle 开始之前,都会先举行一次 sprint planning meeting。团队成员需要在会上决定:在新的 sprint 中究竟要实现哪些产品 feature。这些内容会被正式整理成 sprint backlog。候选 feature 来自按优先级排序的 product backlog;其中的 user story 都是新 feature 的高层 requirement。随后,团队会把这些高层 user story 拆解成更小的 task,并为它们做 effort estimation,再把这些 task 放入 sprint backlog。sprint planning meeting 的时长取决于 sprint 长度和团队规模;通常来说,每两周 sprint 大概需要两到四小时的 sprint planning。这个 meeting 一般会分成两部分:一部分是 product owner 与 team 一起讨论 product backlog 的优先级;另一部分则只有 team,负责最终完善 sprint backlog。

在 Scrum 团队里,你有时会看到一个实体板,也就是 Scrum board 或 sprint board,上面通常分为三列:To Do、In Progress 和 Done。每个 sprint 里的 task 都会写在一张小纸条上,再贴到对应列中。task 并不会在会议上被指派给具体某个人;相反,每个团队成员都可以自己走到 board 前,从 To Do 中挑一项 task,把它移到 In Progress。做完之后,再把它移到 Done。这种方式能让团队成员非常直观地看到:还有哪些工作没做、哪些工作正在做、哪些已经完成。除了实体板,你当然也可以用软件来维护虚拟 Scrum board。

当然,To Do、In Progress 和 Done 这三列并不是不可更改的。团队完全可以加入更多列,以反映更多步骤。例如,一个 Scrum board 可以包含:

  • To Do: 当前 sprint 中计划要做、但尚未开始的任务
  • In Progress: 当前正在某个 development branch 中由开发者处理的任务(见本章后面的“Version Control”)
  • In Review: 已实现、正在等待其他团队成员 review 的任务,也就是 code review
  • In Testing: 已实现并完成 code review,但仍在等待 QA 团队测试批准的任务
  • In Integration: 已通过 code review 和 QA 批准、可以从 development branch 合回主代码库的任务,当然前提是该分支上的所有测试都已成功
  • Done: 已经完整实现、review、测试并集成完毕的任务

一般建议把单个 task 保持得足够小。这样做的直接好处,是 development branch 的生命周期也会更短。

有时,团队每天还会维护一张 burn-down chart:横轴表示 sprint 的天数,纵轴表示剩余开发工时。它能够快速展示当前进展,并帮助团队判断:原计划中的 task 是否大概率能在 sprint 结束前做完。

Scrum process 还强制要求每天召开一次会议,通常叫 daily replanning、daily Scrum 或 daily standup。在这个 meeting 中,所有团队成员会和 Scrum master 一起站着开会。这个会应当每天在固定时间、固定地点开始,而且通常不应超过 15 分钟。开会时,sprint board 应对所有团队成员可见。团队会围绕当前 sprint goal 的进展进行讨论,也会讨论是否存在阻塞点或延期风险。必要时,团队甚至可以在当前 sprint 中移除任务或加入任务,以确保 sprint goal 最终能够实现。

这类 daily meeting 通常会要求每个团队成员回答三个问题:

  • 昨天你做了什么,以帮助团队实现 sprint goal?
  • 今天你会做什么,以帮助团队实现 sprint goal?
  • 你是否看到了任何阻碍你或团队实现 sprint goal 的障碍?

当一个 sprint cycle 结束后,通常还会有两个 meeting:sprint review 和 sprint retrospective。sprint review 的时长同样取决于 sprint 长度与团队规模,一般来说每两周 sprint 需要大约两小时。在 sprint review 中,团队会做一个 demo,向 product manager、support engineer 等相关 stakeholder 展示这次 sprint 的成果,以及软件当前状态。sprint review 还会回顾这次 sprint:哪些任务完成了、哪些没有完成、原因是什么。sprint retrospective 则通常每两周 sprint 需要约一个半小时,它给团队机会反思:上一个 sprint cycle 究竟执行得如何。比如,团队可以识别流程中的不足,并在下个 sprint 里加以调整。像 “What went well?”、“What could be improved?”、“What do we want to start, continue, or stop doing?” 这类问题都会在其中被讨论。这就是所谓的 continuous improvement:也就是说,每个 sprint 结束后,连流程本身都会被检查和改进。

Scrum 对开发过程中冒出来的各种意外问题具有较强韧性。问题出现后,通常都可以在后续某个 sprint 中处理掉。团队会参与项目的每一个环节:他们与 product owner 一起讨论 product backlog 中的 user story,再把这些 story 拆成更小的 task,并纳入 sprint backlog。团队还会借助 Scrum board,自主分配工作。通过这个 board,很容易看出每个成员当前在做什么;而 daily Scrum meeting 又能保证所有人都清楚 sprint goal 当前推进到了哪一步。

对于 stakeholder 来说,每个 sprint 后的 demo 是一个巨大优势,因为它能让他们持续看到项目的最新迭代版本。stakeholder 因而能很快判断项目是否正朝着正确方向发展,并据此调整 requirement;这些新需求通常又可以被纳入未来的 sprint。

一些公司可能很难接受:由团队自己决定“谁做什么”,而不是由 manager 或 team lead 把任务分派给个人。因为在 Scrum 中,成员通常是从 Scrum board 上自己领取任务的。

Scrum master 也是保持团队不偏航的关键人物。因此,Scrum master 必须真正信任团队。对团队成员进行过度控制,通常会直接破坏整个 Scrum process。

Scrum 还可能带来一个常见问题:feature creep。因为 Scrum 允许在开发期间不断向 product backlog 加入新的 user story,project manager 就可能不断往里塞 feature。解决这个问题的一个有效办法,是尽早明确最终发布日期,或者明确最后一个 sprint 的结束时间。

Unified Process(UP)是一种 iterative、incremental 的 software development process。UP 不是一套写死的流程;它更像一个框架,需要你根据具体项目需求去定制。按照 Unified Process,一个项目通常可以分成四个 phase:

  • Inception: 这个 phase 通常很短。它包括 feasibility study、business case、决定项目是内部开发还是购买第三方解决方案、给出成本和时间线的粗略估计,以及界定范围。
  • Elaboration: 大多数 requirement 会在这一阶段被文档化。风险因素会被处理,system architecture 会被验证。为了验证 architecture,系统核心中最关键的部分会被构建成一个可执行交付物,用于证明该 architecture 的确能够支撑整个系统。
  • Construction: 所有 requirement 都会在 elaboration 阶段交付的那个可执行 architecture 基础上被实现出来。
  • Transition: 产品被交付给客户,随后来自客户的反馈会在后续 transition iteration 中被处理。

所有 phase 都会被拆分成带时间边界的 iteration,并且每次 iteration 都应产出一个可见结果。在每次 iteration 中,团队会同时推进项目中的多个 discipline:business modeling、requirements、analysis and design、implementation、testing 和 deployment。每种 discipline 在不同 iteration 中所占的工作比重会发生变化。图 28.6 展示了这种“迭代且重叠”的开发方式。在这个例子里,inception phase 用一轮 iteration 完成,elaboration 用两轮,construction 用四轮,而 transition 则用两轮。

图表展示了一段时间上的软件开发过程。横轴表示时间,并分为多个 phase:1. Inception I1;2. Elaboration E1 到 E2;3. Construction C1 到 C4;4. Transition T1 和 T2。纵轴列出 business modeling、requirements、analysis and design、implementation、test、deployment 等 discipline。

[^FIGURE 28.6]

Rational Unified Process (RUP) 是 Unified Process 最知名的 refinement 之一。它是一种纪律性强、形式化程度高的软件开发过程管理方法。RUP 最重要的特点在于:与 spiral model 或 waterfall model 不同,它并不只是一个理论上的 process model。RUP 实际上还是一个由 IBM 旗下 Rational Software 出售的软件产品。把 process 本身也当作软件产品,会带来一些很有意思的优势:

  • process 本身可以像软件产品一样持续更新与改进
  • RUP 不只是告诉你一个开发框架,还会配套提供一整套围绕该框架工作的工具
  • 作为产品,RUP 可以在整个工程团队中统一推广,从而让所有成员使用完全一致的流程与工具
  • 像很多软件产品一样,RUP 也能根据使用者需求定制

作为产品时,RUP 体现为一整套软件应用,它们会引导开发者走过软件开发流程。该产品还会对其他 Rational 产品给出具体指导,例如可视化建模工具 Rational Rose,以及配置管理工具 Rational ClearCase。RUP 还包含大量 groupware communication tools,形成一种“ideas marketplace”,方便开发者共享知识。

RUP 背后的一个基本原则,是开发周期中的每一次 iteration 都必须有 tangible result。在 Rational Unified Process 中,用户会产出大量 design、requirement document、report 与 plan。RUP 软件则提供了创建这些 artifact 所需的可视化和开发工具。

准确定义模型,是 RUP 的核心原则。按照 RUP 的观点,model 有助于解释软件开发过程中的复杂结构与关系。在 RUP 中,model 往往会用 Unified Modeling Language(UML)的形式表达;见附录 D“UML 简介”。

RUP 把流程中的每一部分都定义为独立的 workflow(在前面对 Unified Process 的讨论中,它对应的是 discipline)。workflow 描述的是:谁负责这个步骤、正在执行哪些 task、这些 task 会产出哪些 artifact 或结果,以及推动这些 task 发生的事件顺序。几乎所有 RUP 细节都可以定制,但它仍然“开箱即用”地提供了若干 core process workflow。

这些 core process workflow 与 Waterfall Model 的阶段有些相似,但每一个都更具迭代性,也定义得更细。Business modeling workflow 用于建模 business process,通常目标是推动软件 requirement 的形成。Requirements workflow 则通过分析系统中的问题并反复修正假设,来形成 requirement 定义。Analysis and design workflow 关注 system architecture 和 subsystem design。Implementation workflow 涵盖软件 subsystem 的建模、编码与集成。Testing workflow 则对软件质量测试的规划、实施与评估进行建模。Deployment workflow 提供的是一个更高层视角,用来统筹 planning、releasing、supporting 和 testing。Configuration management workflow 则覆盖了从新项目立项,到 iteration,再到产品结束场景的整个链路。最后,environment workflow 负责通过开发工具的创建与维护来支持工程组织。

RUP 主要面向大型组织。相较于传统 life cycle model,它提供了不少优势。一旦团队跨过了使用这些软件工具的学习曲线,所有成员就都能在同一平台上进行 design、交流和实现。流程可以根据团队需求进行裁剪,而每个阶段又都会产出大量有价值的 artifact,用于记录开发的各个阶段。

不过,像 RUP 这样的产品,对某些组织来说也可能过于 heavyweight。那些开发环境差异很大、或者工程预算吃紧的团队,未必愿意、也未必能够把整个开发系统标准化为某个软件产品。学习曲线本身也是成本之一:新工程师不仅要快速理解产品和现有代码库,还得同时学会使用这套流程软件。

很多年前,我有个朋友下班回家,告诉妻子他们公司采用了 Extreme Programming 的一些原则。她开玩笑说:“希望你上班记得系安全绳。”尽管名字听起来多少有点戏剧化,但 Extreme Programming (XP) 确实很有效地把其他软件开发指南中的精华打包到了一起,并加入了一些新的内容。

XP 由 Kent Beck 在 Extreme Programming Explained(Addison-Wesley, 1999)中推广。它声称要把优秀软件开发中的最佳实践“再往前推一档”。比如,大多数程序员都会同意:测试是好事。而在 XP 里,测试被视为好到一种“你甚至应该在写代码之前先写测试”的程度。

XP methodology 由十二条指导原则组成,并分成四个类别。这些原则贯穿整个软件开发过程,也会直接影响工程师每天的实际工作。

XP 在 coding、planning 和 testing 上给出了四条粒度很细的指导原则。

XP 建议:所有 production code 都应当由两个人并肩工作来完成,这种技术叫 pair programming。当然,真正控制键盘的人一次只能有一个;另一个人则负责审视同伴正在编写的代码,并以更高视角思考测试、必要的 refactoring,以及整个项目模型等问题。

举个例子:如果你负责为应用中的某个功能编写 user interface,那么最好能请这个功能的原作者跟你一起坐下来做。他可以提醒你该功能的正确用法、警告你注意其中的各种 “gotcha”,并在更高层面上帮助你把控方向。即便找不到原作者,只要拉来团队里的另一个成员,也常常会有帮助。它背后的理论是:两人协作可以形成共享知识,促进正确 design,并建立一套非正式的制衡机制。

在 Waterfall Model 中,planning 通常只在一开始发生一次;在 spiral model 中,planning 则是每次 iteration 的第一阶段。而在 XP 中,planning 根本不只是某个阶段,而是一项永无止境的任务。XP 团队通常从一个粗略计划开始,用它抓住产品的主要方向。在整个过程中的每一次 iteration 里,都会举行 planning game meeting。随着开发推进,计划会被不断细化、不断修订。它的出发点是:环境一直在变化,新信息也一直在出现。planning process 主要包含两部分:

  • Release planning: 由开发者与客户共同进行,目标是决定哪些 requirement 要进入接下来的哪些版本。
  • Iteration planning: 只由开发者参与,用来规划开发者在当前 iteration 中要做的具体 task。

在 XP 中,某个 feature 的工作量估计,总是由真正实现该 feature 的人来给出。这能避免一种常见情况:实现者被迫遵守一个完全不现实的人为进度安排。刚开始时,这种估计通常会比较粗,可能是按“周”来估某个 feature。随着时间范围缩短,估计会越来越细。最终,feature 会被拆成单个耗时不超过五天的 task。

按照 Extreme Programming Explained 的说法,“任何没有自动化测试支撑的程序功能,实际上就等于不存在。” XP 对测试几乎可以说是执着。作为 XP 工程师,你的职责之一就是为自己的代码编写配套 unit test。unit test 通常是一小段代码,用来确保某个独立功能能够正确工作。例如,对于一个基于文件的 object store,单独的 unit test 可能会包括 testSaveObject、testLoadObject 和 testDeleteObject。

XP 甚至比一般的 unit testing 更进一步:它主张在写实际代码之前,先把 unit test 写出来。当然,在代码尚未实现时,这些测试肯定是过不了的。理论上讲,如果测试足够完整,那么一旦所有测试都通过,你也就应该知道代码已经完成了。不过,真正困难的地方在于:你很难确定自己的测试是否真的“足够完整”。你必须提前设想各种可能在实现中引入的 bug,并判断测试是否真能抓住它们。写出好的测试,往往比写出好的代码还更难。在代码之前先写测试的这个过程,叫作 test-driven development(TDD)。我说过了,这确实很“extreme”。

由于 XP 强调不断细化产品计划,只构建当前真正需要的东西,因此让客户参与流程会非常有价值。虽然不一定总能说服客户在开发期间 physically 在场,但“工程团队与最终用户之间应当保持沟通”这一理念本身无疑是有价值的。客户不仅能协助单个 feature 的设计,还能通过表达自身需求,帮助团队安排 task 的优先级。

XP 倡导:subsystem 应不断集成,以便尽早暴露 subsystem 之间的不匹配;代码在必要时应及时 refactor;同时,还应持续构建并部署小而渐进的 release。

所有程序员都熟悉集成代码这件苦差事。一旦你发现:你心目中的 subsystem 与它实际写出来的样子完全对不上,这件事就会迅速演变成噩梦。当 subsystem 汇合时,问题必然会被暴露。XP 认识到了这一点,因此主张在开发过程中频繁把代码集成回项目。

开发过程中,工程师应在 check in 代码前先运行所有测试。另外,还应有一台指定机器持续运行自动化测试。一旦自动化测试失败,团队就应收到邮件,指出问题,并列出最近一次 check-in。

如果项目使用 development branch,推荐把流程设置成:某个 development branch 只有在其上所有测试都成功通过时,才允许 merge 回主代码库。

大多数程序员都会不时 refactor 代码。所谓 refactoring,就是对现有、可工作的代码重新设计,以吸收新知识、新的上游 API,或者代码写成之后才逐渐发现的替代用途。Refactoring 很难被纳入传统软件工程排期,因为它的成果不像“实现了一个新 feature”那样显眼。但优秀的 manager 会意识到:对代码的长期可维护性而言,这件事非常重要。

XP 中更“极端”的做法是:在开发过程中,一旦识别出 refactor 的时机,就当场去做。也就是说,XP 程序员不会在 release 一开始就预先决定“产品中哪些已有部分需要设计工作”,而是会在日常开发中学习如何识别那些已经“准备好该 refactor 了”的代码气味。虽然这种做法几乎肯定会引入意外的、未排期的任务,但在适当时机调整代码结构,往往能显著降低后续开发难度。

XP 的一个核心理论是:当软件项目试图一次做太多事时,就会迅速变得风险高、难以驾驭。与其搞一个动辄包含核心变更、还要附好几页 release note 的大型 release,XP 更倡导时间跨度接近两个月、而不是十八个月的小型 release。release 周期一旦足够短,真正能进产品的就只会是最重要的 feature。这会迫使 engineering 和 marketing 必须认真达成一致:哪些 feature 才是真的重要。

软件是由团队开发出来的。任何写出的代码都不应属于某个个人,而应属于整个团队。XP 因此给出了一组原则,用来确保代码与想法能够真正被共享。

由于 XP 强调 collective ownership 和 pair programming,如果团队中每个工程师都有自己完全不同的命名和缩进风格,那么在这种极端协作环境下编码会变得非常困难。XP 并不要求某种特定风格,但它会提醒你:如果你只看一段代码,就能立刻猜出作者是谁,那么你的团队大概率需要重新定义自己的 coding standard。

关于 coding style 的更多讨论,可见第 3 章“代码风格”。

在很多传统开发环境中,代码所有权会被严格划分,甚至被明确强制执行。我有个朋友以前就在这样一个环境里工作:manager 明文禁止你 check in 任何其他团队成员所写代码的修改。XP 则走向了几乎完全相反的方向:它宣称,代码由所有人共同拥有。

collective ownership 在实践中有不少现实意义。从管理角度看,当某个工程师突然离开时,损失会小得多,因为总还有其他人理解那部分代码。这会提升项目的 bus factor 或 bus number,也就是“最少要有多少工程师被公交车撞到,项目才会真正停摆”。从工程师角度看,collective ownership 会帮助团队建立对系统工作方式的共同理解。这有助于 design 工作,也让个人工程师更有自由去做那些能提升整个项目价值的改动。

需要强调的一点是:collective ownership 并不意味着每个程序员都必须熟悉每一行代码。它更像是一种心态:这个项目是整个团队共同的努力,没有理由让任何一个人囤积知识。

XP 工程师经常挂在嘴边的一句 mantra 是 “avoid speculative generality”,也就是常说的 KISS——“Keep It Simple, Stupid”。这和许多程序员的天然倾向正好相反。比如,假设你接到的任务是设计一个基于文件的 object store,那么你很可能会不由自主地朝“打造一个一统天下的、终极的 file-based storage 方案”那个方向走。你的 design 很快也许就会开始覆盖多种语言、支持任意对象类型。XP 则主张你要往“更具体、更克制”的那一端靠。不要去设计一个能拿奖、能让同行赞不绝口的完美 object store,而应当去设计“那个最简单、但已经足够完成任务”的 object store。你要理解当前 requirement,并严格按这些 requirement 来写代码,避免设计复杂化。

在 design 上真正习惯“简单”,其实并不容易。根据你的工作类型不同,代码可能要存活很多年,并被你现在根本想象不到的其他部分复用。正如第 6 章“面向复用设计”所讨论的,把那些“也许将来会有用”的功能提前做进去,问题在于:你其实根本不知道那些假设用例究竟是什么,也没有办法仅凭猜测就设计出真正好的结构。实际上,你为了某个永远可能不会发生的假设场景提前做设计,反而有可能把另一个后来真实出现、且更有价值的场景卡死,除非你再把整套 design 重构一遍。XP 的观点是:先把今天真正需要的东西做好,同时给未来修改保留空间。

XP 使用 metaphor 这个词,指的是团队所有成员(包括客户和 manager)都应共享对系统的某种共同高层理解。它并不是指对象之间将如何通信、或者精确的 API 会长什么样。metaphor 更像是整个系统组件的 mental model 与 naming model。每个组件都应有一个足够有描述性的名字,使团队成员仅仅看名字就能大致猜出其职责。团队在讨论项目时,也应借助这一 metaphor 来维持共享术语。

XP 的最后一组原则,关注的是开发者本身的福利。

对于你一直以来投入的工时,XP 也有话要说。它的观点是:休息充分的程序员,才会是开心而高效的程序员。XP 倡导大约每周 40 小时的工作时长,并明确警告:不要连续两周以上长期加班。

当然,不同的人需要的休息量并不相同。但它的核心思想是:如果你在头脑不清醒的状态下坐下来写代码,那么你写出的很可能就是糟糕代码,而且你也很难坚持 XP 的那些原则。

XP 的纯粹主义者会认为:Extreme Programming 的这 12 条原则彼此高度缠绕,如果你只采纳其中一部分,而不同时采纳其余部分,那么整个 methodology 就会被严重削弱。比如,pair programming 对测试就非常重要,因为如果你不知道怎么测试某段代码,你的搭档可以帮助你。而且,如果你某天累了,想偷懒不写测试,你的搭档也能立刻让你感到应有的羞耻感。

不过,XP 的一些 guideline 在实践中确实可能难以实施。对某些工程师来说,“在代码之前先写测试”这个想法过于抽象。对这些人而言,也许只要先设计测试,而不必真的在有代码之前把测试全部写出来,就已经足够了。XP 的许多原则都被定义得很严格,但如果你真正理解其背后的理论,往往仍有机会把这些 guideline 调整成更适合自己项目的形式。

XP 的协作面向同样可能很具挑战性。pair programming 的确有可量化的收益,但从 manager 的角度看,很难不去怀疑:“为什么每天真正写代码的人少了一半?”团队中的一些成员甚至会对如此近距离的合作感到不舒服,比如他们可能很难在别人盯着看的情况下打字。XP 天然就更适合某些性格;尤其是 introvert,往往会更讨厌 pair programming。如果团队地理上非常分散,或者很多成员长期远程办公,那么 pair programming 的现实障碍也会非常明显。

对于某些组织来说,Extreme Programming 可能还是过于激进。那些已经建立了严格工程政策的大型成熟公司,往往很难快速接受 XP 这样的方式。不过,即便你的公司对 XP 持保留态度,你依然可以通过理解其背后的理论,提升自己的个人生产力。

在 Edward Yourdon 那本名字就很悲观的书 Death March(Prentice Hall, 1997)中,他描述了一种软件项目中经常出现且相当可怕的现象:项目已经严重落后于计划、人员不足、超出预算,或者 design 一团糟。Yourdon 的理论是:当软件项目进入这种状态时,即便是最好的现代软件开发 methodology,也已经不再适用。正如本章前面所看到的,许多软件开发方法都建立在形式化文档、或以用户为中心的 design 方法之上。而对于一个已经进入 “death march” 模式的项目来说,通常根本已经没有时间按这些方法继续做了。

Software Triage 背后的思想是:当项目已经处于糟糕状态时,资源本身就已经非常稀缺。时间稀缺,工程师稀缺,金钱也可能同样稀缺。此时,manager 和 developer 首先需要跨过的一道心理门槛,是承认:在规定时间内,原始 requirement 已经不可能全部满足。接下来的任务,不再是“尽量全部做完”,而是把剩余功能整理成 must-have、should-have 和 nice-to-have 三类。

Software Triage 是一个艰难、敏感、且经常伴随着痛苦取舍的过程。很多时候,它需要一位真正见过 “death march” 项目的外部老兵来做艰难决策。对工程师来说,最重要的认识是:在某些条件下,为了按时完成项目,你可能必须把熟悉的流程——甚至连一部分现有代码——一起扔出窗外。

几乎不可能有哪一本书、哪一种工程理论,能够完美匹配你的项目或组织需求。我建议你尽可能从不同方法中学习,然后自己设计适合你的流程。把不同方法中的概念揉在一起,往往比你想象得更容易。例如,RUP 就可以选择性地支持一种类似 XP 的工作方式。下面是一些帮助你构建“理想软件工程流程”的建议。

有些工程技术第一次听上去会显得很疯狂,或者看起来根本不可能奏效。面对软件工程 methodology 中的新想法,不妨把它们视为“精炼现有流程”的机会。能试的时候就去试试看。如果 XP 听起来很有趣,但你又不确定它是否适用于自己的组织,那就看看能否循序渐进地引入:比如一次只尝试其中几条原则,或者先在一个较小的试点项目里实验。

你的工程团队大概率由背景非常不同的人组成。团队里可能同时有 startup veteran、长期 consultant、刚毕业的新人,以及 PhD。每个人对“软件项目该如何运作”都可能有不同经验和不同看法。很多时候,最好的流程恰恰是把这些截然不同环境中“各自有效的做法”拼接在一起的结果。

在项目结束后——最好甚至是在项目进行过程中,比如像 Scrum 的 sprint retrospective 那样——把团队聚到一起,认真评估流程。有时,某个重大问题会一直没人意识到,直到整个团队真正停下来思考。也有时候,存在一个“每个人都知道、却没人真正说开”的问题。

想想哪些地方运转得不好,再思考如何修复它们。有些组织会要求在任何 source code check-in 之前,先做正式 code review。如果 code review 长到又臭又闷,导致根本没人认真审,那就应该作为团队一起重新讨论 code review 的方法本身。

同样,也要看看哪些地方运转得很好,并思考是否可以把这些成功经验扩大。例如,如果把 feature task 维护在一个可多人编辑的 wiki 上效果不错,那么也许就值得再投入一些时间,把这个网站本身做得更好。

无论流程是 manager 强制规定的,还是由团队自己设计出来的,它既然存在,就总有原因。如果你的流程要求编写正式 design document,那就请认真写。如果你觉得这个流程已经坏掉了,或者复杂得离谱,那就去和 manager 讨论。不要只是绕开流程走捷径——它迟早会反过来咬你。

对任何公司来说,无论规模大小,管理全部 source code 都非常重要;哪怕是单人项目,也是如此。举个例子,在公司环境里,如果所有 source code 都只是散落在各个开发者自己的机器上,而没有由任何 version control 软件统一管理,那几乎一定会演变成维护噩梦,因为没有人能保证自己手里的总是最新代码。相反,所有 source code 都必须 由 version control software 来管理。此类软件大致可以分成三类:

  • Local: 这类方案把全部 source code 文件及其历史都保存在本地机器上,并不真正适合团队使用。它们属于 70/80 年代的方案,今天已经不应再使用,因此本书不再讨论。
  • Client/server: 这类方案由 client component 和 server component 两部分组成。对个人开发者而言,client 和 server 完全可以运行在同一台机器上;但这种分离方式也让你在需要时,可以很方便地把 server component 迁移到独立物理服务器上。
  • Distributed: 这类方案比 client/server 更进一步。它不依赖一个“所有东西都集中存放”的中心位置。每个开发者手里都有全部文件以及全部历史记录的副本。它采用的是 peer-to-peer 模式,而不是 client/server 模式,代码通过交换 patch 在各 peer 之间同步。

client/server 方案由两部分组成。第一部分是 server software,它运行在中心 server 上,负责追踪全部 source code 文件及其历史。第二部分是 client software,它安装在每位开发者机器上,负责与 server software 通信,以获取某个 source file 的最新版本、获取更早版本、把本地修改提交回 server、把修改回滚到之前版本,等等。

distributed 方案则不依赖中心 server。client software 通过 peer-to-peer 协议与其他 peer 同步,并通过交换 patch 来同步代码。像提交修改、回滚修改这类常见操作都非常快,因为它们不需要通过网络访问中心 server。缺点则是:client 机器需要更多空间,因为它必须保存所有文件以及完整历史。

大多数 version control system 都有一套自己的术语;遗憾的是,不同系统使用的词并不完全统一。下面列出一些最常见的术语:

  • Branch: source code 可以被 branched,也就是并行发展出多个版本。例如,可以为每一个已发布版本维护一个 branch。在这些 branch 上,可以实现对应 release 的 bug fix;与此同时,新 feature 则继续加到主 branch(通常称为 trunk)中。为已发布版本写出的 bug fix,之后也可以 merge 回主 branch。
  • Check out: 在开发者本机上创建一份本地副本,这份副本可以来自中心 server,也可以来自 peer。
  • Check in / commit / merge / push: 开发者先在本地副本上做修改。当本地机器上一切正常之后,就可以把这些本地修改 check in / commit / merge / push 回中心 server。
  • Conflict and resolve: 当多个开发者对同一个 source file 做了相互重叠的修改时,提交该文件时就可能发生 conflict。version control software 通常会尝试自动 resolve;如果做不到,就会要求用户手动解决这些 conflict。
  • Label or tag: 某个时间点上,可以给全部文件、或某个特定 commit 加上人类可读的 label / tag。这使得回到当时那一版 source code 变得非常方便。
  • Repository: 由 version control software 管理的全部文件集合,以及它们的历史记录。它还包括每个 commit 的元数据,例如提交时间、提交者,以及可能附带的 commit message(解释为什么做这次修改)。
  • Revision or version: revision(或 version)是某个文件在特定时间点内容的快照。它代表一个可以被回退、或被拿来比较的历史点。
  • Update or sync: update / sync 表示将开发者机器上的本地副本,与中心 server 上或 peer 上的某个版本同步。要注意,这实际上意味着把上游代码 merge 进本地工作副本,而这可能会引入需要手动解决的 conflict。
  • Working copy: 开发者个人机器上的本地副本。

目前可用的 version control software 方案有很多,有些是 free/open-source,有些是 commercial。下面的表格列出其中一些代表:

FREE/OPEN-SOURCECOMMERCIAL
Local OnlySCCS, RCSPVCS
Client/ServerCVS, SubversionIBM Rational ClearCase, Azure DevOps Server, Perforce
DistributedGit, Mercurial, BazaarTeamWare, BitKeeper, Plastic SCM

本书不会推荐某个具体软件方案。如今,大多数软件公司通常都已经有一套既定的 version control system,而每个开发者都必须遵循它。如果不是这样,公司就应当认真投入时间调研这些可选方案,并挑出一个真正适合自己的。归根结底,没有 version control system 的软件开发,必然会滑向维护噩梦。即便是个人项目,我也建议你认真研究一下现有方案。只要找到一款你喜欢的工具,它就能显著让生活轻松很多:它会自动记录不同版本以及你的修改历史,从而使你在某次改动不如预期时,能轻松回退到更早版本。

本章向你介绍了若干软件开发过程的 model 与 methodology。当然,构建软件的方法远不止这些,既有正式的,也有非正式的。说到底,也许根本不存在某一种“唯一正确”的软件开发方法;唯一真正正确的方法,往往就是最适合你团队的方法。找到它的最好办法,是你自己主动去研究,从各种方法中吸收有价值的部分,与同行讨论他们的经验,并不断迭代自己的流程。记住:评估某种 process methodology 时,唯一真正重要的指标,就是它在多大程度上帮助你的团队更好地写代码。

本章最后还介绍了 version control 的概念。它应当成为任何软件公司——无论大小——不可或缺的一部分,对在家做个人项目也同样有益。如今可用的 version control 软件方案很多,既有免费的,也有商业的。我建议你实际尝试几种,看看哪一种最适合你。

通过完成下面这些练习,你可以巩固本章讨论的内容。所有练习的参考解答都包含在本书网站 www.wiley.com/go/proc++6e 提供的代码下载包中。不过,如果你在某道题上卡住了,建议先回过头重读本章相关部分,尽量自己找到答案,再去看网站上的解答。

  1. 练习 28-1: 请举出一些 software life cycle model 与 software engineering methodology 的例子。
  2. 练习 28-2: XP 提倡 continuous integration。如果这听起来已经够极端了,那么近些年有些公司还更进一步,实践 continuous deployment。请自行做一些调研,看看 continuous deployment 究竟意味着什么。
  3. 练习 28-3: 除了练习 28-2 中的 continuous deployment 之外,还有 continuous delivery。请自行调研 continuous delivery,并把它与 continuous deployment 做个对比。
  4. 练习 28-4: 调研术语 rapid application development(RAD)。它与本章讨论的内容有什么关系?