与建筑施工一样,项目的成败很大程度上在施工开始前就已经确定。如果地基没有打好或者规划不充分,施工期间你能做的最好的事情就是将损害降到最低。

木匠的名言“测量两次,切割一次”,与软件开发的构建部分高度相关。

最糟糕的软件项目最终会进行两次或三次或更多次的构建。对于软件来说,将项目中最昂贵的部分做两次是一个糟糕的主意,就像在任何其他工作中一样。

#3.1 前期工作的重要性

如果在项目结束时才开始强调质量,那么你就会关心系统测试。当许多人想到如何确保软件质量时,他们首先想到的是测试。然而,测试只是完整质量保证策略的一部分,而且不是最有影响力的部分。

  • 测试无法发现根本性的缺陷,例如构建错误的产品或以错误的方式构建正确的产品。这些缺陷必须在测试之前(施工开始之前)得到解决。

如果你在项目中期强调质量,那么被强调的就会是构建的过程。本书中中的绝大部分内容都是关于构建的。

如果你在项目开始时就强调质量,你就会在定义问题,设计解决方案时进行相应的规划。—— 关注先决条件

由于构建处于项目的中期,所以当你开始构建时,项目的前期工作已经为成功或失败奠定了一些基础。

#先决条件适用于现代软件项目吗?

准备工作的首要目标是降低风险:优秀的项目规划者尽早消除主要风险,以便项目的大部分工作能够尽可能顺利地进行。到目前为止,软件开发中最常见的项目风险是不良需求和不良项目规划,因此准备工作往往侧重于改进需求和项目计划。

所使用的方法应该基于选择最新和最好的方法,而不是基于无知。它还应该与旧的和可靠的东西自由搭配。
—— Harlan Mills

#准备不充分的原因

准备不充分的一个常见原因是被分配从事前期工作的开发人员不具备执行相应任务的专业知识。项目规划、开发需求,创建架构所需要的技能绝不是琐碎的零散知识,大多数开发者都从未接受过如何执行这些活动的培训。当开发人员不知道如何做前期准备,那么"做更多前期准备"的建议听起来就像是无稽之谈。

有些程序员确实知道如何执行上游活动,但他们没有做好准备,因为他们无法抗拒尽快开始编码的冲动。

程序员不做好准备的最后一个原因是,项目经理们对花时间构建先决条件的程序员缺乏同情心,这是很普遍的。这种现象被称为 WISCA 或 WIMP 综合症:Why Isn’t Sam Coding Anything? or Why Isn’t Mary Programming?

如果你的项目经理命令你立即开始编码,那么你很容易说:“是的,先生!” (有什么坏处?这个老家伙肯定知道他在说什么)但这是一个不好的回应,你还有几个更好的选择:

  • 第一个选择:断然拒绝以无效的顺序进行工作(你需要保证你和领导的情感账户充裕)
  • 第二个选择:假装在编码,而实际上你没有编码。将代码库放在一边,然后继续开发你的需求和架构,无论是否得到老板的批准。你最终都会更快地完成项目并获得更高质量的结果。有些人认为这种做法在道德上是令人反感的,但从你老板的角度来看,无知就是幸福。
  • 第三个选择:你可以让你的老板了解技术项目的细微差别。这是一个很好的方法,因为它增加了世界上开明老板的数量。

#3.2 确认开发的软件方式

有两种软件开发的方式:

  • 顺序方式:整个项目按照,先进行需求构建,再架构设计,再细节设计,再构建,最后系统测试和质量验收的方式进行。
    顺序方式时间表
  • 迭代方式:整个项目在进行过程中,经过反复的迭代,每一个迭代中都有上述的项目流程。
    迭代方式时间表

#迭代方式对先决条件的影响

一些人声称,使用迭代的项目根本不需要过多关注前期准备,但这种观点是错误的。迭代方法往往会减少上游工作不足的影响,但并不能消除它。

#在迭代方式和顺序方式之间选择

当以下情况下,你可以选择顺序方式:

  • 需求相当稳定
  • 设计非常简单并且很好理解
  • 开发团队熟悉应用领域
  • 项目风险很小
  • 长期可预测性很重要(因为迭代会导致预期一直在变化)
  • 更改下游需求、设计和代码的成本可能很高(这样的话,在迭代中变更需求成本就很大)

