目录
前言
木匠的谚语“瞄两次,切一次”(Measure twice, cut once/三思而后行)与软件开发中的构建部分有密切联系,构建活动差不多占整个项目成本的 65%。最糟糕的软件项目最终会进行两三次(甚至更多)构建。将项目中最昂贵的部分执行两遍,这无论在软件行业还是在其他行业都是愚蠢的主意。
就像修建建筑物一样,项目的成败很大程度上在构建活动开始之前就已经注定了。如果地基没打好,或者计划不充分,那么你在构建期间能做的无非是尽量让损害最小罢了。
本章主题
本章描述软件构建必须做的准备工作。本章是为成功的软件构建打地基,并没有直接讨论构建活动。
1、清楚的知道”准备工作“有哪几个活动组成
准备工作由下面几个活动组成,本章也是重点对以下活动进行叙述:
- 定义问题 (problem definition)
- 需求分析 (requirements development)
- 软件架构 (sofiware architecure),或高层设计(high-level design)
构建活动由下面几个活动组成:
- 详细设计 (detailed design)
- 规划构建 (construction planning)
- 编码与调试 (coding and debugging)
- 单元测试 (unit testing)
- 集成测试 (integration testing)
2、准备工作-架构的先决条件,是我们开发需要认真关注的事情。
3.1 前期准备的重要性
使用高质量的实践方法是那些能创造高质量软件的程序员的共性。这些高质量的实践方法在项目的初期、中期、末期都强调质量。
3.1.1 处于不同阶段强调质量
如果你在项目的末期强调质量,那么你会强调系统测试。当提到软件质量保证的时候,许多人都会想到测试。但是测试只是完整的质量保证策略的一部分,而且不是最有影响的部分。测试是不可能检查出诸如“制造了一个错误的产品”,或者“使用错误的方法制造正确的产品”之类的缺陷的。这样的缺陷必须在测试之前解决——更确切地说是在构建活动之前。
如果你在项目中期强调质量,那么你会强调构建实践。这些实践是本书绝大部分篇幅的关注点。
如果你在项目的开始阶段强调质量,那么你就会计划、要求并且设计一个高质量的产品。如果你用为 Pontiac Aztek 做的设计来开始整个生产过程,那么你可以想尽办法来测试,它也绝对不会变成劳斯莱斯。也许你能造出最好的 Aztek,但如果想要的是一辆劳斯莱斯,那么你就得从头开始做计划。在软件开发中,你也需要在定义问题、定下解决方案的规格,以及设计解决方案的时候做出这种计划"。
3.1.2 前期准备对”构建活动“的影响
由于构建活动是软件项目的中间阶段,在你开始构建的时候,项目前期工作己经或多或少为这个项目的成功或失败打下了基础。然而,在构建过程中,你至少应该能辦明当时的形势如何,如果你看到失败的乌云已经出现在地平线上时,就退回到项目的前期工作吧。
本章的其余部分将仔细讲述为什么合适的准备工作是非常重要的,并且告诉你如何判定“是否已经准备好开始构建工作了”。
3.1.3 准备不周全的诱因
1、造成准备工作不充分的一个常见原因是,那些分配去做前期准备活动的开发人员并不具备完成这一任务的专业技能。(个人补充:或者说是让原本不是旧项目的开发人员去给该旧项目加砖添瓦)
- 项目规划、创作引人注目的商业案例、分析出全面而准确的需求、创建高质量的架构等活动都需要一定的技能,这些技能不是轻而易举就能获得的。但是绝大多数开发人员都没有接受过针对这些活动的训练。当开发人员不知道如何进行这些前期工作的时候,建议“做更多的前期工作”就完全没有用;如果不能首先把这项工作做好,那么做再多也没有意义!说明如何进行这些活动已经超出了本书的范围,不过在本章最后的“更多资源”中,提供了许多获取这些专业技能的途径。
2、程序员不做准备工作的最后一个原因是,管理者们对那些“花时间进行构建活动的前期淮备的程序员”的冷漠已经到了人神共愤的程度(简单理解就是:管理者们只想让我们尽快我们写代码)。Barry Boehm、 GradyBooch 及 Karl Wiegers 等人25 年来一直在擂响需求和设计的战鼓,因此你可以期望,管理者们应该已经开始明白:软件开发不仅仅是写代码。
3.1.4 我理解的准备周全(纯属个人理解)
我理解的准备周全包含两部分。第一部分是”产品文档“足够清晰,第二部分是”详设“已经确定。产品文档让我们知道了“要做什么”?当我们确认”可以做“的时候,详设再来告诉我们具体”怎么做“?
3.2 辨明你所从事的软件的类型
不同种类的软件项目,需要在“准备工作”和“构建活动”之间做出不同的平衡。每一个项目都是独特的,但是项目可以归入若干种开发风格。表3-2 列出了三种最常见的软件项目种类,并且列出了各种项目最适合的典型实践。
在真实项目中,你会找到表中所列这三种主调的无数种变奏。
3.2.1 高度迭代开发法与序列式开发法
1、开发商业系统的项目往往受益于高度迭代的开发法,这种方法的“计划、需求、架构”活动与“构建、系统测试、质量保证”活动交织在一起。
2、性命攸关的系统往往要求采用序列式的方法——“需求稳定”是确保“超高等级的可靠性”的必备条件之一。
3.2.2 两种开发法的选择
绝大多数的项目都不会完全使用序列式开发法或者完全使用迭代式开发法。预先详细说明 100%的需求和设计是不切实际的,不过对绝大多数项目来说,“尽早把哪些是最关键的需求要素和架构要素确定下来”是很有价值的。
一条很有用的经验规则是:
- 计划好预先对大约 80%的需求做出详细说明,并给“稍后再进行详细说明的额外需求”分配一定的时间。然后在项目进行过程中,实施系统化的变更控制措施——只接受那些最有价值的新需求。
- 另一种替代方案是,预先只对最重要的20%的需求做出详细说明,并且计划以小幅增量开发软件的剩佘部分,随着项目的进行,对额外的需求和设计做出详细说明。
下个议题:如何判断前期准备工作是否到位
既然你已经研究过表3-2,并且确定了何种前期准备适合你的项目,那么本章接下来将要讨论的是:如何判断每一项特定的前期准备工作是否到位。
3.3 问题定义的先决条件
在开始构建之前,首先要满足的一项先决条件是,对这个系统要解决的问题做出清楚的陈述。这有时称为“产品设想/product vision ”、“设想陈述/visionstatement”、“任务陈述/mission statement” 或者“产品定义/product definition”。这里将它称为“问题定义/problem definition”。由于这本书是关于软件构建的,本节不打算告诉你如何去写问题定义,而是告诉你如何辦认是否已经写好了问题定义,以及它能否成为构建活动的良好基础。
“未能定义问题”的处罚是,你浪费了大量时间去解决错误的问题。这是双重处罚,因为你也没有解决正确的问题。
3.4 需求的先决条件
“需求”详细描述软件系统应该做什么,这是达成解决方案的第一步。
3.4.1 为什么要有正式的需求
要求一套明确的需求,这点很重要,理由很多。
1、明确的需求有助于确保是用户(而不是程序员)驾驭系统的功能。如果需求明确,那么用户就可以自行评审,并进行核准。否则,程序员就常常会在编程期间自行决定需求。明确的需求免得你去猜测用户想要的是什么。
2、明确的需求还有助于避免争论。在开始编程之前,先把系统的范围 ((scope)确定下来。如果你和另外一个程序员对于“程序应该做什么”意见不一致,你们可以查看书面的需求,以解决分岐。
3、重视需求有助于减少开始编程开发之后的系统变更情况。如果你在编码过程中发现了一个代码上的错误,你只需要修改几行的代码,然后就能继续工作。但是如果你在编码的时候发现了一个需求错误,那你就得改变设计,使之符合更改后的需求。你可能需要扔掉部分旧的设计,并且因为要与已经写好的代码相适应,可能导致新的设计,与在项目之初进行同样的设计相比,花费更长的时间。此外,还需要废弃那些受此次需求变更影响的代码和测试用例,还需要编写新的代码和测试用例。即便是未受影响的代码也需要重新测试地方的改变没有引入任何新的错误。
3.4.2 稳定需求的神话
“一旦客户接受了一份需求文档,就再也不做更改”是一个美好的愿望。然而,对一个典型的项目来说,在编写代码之前,客户无法可靠地描述他们想要的是什么。问题并不在于客户是低级生物。就如同你做这个项目的时间越长,对这个项目的理解也就越深入一样,客户参与项目的时间越长,他们对项目的理解也就越深入。开发过程能够帮助客户更好地理解自己的需求,这是需求变更的主要来源 (Curtis,Krasner, and Iscoe 1988; Jones 1998; Wiegers 2003)。
典型情况下需求会有多少改动?IBM 和其他公司的研究发现,平均水平的项目在开发过程中,需求会有25%的变化(Boehm 1981, Jones 1994, Jones 2000)。在典型的项目中,需求变更导致的返工占到返工总量的75%到85%(Leffingwell1997, Wiegers 2003)。
3.4.3 在构建期间处理需求变更
在构建期间,要最好地应对需求变更,有以下一些可以采用的方式。
1、使用本节末尾的需求核对表来评估你的需求的质量 。如果你的需求不够好,那么就停止工作,退回去,先把它做好,再继续前进。当然,因为在此期间你会停止编码,所以感觉似乎进度会落后。不过,假设你正开车从芝加哥到洛杉矶,突然看到纽约的路牌,那么停下来查看路线图是浪费时间吗?当然不是,如果没有对准正确的方向,那就要停下来检查一下路线”。
2、确保每一个人都知道需求变更的代价。 客户只要想到一个新功能就会很兴奋。在兴奋时血液会涌向大脑,人会晕头晕脑,他会把所有你们开过的讨论需求的会议、签字仪式。以及已经完成的需求文档统统抛诸脑后。最简单的对付这种新功能中毒症患者的办法是说:“咦,这听起来是一个很不错的主意。不过由于它不是需求文档里的内容,我会整理一份修订过的进度表和成本估计表,这样你可以决定是现在实施,还是过一阵子再说” 。“进度”和“成本”这两个字眼比咖啡和洗冷水澡都要提神,许多“必须要有/must haves”很快会变成“有就最好/ nice tohaves"。
3、建立一套变更控制程序。 如果你的客户激情不减,那就要考虑建立一个正式的变更控制委员会,评审提交上来的更改方案。客户改变他们的想法,认识到他们需要更多的功能,这不是坏事。问题是他们提出更改方案太频繁了,让你跟不上进度。如果有一套固定的变更控制程序,那么大家都会很愉快—你知道自己只需在特定时候处理变更;而客户知道你打算处理他们的提议。
4、使用能适应变更的开发方法。 某些开发方法让你 “对需求变更做出响应”的能力最大化。演进原型(evolutionary prototyping)法能让你在投入全部精力建造系统之前,先探素系统的需求。演进交付(evolutionary delivery)是一种分阶段交付系统的方法。你可以建造一小块、从用户获得一点反馈、调整一点设计、做少量改动,再多建造一小块。关键在于缩短开发周期,以便更快地响应用户的要求。
5、放弃这个项目。 如果需求特别糟糕,或者极不稳定,而上面的建议没有一条能奏效,那就取消这个项目。即使你无法真的取消这个项目,也设想一下取消它之后会是怎样的情况。在取消它之前想想它有可能会变得多糟糕。假如在某种情况下你可以放弃这个项目,那么至少也要问问自己,目前的情况和你所设想的那种情况有多大距离。
3.4.4 核对表:需求
这张需求核对表包含了一系列的问题一一问问自己项目的需求工作做得如何。本书并不会告诉你如何做出好的需求分析,所以列表里面也不会有这样的问题。在开始构建之前,用这份列表做一次“心智健全”检查,看看你的地基到底有多坚固——用 “需求里氏震级”来衡量。
并不是核对表中所有的问题都适用于你的项目。如果你做的是一个非正式项目,那么你会发现有些东西根本就不需要考虑。你还会发现一些问题你需要考虑,但不需要做出正式的回答。如果你在做一个大型的、正式的项目,你也许就要逐条考虑了。
1. 针对功能需求
- 是否详细定义了系统的全部输入,包括其来源、精度、取值范围、出率等?
- 是否详细定义了系统的全部输出,包括目的地、精度、取值范围、出率、格式等?
- 是否详细定义了所有输出格式(Web 页面、报表,等等)?
- 是否详细定义了所有硬件及软件的外部接口?
- 是否详细定义了全部外部通信接口,包括握手协议、纠错协议、通信等?
- 是否列出了用户想要做的全部事情?
- 是否详细定义了每个任务所用的数据,以及每个任务得到的数据?
2. 针对非功能需求(质量需求)
- 是否为全部必要的操作,从用户的视角,详细描述了期望响应时间?
- 是否详细描述了其他与计时有关的考虑,例如处理时间、数据传输率、系统吞吐量?
- 是否详细定义了安全级别?
- 是否详细定义了可靠性,包括软件失灵的后果、发生故障时需要保护的至关重要的信息、错误检测与恢复的策略等?
- 是否详细定义了机器内存和剩余磁盘空间的最小值?
- 是否详细定义了系统的可维护性,包括适应特定功能的变更、操作环境的变更、与其他软件的接口的变更能力?
- 是否包含对“成功”的定义?“失败”的定义呢?
3. 需求的质量
- 需求是用用户的语言书写的吗?用户也这么认为吗?
- 每条需求都不与其他需求冲突吗?
- 是否详细定义了相互竞争的特性之间的权衡--例如,健壮性与正确性之间的权衡?
- 是否避免在需求中规定设计(方案)?
- 需求是否在详细程度上保持相当一致的水平?有些需求应该更详细地描述吗?有些需求应该更粗略地描述吗?
- 需求是否足够清晰,即使转交给一个独立的小组去构建,他们也能理解吗?开发者也这么想吗?
- 每个条款都与待解决的问题及其解决方案相关吗?能从每个条款上溯到它在问题域中对应的根源吗?
- 是否每条需求都是可测试的?是否可能进行独立的测试,以检验满不满足各项需求?
- 是否详细描述了所有可能的对需求的改动,包括各项改动的可能性?
4. 需求的完备性
- 对于在开始开发之前无法获得的信息,是否详细描述了信息不完全的区域?
- 需求的完备度是否能达到这种程度:如果产品满足所有需求,那么它就是可接受的?
- 你对全部需求都感到很舒服吗?你是否已经去掉了那些不可能实现的需求——那些只是为了安抚客户和老板的东西?
3.5 架构的先决条件
由于本书是关于软件构建的,因此本节不会告诉你如何开发一个软件的架构。因为架构比需求离构建活动又近了一步,所以对架构的讨论也会比对需求的讨论更详细一些。
好的架构使得构建活动变得更容易。糟糕的架构则使构建活动几乎寸步难行。图3-7显示了糟糕的架构的另一个问题。
在构建期间或者更晚的时候进行架构变更,代价也是高昂的。修复软件架构中的错误所需的时间与修复需求错误所需的时间处于同一数量级——即,多于修复编码错误所需的时间(Basili and Perricone 1984, Willis 1998)。架构变更如同需求变更一样,看起来一个很小的改动,影响也许是非常深远的。无论为了修正错误还是改进设计而引发架构变更,越早识别出变更越好。
3.5.1 架构的典型组成部分
很多组成部分是优秀的系统架构所共有的。
如果你自己构建整个系统,那么在架构工作会与更详细的设计工作有重叠部分。在这种情况下,你至少应该思考架构的每个组成部分。
如果你目前从事的系统的架构是别人做的,你应该能够不费力地找出其中重要的组成部分(无须戴.上猎鹿帽、牵着猎犬、手拿放大镜)。
在这两种情况中,你都需要考虑以下的架构组成部分。
1. 程序组织
系统架构首先要以概括的形式对有关系统做一个综述。如果没有这种综述,要想将成干的局部图片(或十多个单独的类)拼成一幅完整的图画是相当伤脑筋的。如果系统是小小的只有12块的智力拼图玩具,你那一岁的小孩也能在眨眼功夫解决它。不过把12 个子系统拼到一起要困难一些,而且如果你不能将它们拼起来,那么就无法理解你正在开发的那个类对系统有何贡献。
在架构中,你应该能发现对那些曾经考虑过的最终组织结构的替代方案的记叙,找到之所以选用最终的组织结构,而不用其他替代方案的理由。如果对某个类在系统中的角色没有一个清晰的构思,那么编写这个类就是一件令人灰心丧气的工作。描述其他组织结构,才能说明架构最后选定的这种系统组织结构的缘由,并且表明各个类都是慎重考虑过的。有一份对设计实践的综述发现,“维护‘设计的缘由’”至少与“维护设计本身”一样重要(Rombach 1990)。
架构应该定义程序的主要构造块 (building blocks)。根据程序规模不同,各个构造块可能是单个类,也可能是由许多类组成的一个子系统。每个构造块无论是一个类还是一组协同工作的类和子程序,它们共同实现一种高层功能,诸如与用户交互、显示 web 页面、解释命令、封装业务规则、访问数据,等等。每条列在需求中的功能特性(feature)都至少应该有一个构造块覆盖它。如果两个或多个构造块声称实现同一项功能,那么它们就应该相互配合而不会冲突。
应该明确定义各个构造块的责任。每个构造块应该负责某一个区域的事情,并且对其他构造块负责的区域知道得越少越好。通过使各个构造块对其他构造块的了解达到最小,你能将设计的信息局限于各个构造块之内。
2. 主要的类
关于”类的设计“可以详细看:第6章 可以工作的类。
架构应该详细定义所用的主要的类。它应该指出每个主要的类的责任,以及该类如何与其他类交互。
- 它应该包含对类的继承体系、状态转换、对象持久化等的描述。如果系统足够大,它应该描述如何将这些类组织成一个个子系统。
- 架构应该记述曾经考虑过的其他类设计方案,并给出选用当前的组织结构的理由。
- 架构无须详细说明系统中的每一个类。瞄准 80/20 法则:对那些构成系统80%的行为的 20%的类进行详细说明 (Jacobsen, Booch, and Rumbaugh 1999;Kruchten 2000)。
3. 数据存储设计
架构应该描述所用到的主要文件和数据表的设计。它应该描述曾经考虑过的其他方案,并说明做出选择的理由。
- 如果应用程序要维护一个客户ID 的列表,而架构师决定使用顺序访问的列表(sequential-access list)来表示该ID 表,那么文档就应该解释为什么顺序访问的列表比随机访问的列表(random-access list)、堆栈、散列表要好。
- 在构建期间,这些信息让你能洞察架构师的思想。在维护阶段,这种洞察力是无价之宝。离开它,你就像看一部没有字幕的外语片。
数据通常只应该由一个子系统或 一个类直接访问;例外的情况就是透过访问器类(access class)或访问器子程序(access routine)——以受控且抽象的方式——来访问数据。详细的解释请看第5.3节中的“隐藏秘密(信息隐藏)”。
架构应该详细定义所用数据库的高层组织结构和内容。
- 架构应该解释为什么单个数据库比多个数据库要好(反之亦然),
- 解释为什么不用平坦的文件而要用数据库,指出与其他访问同一数据的程序的可能交互方式,说明会创建哪些数据视图(view),等等。
4. 业务规则
如果架构依赖于特定的业务规则,那么它就应该详细描述这些规则,并描述这些规则对系统设计的影响。例如,假定要求系统遂循这样-条业务规则:客户信息过时的时间不能超过30 秒。在此情况下,架构就应该描述这条规则对架构采用的“保持客户信息及时更新且同步”的方法的影响。
5. 用户界面设计
用户界面常常在需求阶段进行详细说明。如果没有,就应该在软件架构中进行详细说明。架构应该详细定义 web 页面格式、GUI、命令行接口 (command lineinterface) 等的主要元素。用户界面设计值得用整本书的篇幅来讨论,不过这超出了本书的范围。
6. 资源管理
架构应该描述一份管理稀缺资源的计划。稀缺资源包括数据库连接、线程、句柄(handle)等。在内存受限的应用领域,如驱动程序开发和嵌入式系统中,内存管理是架构应该认真对待的另一个重要领域。架构应该估算在正常情况和极端情况下的资源使用量。在简单的情况下,估算数据应该说明:预期的实现环境(运行环境)有能力提供所需的资源。在更复杂的情况中,也许会要求应用程序更主动地管理其拥有的资源。如果是这样,那么“资源管理器/resource manager” 应和系统的其他部分一样进行认真的架构设计。
7. 安全性
架构应该描述实现设计层面和代码层面的安全性的方法。如果先前尚未建立威胁模型(threat model),那么就应该在架构阶段建立威胁模型。在制定编码规范的时候应该把安全性牢记在心,包括处理缓冲区的方法、处理非受信(untrudted)数据(用户输入的数据、cookies、配置数据(文件)和其他外部接口输入的数据)的规则、加密、错误消息的细致程度、保护内存中的秘密数据,以及其他事项。
8. 性能
如果需要关注性能,就应该在需求中详细定义性能目标。性能目标可以包括资源的使用,这时,性能目标也应该详细定义资源(速度、内存、成本)之间的优先顺序。
架构应该提供估计的数据,并解释为什么架构师相信能达到性能目标。如果某些部分存在达不到性能目标的风险,那么架构也应该指出来。如果为了满足性能目标,需要在某些部分使用特定的算法或数据类型,架构也应该说清楚。架构中也可以包括各个类或各个对象的空间和时间预算。
9. 可伸缩性
可伸缩性是指系统增长以满足未来需求的能力。架构应该描述系统如何应对用户数量、服务器数量、网络节点数量、数据库记录数、数据库记录的长度、交易量等的增长。如果预计系统不会增长,而且可伸缩性不是问题,那么架构应该明确地列出这一假设。
10. 互用性
如果预计这个系统会与其他软件或硬件共享数据或资源,架构应该描述如何完成这一任务。
11. 国际化/本地化
12. 输入/输出
输入输出(VO)是架构中值得注意的另一个领域。架构应该详细定义读取策略(reading scheme)是先做(look-ahead)、后做(look-behind)还是即时做(just-in-time)。而且应该描述在哪一层检测 VO 错误:在字段、记录、流,或者文件的层次。
13. 错误处理
错误处理己被证实为现代计算机科学中最棘手的问题之一,你不能武断地处理它。有人估计程序中高达 90%的代码是用来处理异常情况、进行错误处理、或做簿记(housekeeping)工作,意味着只有10%的代码是用来处理常规的情况(Shaw inBentley 1982)。既然这么多代码致力于处理错误,那么在架构中就应该清楚地说明一种“一致地处理错误〞的策略。
错误处理常被视为是“代码约定层次/coding-convention-level” 的事情一—如果真有人注意它的话。但是因为错误处理牵连到整个系统,因此最好在架构层次上对待它。下面是一些需要考虑的问题。
- 错误处理是进行纠正还是仅仅进行检测?如果是纠正,程序可以尝试从错误中恢复过来。如果仅仅是检测,那么程序可以像“没有发生任何事”一样继续运行,也可以退出。无论哪一种情况,都应该通知用户说检测到一个错误。
- 错误检测是主动的还是被动的?系统可以主动地预测错误—例如,通过检查用户输入的有效性——也可以在不能避免错误的时候,被动地响应错误——例如,当用户输入的组合产生了一个数值溢出错误时。前者可以扫清障碍,后者可以清除混乱。同样,无论采用哪种方案,都与用户界面有影响。
- 程序如何传播错误?程序一旦检测到错误,它可以立刻丢弃引发该错误的数据;也可以把这个错误当成一个错误,并进入错误处理状态;或者可以等到所有处理完成,再通知用户说在某个地方发现了错误。
- 错误消息的处理有什么约定?如果架构没有详细定义一个一致的处理策略,那用户界面看起来就像“令人困惑的乱七八糟的抽象拼贴画”,由程序的不同部分的各种界面拼接而成。要避免这种外观体验,架构应该建立一套有关错误消息的约定。
- 如何处理异常(exceptions)? 架构应该规定代码何时能够抛出异常,在什么地方捕获异常,如何记录(log)这些异常,以及如何在文档中描述异常,等等。
- 在程序中,在什么层次上处理错误?你可以在发现错误的地方处理,可以将错误传递到专门处理错误的类进行处理,或者沿着函数调用链往上传递错误。
- 每个类在验证其输入数据的有效性方面需要负何种责任?是每个类负责验证自己的数据的有效性,还是有一组类负责验证整个系统的数据的有效性?某个层次上的类是否能假设它接收的数据是干净的(clean,即,没有错误)?
- 你是希望用运行环境中内建的错误处理机制,还是想建立自己的一套机制?事实上,运行环境所拥有的某种特定的错误处理方法,并不一定是符合你的需求的最佳方法。
14. 容错性
架构还应该详细定义所期望的容错种类。容错是增强系统可靠性的一组技术,包括检测错误;如果可能的话从错误中恢复;如果不能从错误中恢复,则包容其不利影响。
其他容错方法包括,在遇到错误的时候,让系统转入某种“部分运转/partialoperation” 的状态,或者转入某种“功能退化/degraded functionality” 的状态。系统可以自动关闭或重启。这些例子经过了必要的简化。容错是一个吸引人的复杂主题——可惜,它超出了本书的范围。
15. 架构的可行性
设计师多半会关注系统的各种能力,例如是否达到性能目标,能够在有限的资源下运转,实现环境(运行环境)是否有足够的支持。架构应该论证系统的技术可行性。如果在任何一个方面不可行都会导致项目无法实施,那么架构应该说明“这些问题是如何经过研究的〞-—通过验证概念的原型(proof-of-conceptprototype)、研究、或其他手段。必须在全面开展构建之前解决掉这些风险。
16. 过度工程
架构应该清楚地指出程序员应该“为了谨慎起见宁可进行过度工程(overengineering)”,还是应该做出最简单的能工作的东西。
详细定义一种过度工程(裕度工程)的方法尤其重要,因为许多程序员会出于专业自豪感,对自己编写的类做过度工程。通过在架构中明确地设立期望目标,就能避免出现“某些类异常健壮,而其他类勉强够健壮”的现象。
17. 关于”买“还是”造“的决策
最激进的构建软件的解决方案是根本不去构建它——购买软件,或者免费下载开源的软件。
如果架构不采用现货供应的组件,那么就应该说明“自己定制的组件应该在哪些方面胜过现成的程序库和组件”。
18. 关于复用的决策
如果开发计划提倡使用业已存在的软件、测试用例、数据格式或其他原料,架构应该说明:如何对复用的软件进行加工,使之符合其他架构目标——如果需要使之符合的话。
19. 变更策略
1、因为对于程序员和用户来说,构建软件产品都是一个学习过程,所以在开发过程中产品很可能会发生变化。这些变更来自不稳定的数据类型和文件格式、功能需求的变更、新的功能特性,等等。这些变更可能是计划增加的新功能,也可能是没有添加到系统的第一个版本中的功能。因此,软件架构师面临的一个主要挑战是,让架构足够灵活,能够适应可能出现的变化。
交叉参考:关于有系统地处理变更的具体办法,见第 28.2节“配置管理”。
2、 架构应当清楚地描述处理变更的策略。架构应该列出已经考虑过的有可能会有所增强的功能,并说明“最有可能增强的功能同样也是最容易实现的”。如果变更很可能出现在输入输出格式、用户交互的风格、需求的处理等方面,那么架构就应该说明:这些变更已经被预料到了,并且任何单一的变更都只会影响少数几个类。架构应对变更的计划可以很简单,比如在数据文件中放入版本号、保留一些供将来使用的字段、或者将文件设计成能够添加新的表格。如果使用了代码生成器,那么架构应该说明,可预见的变更都不会超出该代码生成器的能力范围。
设计中的 bug 常常不易发现;随著演化的进行,系统不断增加新的功能特性和用途,早期的设计假设渐渐被忘记,这时设计中的bug 就会现身。—FerandoJ. Corbato
3、架构应该指出“延迟提交/delay commitment”所用的策略(延迟提交是指推迟茉些因素的确定时间,做晚绑定,以增强灵活性)。比如说,架构也许规定使用表驱动(table-driven)技术(而不使用硬编码的if语句)。它也许还规定“表”中的数据是保存在外部文件中,而非直接写在程序代码中,这样就能做到在不重新编译的情况下修改程序。
交叉引用:关于延迟提交的完整描述,见第5.3节中的“有意识地选择绑定时间”
20. 架构的整体质量
3.5.2 核对表:架构
以下是一份问题列表,优秀的架构应该关注这些问题。这张核对表的意图并非用做一份有关如何做架构的完全指南,而是作为一种实用的评估手段,用来评估软件食物链到了程序员这一头还有多少营养成分。这张核对表可用做你自己的核对表的出发点。就像“需求”的核对表一样,如果你从事的是非止式项目,那么你会发现其中某些条款甚至都不用去想。如果你从事的是更大型的项目,那么大多数条款都会是很有用的。
针对各架构主题
- 程序的整体组织结构是否清晰?是否包含一个良好的架构全局观(及其理由)?
- 是否明确定义了主要的构造块(包括每个构造块的职责范围及与其他构造块的接口)?
- 是否明显涵盖了“需求”中列出的所有功能(每个功能对应的构造块不太多也不太少)?
- 是否描述并论证了那些最关键的类?
- 是否描述并论证了数据设计?
- 是否详细定义了数据库的组织结构和内容?
- 是否指出了所用关键的业务规则,并描述其对系统的影响?
- 是否描述了用户界面设计的策略?
- 是否将用户界面模块化,使界面的变更不会影响程序其余部分?
- 是否描述并论证了处理 VO 的策略?
- 是否估算了稀缺资源(如线程、数据库连接、句柄、网络带宽等)的使用量,是否描述并论证了源管理的策略?
- 是否描述了架构的安全需求?
- 架构是否为每个类、每个子系统、或每个功能域(functionality area)提出空间与时间预算?
- 架构是否描述了如何达到可伸缩性?
- 架构是否关注互操作性?
- 是否描述了国际化/本地化的策略?
- 是否提供了一套内聚的错误处理策略?
- 是否规定了容错的办法(如果需要)?是否证实了系统各个部分的技术可行性?
- 是否详细描述了过度工程(overengineering)的方法?
- 是否包含了必要的“买 vs. 造”的决策?
- 架构是否描述了如何加工被复用的代码,使之符合其他架构目标?
- 是否将架构设计得能够适应很可能出现的变更?
架构的总体质量
- 架构是否解决了全部需求?
- 有没有哪个部分是“过度架构/overarchitected”或“欠架构/underarchitected'是否明确宣布了在这方面的预期指标?
- 整个架构是否在概念上协调一致?
- 顶层设计是否独立于用作实现它的机器和语言?
- 是否说明了所有主要的决策的动机?
- 你,作为一名实现该系统的程序员,是否对这个架构感觉良好?
3.6 花费在前期准备上的时间长度
花费在问题定义、需求分析、软件架构上的时间,依据项目的需要而变化。一般说来,一个运作良好的项目会在需求、架构以及其他前期计划方面投入10%~20%的工作量和 20%~30%的时间(McConnell 1998, Kruchten 2000)。这些数字不包括详细设计的时间——那是构建活动的一部分。
如果需求不稳定,同时你从事的是一个大型的正式项目,那你就很可能需要与需求分析师合作,以解决构建活动早期指出的需求问题。你要为“与需求分析师协商〞预留一些时间,还应预留时间给需求分析师修订需求,这样你才能得到一份可行的需求。
如果需求不稳定,同时你从事的是一个小型的非正式的项目,那你很可能需要自己解决需求方面的问题。要预留足够的时间,将需求定义足够清晰,让需求的不稳定性对构建活动的负面影响降至最低。
如果需求在任何项目上都不稳定——无论正式项目或非正式项目——那就将需求分析工作视为独立的项目来做。在完成需求之后,估计项目余下的部分要花多少时间。这是明智的办法,因为在弄清楚要做的是什么之前,没人相信你能估算出合理的进度表。这就好比你是一名承包商,有人请你建一栋房子。客户问你:“完成这项工作要花多少钱?”你会合理地询问:“你想要我做什么?”客户说:“我不能告诉你,不过我想知道需要花费多少钱?”你该明智地感谢他浪费了你的时间,然后转身回家。
在为软件架构分配时间的时候,要使用与需求分析类似的方法。如果软件是你以前没有做过的类型,应当为“在新的领域中做设计”的不确定性预留更多时问。你要确保创建良好架构所需要的时间,不会被“为做好其他方面工作所需要的时间”所挤占。如果有必要,将架构工作也作为独立的项目来对待。
核对表:前期准备
- 你是否辨明了自己所从事的软件的类型,并对所用的开发方法做出相应的剪裁?
- 是否充分明确地定义了需求?而且需求足够稳定,能开始构建了?(详见需求核对表。)
- 是否充分明确地定义了架构,以便开始构建?(详见架构核对表。)
- 是否已经指出你的(当前)项目中独有的风险(以避免构建活动面临不必更的风险)?