当以下情况时,你可以选择迭代性的方法:

  • 需求没有被很好地理解,或者它们由于其他原因而不稳定
  • 设计是复杂的、具有挑战性的,或者两者兼而有之
  • 开发团队不熟悉应用领域
  • 该项目存在很大的风险
  • 长期的可预测性并不重要
  • 更改下游需求、设计和代码的成本可能很低

有些项目在先决条件上花费的时间太少,这使施工面临不必要的高频率的不稳定变化,并阻止项目取得持续进展。
有些项目前期做了太多事情,且当后续实施时发现了计划失效时,还顽固的遵守之前的要求和计划,这也可能阻碍施工进度。

#3.3 前期准备:问题定义

在开始构建之前,你需要满足的第一个先决条件是明确说明系统要解决的问题。这有时称为“产品愿景”、“愿景声明”、“使命声明”或“产品定义”。这里称为“问题定义”。

由于本书是关于构建的,因此本节不会告诉你如何编写问题定义,而是告诉你如何识别一个问题的是否已经被定义,以及所写的内容是否会为构建奠定良好的基础。

如果“盒子”是约束和条件的边界,那么诀窍就是找到盒子……不要跳出它思考,你要先找到它。
—— Andy Hunt / Dave Thomas The Pragmatic Programmer 作者

问题定义定义了问题是什么,但不需要提及任何可能的解决方案

  • “我们无法跟上 Gigatron 的订单”这句话听起来像是一个问题,也是一个很好的问题定义
  • “我们需要优化我们的自动数据输入系统以跟上 Gigatron 的订单”这一说法是一个糟糕的问题定义(它指明了解决方案,而限制了思考)

问题定义应该用用户语言,并且应该从用户的角度描述问题。通常不应该用计算机技术术语来表述。最好的解决方案可能不是计算机程序。

假设您需要一份显示你的年利润的报告,你已经有了显示季度利润的工具:

  • 如果你陷入了程序员的思维模式,你会认为向已经提供季度报告的系统添加年度报告应该很容易。然后,你将付钱给程序员来编写和调试一个计算年利润的耗时程序。
  • 如果你没有陷入程序员的思维模式,你可以付钱给你的秘书,让他花一分钟在计算器上将季度数据相加来创建年度数据。

未能定义问题的代价是你可能会浪费大量时间来解决错误的问题。这是双重惩罚,因为你也没有解决正确的问题。

#3.4 前期准备:需求

需求详细描述了软件系统应该做什么,它们是解决方案的第一步。需求活动也称为“需求开发”、“需求分析”。

#为什么需要正式需求

明确的需求有助于确保是用户而不是程序员驱动最后的功能产生。

如果需求明确,普通用户可以查看并给出意见。而如果需求不明确,程序员通常会在编程过程中做出需求决策,这可能与用户想要的不一致,明确的需求让程序员不需要猜测用户想要什么。

明确的要求也有助于避免争论。在开始编程之前,你需要决定功能的范围,如果您与其他程序员对于程序应该做什么有分歧,可以通过查看需求来解决。

关注需求有助于最大限度地减少开发开始后对系统的更改。如果您在编码过程中发现编码错误,只需更改几行代码即可继续工作。如果在编码过程中发现需求错误,则必须更改设计以满足更改后的需求。

  • 在大型项目中,在架构阶段发现需求错误的纠正成本,是在需求阶段检测到需求错误的成本的 3 倍。
  • 如果在编码过程中被检测到,成本会增加到 5 至 10 倍。
  • 系统测试时,则是 10 倍。
  • 在功能发布后,其成本比在需求开发期间检测到的成本高出 10 到 100 倍。。

#需求稳定的神话

稳定的需求是软件开发的圣杯。有了稳定的需求,项目就可以以有序、可预测和平静的方式从架构到设计、编码到测试。这是软件的天堂。

需求就像水一样。当它们被冷冻时,更容易建造。

#处理施工期间的需求变化

评估需求的质量,如果发现需求的质量不够好,请停止工作。当然,如果你在这个阶段停止编码,感觉就像你在浪费时间。但是,如果你从芝加哥开车前往洛杉矶,当你看到前往纽约的路标时,停下来查看路线图是否是浪费时间?如果你没有朝着正确的方向前进,请停下来检查你的路线。

确保每个人都知道需求变更的成本。当客户想到新功能时会感到兴奋。在兴奋中,他们的血液变稀并流向延髓,他们变得头晕目眩,忘记了所有讨论需求的会议、签字仪式和已完成的需求文件。处理这些沉迷于功能的人的最简单方法是说:“哎呀,这听起来是个好主意。但由于它不在需求文档中,我将制定修订后的时间表和成本估算,以便你可以决定是否要现在或以后这样做。” “日程”和“费用”这两个词比咖啡和冷水澡更能让人清醒,许多“必须有”的需求很快就会变成“可以有”。

当需求变化时,你可以考虑以下的方式:

  • 建立一个需求变更的控制程序。改变主意并意识到他们需要更多功能是正常的,但问题是他们的变化太频繁,以至于研发无法跟上需求的变更。
  • 使用适合与需求变化的开发方式:迭代式开发。渐进式交付是一种分阶段交付系统的方法。你可以构建一些,从用户那里获得一些反馈,稍微调整你的设计,在进行一些更改,然后构建更多。关键是使用较短的开发周期,以便你可以快速响应用户。
  • 如果需求特别糟糕或不稳定,并且上述建议均不可行,可以考虑取消该项目

密切关注项目的业务问题来源,当你回顾执行该项目的业务原因时,许多需求问题就会消失在眼前:如果你去评估功能可以带来的业务增长时,一些仅从功能角度出发非常好的想法,就可能变为了糟糕的想法。

需求检查清单:
Requirement Checklist

#3.5 前期准备:架构

软件架构是软件设计的更高层次部分,框架比设计拥有更多的细节。一些人对软件架构和软件设计的区分方法是:架构是系统范围内的设计约束,而设计是子系统或类级别的设计约束。

本书主要是关于构建的,因此本书不会告诉你如何进行软件架构设计,但会关注如何确保架构的质量。

为什么要把架构作为先决条件?因为架构的质量决定了系统概念性上的完整性,而这又决定了系统的最终质量。经过深思熟虑的架构提供了系统从顶层到底层的保持概念完整性所需的结构。它为程序员提供了指导——提供了适合程序员技能水平与当前工作的细节信息。它将工作进行了划分,以便多个开发人员或多个开发团队可以独立工作。

在施工期间或之后进行架构更改的成本很高。修复软件架构中的错误所需的时间与修复需求设计的错误一样,都比简单的修复编码错误所需的时间更长。

#典型的架构组成部分

以下部分都是一个良好的系统架构应当考虑的部分:

#项目结构

一个系统架构首先需要在广义上描述整个系统。没有这样的概览,你很难从一堆细节和成千上万的类中拼凑出一个连贯的视图。

在架构中,应当体现出已经考虑过整体架构的其他解决方案,并说明使用现在的这个方案而不是其他解决方案的原因。如果架构中一个类的作用没有被清晰的表明,那人们只能沮丧的使用该类进行工作(我知道我应该这么做,但我不能理解这么做的原因)。研究表明,说明一个设计的合理性,和维护这个设计本身一样重要。

一个架构应当定义一个程序的主要组成部分(构建块,Building Block)。根据程序的大小,每一个构建块可以是一个类或者由一系列类构成的子模块。

每一个构建块的内容应当被明确的定义,每一个构建块应当明确自己的责任范围,并且尽可能地少了解其他构建块责任范围。

每个构建块之间的通行规则应当被明确,架构应当描述一个构建块可以直接使用哪些其他构建块,可以间接使用哪些其他构建块,以及不能使用哪些其他构建块。

#主要类

系统结构应当要说明需要使用的主要类,它应该要确定每个类的主要指责,该类该如何与其他类交互。它应该包括类层次结构、状态转换和对象生命周期的描述。如果系统足够大,还应当描述类是如何构成子系统的。

架构还应当描述它所考虑的其他类设计,以及为什么选择了当前的设计而不是其他设计。

#数据设计

架构应当描述其主要使用的文件和数据结构,且它应当描述替代方案,以及为什么选择了当前的设计而不是其他设计。比如一个应用程序需要维护一系列用户的 ID,架构选择了使用 List 来表示,而不是使用 Set,那么架构应当说明为什么选择了 List 而不是 Set

数据通常只能由一个子系统或一个类修改,但可以由多个子系统或类读取。架构应当描述哪些类可以修改数据,哪些类可以读取数据。

#业务规则

如果架构依赖于特定的业务规则,则应该标明它们并描述这些规则对系统架构的影响。

例如,系统需要遵循一条业务规则 “客户信息的过期时间不得超过 30 秒”。在这种情况下,应该描述该规则对架构关于“同步用户信息”这部分的设计的影响。

#用户界面设计

用户的界面设计应当在 前期准备:需求 阶段都已经准备好,如果没有的话,那就需要在架构部分指定。

架构应该模块化,以便可以替换新的用户界面,而不会影响程序的业务规则和输出部分。

例如,架构应该可以让“删除一个 UI 界面,并通过命令行执行相同逻辑” 的需求变得相当容易实现。这种能力通常很有用,特别是因为命令行界面可以方便地进行单元或子系统级别的软件测试。

#资源管理

一个架构应当描述如何管理系统的稀缺资源(线程数,数据库连接,句柄,内存等),应当估计在正常和极端情况下所使用的资源情况。

#安全性

架构应当描述设计层面和代码层面的安全性问题。指定编码规范时,应当要考虑到安全隐患,比如内存的处理方式,对于未经授权的输入信息的处理,加密,错误日志中体现信息的详细程度等问题。

#性能

如果性能是一个问题,那么就应该在需求中定义性能目标,性能目标可以是使用的资源数量,在这种情况下,目标还应该要说明资源之间的优先级,比如执行效率和内存哪个更重要。

架构应该要提供对性能目标的评估,并说明为什么架构师相信在此架构下目标是可以实现的。

  • 如果某些领域面临无法实现其目标的风险,架构应该要说明。
  • 如果某些领域必须使用特定的算法或数据类型才能满足其性能目标,架构应该要说明。
  • 架构还应该要说明每个类和对象的空间和时间预算

#可拓展性

可拓展性是系统满足未来需求的能力。架构应当要描述该系统如何解决用户量增大,数据量增大,以及其他未来需求的问题。

如果架构预计这些数据不会增长,或者可拓展性不是问题,那么架构也应该明确说明这个假设。

#互操作性

如果系统期望与其他的软件或硬件共享数据或资源,那么架构应当要描述如何实现这一点。

#本地化

在交互系统的架构中,本地化问题值得关注,架构应表明已经考虑了典型的字符串和字符集问题,在不更改代码的情况下维护字符串,并将字符串翻译成外语,同时对代码和用户界面的影响最小。

#输入/输出

架构应当要描述预先,事后或实时的信息读取方案,它还需要描述在字段,记录,流或文件级别的错误处理。

#错误处理

错误处理是现代计算机科学中最棘手的问题之一,你不能随意的处理它。

有人估计,程序中 90% 的代码是为异常、错误情况和边界情况而编写的。正因为如此多的代码是专门处理错误的,所以应该在架构中描述错误处理的策略。

错误处理通常被认为是编码规范 级别的问题,但因为它具有系统范围的影响,因此最好在架构层级进行处理,以下是一些需要考虑的问题:

  • 错误处理是纠正性的还是仅仅是检测性的?
    • 如果是纠正性的,程序应当尝试从错误中恢复。
    • 如果是检测性的,程序可以继续执行,就好像什么也没发生一样,也可以选择退出。
      无论哪种情况,程序都应该提示用户它检测到了错误
  • 错误检测是主动的还是被动的?
    • 系统可以主动预测错误(例如,通过检查用户输入的有效性),
    • 或者仅在无法避免错误时才被动响应错误(例如,当用户输入的组合产生数字溢出时)。
  • 程序如何传播错误? 一旦检测到错误,程序可以
    • 选择丢弃导致错误的数据,
    • 将错误视为异常并进入异常处理流程
    • 等到所有逻辑处理完成并通知用户在某地检测到错误
  • 处理错误消息的约定是什么?如果架构没有定义一个单一且一致的策略,那么用户界面会变成一个令人困惑的存在。
  • 异常情况该如何处理?架构应该解决代码何时可以抛出异常,在哪里捕获异常,如何打印异常,如何记录异常等问题。
  • 在程序内部,异常需要在哪一个层级进行处理?是在检测的地方处理,还是将他们传递给异常处理类,还是将它们抛给调用链?
  • 每个类验证其输入数据的责任级别是什么?每个类都需要检测自己的数据,还是有一组类专门负责验证系统的数据?任何级别的类都可以假设它们接收到的数据是干净的吗?
  • 你希望使用已有环境内置的异常处理机制还是自己建立一套异常处理机制,已有环境内的异常处理可能并不能满足你的需求,但是自己建立一套异常处理机制可能会增加额外的复杂性。

#容错能力

架构还应该表明预期的容错类型。容错是一系列技术的集合,这些技术通过检测错误、在可能的情况下从错误中恢复以及在没有错误的情况下遏制其不良影响来提高系统的可靠性。

#可行性

架构应证明该系统在技术上是可行的。

如果任何领域的不可行性可能导致项目无法实施,则架构应表明如何通过概念原型验证、调用或其他方式确认这些问题,这些风险应在全面构建开始之前得到解决。

#饱和设计

鲁棒性是指系统在检测到错误后继续运行的能力。通常,架构定义的系统比需求定义的系统更健壮。

在软件领域中,系统的薄弱等级并不等于所有组成模块中最薄弱的那个模块等级,而可能是所有模块薄弱程度的叠加。

通过在架构中明确指定鲁棒性的期望,也能避免出现某些类异常健壮,而某一些类勉强够用的情况。

#购买/构建 决策

构建一个软件最好的方式是压根不构建它,而是用购买或免费使用开源软件的方式。

如果架构决定不使用已有的库,它应该解释它期望的 自建库要比市面上已有库强大的能力 是什么。

如果一个架构决定要使用已有的库 / 测试用例 / 数据结构或其他,架构也应当解释这些已有的材料为何能满足架构中其他部分的需求。

#应对变动的策略

产品在整个开发过程中可能会发生变化,变化可能源于被破坏的数据类型和文件格式、更改的功能、新特性等等。因此,软件架构师面临的主要挑战之一是使架构足够灵活以适应可能的变化。

架构应该清楚地描述应对变动的策略。架构应表明已经考虑了可能的新增功能,并且这些新增功能在现有架构下最有可能也是最容易实现的。

如果输入或输出格式、用户交互风格或处理要求发生变化,架构应该表明这些变化都是预期的,并且任何单个变化的影响将仅限于少数类。

#架构质量

一个好的架构特点,是它对系统中的每个类,每个类的隐藏信息,和其他所有可能的设计方案都进行了原理性的讨论。

大型系统的基本问题是保持其概念完整性,一个好的架构应该能够解决问题。

  • 当你查看架构时,你应该会对解决方案看起来如此自然和简单感到满意。
  • 要解决的问题和架构本身不应该看起来像是用胶带强行绑在一起的。

架构应该表述所有重大决策的动机,并且要避免出现 “我们一直都是这样做的” 这样的理由。

好的架构应当是独立于硬件和语言的。当然,构建环境(硬件和语言)是不可被忽略的,但是尽可能地脱离于环境,可以有效地避免过度设计地诱惑。

架构应当明确地区分 未设计过度设计的部分,不应该出现架构中的某一个部分受到了超出其应有的设计,而又有部分不受到关注。

架构应明确指出可能存在风险的区域,架构也应该解释为什么这些区域有风险以及已采取哪些措施来最大程度地降低风险。

架构应该包含多个角度的说明,这样可以消除错误和不一致,并帮助程序员充分理解系统的设计。

最后,你不应该对架构的任何部分感到不安,架构不应该包含任何只是为了取悦老板的内容。它不应包含任何你难以理解的内容。你是负责实施它的人,如果它对你来说没有意义,你如何实施它?

架构检查清单:
Architecture Checklist

#3.6 花在上游决策条件上的时间量

花在问题定义、需求和软件架构上的时间根据项目的需求而变化。一般来说,一个运行良好的项目应当将大约 10% 到 20% 的精力和大约 20% 到 30% 的进度投入到需求、架构和前期规划上。

  • 如果需求不明确,并且你正在处理大型正式项目,则需要与需求分析师合作来解决在构建早期发现的需求问题。
  • 如果需求不明确,并且你正在处理一个小型的非正式项目,那么你可能需要自己解决需求问题:留出足够的时间来定义需求,使其波动对施工的影响降至最低。

无论是什么项目,如果需求不明确,请将需求的明确作为自己的工作一部分。

  • 如果你不知道自己要做什么,那么没有人能理智的估算出周期。
  • 就好比你是一个建筑承包商,被要求建一所房子,你的客户说“这项工作要花多少钱?” 理智的话,你应该问“你想让我做什么?” 如果你的客户说“我不能告诉你,但是你需要花多少钱?” 这时,你应该做的就是感谢下这个顾客浪费了你宝贵的时间,然后回家。

完整的上游决策的检查清单:
Prerequisites Checklist