软件工程知识整理
学习指南
在阅读各章之前,先带着下面的问题链通览全文,能帮助你建立“概念→过程→方法→管理”的整体认知。
全课程问题链
- 软件到底是什么? 它和硬件、文档有什么关系?
- 为什么要用工程化方法开发软件? 软件危机、成本、风险告诉我们什么?
- 软件过程包含哪些基本活动? 瀑布、增量、螺旋、RUP、敏捷各适合什么场景?
- 需求为什么重要? 如何导出、描述、验证和管理需求?
- 怎样把需求变成设计? 建模、体系结构、面向对象设计原则起什么作用?
- 如何证明软件是对的? 测试、审查、V&V 有什么区别?
- 软件交付后怎么办? 维护、进化、再工程、重构如何降低成本?
- 项目管理管什么? 风险、成本、进度、质量如何平衡?
阅读建议
- 第一步:看框架。每章开头的“本章问题链”和“学习框架”帮你定位重点。
- 第二步:读原话。
原话和原定义引用块是 PPT 原文,必须熟悉。 - 第三步:读解释。“换句话说”把抽象定义翻译成可记忆的语言。
- 第四步:看表格。易混淆概念用表格对比记忆。
- 第五步:用附录。公式速查和概念对照用于考前快速复习。
这份笔记按 PPT 章节顺序整理,尽量保留 PPT 中的原话/原定义,再用“换句话说”做解释。目的是既覆盖原文,又帮助理解记忆。
一、软件工程概述
本章问题链
- 软件和软件产品有什么区别?通用软件和定制软件各由谁决定需求?
- 软件工程为什么关注成本?开发和维护哪个更贵?
- 软件工程的基本活动有哪些?
- 好的软件应该具备哪些属性?
- 软件工程师有哪些职业责任?
学习框架
软件工程概述 = 软件本质 + 软件成本 + 软件工程定义 + 软件过程 + 软件属性 + 职业道德一句话总结:软件工程是用系统化、可度量、可控制的方法,在有限成本下持续交付可依赖、可维护、可接受的软件。
1.1 软件与软件产品
通用软件产品 vs 定制软件产品对比:
| 维度 | 通用软件产品 | 定制软件产品 |
|---|---|---|
| 需求来源 | 软件开发者自己定义 | 客户提出 |
| 变更决定权 | 开发者 | 客户 |
| 销售方式 | 向市场/顾客销售 | 为特定客户开发 |
| 典型例子 | 图形程序、CAD、项目管理工具 | 嵌入式控制、空中交通管制 |
原话:
所有发达国家的经济都依赖于软件。越来越多的系统由软件来控制。软件工程涉及专业软件开发的理论、方法和工具。软件的支出是所有发达国家国民生产总值的一个重要部分。
软件产品:
- 通用软件产品:独立的系统一般会销售给那些想购买他们的顾客。例子:个人电脑软件,比如图形程序、项目管理工具;CAD 软件;特殊市场软件,比如牙医预约系统。
- 定制软件产品:由特定客户委托来满足自己的需求的软件。例子:嵌入式控制系统,空中交通管制软件,交通监控系统。
产品描述:
- 通用软件产品:有关软件应该做些什么的说明由软件开发者撰写,软件更改的决定也由开发者制定。
- 定制软件产品:有关软件应该做些什么的说明由客户给出,同样软件更改的决定也由客户根据自己的需求给予说明。
换句话说:软件 = 程序 + 文档。通用软件是“做好了等人买”,定制软件是“按人要求做”。
1.2 软件成本
原话:
软件成本往往主宰计算机系统成本。PC 上的软件成本往往大于硬件成本。软件维护上的成本要多于软件开发的成本。对于寿命长的系统,维护成本可能是开发成本的数倍。软件工程关注的是具有成本效益的软件开发。
原话:
从长远来看,运用软件工程方法和技术去开发软件系统比单纯为个人程序项目写程序更加便宜。对于大多数类型的系统来说,绝大多数的钱都花费在软件投入使用后对产品的变更上。
换句话说:写软件花钱,改软件花更多钱;软件工程追求的是“全生命周期成本低”。
1.3 什么是软件工程
原定义:
软件工程是一门工程学科,涉及软件生产的各个方面,从最初的系统描述一直到投入使用后的系统维护,都属于其学科范畴。
工程学科:利用合适的理论与方法来解决机构或财政状况限制内的问题。
软件生产的各个方面:软件工程不仅涉及软件开发的技术过程,也涉及诸如软件项目管理以及对那些支持软件生产的工具、方法和理论的开发等活动。
原话:
软件工程涉及专业软件开发的理论、方法和工具。
换句话说:软件工程 = 在约束条件下,用系统方法持续交付和维护可靠软件。
1.4 软件工程的重要性
原话:
个人和社会越来越多地依赖于先进的软件系统。这就需要我们能够既经济又快速地生产出可依赖和值得信赖的系统。
换句话说:好软件 = 可依赖 + 可信赖 + 经济 + 快速交付。而我们运用软件工程的方法和思想,可以更加稳定地产出好软件。
1.5 软件过程的基本活动
原话:
软件描述 – 客户和工程师定义所要生产的软件以及对其操作的一些约束。
软件开发 – 软件得以设计和编程实现。
软件有效性验证 – 软件经过检查以保证它就是客户所需要的。
软件进化 – 软件随不同的客户和变化的市场需求而进行修改。
换句话说:软件过程四部曲:做什么 → 做出来 → 验对错 → 持续改。
1.6 影响大多数软件的一般问题
原话:
- 异质性:人们越来越要求系统像基于网络的分布式系统一样运行,而网络中包含不同类型的计算机和移动设备。
- 业务和社会的变革:随着新经济成长和新技术的不断涌现,业务和社会正在发生着前所未有的快速变革。这对现有软件的变更和快速地开发出新软件提出新需求。
- 安全和可信:软件和我们生活的方方面面息息相关,最关键的一点是软件要让人们信得过。
1.7 软件工程的多样性
原话:
软件系统类型种类繁多,但是没有一个通用的软件工程方法和技术适合所有的系统。所使用的软件工程方法和工具取决于正在开发的应用程序类型、客户需求以及开发团队的背景。
常见软件系统类型:
- 独立的应用:运行在本地计算机上,拥有所有必要的功能但不需要连接到网络上。
- 交互式交易的应用:在远程计算机上执行,用户通过自己的 PC 或终端进行访问。包括 Web 应用,例如电子商务应用。
- 嵌入式控制系统:有一个软件控制系统来控制和管理硬件设备。嵌入式系统在数量上远远多于其他类型的系统。包括移动电话中使用的软件、汽车上控制防抱死的软件、微波炉上控制烹饪过程的软件。
- 批处理系统:设计用来处理大批量的数据。它们处理大量的单个输入以创建相应的输出。包括定期账单系统,比如手机账单系统和工资支付系统。
- 娱乐系统:主要是个人用户用于娱乐,大多数是各种各样的游戏。
- 建模和仿真系统:科学家和工程师用这类系统模拟物理过程和环境,这里有许多独立且相互交互的对象。
- 数据采集系统:用一些传感器从环境中采集数据并发送这些数据给其他系统进行处理。
- 集成的系统:由许多其他的软件系统所构成。
1.8 软件工程基本原则(方法)
原话:
不管怎样,还是有很多软件工程的基本原则(方法)适用于所有类型的软件系统:
- 应使用有管理的和理解了的开发过程进行开发。当然,对于不同类型的软件使用不同的开发过程。
- 可依赖性和性能对所有类型的系统来说都很重要。
- 理解和管理系统描述和需求(系统应该做的是什么)是很重要的。
- 你应当尽可能高效地使用当前存在的资源。
1.9 互联网软件工程
原话:
现在 Web 已经成为一个运行应用程序的新平台。同时,越来越多的公司更倾向于开发基于 Web 的系统,而非本地系统。Web 服务允许客户通过 Web 来访问其所需要的应用功能。云计算就是这样一种方案,软件不在本地计算机上运行,而是将它放在所谓的“计算云”里。用户只需依照使用软件的次数付费而不需要购买软件。
原话:
- 软件复用已经成为构建基于 Web 的系统的主要技术。当你在构建这样的系统时就需要考虑怎样从已有的软件组件和系统开始工作。
- 应逐步开发和交付这种基于 Web 的系统。现在人们普遍认识到,提前指定这些系统的所有需求是不切实际的。
- 用户界面受到 Web 浏览器能力的约束。尽管类似于 AJAX 这样的技术可以在浏览器内创建丰富的界面,但这些技术目前还是非常难用。用 Web 表格加本地脚本的方法更常用一些。
原话:
尽管基于 Web 的系统是复杂的分布式系统,但是之前讨论的软件工程的基本方法依然适用于基于 Web 的系统。前面一节讨论的软件工程的基本思想,同样适用于基于 Web 的软件,如同其他类型的软件一样。
换句话说:Web 开发 = 复用现有组件 + 增量交付 + 快速迭代。
1.10 专业化软件应具备的基本属性
原话:
软件产品的基本属性是可维护性、可靠性、信息安全性、效率以及可接受性。
优良软件的重要属性:
- 可维护性(Maintainability):软件必须能够进化,以适应客户需求的变化。
- 可依赖性和信息安全性(Dependability & Security):可靠、保密、安全,不能因为故障造成物理或经济损失,也不能被恶意破坏。
- 效率(Efficiency):不浪费内存和处理器资源,响应时间和处理时间合理。
- 可用性(Usability):容易学、容易用,能与其他系统配合。
- 可接受性(Acceptability):用户愿意接受并使用。
换句话说:好软件 = 可维护 + 可依赖/安全 + 高效 + 可用 + 可接受。
1.11 软件工程人员的职业道德
原话:
软件工程人员涉及更广泛的责任,而不仅仅是技术技能的应用。软件工程人员必须坚持诚实正直的行为准则。道德行为不仅仅是坚持法律,而是遵循一系列道德上正确的原则。
四项基本职业责任:
- 保密:工程人员必须严格保守雇主和客户的机密,而不管是否签署了保密协议。
- 工作能力:工程人员应该实事求是地表述自己的工作能力,不应有意接受超出自己能力的工作。
- 知识产权:工程人员应该知晓有关专利权、著作权等知识产权的地方法律,必须谨慎行事,以确保雇主和客户的知识产权受到保护。
- 计算机滥用:软件工程人员不应运用自己的技能滥用他人的计算机。滥用计算机有时对他人影响不大(如在雇主的机器上玩游戏),但有些时候后果非常严重(传播病毒)。
原话:
本行为准则包括八项基本原则,针对包括软件工程行业的从业者、教育者、管理者、监督者、政策制定者、接受培训者和学生在内的职业软件工程人员。
准则的简写版把对软件工程人员的要求做了高度抽象的概括,完整版中的条款把这些要求细化,并给出了实例,用以规范软件工程专业人员的工作方式。没有这些总体要求,所有的细节都是教条而又枯燥的;而没有这些细节,总体要求就会变成空洞的高调。只有把二者紧密结合才能形成有机的行为准则。
软件工程人员应当作出承诺,使软件的分析、描述、设计、开发、测试和维护等工作对社会有益且受人尊重。基于对公众健康、安全和福利的考虑,软件工程人员应当遵守以下 8 条原则:
ACM/IEEE-CS 软件工程职业道德和职业行为准则 8 项原则:
- 公众感:软件工程人员应始终与公众利益保持一致。
- 客户和雇主:软件工程人员应当在与公众利益保持一致的前提下,保证客户和雇主的最大利益。
- 产品:软件工程人员应当保证他们的产品及其相关附件达到尽可能高的行业标准。
- 判断力:软件工程人员应当具备公正和独立的职业判断力。
- 管理:软件工程管理者和领导者应当维护并倡导合乎道德的有关软件开发和维护的管理方法。
- 职业感:软件工程人员应当弘扬职业正义感和荣誉感,尊重社会公众利益。
- 同事:软件工程人员应当公平地对待和协助每一位同事。
- 自己:软件工程人员应当毕生学习专业知识,倡导合乎职业道德的职业活动方式。
道义困境:
- 假如你原则上不赞成公司高级管理层的决策,你该怎么办?
- 你的雇主以不道德的方式在没有完成系统测试的情况下发行了一个对安全性要求极高的系统时,你该怎么办?
- 另一个道德问题是军事项目和核项目的参与。
1.12 案例研究
案例 1:胰岛素泵控制系统
原话:
此系统的软件控制部分是一个嵌入式系统,它从传感器收集数据、然后控制泵输送指定剂量的胰岛素给患者。
其收集血糖传感器的数据并计算所需要注射胰岛素的剂量。胰岛素剂量的计算的基础是血液中葡萄糖水平的变化率。之后其向微型泵传递正确的胰岛素剂量信号。
在传统的治疗方法中,这个安全性要求极高的系统存在以下问题:短时间内的低血糖会导致暂时的脑功能障碍,最后失去知觉甚至死亡;长期处于高血糖会导致眼睛损伤、肾损伤和心脏问题。
必要的高层次需求:
- 当需要输入胰岛素的时候,这个系统可以提供胰岛素。
- 该系统应可靠地执行,并提供正确的胰岛素量,以抵消目前的血糖水平。
- 系统的设计和实现必须确保始终满足这些需求。
关注点:安全性极高,错误剂量可能危及生命。
案例 2:MHC-PMS 心理健康患者管理系统
原话:
支持心理健康治疗的的患者信息系统是一个医疗信息系统,它维护着心理疾病患者接受治疗的信息。大多数有心理问题的人不需要专门的医院治疗,但是需要定期去那些详细了解他们病情的专科诊所看医生。为了方便病人看病,这些诊所不只是在医院里,也有可能开在当地私人诊所或社区中心。
MHC-PMS(心理健康治疗-患者管理系统)是打算在诊所使用的一个信息系统。它利用一个患者信息的中心数据库,也可以运行在 PC 计算机上,这样它就可以在没有安全网络连接的地方运行。在有安全网络连接时,他们可以从数据库中获取用户信息并复制到本地,当没有网络连接时,系统可以利用下载的患者信息。
总体目标:
- 生成管理信息,使医疗卫生管理部门能够根据此评估本地和政府在此方面的执行情况。
- 为医疗服务人员提供及时的相关信息以保证对病人的治疗。
主要特征:
- 病例管理:医生可以为病人创建记录、在系统中编辑信息、查看病人历史等。系统支持数据汇总,这样某个先前未接触过病人的医生也可以快速了解此病人的主要问题和当前的治疗情况。
- 病人监测:系统定期监测那些正在接受治疗的病人的记录,若探测到可疑问题就会发出提醒。
- 管理报告:系统产生月度管理报告,显示每个诊所接治的病人数目、进入和离开护理系统的病人数目、处方药物的使用情况及其价格等。
关注点:
- 隐私:病人信息是保密的,不能暴露给除相关医护人员和病人自己之外的任何人。
- 安全:一些精神疾病可导致病人自杀或者对其他人造成人身伤害。系统应尽可能向医护人员警示潜在有自杀倾向或者危害倾向的病人。
- 有效性:系统必须是有效的,不然的话其安全性就会大打折扣,使得医生不能及时为病人拿出治疗方案。
案例 3:野外气象站系统
原话:
为了监控偏远地区的气候变化、提高气象预报的准确度,那些幅员辽阔的国家的政府会在偏远地区部署几百个气象站。气象站通过一组装置来采集气象数据,比如温度、气压、光照、降雨、风速和风向。每一个气象站都有许多采集天气数据的仪器,比如风速、风向、气温、气压、24 小时降雨量等。所有这些设备都是在软件系统的控制下周期地读入并管理所采集到的数据。
子系统:
- 气象站系统:负责收集气象数据,做一些初始处理,然后传输给数据管理系统。
- 数据管理与存档系统:从野外气象站收集数据,执行数据处理与分析,将数据存储为容易被其他子系统如天气预报系统检索的格式。
- 气象站维护系统:可以通过卫星与所有野外气象站通信,监控它们的运行状态,并报告出现的问题。
附加的软件功能:
- 监控仪器、电源、通信硬件,并向管理系统报告故障。
- 管理系统电源,确保电池在环境条件允许的情况下得到充电,也确保在恶劣天气情况下,比如大风天气,及时关闭发电机以避免受到破坏。
- 在部分软件版本更新时,或者是系统发生失败而切换备份装置时,允许动态配置。
关注点:可靠性、低功耗、远程通信。
1.13 要点
原话:
- 软件工程是一门涉及软件生产的各个方面的一门工程学科。
- 软件产品的基本属性是可维护性、可靠性、信息安全性、效率以及可接受性。
- 软件过程的活动主要有:软件描述、开发、有效验证和进化。
- 软件工程的基本概念适用于所有类型的系统开发。
- 世界上存在着很多类型的系统。每一种类型的系统的开发都需要一种与之相适应的软件工程工具和技术。
- 软件工程人员对社会负有责任,不应该只关心技术问题。
过渡:清楚了“什么是软件工程”之后,下一步就要回答“软件是怎么被做出来的”。软件过程就是软件工程的执行框架。
二、软件过程
本章问题链
- 软件过程和软件工程有什么区别?
- 计划驱动和敏捷过程的核心差异是什么?
- 瀑布、增量、螺旋、RUP 分别适用于什么场景?
- 需求工程、设计与实现、V&V、演化各自做什么?
- 变更不可避免,软件过程如何“避免”和“容忍”变更?
学习框架
软件过程 = 过程定义 → 过程模型 → 过程活动 → 应对变更一句话总结:软件过程是做软件的“流程说明书”,目标是让描述、开发、验证、演化四类活动以合适的方式组织起来。
2.1 软件过程的定义
原定义:
过程:一个用来产生某种输出有关的活动、约束和资源的步骤序列,即一组有序的任务。为建造高质量软件需要完成的任务的框架。一个过程通常涉及一组工具和技术。
原话:
软件工程和软件过程的区别:一个软件过程定义了软件开发中采用的方法,但软件工程还包含该过程中应用的技术 – 技术方法和自动化工具。更重要的一点,软件工程是有创造力、有知识的人在定义好的、成熟的软件过程框架中进行的。
原话:
软件过程是一组引发软件产品生产的活动。虽然有许多不同的软件过程,但所有软件过程都具有以下 4 种基本的活动:
- 软件描述 – 必须定义软件的功能以及软件操作上的约束。
- 软件设计与实现 – 必须生产符合描述的软件。
- 软件有效性验证 – 软件必须得到有效性验证,即确保软件是客户想要的。
- 软件进化 – 软件必须进化以满足不断变化的客户需要。
软件过程模型是软件过程的简化表示。每个过程模型都是从一个特定的侧面表现软件过程的。
原话:
当我们描述和讨论所谓的软件过程时,我们总是讨论过程中的活动(如数据模型的定义、设计用户界面等)以及这些活动的顺序。除了这些活动以外,过程描述还会包括:
- 产品,这是软件过程活动的结果。
- 角色,反映了人在软件过程活动中的职责。
- 前置和后置条件,是指在一个过程活动执行的前后或产品生产的前后,陈述语句为真。
换句话说:软件过程 = 做软件的“标准操作流程(SOP)”。(智能体里面也有这个概念,看来是从软件工程来的。)
2.2 计划驱动 vs 敏捷过程
计划驱动 vs 敏捷开发对比:
| 维度 | 计划驱动开发 | 敏捷开发 |
|---|---|---|
| 计划方式 | 提前制定详细计划 | 增量式计划,随时调整 |
| 文档 | 重视正式文档 | 最小化必要文档 |
| 客户参与 | 阶段评审时参与 | 全程参与 |
| 需求变化 | 变更成本高 | 拥抱变化 |
| 典型场景 | 大型、关键、长寿命系统 | 需求不稳定、小团队 |
原话:
计划驱动的过程是提前计划好所有的过程活动,然后按计划去考核过程的执行。在敏捷过程中,计划是增量式的,而且很容易根据不断变化的客户需求变更过程。在实践中,大多数实际软件过程包含计划驱动和敏捷方法的所有元素。没有所谓的正确和错误的软件过程。
换句话说:计划驱动 = 先规划再执行;敏捷 = 边执行边调整;实际项目常是混合体。
2.3 软件过程模型
常见软件过程模型对比:
| 模型 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 瀑布模型 | 阶段分明、顺序执行 | 文档完整、阶段清晰 | 难以应对变化 | 需求明确、低风险 |
| 增量式开发 | 分块交付、逐步完善 | 反馈早、变更成本低 | 过程不可见、结构可能退化 | 需求易变、用户参与 |
| 螺旋模型 | 风险驱动迭代 | 强调风险管理 | 进度难控、实践中少用 | 大型、高风险项目 |
| RUP | 用例驱动、迭代增量 | 体系化、工具丰富 | 复杂、缺少运维支持 | 大型面向对象项目 |
原话:
软件过程模型也叫软件开发生命周期(SDLC),是软件过程的简化表示,只从一个侧面表现软件过程。
一般过程模型描述软件过程的组成。一般过程模型实例包括瀑布模型、增量式开发、面向复用的开发。这 3 个模型相互不排斥,而且经常一起使用,尤其是对大型系统的开发。
2.3.1 瀑布模型
原话:
瀑布模型是计划驱动模型:该模型将基本的过程活动、描述、开发、有效性验证和进化,看成是一些界限分明的独立的过程阶段。由 Winston Royce 于 1970 年提出的,该模型的出现成为软件开发史上的一个重要转折点。
原话:
瀑布模型将基本的过程活动看成是界限分明的独立的过程阶段:
- 需求分析和定义
- 系统和软件设计
- 实现和单元测试
- 集成和系统测试
- 运行和维护
瀑布模型的主要缺点是当过程活动开始之后,其很难适应变化的新情况。原则上,直到上一个阶段完成,下一阶段才能启动。
原话:
它将项目生硬地分解成清晰的阶段,使得其对用户需求变更的响应较困难。因此,只有在对需求了解得好,而且在系统开发过程中不太可能发生重大改变的时候,适合采用瀑布模型。很少有系统的需求是一成不变的。瀑布模型经常用于分布式的大型系统工程项目。在这些环境下,瀑布模型的计划驱动的本性能够帮助协调各项工作。
选择瀑布模型的条件:
- 在开发时间内需求不变化或变化较少的项目;
- 分析设计人员对应用领域较熟悉的项目;
- 低风险的项目;
- 用户使用环境相对稳定的项目。
有效应用瀑布模型的策略:
尽管有许多不足,尽管有许多新的过程模型出现,瀑布模型仍是使用最广泛的过程模型。
- 微软公司:严格的里程碑制度。
- CMM(Capability Maturity Model for Software):严格的过程管理。
- 加入原型特征:允许反复。
优点:阶段清晰、文档完整、适合需求稳定的项目。
缺点:难以适应变化;问题往往在后期才暴露。
适用场景:需求明确、变化少的大型项目。
换句话说:瀑布 = 阶段顺序推进,回头代价高;适合需求明确、变化少的项目。
2.3.2 增量式开发
原定义:
该方法使得描述活动、开发活动和有效性验证活动交织在一起。可能是计划驱动或者敏捷方法。
原话:
增量式开发的优点:
- 降低了适应用户需求变更的成本。重新分析和修改文档的工作量较之瀑布模型要少很多。
- 在开发过程中更容易得到用户对于已做的开发工作的反馈意见。用户可以评价软件的现实版本,并可以看到已经实现了多少。
- 使更快地交付和部署有用的软件到客户方变成了可能。相比于瀑布模型,用户可以更早地使用软件并创造商业价值。
原话:
增量式开发的问题:
- 过程不可见。管理者需要通过经常性的可交付文档来把握进度。如果系统开发太快,要产生反映系统每个版本的文档就很不划算。
- 伴随着新的增量的添加,系统结构在逐步退化。除非投入时间和金钱用在重构系统结构上已改善软件,否则定期的变更会损坏系统的结构。
选择增量模型的条件:
- 在整个开发过程中,需求都可能发生变化,客户接受分阶段交付产品的项目;
- 分析设计人员对应用领域不熟悉的项目;
- 中高等风险项目;
- 用户可参与到整个开发过程中的项目;
- 使用面向对象语言或第四代语言开发的项目;
- 软件公司自己有较好的类库、构件库;
- 大型软件系统,开发周期较长(超过 1 年);
- 分批投资的项目。
换句话说:增量开发 = 先交付核心版本,再逐步叠加功能,边做边反馈。
2.3.3 面向复用的软件工程 / 集成与配置
原话:
面向复用的方法依赖于存在大量可复用的软件组件以及能组合这些组件的集成框架。这些组件本身就是一个系统(COTS 即商业现货系统),能提供专门的功能,例如字处理和制表软件。
复用过程的阶段:
- 组件分析
- 需求修改
- 使用复用的系统设计
- 开发和集成
目前复用已经成为建设多种商业系统的标准方法。
软件组件的类型:
- 通过标准服务开发的 Web 服务,可用于远程调用。
- 对象的集合,作为一个包和组件框架,如 .NET 或者 J2EE 等集成在一起。
- 独立的软件系统(COTS),通过配置在特定的环境下使用。
换句话说:面向复用 = 少写新代码,多拼已有组件。
2.3.4 螺旋模型
原话:
螺旋模型(Spiral Model)是 Boehm 提出来的,他从风险角度看待软件开发过程,建议螺旋模型将风险管理结合到开发活动中,以最小化风险并可控制风险。
螺旋过程:先进行需求分析和生命周期计划,包括预算、方案、约束,进行评审,然后进行风险分析,建立原型 1,明确操作方法。修正预算、方案和约束,再评审,进行风险分析,建立原型 2,明确软件需求,确认需求,制定开发计划。修正预算、方案和约束,再评审,进行风险分析,建立原型 3,进行软件设计,确认并验证设计,做集成和测试计划。修正预算、方案和约束,再评审,进行风险分析,建立原型 4,进行详细设计,编码,单元测试,系统测试,验收测试,实现计划。
原话:
它不是将软件过程用一个伴随着从一个活动到另一个活动的回溯的活动序列来表示,而是将过程用螺旋线表示。在螺旋线中,每个回路表示软件过程的一个阶段。没有像软件描述或软件设计这样特定的阶段 – 螺旋线中回路的选择是根据需求确定的。螺旋模型避免了变更风险,它假定项目的结果是有风险的,并在模型中明确地包括风险管理活动以减少开发中的风险。
四个象限:
- 目标设定:为项目的这个阶段定义专门目标。
- 风险评估和规避:每一个项目风险确定以后要进行详细的分析,并采取措施规避这些风险。
- 开发和有效性验证:在风险预估以后,就可以为系统选择开发模型,这种模型可以是任意一个普通模型。
- 规划:对项目进行评审以确定是否需要进入螺旋线的下一个环路。
优点:强调风险管理,开发大型软件时可降低风险。实际上是一个过程管理模型,而非开发模型。每一次循环可以采用不同的模型,适应风险变化。
缺点:开发进度可能难以控制。
原话:
螺旋模型已经深深地影响并帮助人们思考软件过程中的迭代,同时介绍了一种风险驱动的开发方法。然而,在实践中,该模型很少用在实际的软件开发过程中。
换句话说:螺旋模型 = 风险驱动的迭代,每一轮都评估风险再推进。
2.3.5 Boehm 的 7 条软件工程基本原理
原话:
B·W·Boehm 综合学者们的意见并总结了 TRW 公司多年开发软件的经验,于 1983 年在一篇论文中提出了软件工程的 7 条基本原理:
- 用分阶段的生命周期计划严格管理
- 坚持进行阶段评审
- 实行严格的产品控制
- 采用现代程序设计技术
- 结果应能清楚地审查
- 开发小组的人员应该少而精
(PPT 中此页标题为“7 条基本原理”,但实际只列出了 6 条;完整 7 条通常还包括“承认不断改进软件工程实践的必要性”。)
2.3.6 Rational 统一过程(RUP)
原定义:
RUP 是 Rational Unified Process 的简称。是一个面向对象且基于网络的程序开发方法论。RUP 软件开发生命周期是一个二维的软件开发模型。横轴通过时间组织,是过程展开的生命周期特征,体现开发过程的动态结构。纵轴以内容来组织,为自然的逻辑活动,体现开发过程的静态结构。
原话:
RUP 一般从 3 个视角来描述过程:
- 动态视角,给出模型中随时间所经历的各个阶段;
- 静态视角,给出所进行的过程活动;
- 实践视角,提出在过程中可以采用的良好实践的建议。
四个阶段:
- 开端(Inception):建立系统的一个业务案例。
- 细化(Elaboration):增进对问题域的理解,建立系统的体系框架,给出项目计划并识别关键项目风险。
- 构造(Construction):主要关心的是系统设计、编程和测试。
- 移交(Transition):关注如何将系统从开发单位转移到用户单位,并使之在真实环境中工作。
RUP 迭代:
- 阶段内迭代(In-phase iteration):每一个阶段可能被迭代地执行,其结果一次次增量式地得到改善。
- 阶段间迭代(Cross-phase iteration):所有阶段作为一个整体也会增量地执行。
核心工作流:业务建模、需求、分析与设计、实现、测试、部署、配置与变更管理、项目管理、环境。
原话:
现在软件产业界普遍认为,开发复杂软件项目必须采用基于 UML 的、以构架为中心、用例驱动与风险驱动相结合的迭代式增量开发过程。
最佳实践:
- 迭代地开发软件:根据客户的轻重缓急来规划系统的增量,在开发过程中先开发和交付最高优先权的系统特性。
- 对需求的管理:明确地记录客户的需求并跟踪这些需求的变更。
- 使用基于组件的(component-based)体系结构:将系统体系结构组织成组件的形态。
- 可视化地建模软件:使用图形 UML 模型表现软件的静态和动态视图。
- 检验软件质量:保证软件满足了机构质量标准。
- 控制对软件的变更:使用变更管理系统、配置管理程序和工具来管理软件的变更。
RUP 的长处:
- 提高了团队生产力。
- 在迭代的开发过程、需求管理、基于组件的体系结构、可视化软件建模、验证软件质量及控制软件变更等方面,针对所有关键的开发活动为每个开发成员提供了必要的准则、模板和工具指导,并确保全体成员共享相同的知识基础。
- 它建立了简洁和清晰的过程结构,为开发过程提供较大的通用性。
RUP 的不足:
- RUP 只是一个开发过程,并没有涵盖软件过程的全部内容。例如,它缺少关于软件运行和支持等方面的内容。
- 此外,它没有支持多项目的开发结构。
2.4 过程活动
原话:
真正的软件过程是交织着技术、写作、管理等内容的一个活动序列,围绕着一个总的目标,即实现系统的描述、设计、实现和测试。对于不同的软件开发过程,描述、开发、验证和进化这 4 个基本活动的组织是不同的。在瀑布模型中,它们是顺序组织的,而在增量式开发中它们是交织在一起的。
软件过程包含四个基本活动:
2.4.1 需求工程
原话:
软件描述或需求工程是理解和定义系统需要提供哪些服务,以及找出开发和运行中受到的约束阶段。
需求工程过程:
- 可行性研究:指明现有的软件、硬件技术能够实现用户对新系统的要求。从业务角度来决定系统开发是否划算以及在预算范围内能否开发出来。
- 需求导出和分析:这是一个通过对现有系统分析、与潜在用户和购买者讨论、进行任务分析等导出系统需求的过程。
- 需求描述:就是把在分析活动中收集的信息以文档的形式确定下来。
- 需求有效性验证:该活动检查需求的现实性、一致性和完备性。
2.4.2 设计与实现
原话:
软件开发的实现阶段是把系统描述转换成一个可运行的系统的过程。
软件设计:是对实现软件的结构、系统的数据、系统组件间的接口以及所用的算法的描述。
软件实现:将软件结构转换成可执行程序的过程。
软件设计与软件实现的活动之间是比较相近的,同时也是相互交织的。
设计活动:
- 体系结构设计:识别系统总体结构、基本组件(有时候也叫子系统或模块),它们之间的关系以及它们是怎样分布的。
- 接口设计:定义系统组件间的接口。
- 组件设计:针对每个系统组件设计它的运行方式。
- 数据库设计:设计系统数据结构,以及如何在数据库中表示这些数据结构。
2.4.3 验证与确认(V&V)
原话:
软件有效性验证,通常也称为检验和有效性验证(Verification & Validation,简称 V&V),是要看系统是否符合它的描述以及系统是否符合客户的预期。包含验证、评估过程以及系统测试。系统测试包括从用户需求定义到程序开发的每个软件过程阶段进行检查的过程。程序测试是最基本的有效性验证技术。
- Verification(验证):Are we building the product right?(是否正确地构造了软件?)
- Validation(确认):Are we building the right product?(是否构造了正确的软件?)
测试阶段:
- 开发或组件测试:每个组件都单独测试,而不受其他系统组件的影响。组件可能是简单的实体,如一个函数或对象类、或者是这些实体的一个相关集合。
- 系统测试:集成组件形成完整的系统。这个过程主要关注无法预测的组件间交互和组件间问题引发的错误。
- 接收测试:这个阶段不再是用模拟数据来测试系统,而是用客户提供的真实数据测试系统。
2.4.4 演化
原话:
软件是灵活且可变更的。随着商业环境的变化,需求也在变化,所以支持该商业的软件也必须进化并变更。虽然已经有了软件开发与软件进化的界定,但是现在完全从头开发的系统很少,将软件的开发和维护看成一个连续体显得更有意义。
2.5 应对变更
原话:
对于所有大型项目来说,变更是无法避免的。商业变化带来了新的并且变化的系统需求。当有新技术可用的时候,新的设计和实现方法就会出现。平台的变更带来了应用系统的变更。变更带来了返工,所以变更的成本包括返工的成本(比如重新分析需求)以及实现新功能的成本。
2.5.1 变更避免
原话:
变更避免 – 软件过程包括一些能够在重大返工发生之前预测变更的活动。例如,原型系统的开发,要先给客户看系统的一些重要特征。
2.5.2 变更容忍
原话:
变更容忍 – 所设计的过程使得变更以相对较低的成本得到处理。这通常需要增量开发。提出的变更可能是在还没有开发的增量上实现。假如不是这样的,只需要更改单个增量(系统的一小部分)来适应变更。
2.5.3 原型构造
原话:
原型模型(Prototype Model)由客户和开发者进行初步需求分析后,产生初步的系统规格说明书,根据规格说明书快速产生一个可运行的原型产品。向客户展示该原型产品,对客户进行培训,让客户试用,与客户一起评价规格说明书的完整性、准确性和一致性;根据评价结果,进一步修改规格说明书;根据修改过的规格说明书,对原型进行修改,产生一个新的原型,让客户试用,再与客户一起评价该原型……这一过程可重复多次,直到最终的规格说明演化成与用户和客户一致的、可执行的系统为止。
原型模型的总体目标:降低开发的风险和不确定因素。
原型法的分类:
-
抛弃式模型(Throwaway Model):
- 由 H. Gomaa 于 1981 年提出的,该模型用于确认需求和设计,而不是设计实际可用的系统。
- 首先制定一个开发计划,然后进行需求分析、设计、编码、测试,由用户或客户评价,如果不满意再返回需求分析部分或设计部分或编码部分,直到满意为止。
- 该模型开发的结果不是为最后使用,而是为了理解需求和开发方法,验证那些模糊的、没有把握的需求。任务完成后该模型被抛弃,真正的系统开发将依据该模型的结果从头开始。
- 应该丢弃该原型的情况:不可能调整原型以满足非功能性的要求;开发过程中的快速变更必然意味着原型是没有文档的;原型开发过程中的变更可能会破坏系统的结构;在原型开发中机构的质量标准是被放松了的。
-
进化式模型(Evolutionary Model):
- 首先设计一个全局性框架,然后建立一个初始的稳定模型,运行该原型,并反复添加、修改和完善,逐步向目标系统进化。每一次进化,都产生一个逼近目标的新版本。
- 该模型在进化过程中,始终强调可靠性、可维护性、适应性和效率等软件质量问题。
- 该模型的最终成果就是投入使用的系统。
- 优点:客户可跟踪开发过程,及时了解并建议修改开发的过程,最终系统能令用户满意。
- 缺点:开发进度和成本难以控制。
原话:
原型是一个软件系统的最初版本,用于验证概念、试用设计选项、发现更多的问题和可能的解决方法。系统原型可用来:
- 在需求工程过程中,原型有助于启发和验证系统需求;
- 在系统设计过程中,原型可用于探索特定软件的解决方法,并支持用户接口设计;
- 在系统测试过程中,原型可用于连续两次测试。
原型构造的好处:
- 提升系统的可用性
- 逼近用户的真实需求
- 提升设计质量
- 提升可维护性
- 降低开发工作量
原型开发注意事项:
- 可能基于快速构建语言或工具;
- 可能会遗漏功能;
- 原型应该关注产品的那些不好理解的方面;
- 错误检查和恢复可能不包含在系统原型中;
- 应该关注功能性需求而不是非功能性需求(比如可靠性、安全性等)。
选择原型模型的条件:
- 用户定义了软件的一组一般性目标,但不能标识出详细的输入、处理及输出需求;
- 开发者可能不能确定算法的有效性、操作系统的适应性或人机交互的形式;
- 投标时向客户展示产品的框架;
- 为对于不熟悉的领域或行业开发项目。
2.5.4 增量式交付
原话:
增量式开发是将开发和交付分解成若干个增量来逐步交付的,而不是将系统一次性交付。对增量中服务的分配取决于服务优先次序,具有最高优先权的服务首先被交付。一旦系统增量开始开发,对目前增量需求的变更就不会被接受,直至该增量的开发结束。
增量开发与增量交付的区别:
- 增量开发:以增量的方式开发系统,并且在开发另外一个增量之前评估该增量的质量;该方法可用于敏捷开发中;评估工作由用户/客户代理完成。
- 增量交付:部署增量以供最终用户使用;对实际中利用较多的部分实施更多的现实测试;因为增量比替代系统具有更少的功能,所以很难实现替代系统。
增量式交付的优点:
- 客户无需等到整个系统的实现才能从中获益(获益早)。
- 客户可以将早期的增量作为原型,从中获取对后面系统增量的需求经验(取经)。
- 在系统的最重要的部分,客户不太可能遇到失败(核心不易失败)。
- 这就使得最重要的系统服务肯定接受了最多的测试(重要服务测试多)。
增量式交付存在的问题:
- 难以确定全体增量所需的公共设施。大多数系统需要一组基本设施,这些基本设施会被系统不同部分所使用。因为需求的详细定义是当增量将要被开发时才完成的,所以就很难确定全体增量所需要的公共设施。
- 替换系统时,难以迭代开发。软件描述和软件本身一起开发是迭代过程的本质。
- 与大型机构签订匹配的合同有困难。与许多机构的采购模型是相冲突的,因为完整的系统描述往往是系统开发合同的一部分。
2.6 要点
原话:
- 软件过程是产生一个软件系统的一系列活动。软件过程模型是这些过程的抽象表示。
- 需求工程是开发软件描述的过程。描述的目的是向开发者传达客户方对系统的需要。
- 设计和实现过程是将需求描述转换为一个可运行的软件系统的过程。
- 软件有效性验证是检查系统是否与它的描述相一致,以及是否符合系统用户的真正需要的过程。
- 软件进化是修改已存在的软件系统以适应用户新的需求的过程。变更是一个持续的过程,软件必须在变更过程中保持可用。
- 过程应包含应对变更的活动。这可能包含一个原型构造阶段,以帮助避免在需求和设计上的错误决定。过程应该适应迭代开发和交付,这样变更时就不会对整个系统带来干扰。
- Rational 统一过程是新式基本过程模型,其特点是由阶段(开端、细化、构造和转换)所构成,但是它把活动(需求、分析和设计等)和阶段相区别。
过渡:传统软件过程强调计划和文档,但当需求变化快、市场不确定时,就需要更轻量、更灵活的过程。敏捷方法应运而生。
三、敏捷软件开发
本章问题链
- 敏捷方法为什么会出现?它反对什么、保留什么?
- 敏捷宣言的四组价值观是什么?
- XP 的核心实践有哪些?Scrum 的三个阶段是什么?
- XP 和 Scrum 有什么区别?
- 敏捷方法适用于所有项目吗?扩展时会遇到什么问题?
学习框架
敏捷开发 = 敏捷宣言 → XP(工程实践) + Scrum(管理框架) → 适用性与扩展一句话总结:敏捷不是“快”,而是“快速获得反馈、快速适应变化”,用小步快跑和持续交付降低风险。
3.1 快速软件开发
原话:
目前,快速的软件开发和交付已成为软件系统最为关键的需求。业务运行于变化的环境中,几乎不能导出完全和稳定的软件需求。软件必须快速响应,迎合系统的需求变化。
快速软件开发:
- 描述、设计和实现过程是交织在一起的。
- 系统通过一系列版本开发出来。
- 系统用户界面通常是采用 IDE 或图形化工具集进行开发的。
3.2 敏捷方法
原话:
对重量级开发方法的不满致使众多软件人员在 20 世纪 90 年代提出了敏捷软件开发方法。将精力集中于软件本身而不是设计文档。依赖迭代的方法完成软件描述、开发和交付。旨在快速完成、快速交付,及时响应客户的需求变化。敏捷方法的目标是减少开发过程中的繁琐多余的部分,快速响应需求的变化,避免过度的返工。
3.3 敏捷宣言
原话:
我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。由此我们建立了如下价值观:
- 个体和互动 高于 流程和工具
- 工作的软件 高于 详尽的文档
- 客户合作 高于 合同谈判
- 响应变化 高于 遵循计划
也就是说,尽管右项有其价值,我们更重视左项的价值。
换句话说:敏捷宣言 = 右项有价值,但左项更重要:人 > 流程,可运行软件 > 文档,合作 > 合同,响应变化 > 遵循计划。
3.4 极限编程(XP)
原话:
极限编程(Extreme Programming,简称 XP)也许是最为熟知也是流行最广的一种敏捷方法。极限编程采用一种“极限”的方式进行迭代开发:一天之内建立多个新版本;每两周向客户提供一个增量(周期 1~2 周);每次发布前都进行测试,测试通过才能发布。
核心价值观:沟通(Communication)、简单(Simplicity)、反馈(Feedback)、勇气(Courage)。
XP 与敏捷原则:
- 增量式开发,即通过系统小的频繁的版本来支持。
- 客户全程参与到开发团队。
- 注重人、而不是过程,采用结对编程、集体拥有代码等方式,不鼓励加班。
- 通过经常性的发布版本来支持变更。
- 通过不断的重构来维护简单性。
核心实践:
- 增量式规划
- 小型发布
- 简单设计
- 测试优先开发
- 重构
- 结对编程
- 代码集体所有权
- 持续集成
- 可持续的开发速度
- 现场客户
3.4.1 XP 与变化
原话:
传统软件工程认为必须为变更而设计,应该预见软件的未来变更,面向变更设计以便变更更容易实现。极限编程认为为变更而设计太浪费时间,所预见的变更通常不发生,未预见的变更却层出不穷。XP 只接受即将发生的变更,通过不断重构代码,在变更发生时重组软件。
3.4.2 重构
原话:
开发团队寻找可能的改进,并马上改善。重构提高代码的可读性,降低对文档的需求。代码结构清晰,容易修改。但是,某些变更在代码级不能解决,就需要进行架构级的重构,这样成本很高。
重构的例子:
- 重组类的继承关系,消除重复代码。
- 整理和重命名属性和方法,使它们更易理解。
- 调用程序库中的方法替换代码,简化设计。
3.4.3 需求场景(用户故事)
原话:
在 XP 中,客户或用户是 XP 团队的一部分,负责对需求做出决策。用户需求表示为场景或用户故事。这些都写在卡上,开发团队将它们分解为实现任务。这些任务是时间表和成本估算的基础。客户根据其优先级和时间表估计选择要包含在下一个版本中的故事。
3.4.4 测试优先开发
原话:
在代码前写测试阐明了要实现的要求。测试被写为程序而不是数据,以便它们可以自动执行。测试包括检查它是否正确执行。通常借助于测试框架,如 JUnit。添加新功能时,将自动运行所有先前的和新的测试,从而检查新功能是否未引入错误。
XP 测试特性:
- 测试优先的开发;
- 来自脚本的增量式测试开发;
- 用户参与测试开发和有效性验证;
- 自动测试系统的应用。
原话:
所谓测试自动化,就是在任务实现之前就将测试写成了可执行的组件。这些测试组件应该是独立的,应该模拟待测试的输入提交,而且应该检查结果是否满足了输出描述。自动测试框架(如 JUnit)是这样一个系统,它使编写可执行测试程序和提交测试集变得容易。由于测试是自动的,我们就总会有一个能快速且容易执行的测试集合。这意味着无论什么时候有功能添加进来,测试都可以执行,由新代码所引入的问题能够马上被查出来。
客户参与:
客户在测试过程中的作用是:为将要在系统的下一个版本中实现的故事情节开发接收测试。当开发继续进行时,作为团队一部分的客户进行测试。因此,所有的代码都是经过验证的,确保是满足顾客需要的。
接受客户角色的人可能只有有限的时间,不太可能以全时方式与开发团队一起工作。客户可能感觉到提供了需求已经尽到了义务,所以也就不太情愿投入到测试过程当中。
XP 测试难点:
- 程序员更喜欢编程而不是测试,有时在写测试时会走捷径。例如:写出不完整的测试,无法检查所有可能的异常情况。
- 有一些测试是非常难写的。例如,在复杂用户界面中,通常编写用于实现“显示逻辑”和屏幕间工作流的单元测试是十分困难的。
- 对一组测试的完整性的判断也是困难的。尽管你可以有很多系统测试,但是你的测试集合可能不能提供完整的覆盖。
换句话说:TDD = 先写测试 → 再写代码 → 跑通 → 重构。
3.4.5 结对编程
原话:
在 XP 中,程序员成对工作,坐在一起开发代码。这有助于开发共同所有权的代码,并在整个团队中传播知识。它担当了一个非正式的复查过程,因为每一行代码至少经过两个人的批阅。它鼓励重构,同时整个团队都可以从中受益。统计表明,结对编程的开发生产力类似于两个独立工作的人的开发生产力。
结对编程的优点:
- 支持共同拥有软件和共同对系统负责。
- 个人不对代码中所出现的问题负责。相反,团队有解决这些问题的集体责任。
- 担当了一个非正式的复查过程,因为每一行代码至少经过两个人的批阅。
- 有助于支持重构,是一个软件改善的过程。
- 结对编程和集体所有的做法,使得其他成员马上就能从重构中收益,所以他们也比较乐意支持这个过程。
换句话说:结对编程 = 两人一桌,一人写一人看,实时审代码、共担责任。
3.5 敏捷项目管理(Scrum)
原话:
项目经理的基本职能是项目管理,确保软件在预算范围内按时交付。项目管理的标准(传统)方法是计划驱动,它要求项目经理对项目每件事情有稳定的看法。经理制定一个计划,包括该交付什么,何时交付以及谁来完成交付。敏捷方法需求开发是增量式的,短期内快速交付增量,经常有需求和软件变更。
原定义:
Scrum 方法是一个通用的敏捷方法,但它主要是注重迭代开发的管理,而不是管理敏捷软件工程的专门技术方法。它提供了项目管理的框架。
三个阶段:
- 规划纲要阶段:建立大致的项目目标和设计软件体系结构。
- 冲刺循环:每个循环开发出一个系统增量。
- 项目结束阶段:总结项目、完善文档。
Sprint:
- 固定长度(通常 2–4 周),在 XP 中对应于一个系统版本的开发。
- 规划的起点是所谓的 backlog(积压的任务),是项目中要完成的工作清单。
- 在选择阶段,项目团队的所有人员都要参加,和用户一起选择在冲刺循环中要开发的特性和功能。
- 一旦这些都得到同意,团队将组织进行软件开发。这个阶段,开发团队是隔离于客户和机构的,所有的交流都通过所谓的“Scrum master”进行。
- Scrum master 使开发团队免受外界干扰。
- 冲刺循环的结束,对已做工作复查并交付给用户。接着,下一个循环就开始了。
冲刺循环中的团队:
- Scrum master 是一个调解人,安排每天的会议、跟踪日志上要做的工作,记录决定、衡量工作进展情况、与客户和团队外的管理者进行沟通。
- 整个团队必须出席每天的会议,有时是“站立”着开会的,以使时间短且重点明确。会上,团队成员们共享信息,说明工作进度,遇到的问题及接下来一天的工作计划。
- 这就意味着每一个团队成员都知道接下来干什么,假如出现问题,也可以重新做短期工作计划。
Scrum 的好处:
- 产品被分解成一组可管理和被理解的块。
- 不稳定的需求并不阻碍工程进展。
- 整个团队的所有事情都是可见的,因此改善了团队的沟通。
- 用户看到增量的按时交付,且得到对于产品如何工作的反馈。
- 客户和开发者之间彼此信任,创造了积极的文化,每个人都希望项目成功。
3.6 XP 与 Scrum 的区别
- 迭代长度不同:XP 的一个 Sprint 的迭代长度大致为 1
2 周,而 Scrum 的迭代长度一般为 24 周。 - 是否允许修改需求:XP 在一个迭代中,如果一个 User Story 还没有实现,则可以考虑用另外的需求将其替换,替换的原则是需求实现的时间量是相等的。而 Scrum 是不允许这样做的,一旦迭代开工会完毕,任何需求都不允许添加进来。
- User Story 是否严格按照优先级别来实现:XP 是务必要遵守优先级别的。但 Scrum 在这点做得很灵活,可以不按照优先级别来做。
- 工程方法严格程度:Scrum 没有对软件的整个实施过程开出工程实践的处方,要求开发者自觉保证。但 XP 对整个流程方法定义非常严格,规定需要采用测试驱动开发 TDD、自动测试、结对编程、简单设计、重构等约束团队的行为。
3.7 用户故事
原话:
关于用户故事,Ron Jeffries 用 3 个 C 来描述:
- 卡片(Card):用户故事一般写在小的记事卡片上。卡片上可能会写上故事的简短描述,工作量估算等。
- 交谈(Conversation):用户故事背后的细节来源于和客户或者产品负责人的交流沟通。
- 确认(Confirmation):通过验收测试确认用户故事被正确完成。
用户故事示例(饮料自动售货机):
假定这个项目的客户是个饮料自动售货机的制造商。他们要求我们为他们的售货机开发一款软件。我们的客户就是他们的市场经理。谈需求的时候,他这样说:“用户往售货机每塞一个硬币,售货机都要显示当前该客户已经投了多少钱。当用户投的钱够买某一款饮料时,代表这款饮料的按钮的灯就会亮。如果那个用户按了这个按钮,售货机就放一罐饮料到出口,然后找零钱给他。”
名称:卖饮料
事件:
- 用户投入一些钱。
- 售货机显示用户已经投了多少钱。
- 如果投入的钱足够买某种饮料,这种饮料对应的按钮的灯就会亮。
- 用户按了某个亮了的按钮。
- 售货机卖出一罐饮料给他。
- 售货机找零钱给他。
3.8 可扩展的敏捷方法
原话:
敏捷方法的开发是为在同一个房间办公与交流的小开发团队使用的。有时人们认为,这些方法的成功是因为增进了沟通,因为当所有人都在一起工作的话,很方便沟通。可扩展的敏捷方法涉及到更改这些方法以应对更大、更长的项目,其中有多个开发团队,并且可能在不同的地域。
大型软件系统开发的特点:
- 大型系统经常是由独立的、交互的子系统组成。不同的团队独立开发不同的子系统。通常,这些团队在不同的地点甚至不同的时区工作。
- 大型系统是“brownfield systems”,包含了一系列已存在的系统并与他们进行交互。许多系统需要关注这种交互,因此不是真的适应于灵活性和增量式开发。
- 当一个系统是由多个系统集成产生时,开发工作中的一个重要部分就是系统配置而不是原始代码的开发。
- 大型系统和它们的开发流程通常受制于外部规则和规章限制,如要求写各种文档。
- 大型系统有很长的采购和开发时间。很难保持团队在整个周期中保持对系统有连贯的认识。
- 大型系统通常具有不同的信息持有者。实际上让所有这些不同的信息持有者参与软件开发过程中是不现实的。
Scaling up vs Scaling out:
- Scaling up(照搬放大):关注如何将这些方法应用到那些小团队无法开发的大型项目中。
- Scaling out(渗透):关注如何将敏捷方法介绍推广到拥有多年开发经验的大机构中。
在扩展敏捷方法时,保留敏捷方法的基本内涵是非常重要的:弹性计划、频繁发布、持续集成、测试驱动开发、良好的团队沟通。
照搬放大到大型系统:
- 对于大型系统,不可能只关注系统的代码。你需要做更多预先的设计和系统文档。
- 跨团队沟通机制必须建立和使用。这包括团队间常规的电话和视频会议,团队间用于更新各自进度的频繁短暂的电子会议。
- 持续集成,当系统是由很多个程序模块组成时,由于任何一个时刻都会有程序员正在处理一个变更,所以持续的集成在实际中是不可行的。但是,保持频繁的系统构建和定期的发布系统是十分关键的。
渗透到大型机构:
- 没有敏捷开发经验的项目经理可能不愿意接受新方法带来的风险。
- 大型公司通常有质量保证流程和标准,每一个项目都需要去遵循,他们的官僚性质,很可能跟敏捷方法时不兼容的。
- 当团队成员技术都比较高时,敏捷方法似乎能工作得更好。然而,在大型机构中,员工的技术和能力很可能是参差不齐的。
- 可能从文化上抵制敏捷开发,特别是那些长久使用传统软件工程流程的机构。
3.9 敏捷方法的适用性与问题
适用性:
- 软件公司开发并准备出售的是一个小型或中型的产品。
- 机构内部的定制系统开发。客户会有一个关于参与到开发过程中去的明确承诺,且没有许多来自外部的规章和法律的影响。
- 因为敏捷方法致力于小的、紧密集合的团队,将其用于大型系统中会产生许多问题。
敏捷方法的问题:
- 客户代表难以全身心的投入到软件开发中。
- 团队成员不太适应大强度的投入,而这正是敏捷方法的特征。
- 对于那些拥有多个信息持有者的系统,做出优先级变更是很困难的。
- 维持简单性需要额外的工作。
- 与许多机构(大公司)的公司文化不符。
3.10 计划驱动开发与敏捷开发
计划驱动开发:
识别软件过程中的每个阶段及其相关输出。前一个阶段的输出作为下一个阶段的基础。计划驱动不一定是瀑布模型,它可以支持增量式开发和交付。在计划驱动的方法中,迭代发生在各个活动之中,用正式文档沟通。
敏捷开发:
规范、设计、实现和测试是交错的,设计和实现是核心。迭代发生在所有活动之间,需求与设计同步进行。开发过程要产生的结果通过和用户的沟通决定。敏捷开发也可以产生一些设计文档。
3.11 技术、人员和机构方面的问题
原话:
大多数软件工程都包括计划驱动和敏捷开发的实践。要在二者间达到平衡,要考虑以下问题:
- 在实现之前,有非常详细的描述和设计很重要吗?假如是,你可能需要用计划驱动的方法。
- 增量交付策略,即软件交付给用户并快速地取得反馈,切实吗?假如是,考虑使用敏捷方法。
- 开发的系统有多大?敏捷方法对于小的、处于同一地点的开发团队来说大多是有效的,这种团队的交流往往是非正式的。可能不适用于需要大的开发团队的大型系统,这种系统可能要用计划驱动的方法。
- 开发系统类型是什么?如果是需要大量分析的系统(如复杂时序需求的实时系统),通常采用计划驱动。
- 预想的系统寿命有多长?长寿命系统采用计划驱动。其需要通过更多的设计文档来交流系统开发者的最初意向,以支持团队工作。
- 有什么样的技术来支持开发?敏捷依赖好的工具,以跟踪设计进化。有功能强大的 IDE 可采用敏捷开发。
- 开发团队如何组织?如果团队分散或部分功能外包,那么就需要开发设计文档,以在团队之间进行沟通。采用计划驱动。
- 有没有影响系统开发的文化或企业问题?传统的工程机构有计划驱动开发的文化。
- 开发团队的设计人员和编码人员的能力如何?有时敏捷方法需要比计划驱动的方法更高的技术水平。
- 系统是否受制于外部的法规?假如系统有其他外部的法规限制,可能就需要有详细的文档,作为系统安全案例的一部分。
3.12 敏捷方法与软件维护
原话:
大多数组织在软件维护方面比开发新软件投入更大,敏捷方法要想成功必须能够同时很好的支持软件的开发与维护。两个关键问题:
- 由于开发过程中强调文档的最小化,用敏捷技术开发的系统是否可维护?
- 敏捷方法是否能有效地用于进化系统以响应用户的变更请求?
如果原始的开发团队解散,会带来问题。
3.13 要点
原话:
- 敏捷方法是一种专注于快速开发的增量式开发,频繁地发布软件、降低过程开销、生产高质量的代码。他们使客户直接地参与到开发过程中。
- 决断是否是使用敏捷或计划驱动的方法取决于所开发系统的类型、开发团队的能力和开发系统的公司的文化。
- 极限编程是一种著名的敏捷方法,它集成了一系列好的编程经验,例如频繁的软件发布,连续软件改善和客户参与到软件开发团队。
- 极限编程的一个特别长处是在创建程序特征之前开发自动测试。在增量集成进系统的时候所有的测试必须成功执行。
- Scrum 方法是一种提供项目管理框架的敏捷方法。它的核心是一组冲刺循环,开发一个系统增量是有固定的时间周期的。
- 伸缩的敏捷方法对于大型系统是很困难的。大型系统需要前期设计和一些文档。
过渡:无论采用瀑布还是敏捷,软件过程的第一步都是搞清楚“做什么”。需求工程是整个软件工程的基础,需求错误会导致后期代价成倍增长。
四、需求工程
本章问题链
- 什么是需求?用户需求和系统需求有什么区别?
- 功能需求、非功能需求、领域需求各自约束什么?
- 需求应该如何描述?自然语言和结构化描述各有什么优缺点?
- 需求工程过程包含哪些活动?
- 需求变更如何管理?
学习框架
需求工程 = 需求获取 → 需求分析 → 需求规格说明 → 需求验证 → 需求管理一句话总结:需求工程是“把用户真正想要的东西说清楚、写明白、管得住”的过程。
4.1 什么是需求
原定义:
系统需求是系统应提供的服务和所受到的约束的描述。这些需求反映了客户对系统帮助其解决某些问题的需要。对服务和约束的发现、分析、建立文档、检验的过程叫做需求工程。
原话:
有时,需求被视为对系统应该提供的服务或对系统的约束的一个高层抽象描述。有时,需求又被定义为对系统功能的详细的、用数学方法的形式化描述。这是不可避免的,因为需求可以服务于双重功能:
- 可能是投标合同的基础 - 因此必须开放解释;
- 可能是合同本身的基础 - 因此必须详细定义;
这两种说法都可以称为需求。
需求抽象(Davis):
如果一家公司要与另一机构签订一个大型软件开发项目合同,那这家公司就要尽量概要地定义对项目的要求,而且在这样的描述中不应限制解决方案。这时候,就需要一个文本形式的需求以便多个承包商竞标,或者以不同方式满足客户的机构需求。一旦签订了合同,承包商就要为客户写出更详细的系统定义,要让用户能看懂并要再次确认系统到底需要提供哪些服务。这两种文档都被称为需求文档。
4.2 用户需求 vs 系统需求
原话:
| 类型 | 说明 |
|---|---|
| 用户需求 | 表达高层的概要需求。是用自然语言加图的形式给出的、关于系统需要提供哪些服务以及系统操作受到哪些约束的声明。 |
| 系统需求 | 表达系统应该提供哪些服务的详细描述。详细地给出系统将要提供的服务以及系统所受到的约束。 |
需求文档的用户:
- 系统客户:指定需求,检查需求描述是否满足需要,指定对需求的变更。
- 管理者:用需求文档来计划对系统的投标和计划对系统的开发过程。
- 系统工程师:通过需求来了解系统将要开发哪些内容。
- 系统测试工程师:使用需求来开发系统的有效性验证测试。
- 系统维护工程师:使用需求来帮助理解系统及其中的各个部分间的关系。
4.3 需求类型
三种需求类型对比:
| 类型 | 关注点 | 例子 | 未满足的后果 |
|---|---|---|---|
| 功能需求 | 系统应提供的服务 | 搜索预约单、生成病人名单 | 功能缺失 |
| 非功能需求 | 对服务或整体的约束 | 响应时间、安全性、可用性 | 系统可能无法使用 |
| 领域需求 | 应用领域施加的约束 | 列车刹车减速度计算 | 系统不能正常运行 |
原话:
- 功能需求:包括对系统应该提供的服务、如何对特殊输入做出反应,以及系统在特定条件下的行为的描述。在某些情况下,功能需求可能还需声明系统不应该做什么。
- 非功能需求:对系统提供的服务或功能给出的约束。包括时间约束、开发过程的约束和所受到的标准的约束。非功能需求经常适用于整个系统而不是个别的系统功能或服务。
- 领域需求:来自运行领域对系统的约束。
4.3.1 功能需求
原话:
功能需求描述系统所提供的功能或服务。包括对系统应该提供的服务、如何对特殊输入做出反应,以及系统在特定条件下的行为的描述。在某些情况下,功能需求可能还需声明系统不应该做什么。功能需求取决于开发的软件类型、软件潜在的用户,以及机构在写需求时所采用的一般方法。
如果是用户需求,就要用可以被用户理解的一种抽象方法来描述功能需求。更具体的功能性系统需求则需要详细地描述系统功能、输入和输出、异常等。
MHC-PMS 系统的功能需求示例:
- 用户能够搜索到所有诊所的预约挂号单。
- 系统每天能够为每个诊所生成一份想预约在那天就诊的病人名单。
- 每位使用该系统的职员都可以通过他的 8 位雇员号码被唯一识别。
需求不精确:
当需求没有精确性陈述时,问题就会产生。模糊的需求可能由开发人员和用户以不同的方式解释。在功能需求中考虑术语“搜索”:用户意图 – 在所有诊所的预约单中查找该姓名;开发者解释 – 在个别诊所中搜索该患者姓名。用户选择诊所,然后搜索。
需求完整性和一致性:
- 完备:用户所需的所有服务都应该给出描述。
- 一致:需求描述不能前后矛盾。
- 在实际中,对大型而又复杂的系统而言,要做到描述既完备又一致几乎是不可能的。
4.3.2 非功能需求
原话:
所谓非功能需求,是指那些不直接关系到系统向用户提供的具体服务的一类需求。它们与系统的总体特性相关,如可靠性、响应时间和存储空间。换言之,它们对系统的实现定义了约束,如 I/O 设备的能力、与其他系统接口的数据的表示等。
非功能性需求通常会比个别的功能性需求更加关键。然而,如果一个非功能性需求没有满足,则可能使整个系统无法使用。例如,飞机系统、实时控制系统。
非功能需求实现:
识别哪个系统组件实现特别的功能性需求比较容易,但是把这些组件与非功能性需求联系起来却是相当的困难。非功能性需求会影响这个系统的体系结构,而不是个别的组件。例如,为了保证系统的性能需求,就必须合理组织系统使得组件之间的通信量达到最小。单个的非功能性需求,比如一个信息安全性要求,可能会产生数个相关的功能性需求,这些功能性需求定义了新系统所要求的服务。它还可以生成限制现有需求的需求。
非功能需求分类:
| 类型 | 说明 | 例子 |
|---|---|---|
| 产品需求 | 定义或约束软件的行为 | 系统执行速度、内存消耗、信息安全性、可用性等 |
| 机构需求 | 起源于客户/开发者所在的机构的政策和规定 | 过程标准、实现要求、交付需求等 |
| 外部需求 | 来自于系统外部因素和开发过程的需求 | 监管需求、立法需求、道德要求等 |
目标与可测试的非功能需求:
- 目标:用户的基本意图 — 易用性、恢复性、快速反应能力。
- 可测试的非功能需求:应该使非功能需求得以量化,从而使其验证更客观。目标有助于开发人员传达系统用户的意图。
可用性需求举例:
- 目标:系统对医务工作人员来说应该是容易使用的,并且以一种用户错误最小的方式来管理系统。
- 可测试:医务工作人员应该能够在 4 小时的培训后学会使用系统的所有功能。在培训后有经验的用户在系统的使用中所犯的错误不能超过 2 个小时。
4.3.3 领域需求
原话:
领域需求是系统实际运行的领域强加给系统的需求。例如,列车控制系统所必须考虑的在各种天气下的刹车特点。领域需求可能本身就是新的系统需求,也可能是约束现有的功能需求,或者是特别的计算必须如何进行的规定。如果领域需求未被满足,系统将不能正常运行。
领域需求存在的问题:
- 难懂:需求通常以应用程序领域的语言来表示;开发系统的软件工程师通常不理解这一点。
- 晦涩(Implicitness):领域专家非常了解该领域,以至于他们不想明确地制定领域需求。
火车保护系统领域需求示例:
列车的减速度应计算为:Dtrain = Dcontrol + Dgradient,其中 Dgradient 为 9.81ms² × compensated gradient / alpha,9.81ms² / alpha 的值可以用于不同类型的火车。非专家很难理解这一点的影响以及它如何与其他要求相互作用。
4.4 非功能需求的度量
| 属性 | 度量方式 |
|---|---|
| 速度 | 事务数/秒、响应时间、屏幕刷新时间 |
| 大小 | Mbytes、ROM 芯片数 |
| 易用性 | 培训时间、帮助页数 |
| 可靠性 | 平均无故障时间(MTTF)、可用性、故障频率 |
| 健壮性 | 重启时间、事件导致故障的比例、数据损坏概率 |
| 可移植性 | 目标相关语句比例、目标系统数量 |
4.5 软件需求文档(SRS)
原定义:
软件需求文档(SRS)是对系统开发者需要实现什么的正式陈述。它应该包含系统的用户需求和一个详细的系统需求描述。这个不是设计文档。所以它应该尽可能多的说明系统做什么,而不是系统如何做。
敏捷方法和需求:
许多敏捷方法认为,生成需求文档是浪费时间,因为需求变化如此之快。因此,文档始终过时。极限编程 XP 等方法使用增量需求工程和表达要求作为“用户故事”。这对于业务系统是实用的,但对于需要许多预交付分析的系统(例如关键系统)或由几个团队开发的系统而言是有问题的。
需求文档的灵活性:
- 需求文档的内容与系统的类型和使用的开发方法有关。
- 增量开发的系统通常在需求文档中具有较少的细节。
- 需求文档标准已经被设计好,如 IEEE 标准。这些标准主要适用于大型系统工程项目的需求。
典型结构:
- 前言
- 引言
- 术语表
- 用户需求定义
- 系统架构
- 系统需求规格说明
- 系统模型
- 系统演化
- 附录
- 索引
4.6 需求描述方式
原话:
需求描述就是在需求文档中写下用户需求和系统需求。用户需求必须让那些不具备专业技术方面知识的用户看懂。系统需求是更详细的要求,可能包括更多的技术信息。这些需求可能是系统开发合同的一部分。因此,需求尽可能的完整是非常重要的。
需求与设计:
原则上,需求应该说明系统应该做什么,设计应该描述它是如何做到的。在实践中,要求和设计是不可分割的:一个系统结构的设计可能用来结构化需求;系统可以与产生设计需求的其他系统互操作;使用特定架构来满足非功能性需求可能是一个领域需求。这可能是监管需求的结果。
4.6.1 自然语言描述
原话:
需求被写成自然语言句子,并由图表和表格补充。用于写作要求,因为它是有表现力的,直观和普遍的。这意味着用户和客户可以理解这些需求。
需求书写准则:
- 设计一个标准格式,并保证所用的需求定义都遵循此格式书写。
- 使用一致的语言来区分强制性需求和可选性需求。
- 对文本加亮(粗体、斜体、颜色)来突出显示关键性需求。
- 避免使用专业术语和缩写词。
- 包括为什么需求是必要的解释(理由)。
自然语言描述的问题:
- 缺乏清晰度:实现不使文档难以阅读、达到精确的目标是困难的。
- 需求混乱:功能和非功能性需求往往混杂在一起。
- 需求合并:几个不同的需求可以一起表达。
4.6.2 结构化描述
原话:
结构化自然语言是书写系统需求时的一种方法,需求的作者的自由受到限制,所有的需求都要以一种标准方式来书写。对于某些类型的需求,该方法工作得很好。比如,对嵌入式控制系统的需求。但有时对编写业务系统的需求则过于死板或严格。
标准格式描述:
- 关于所定义的功能或实体的描述。
- 关于输入及输入来源的描述。
- 关于输出及输出去向的描述。
- 关于计算所需要的信息以及系统中所使用的其他实体的信息(是“必须”的那一部分)。
- 关于所采取的行动的描述。
- 前置和后置条件(如果适用)。
- 关于操作的副作用(如果有的话)的描述。
4.6.3 表格描述
原话:
表格描述用于补充自然语言。在有多个可能情形时,表格描述尤其有用。例如,胰岛素泵系统根据血糖水平的变化速率来计算胰岛素需求量。表格方法会给出不同情形下胰岛素需求量是如何计算的。
4.6.4 图形符号
UML 用例图、顺序图等。
4.6.5 数学描述
形式化规格说明。
4.7 需求工程过程
原话:
用于需求工程(RE)的过程根据应用领域、所涉及的人员以及开发需求的组织而广泛变化。然而,所有过程都有一些共同的通用活动:需求导出、需求分析、需求验证、需求管理。在实践中,需求工程 RE 是一个活动相互交错的迭代过程。
原话:
需求工程过程包括可行性研究、需求导出和分析、需求描述、需求有效性验证和需求管理。
主要活动:
-
需求导出(Elicitation):
- 有时称为需求启发或需求发现。
- 让与客户一起工作的技术人员找出所开发系统的应用领域、系统应提供的服务和系统的操作约束。
- 可能涉及最终用户、管理者、参与维护的工程师、领域专家、工会等。这些被称为利益相关者(系统信息持有者)。
-
需求分析(Analysis):
- 过程活动:需求发现、需求分类和组织、需求优先权排序和协商、需求描述。
- 需求发现:与系统的信息持有者交流从而发现他们的需求的过程。来自信息持有者的领域需求和文档也在这个活动中得以发现。
- 需求分类和组织:将无序的需求收集起来,对其重新组织和整理,将其分成相关的几个组。
- 需求优先权排序和协商:对需求优先权排序,并通过协商发现且解决这些冲突。
- 需求描述:记录需求并将它作为螺旋下一循环的输入,产生形式化或非形式化的需求文档。
-
需求规格说明(Specification):形成文档。
-
需求验证(Validation):检查需求的现实性、一致性和完备性。
-
需求管理(Management):
- 需求管理是在需求工程过程和系统开发期间管理不断变化的需求的过程。
- 一个系统一旦安装并正式使用,就会不可避免地产生新的需求。
- 需要跟踪各个需求,并保持相关需求之间的链接,以便可以评估需求更改的影响。
- 需要建立一个正式的过程,用于制定更改提议,并将它们与系统要求相关联。
需求分析存在的问题:
- 系统信息持有者除了能通过泛泛的表述外,通常并不知道他们希望计算机系统做什么。
- 系统信息持有者使用他们自己的专业术语表达需求。
- 不同的信息持有者有不同的需求,这些需求有冲突之处。
- 机构和政治因素可能影响系统需求。
- 在分析过程中需求会发生变化。新的信息持有者可能出现,业务环境可能会改变。
4.8 需求发现技术
4.8.1 采访
原话:
对信息持有者的正式和非正式的采访是绝大多数需求工程过程的组成部分。
采访的类型:
- 封闭式采访:信息持有者回答一组预定的问题;
- 开放式采访:没有一个准备好的程序,即兴提问。
有效的采访:
- 思想开放,对需求没有先入之见,而且很愿意听取信息持有者的意见。
- 会用基本问题、需求建议或者是一个原型系统中共同工作来引导被采访者开始讨论。
实践中的采访:
- 通常是闭合和开放式采访的组合。
- 采访有助于全面了解信息持有者是如何工作的,以及他们是如何与系统进行互动的。
- 对于应用领域需求,通过采访的方式是很难获得的:需求工程师不能理解特定领域的术语;有些领域知识对于这些信息持有者来说太熟悉了,以至于他们要么是难以找到合适的词语来解释,要么会不自觉地认为这些概念太基本,不值得去解释。
4.8.2 脚本(Scenarios)
原话:
脚本是如何使用系统的现实例子。一个脚本可能包括以下内容:起始状况的描述;标准事件流的描述;描述什么可能出错;关于其他并发活动的信息;脚本完成时的状态的描述。
4.8.3 用例(Use Cases)
原话:
用例是 UML 中基于场景的技术,用于标识交互中的参与者,并描述交互本身。一组用例应该描述与系统的所有可能的交互。高级图形模型辅以更详细的表格描述。序列图可以用于通过显示系统中的事件处理的序列来向用例添加细节。
4.8.4 深入实际(Ethnography)
原话:
社会科学家花了相当多的时间观察和分析人们实际工作的方式。人们不必解释或表达他们的工作。可以观察到重要的社会和组织因素。深入实际研究表明,工作通常比简单的系统模型更加丰富和复杂。
深入实际的范围:
- 需求源于人们实际工作的方式,而不是流程定义建议他们应该工作的方式。
- 来自合作和对其他人的活动的认识的需求。
- 意识到其他人正在做什么导致我们做事情的方式的变化。
- 深入实际的方法对于理解现有流程是有效的,但无法识别应添加到系统的新功能。
聚焦深入实际的方法:
- 深入实际方法可以和原型法结合使用。
- 原型法通过识别问题和疑问并和深入实际的人员一起讨论这些问题和疑问来执行深入实际法。
- 深入实际的问题是,它研究那些可能具有某些不再相关的历史基础的现有做法。
4.9 需求有效性验证
原定义:
需求有效性验证是检验需求是否真正按客户的意愿定义系统的过程。需求错误成本很高,因此验证非常重要。系统交付后修复需求错误的成本可能是修改实现过程中错误成本的 100 倍。
需求检查:
- 有效性检查:系统是否提供最能满足客户需求的功能?
- 一致性检查:是否有任何需求冲突?
- 完整性检查:客户需要包括所有的功能吗?
- 真实性检查:可以根据可用的预算和技术实施需求吗?
- 可检验性检查:需求可以检查吗?
需求有效性验证技术:
- 需求评审:系统手动分析需求,进行错误检查和不一致性检查。
- 原型建立:使用系统的可执行模型来检查需求。
- 测试用例生成:开发用于检查可测性的需求的测试。
评审检查(Review checks):
- 可验证性(Verifiability):这个需求是否可以验证?
- 可理解性(Comprehensibility):需求是否被正确理解?
- 可追溯性(Traceability):是否明确陈述需求的发源地?
- 适应性(Adaptability):是否可以在不对其他需求产生重大影响的情况下更改需求?
4.10 需求管理
需求变更的原因:
- 系统业务和技术环境在安装之后总是在发生变化。新的硬件可能被引进,系统与其他系统的接口是必需的,业务优先次序可能会变更,新的立法和规章制度的实行使得系统必须做相应的调整。
- 系统购买者和系统最终用户很少是同一人。系统客户可能因为机构原因或预算约束对系统提出一些需求,而这些需求可能和最终用户需求冲突。而且在交付之后,如果系统要满足目标要求,就必须加入新的功能。
- 大型系统通常拥有不同的用户群。不同的用户群有不同的需求和优先次序。这些可能是冲突的或矛盾的。最终的系统需求不可避免要在他们之间进行折中,随着经验的积累,对不同用户支持上的这种平衡需要改变。
4.10.1 需求管理规划
原话:
规划阶段要明确需求管理所需的细节水平。需求管理决策内容:
- 需求识别:每个需求必须唯一标识,以便可以与其他需求进行交叉索引。
- 变更管理过程:这是一组对变更带来的影响和成品进行评估的活动。
- 可追溯策略:这些策略定义每个需求之间以及需求和系统设计之间的关系。
- 工具支持:可以使用的工具的范围很广,从专家需求管理系统到电子表格和简单的数据库系统。
4.10.2 需求变更管理
原话:
决定是否应接受需求变更:
- 问题分析和变更描述:在这个阶段,要对问题或变更提议进行分析以检查它的有效性。分析结果反馈给变更请求者,请求者进而产生一个更加详尽的需求变更提议或取消该变更请求。
- 变更分析和成本计算:使用可追溯性信息和系统需求的一般知识来评估所提议的变更的效果。一旦该分析完成,就做出是否继续进行需求变更的决定。
- 变更实现:必要的话,需求文档以及系统设计和实现都要做修改。理想情况下,文档应该组织起来,以便可以轻松实现更改。
4.11 要点
原话:
- 一个软件系统的需求描述了系统应该做什么以及定义系统运行和实现时的约束。
- 功能需求是有关系统一定要提供的服务或者是必须执行的计算的描述。
- 非功能性需求约束所开发的系统和所采用的开发过程。它们可能是产品需求、机构需求或者是外部需求。这些需求与系统的总体特性相关,且作用于系统整体。
- 软件需求文档是经过认可的系统需求描述。它应该被很好地组织,以便既可以为系统客户使用又可以为软件开发者使用。
- 需求工程过程包括可行性研究、需求导出和分析、需求描述、需求有效性验证和需求管理。
- 需求的导出和分析是一个迭代的过程,可以看做是一种螺旋式活动 – 需求发现、需求分类和组织、需求协商和需求文档化。
- 可以使用一系列技术来进行需求导出,包括采访、脚本、用例和深入实际。
- 需求有效性验证是检查需求的有效性、一致性、完备性、真实性和可检验性的需求的过程。
- 业务、组织机构和技术变化不可避免地导致对软件系统的需求的变更。需求管理就是管理和控制这些变更的过程。
过渡:需求往往抽象而分散。系统建模用图形化方式把需求结构化,帮助团队理解、沟通和验证系统应该做什么。
五、系统建模
本章问题链
- 为什么要对系统建模?模型和代码有什么关系?
- UML 有哪些常用图?分别描述什么视角?
- 用例图、顺序图、类图、活动图、状态图各自解决什么问题?
- 模型驱动工程(MDE)和 MDA 的基本思想是什么?
学习框架
系统建模 = 上下文模型 + 交互模型(用例/顺序) + 结构模型(类图) + 行为模型(活动/状态) + MDE一句话总结:建模是用抽象图形把系统“画出来”,便于沟通、分析和设计。
5.1 系统建模概述
原话:
系统建模就是建立系统抽象模型的过程,每一个模型表示一个系统不同的角度或方面。系统建模通常意味着用一些图形符号表示系统,现在几乎都是用统一建模语言(UML)中的符号。系统模型可以帮助分析员更好理解系统的功能,易于与用户沟通。
现有和计划建立的模型:
- 在需求工程中要用到现有系统的模型。它们给我们阐述现有系统的功能,并作为讨论系统功能扩展和不足的基础。这些都会导出新系统的需求。
- 在需求工程中使用新系统的模型有助于解释其他系统信息持有者提出的需求。工程师使用这些模型来讨论设计想法,并为系统实现进行论证。
- 在模型驱动工程过程中,由一个模型有可能实现一个完整的系统,也可能只是实现系统的一部分。
5.2 建模的四个视角
原话:
- 从外部来看,它是对系统上下文或系统环境建模。
- 从交互上看,它是对系统与环境之间或一个系统各组成部分之间的交互模型。
- 从结构上看,对系统的体系结构和系统处理的数据的结构建模。
- 从行为上看,对系统的动态行为和对事件的响应方式建模。
5.3 UML 统一建模语言
原话:
公认的面向对象建模语言出现于 20 世纪 70 年代中期。从 1989 年到 1994 年,其数量从不到十种增加到了五十多种。在众多的建模语言中,语言的创造者努力推崇自己的产品,并在实践中不断完善。但是,OO 方法(Object-Oriented Method,面向对象的方法)的用户并不了解不同建模语言的优缺点及相互之间的差异,因而很难根据应用特点选择合适的建模语言,于是爆发了一场“方法大战”。
UML 是一种定义良好、易于表达、功能强大且普遍适用的建模语言。它的作用域不限于支持面向对象的分析与设计,还支持从需求分析开始的软件开发的全过程。在美国,截止 1996 年 10 月,UML 获得了工业界、科技界和应用界的广泛支持,已有 700 多个公司表示支持采用 UML 作为建模语言。1996 年底,UML 已稳占面向对象技术市场的 85%,成为可视化建模语言事实上的工业标准。1997 年 11 月 17 日,OMG 采纳 UML 1.1 作为基于面向对象技术的标准建模语言。
UML 图的类型:
- 活动图:表示一个过程或数据处理中所涉及的活动。
- 用例图:表示一个系统和它所处环境之间的交互。
- 时序图:表示参与者和系统之间以及系统各部分之间的交互。
- 类图:表示系统中的对象类以及这些类之间的联系。
- 状态图:表示系统是如何响应内部和外部事件的。
图形建模的用途:
- 为讨论现有系统或提出新的系统提供便利。不完整和不正确的模型也是可以的,因为其角色是用来讨论。
- 讨论现有系统。模型应该精确地表示系统,但是不必完整。
- 作为详细的系统描述,该描述可用来产生系统实现。模型必须既正确又完整。
5.4 上下文模型
原话:
上下文模型用来描述一个系统的运行环境。用它来界定系统的边界。社会和组织的关注可能会影响决定在哪里定位系统边界。架构模型表示系统、及其与其它系统的关系。
系统边界:
- 系统边界的界定可以帮助我们确定哪些是系统内部的,哪些是系统外部的。
- 它们显示那些使用或依赖于正在开发的系统的其他系统。
- 系统边界的位置对需求分析有深远的影响。
- 定义系统边界是一种政治判断。可能会有发展那些增加/减少组织的不同部分的影响或工作量的系统边界的压力。
过程视角:
上下文模型只是简单的表示了环境中包含的其他模型。并没有显示即将开发的系统在环境中是如何被使用的。过程模型可以显示出来即将开发的系统在业务流程中是如何被使用的。UML 中的活动图可以定义业务流程模型。
5.5 用例模型
原话:
用例建模最早提出用来支持需求导出,现在收录 UML 中。每一个用例表示一个具体的任务,涉及参与系统的外部交互。一个用例中的参与者可能是用户或者其他系统。用例图所给出的只是交互非常简单的概要,所以必须提供更多的细节才能理解交互的内容。
用例模型组成:
- 参与者(Actor):用例图显示了系统和系统外实体之间的交互。这些实体被引用为执行者。执行者代表角色,可以包括:用户,外部硬件和其他系统。执行者往往被画成简笔画小人。也可以用带«actor»关键字的类矩形表示。
- 用例(Use Case):有意义的单独工作单元。它向系统外部的人或事提供一个易于观察的高层次行为视图。用例的标注符号是一个椭圆。
- 系统边界:用于界定系统功能范围,描述该系统功能的用例都置于其中,而描述外部实体的执行者都置于其外。
- 关系:include、extend、generalization。
获取执行者(角色):
- 谁使用系统的主要功能(主要使用者)?
- 谁需要系统支持他们的日常工作?
- 谁来维护、管理系统使其能正常工作(辅助使用者)?
- 系统需要控制哪些硬件?
- 系统需要与其他哪些系统交互?
- 对系统产生的结果感兴趣的是哪些人?
获取用例:
- 识别用例最好的方法就是从分析系统的参与者开始,考虑每一个参与者是如何使用系统的。
- 特定参与者希望系统提供什么功能。
- 系统是否存储和检索信息。如是,由哪个参与者触发。
- 当系统改变状态(内部事件)时,是否通知参与者。
- 是否存在影响系统的外部事件,哪个参与者通知系统这些事件。
用例间的关系:
-
包含关系(include):
- 一个用例可以简单地包含其它用例具有的行为,并把它所包含的用例行为作为自身行为的一部分。
- 包含关系把几个用例的公共步骤分离成一个单独的被包含用例。被包含用例称作提供者用例,包含用例称作客户用例,提供者用例提供功能给客户使用。
-
扩展关系(extend):
- 一个用例也可以被定义为基础用例的增量扩展。
- 扩展关系是把新的行为插入到已有的用例中的方法。同一个基础用例的几个扩展用例可以在一起应用。
- 基础用例不必知道扩展用例的任何细节,它仅为其提供扩展点。事实上,基础用例即使没有扩展用例也是完整的,这点与包含关系有所不同。
- 一个用例可能有多个扩展点,每个扩展点可以出现多次。但是一般情况下,基础用例的执行不涉及到扩展用例,只有特定的条件发生,扩展用例才被执行。
- 扩展关系为处理异常或构建灵活的系统框架提供了一种有效的方法。
-
泛化关系(Generalization):
- 一个用例可以被特别列举为一个或多个用例。
- 当父用例能够被使用时,任何子用例也可以被使用。
用例图的建立步骤:
- 找出系统外部的活动者和外部系统,确定系统的边界和范围。
- 确定每一个活动者所希望的系统行为和所有系统事件。
- 把这些系统行为命名为用例。
- 把一些公共的系统行为分解为一批新的用例,供其它的用例引用。把一些变更的行为分解为扩展用例。
- 绘制用例图。
- 区分主业务流和例外情况的事件流。可以把表达例外的情况的事件流的用例图画成一个单独的子用例图。
- 细化用例图,解决用例间的重复与冲突问题,简化用例中的对话序列,用例图可以有不同的层次(包),高层次系统的用例可以分解为若干个下属子系统中的子用例。
用例驱动开发:
在每一次增量或迭代中,可以选择一个或多个用例来实现,直到所有用例都实现,系统完成为止,该方法被称为用例驱动开发。用例驱动开发首先创建完整的用例集合,确定用例的优先级,基于用例的优先级、成本和时间,把它们安排在不同的开发周期中。
换句话说:用例图 = “谁用系统做什么”的一幅速写照。
5.6 顺序图
原话:
时序图主要用来为参与者与系统对象之间的交互和对象与对象之间的交互建模。时序图表示在特定用例中的交互发生顺序。涉及的对象和参与者列在图表顶端,向下垂直画一条虚线。对象之间的交互用带注释的箭头表示。
顺序图(时序图的别称):
顺序图是交互图的一种形式,它显示对象沿生命线发展,对象之间随时间的交互表示为从源生命线指向目标生命线的消息。顺序图能很好地显示哪些对象与其它哪些对象通信、什么消息触发了这些通信。顺序图不能很好地显示复杂过程的逻辑。
组成:
- 顶部是对象/参与者
- 垂直方向表示时间
- 消息用箭头表示
- 生命线、执行条、组合片段(alt/opt/par/loop 等)
生命线:
- 一条生命线在顺序图中代表一个独立的参与者,表示为包含对象名的矩形。
- 对象下面延伸的虚线,表示对象的生存时间。
- 如果它的名字是“self”,则说明该生命线代表控制顺序图的类元。
消息:
- 消息用箭头表示。消息可以完成传输,也可能丢失和找回。
- 它可以是同步的,也可以是异步的。既可以是调用,也可以是信号。
- 第一条消息是同步消息(标为实箭头),并隐含一条返回消息。
- 第二条消息是异步消息(标为实线箭头),第三条是异步返回消息(标为虚线)。
执行发生:
- 向下延伸的细条状矩形表示执行事件或控制焦点的激活。
- 第一个是源对象发送两条消息并收到两条回复。
- 第二个是目标对象收到一条同步消息并返回一条回复。
- 第三个是目标对象收到一条异步消息并返回一条回复。
迷路消息和拾取消息:
- 迷路消息是那些已经发送却没有到达指定接收者的消息,或者到达的接收者却不在当前图中的消息。
- 拾取消息是来自那些未知的发送者的消息,或者来自没有显示在当前图的发送者的消息。
- 它们都表明是去往或来自一个终点元素。
生命线开始与结束:
- 生命线可以在顺序图的时间刻度范围内创建和销毁。
- 图中,生命线被停止符号(叉号)终止。
时间和期限约束:
- 消息默认显示为水平线。
- 因为生命线显示为沿屏幕向下的时间通道,所以当给实时系统建模、或是有时间约束的业务过程建模时,考虑执行动作所需的时间长度是很重要的。
- 因此,可以给消息设置一个期限约束,这样的消息显示为下斜线。
组合片段:
| 类型 | 说明 |
|---|---|
alt | 选择性片段,为 if…then…else 结构建模 |
opt | 选项片段,为 “switch” 结构建模 |
break | 中断片段,对被处理事件的可选择顺序建模 |
par | 并行片段,为并发处理建模 |
seq | 弱顺序片段,包含了一组消息,这组消息必须在后继片段开始之前被处理,但不会把片段内消息的先后顺序强加到不共享同一条生命线的消息上 |
strict | 严格顺序片段,包含了一系列需要按照给定顺序处理的消息 |
neg | 非片段,包含了一系列不可用的消息 |
critical | 关键片段,具有关键部分 |
ignore | 忽略片段,声明一个没有意义的消息 |
consider | 考虑片段,与忽略片段相反,不包含在考虑片段内的消息都应该被忽略 |
assert | 断言片段,标明任何没有显示为声明操作数的顺序都是无效的 |
loop | 循环片段,包含一系列被重复的消息 |
门:
门是连接片段内消息和片段外消息的连接点。作用为顺序图与页面外的连接器。用来表示进来的消息源,或者出去消息的终点。
内部通信:
内部消息表现为一个操作的递归调用,或一个方法调用属于同一个对象的其他方法。显示为生命线上执行事件的嵌套控制焦点。
5.7 类图
原话:
当开发面向对象的系统模型时,用类图来表示系统中的类和这些类之间的关系。不严格地讲,对象类可以认为是某种系统对象的一般定义。所谓关联就是类与类之间的链接,表示类与类之间具有的某种关系。在软件工程的早期阶段,当你要开发模型时,我们用对象来表示现实世界的事物,比如病人、处方、医生等。
类图:
类图展示了面向对象系统的构造模块。描绘了模型或部分模型的静态视图,显示它包含的属性和行为,而不是详细描述操作的功能或完善方法。类图最常用来表达多个类和接口之间的关系。
三种关系英文说明:
- 泛化(Generalizations)
- 聚合(Aggregations)
- 关联(Associations)
- 分别是类之间继承,复合或应用,及连接的表现。
类(Classes):
类是定义对象所具有的属性和行为的元素。行为用类能理解的合适消息和适合每条消息的操作来描述。类中也可能定义约束、标记值、构造型。
类的标注:
- 类用矩形表示。
- 除类的名称外,还可以选择性地显示属性和操作。
- 分别用来显示类的名称、属性和操作。
如何确定类:
- 确定实体类。首先对系统将要存储的信息进行聚类分析,形成实体类。然后分析实体类间的关系。
- 构建适当的子系统,确定控制类,通过控制类和实体模型进行交互(协调类)。
- 建立边界类(UI 类、设备接口类、通信接口)。
接口(Interfaces):
是实施者同意满足的行为规范,是一种约定。实现一个接口,类必需支持其要求的行为,使系统按照同样的方式,即公共的接口,处理不相关的元素。
关联(Associations):
关联表明两个模型元素之间有关系,通常用在一个类中被实现为一个实例变量。连接符可以包含两端的命名的角色、基数性、方向和约束。关联是元素之间普通的关系。如果多于两个元素,也可以使用菱形的关联关系。当从类图生成代码时,关联末端的对象将变成目标类中实例变量。
泛化(Generalization):
泛化是一项常用技术,用来管理复杂性。并不是去学习我们所经历的每一个实体的所有细节特性,而是用更一般的类(比如,动物、汽车、房子等)来表示他们,然后研究这些类的特性。这样就允许我们推断这些类的成员具有某种共同的特征。
在面向对象语言中,比如 Java,泛化是使用内嵌在语言中的类继承机制来实现的。在泛化关系中,更高层次上的类中的属性和操作也是较低层次上的类的属性和操作。简言之,低层次的类是子类,它继承了它们的超类的属性和操作。这些低层次类然后添加其他专门属性和操作。
关联类(Association Classes):
关联类是一个允许关联连接有属性和操作的构造。
聚合(Aggregation):
聚合模型表示一个对象(相当于全体)如何由其他对象(相当于部分)组成。聚合模型与语义数据模型中的部分关系类似。
聚合通常用来描述由更小的组件所构成的元素。聚合关系表示为白色菱形箭头,其指向目标类或父类。
组合(Composition):
聚合的更强形式 - 组合聚合(强聚合) - 显示为黑色菱形箭头,用来组合每次最大化的包含组件。如果一个组合聚合的父类被删除,通常与他相关的所有部分都会被删除。但是,如果一个部件从组合中去掉,将不用删除整个组合。组合是可迁,非对称的关系和递归的。
嵌套(Nestings):
嵌套连接符用来表示源元素嵌套在目标元素中。
依赖关系(Dependency):
对于两个相对独立的对象,当一个对象负责构造另一个对象的实例,或者依赖另一个对象的服务时,这两个对象之间主要体现为依赖关系。与关联关系不同的是,依赖关系是以参数变量的形式传入到依赖类中的,依赖是单向的,要避免双向依赖。一般来说,不应存在双向依赖。依赖是一种弱关联,只要一个类用到另一个类,但是和另一个类的关系不是太明显的时候(可以说是“uses”了那个类),就可以把这种关系看成是依赖。
代码示例:
- 依赖关系表现在局部变量、方法的参数、以及对静态方法的调用。
- 关联关系是使用实例变量来实现。
- 聚合关系的类里含有另一个类作为参数。
- 组合关系的类里含有另一个类的实例化。
5.8 行为模型
原话:
行为模型是描述系统运行时的动态行为的模型,表示当系统响应来自所处环境的激励时所发生的或有可能发生的事情。这样的激励有两种:数据 – 一些数据到达必须由系统处理;事件 – 某些触发系统处理的事件发生。事件可能会有相关联的数据,但并不总是这样。
5.8.1 数据驱动的建模
原话:
数据驱动模型描述一个动作序列,该动作序列涉及输入数据的处理和相关输出的产生。在需求分析过程中,数据驱动模型非常有用,因此它们可以来表示系统中端到端的过程。两种表示系统处理序列的方法:DFD(数据流图)、时序图。
5.8.2 活动图
用于描述数据驱动或工作流驱动的行为。
元素:
- 活动:活动是行为参数化顺序的规范。活动被表示为圆角矩形,内含全部的动作、工作流和其他组成活动的元素。
- 动作:一个动作代表活动中的一个步骤。动作用圆角矩形表示。动作可以附带约束,该图显示了一个带前置条件和后置条件的动作。
- 控制流:控制流显示一个动作到下一个动作的流。表示为带箭头实线。
- 初始节点:一个开始或起始点用大黑圆点表示。
- 结束节点:
- 活动结束节点表示为中心带黑点的圆环。
- 流结束节点表示为内部为叉号的圆环。
- 活动结束节点是活动图内所有控制流的结束。
- 流结束节点表明单独的控制流的终点。
- 对象和对象流:
- 对象流是对象和数据转递的通道。对象显示为矩形。
- 对象流显示为带箭头的连接器,表明方向和通过的对象。
- 一个对象流在它的终端至少有一个对象。
- 数据存储显示为带 «datastore» 关键字的对象。
- 判断节点和合并节点:
- 判断节点和合并节点具有相同的标注:菱形。
- 从判断节点出来的控制流有监护条件。
- 合并节点直接将控制流传递通过。如果两个或更多的输入量到达合并节点,则它的输出流指定的动作会被执行两次或更多次。
- 分叉和结合节点:
- 分叉和结合节点有同样的标柱:垂直或水平条。
- 它们说明了控制的并发线程的起始和终点。
- 结合节点同步两个输入量,产生一个单独的输出量。来自结合节点的输出量要接收到所有的输入量后才能执行。
- 扩展域:扩展域是会执行多次的结构活动域。输入输出扩展节点表示为一组“3 厢”,代表多个选择项。关键词 “iterative”,“parallel” 或 “stream”显示在区域的左上角。
- 异常处理器:异常处理器在活动图中可以建模。
- 可中断活动区:可中断活动区环绕一组可以中断的动作。
- 分割(泳道):一个活动分割显示为垂直或水平泳道,用来在活动图中分隔动作。
5.8.3 状态图
用于描述事件驱动的行为。
原话:
状态机模型用来建模响应外部和内部事件的系统的行为。他们显示系统对于激励的响应,所以经常被用于建模实时系统。在状态机模型中,系统状态显示为节点,事件显示为弧线。当事件发生时,系统从一个状态移动到另一个状态。状态图是 UML 的一个组成部分,用来表示状态机模型。
原话:
状态图用来详细描述系统中,对象经历的状态转移和变化。它们显示一个对象如何从一个状态到另一个状态,以及控制这种变化的规则,通常有一个开始和结束状态。
元素:
- 状态:圆角矩形,状态名写在里面。
- 转换:带箭头实线。转移可以有一个 Trigger 触发器,一个 Guard 监护条件和一个 Effect 效果。
- Trigger 触发器是转移的起因,它可以是某个条件下的一个信号,一个事件,一个变化或一个时间通路。
- Guard 监护是一个条件,而且必须为真,以便于让触发器引起转移。
- Effect 效果是直接作用到对象上的一个动作,该对象具有作为转移结果的状态机。
- 起始和结束状态:
- 初始状态表示为实心黑圆环。
- 结束状态表示为中心带黑点圆环。
- 进入/退出动作:如果目标状态有多个转移到达,并且每一个转移都有相同的效果与它相关联,那最好将该效果与目标状态相关联,而不与转移相关联。可以通过为这个状态定义初始动作来实现。
- 自转换:一个状态可能有一个返回到自身的转移。
- 组合状态:一个状态机图可以有子状态机图。
- 选择伪状态:选择伪状态显示为菱形,有一个转移输入,两个或多个输出。
- 终止伪状态:进入终止伪状态是指状态机生命线已经终止。终止伪状态表示为叉号。
- 并发区:一个状态可以被分成几个不同的区,包含同时存在和执行的子状态。注意使用了分叉和结合伪状态而不是选择和合并伪状态。这些符号用来同步并发的线程。
经典例子:微波炉
- 状态包括:Waiting、Half power、Full power、Set time、Disabled、Enabled、Operation。
- 事件包括:Half power、Full power、Timer、Number、Door open/close、Start、Cancel。
5.9 模型驱动工程(MDE)
原定义:
模型驱动工程(MDE)是软件开发的一种方法,模型而不是程序成为开发过程中主要输出。在硬件/软件平台上执行的程序会由模型自动生成。MDE 的支持者认为这样提高了软件工程的水平,使得工程师不必再去关心编程语言的细节或者执行平台的特性。
MDE 的赞成与反对意见:
- 赞成:允许工程师在更高的抽象水平上考虑系统。自动生成代码意味着它更便宜的适应系统到新的平台。
- 反对:模型是为了抽象,而不必能够正确的实现。生成代码的节省可能会超过开发新平台的翻译成本。
5.9.1 模型驱动体系结构(MDA)
原话:
模型驱动体系结构(MDA)是较为通用的模型驱动工程的先驱。MDA 是一种以模型为中心进行软件设计和实现的方法,它使用 UML 模型的子集来描述系统。这里需要建立不同抽象层次的模型。原则上,对于一个高水平的平台独立的模型来讲,在没有人为干预情况下产生一个工作程序是有可能的。
MDA 的三种模型:
| 模型 | 说明 |
|---|---|
| CIM(Computation-Independent Model) | 计算独立模型,为系统中使用的重要的领域抽象建模。CIM 有时也被称为领域模型。 |
| PIM(Platform-Independent Model) | 平台独立模型,在没有它的实现作为参考下为系统的建模。我们通常使用 UML 模型来描述 PIM。 |
| PSM(Platform-Specific Model) | 平台特定模型,是由平台独立模型转化得到的,其中每一个应用平台都有一个独立的 PSM。 |
5.9.2 可执行 UML(xUML)
原话:
模型驱动工程背后的基本概念是,模型向代码的完全自动化转化是可行的。这是可行的,通过使用 UML2.0 的子集,即可执行 UML 或 xUML。
可执行 UML 的特点:
为了建立一个 UML 的可执行子集,模型类型的数量已经被删减为 3 个主要的模型类型:
- 领域模型,识别出系统的主要关注点,然后由 UML 的类图定义,其中包括对象、属性和关联。
- 类模型,在类模型中定义类以及它们的属性和操作。
- 状态模型,状态模型中的每一个状态图都与一个类相关联,并且用来描述类的生命周期。
系统的动态行为可以使用对象约束语言(OCL)详细说明,或者使用 UML 的语言表达。
5.9.3 敏捷方法和 MDA
原话:
MDA 的开发者认为,MDA 支持迭代的开发方法,所以可以应用到敏捷方法中。大规模的前期建模与敏捷宣言中的基本思想有冲突,很少会有敏捷开发者对敏捷驱动工程感到很舒服。如果 PIM 可进行完全自动的转换并产生完整的程序,那么,原则上,MDA 就可以应用在敏捷开发过程中,因为不再需要单独编写代码。
5.10 要点
原话:
- 模型是忽略了一些系统细节的系统抽象表示。建立互补的系统模型来表示系统的上下文、交互、结构和行为。
- 上下文模型描述所建模的系统是如何在含有其他系统和流程的环境中工作的。其帮助定义被建系统的边界。
- 用例图和时序图用来描述系统用户之间或系统用户和其他系统之间的交互。用例描述的是系统和外部参与者之间的交互,时序图通过表示系统对象之间的交互为用例图添加更多的信息。
- 结构模型表示的是系统的体系结构。类图用来定义系统中类的静态结构和类之间的关联关系。
- 行为模型用来描述可执行系统的动态行为。我们可以从系统处理数据的角度和事件激励系统产生响应的角度来建立这种模型。
- 活动图用来为数据的处理过程建模,其中每一个活动图代表一个处理步骤。
- 状态图用来为响应内外部事件的系统行为建模。
- 模型驱动工程是一种软件开发的方法,其中的系统表示为可自动转换为可执行代码的模型的集合。
过渡:模型帮助我们理解系统;体系结构设计则决定系统的宏观结构。它是从需求到实现的关键桥梁。
六、体系结构设计
本章问题链
- 为什么需要软件体系结构?
- 软件体系结构关注哪三个核心问题?
- 4+1 视图分别面向哪些利益相关者?
- 常见的体系结构模式有哪些?各有什么优缺点?
- 事务处理系统、信息系统、Web 系统、语言处理系统有什么典型结构?
学习框架
体系结构设计 = 为什么需要 → 是什么 → 设计决策 → 4+1 视图 → 体系结构模式 → 应用体系结构一句话总结:体系结构是软件的“骨架设计”,决定系统由哪些部分组成、如何交互、如何满足非功能需求。
6.1 为什么需要体系结构
原话:
- 规模增加
- 复杂性增加
- 需要演化
- “算法+数据结构”对于软件的描述不全面
- 需要提高对软件总体结构的认识
- 构成成分、成分之间的关系
- 提供交流基础
- 反映高层设计
- 便于系统演化
6.2 什么是软件体系结构
原定义:
软件体系结构是对子系统、软件系统组件以及它们之间相互关系的描述。子系统和组件一般定义在不同的视图内,以显示软件系统的相关功能性属性和非功能性属性。对系统结构的总体设计与说明。是一种高层设计。关注三方面问题:组件、组件间关系、关系约束。
构件:
- 软件系统由哪些部分构成:数据库、服务器、客户、层、过滤器、监听器。
构件间的关系:
- 这些部分之间的交互关系是什么:例如 RPC(remote process call)、共享内存、管道等。
关系的约束:
- 这些关系有什么约束:例如互斥等。
更具体的体系架构讨论:
- 系统元素如何组织?
- 系统如何实现所需功能?
- 系统如何满足预期性能、可靠性和其它质量特性(安全、并发、错误恢复、系统管理等)?
- 系统需要什么技术(分布式、集中式、J2EE、SOA 等)?
- 系统内部组织的结构是否能够弹性响应功能、技术、平台变化?
- 和原有系统如何集成?
- 数据持久化问题?
- 标准是否能够确保系统开发始终保持一致?采用什么设计模式?
- 经济上和技术上的冲突和平衡。
- 美学上的意义。
其它的软件体系结构定义:
- Dewayne Perry 和 Alex Wolf:软件体系结构是具有一定形式的结构化元素,即构件的集合,包括处理构件、数据构件和连接构件。处理构件负责对数据进行加工,数据构件是被加工的信息,连接构件把体系结构的不同部分组组合连接起来。
- Kruchten:软件体系结构有四个角度:
- 概念角度描述系统的主要构件及它们之间的关系;
- 模块角度包含功能分解与层次结构;
- 运行角度描述了一个系统的动态结构;
- 代码角度描述了各种代码和库函数在开发环境中的组织。
- Bass,Clements 和 Kazman:一个程序或计算机系统的软件体系结构包括一个或一组软件构件、软件构件的外部的可见特性及其相互关系。其中“软件外部的可见特性”是指软件构件提供的服务、性能、特性、错误处理、共享资源使用等。
常见误解:
体系结构和设计是同一件事;体系结构和基础结构是相同的;一个好的体系结构是简单的结构;体系结构一个蓝图就足够了;体系结构就是结构;系统体系结构优先于软件结构;体系结构难以被标准化和验证;体系结构是科学;体系结构是艺术。
好的体系结构的特征:
- 可伸缩性的
- 简单
- 亲切的
- 关系清楚明了
- 职责分布明确
- 效益和技术平衡
体系结构设计的目的与输出:
体系结构的设计主要(目的)是为了将系统分解为一些子系统,以及确定这些子系统之间的关系。体系结构设计过程的输出是体系结构模型,它描述系统是如何由一组相互交互的组件组成的。
原话:
体系结构设计是软件设计过程中的第一步。体系结构设计是设计和需求工程之间的桥梁。实际过程中,需求工程和体系结构设计之间有很大的重叠。其涉及到识别主要的系统组件和这些组件之间的关系。
体系结构的抽象层次:
- 小规模体系结构:与单个程序的体系结构有关。关注将单个程序分解成许多组成部分的方法。
- 大规模体系结构:与复杂企业系统的体系结构有关,包括其他的系统、程序和程序组件。这些企业系统分布在不同的计算机上,这些计算机可能由不同的企业管理。
明确体系结构的优点:
- 信息持有者之间的沟通:体系结构可以作为信息持有者之间讨论的焦点。
- 系统分析:体系结构设计决策对系统能否满足关键性需求(如系统的性能、可靠性和可维护性等)具有深远影响。
- 大规模复用:体系结构可以被大量的系统复用。可以开发出产品线体系结构,供相关系统使用。
体系结构的表示:
- 方块图的优点:方块图可以表示实体和关系,所以是最常用的软件体系结构建模方法。对于信息持有者的沟通和项目计划的制定是有用的。
- 方块图的缺点:过于抽象,没有给出系统组件之间的关系类型,也没有显示出组件可见的外部特性。
体系结构模型的使用:
- 作为一种加快系统设计商讨的方法:一个高层次的系统体系结构视图对于与信息持有者之间沟通以及项目规划是非常有效的,因为它不掺杂细节。
- 作为一种对已设计好的体系结构文档化的方法:目标是制作出一个完整的系统模型,来显示系统中的不同组件以及它们之间的结构和关联。
6.3 体系结构设计决策
原话:
体系结构设计是一个创造性的活动,工程中的活动很大程度上依赖于所要开发的系统类型。一些公有的决策会跨越整个设计阶段,影响系统的非功能特性。
设计决策问题:
- 对于所要设计的系统,是否存在一个一般性的应用体系结构可以当作模板?
- 系统将如何分配到多个处理器上?
- 哪些体系结构模式和风格可能会用到?
- 如何将系统中的结构组件分解为子组件?
- 应该使用什么样的策略来控制系统中组件的操作?
- 什么样的体系结构组织方式对于交付非功能性的系统需求是最好的?
- 如何评估体系结构设计?
- 如何对系统体系结构文档化?
体系结构复用:
同一个领域的系统通常具有相似的体系结构,这种体系结构能反映基本的领域概念。应用产品线是围绕一个核心体系结构建立的一些应用,核心体系结构会有一些变量,通过调整这些变量来满足专门用户的需求。系统的体系结构可以围绕更多的体系结构模式或“风格”设计。这些捕获了一个架构的本质,并且可以以不同的方式实例化。
针对非功能需求的组织策略:
| 非功能需求 | 组织策略 |
|---|---|
| 性能 | 如果性能是关键性需求,那么体系结构的设计就要定位在少数几个组件上的一些关键操作上 |
| 信息安全性 | 采用分层结构,把重要资源放在内层 |
| 安全性 | 将与安全相关的操作集中在一个或少数几个组件中 |
| 可用性 | 采用冗余组件 |
| 可维护性 | 使用小粒度的组件 |
6.4 4+1 视图
原话:
当设计和文档化一个系统的体系结构时,什么样的视图或角度是有效的?应该使用什么样的符号系统来描述体系结构模型?每种模型只能显示系统的一种角度和视图。它能说明系统是如何分解成许多的模块,运行时进程之间是如何交互的,或系统组件通过网络是如何以不同的方式分布的。对于设计和文档化,在不同的时期,所有这些都是非常有用的,通常需要提供系统体系结构的多重视图。
软件体系结构的 4+1 视图模型:
| 视图 | 面向对象 | 说明 |
|---|---|---|
| 逻辑视图 | 最终用户 | 显示系统中对象和对象类的一些主要抽象 |
| 进程视图 | 系统集成人员 | 显示了在运行时系统是如何组织为一组交互的进程 |
| 开发视图 | 程序员 | 显示了软件是如何为了开发而被分解的,即将软件分解成可以由单独的开发人员与开发团队实现的组件 |
| 物理视图 | 系统工程师 | 显示了系统硬件和系统中软件组件是如何分布在处理器上的 |
| +1 用例视图 | 所有利益相关者 | 这些视图通过用例或脚本关联在一起 |
6.5 体系结构模式
原定义:
模式是一种表示、共享和复用软件体系知识的方法。体系结构模式是对好的实践所做的格式化的抽象描述,它们已经在不同的系统和环境中多次尝试和测试过。这些模式应包括有关何时适用以及何时不适用的信息。体系结构模式应当描述一种系统构成,这种构成在以往的系统中是很成功的。可以使用表格和图形描述来表示模式。
6.5.1 MVC(Model-View-Controller)
- Model:数据和业务逻辑
- View:数据显示
- Controller:处理输入,协调 Model 和 View
适用:数据有多种显示和交互方式。
优点:数据和视图可以独立变化,可支持多个视图。
缺点:代码量增加,简单场景 unnecessary。
6.5.2 分层体系结构
原话:
系统的功能被划分成几个独立的层次,每一层只依赖紧接的下一层所提供的服务和设施。分层的方法支持系统的增量式开发。如一个层被开发完,该层提供的服务就可以被用户使用了。这个体系结构是可改变和可移植的。将系统人为的划分为此种结构。
优点:层可替换、可冗余。
缺点:清晰分层有时困难,可能带来性能开销。
6.5.3 容器/仓库体系结构(Repository)
原话:
描述的是一组交互组件如何共享数据。共享数据可以通过两种方式实现:共享数据保存在中央数据库或存储库中,并可由所有子系统访问;每个子系统维护自己的数据库并将数据显式传递给其他子系统。此模型适合于数据是由一个组件产生而由其他组件使用的情形。
优点:组件独立、数据一致。
缺点:单点故障、效率问题、难分布。
6.5.4 客户机-服务器体系结构
原话:
由一个服务集合和相关的服务器以及访问和使用这些服务的客户机组织起来。该模型主要组成部分:
- 一组给其他组件提供服务(打印、数据管理、编译)的服务器。
- 一组向服务器请求服务的客户机。
- 一个连接客户机和服务器的网络。
是一个分布式系统体系结构。
优点:可分布、共享通用功能。
缺点:单点故障、性能不可预测、管理复杂。
6.5.5 管道-过滤器体系结构(Pipe-Filter)
原话:
一般是函数转换处理输入并产生输出。数据从一个处理单元流到另一个处理单元,每经过一个单元就做一次变换。输入数据流经过这些变换直接转换为输出。这些转换可能顺序或并行地执行,数据加工可以是一项一项地处理,也可以成批处理。不太适用交互式系统。
适用:数据处理、批处理、事务处理。
优点:易理解、可复用、匹配工作流、可串行可并发。
缺点:数据格式必须统一、解析开销大。
6.6 应用体系结构
原话:
应用系统的目的是满足某些商业和机构的需要。需要处理的业务有很多共通之处,这样导致它们的应用系统也有很多相同的体系结构来满足应用需求。应用体系结构封装了一类系统的基本特征。当开发同一类型的新系统时,可以复用通用体系结构架构。
应用体系结构模型的方式:
- 作为体系结构设计过程的一个起点;
- 作为设计检查列表;
- 作为开发团队工作的组织方式;
- 作为评估组件以便复用的手段;
- 作为交流应用类型的词汇。
应用类型:
- 数据处理应用:在处理过程中,数据驱动的应用程序在没有明确的用户干预下批量地处理数据。
- 事务处理应用:以数据为中心的,处理来自用户对信息的请求并更新数据库中的数据。
- 事件处理系统:系统操作取决于来自系统环境中的解释事件。
- 语言处理系统:用户意图用形式化语言来表达的系统。
6.6.1 事务处理系统
原话:
事务处理系统用来处理用户对数据库信息查询或者请求对数据库更新。数据库事务是一个操作序列,每个这样的操作可以看成是一个原子单元。事务中的所有操作都必须在数据库永久改变之前完成。
特点:事务处理系统总是交互式系统,用户异步地提出对服务的请求。
6.6.2 信息系统
原话:
涉及与共享数据库交互的系统都可以看成是基于事务的信息系统。信息系统,具有一个通用的体系结构——分层模型。分层包括:用户界面、用户通信、信息检索、系统数据库。
6.6.3 基于 Web 的信息系统
原话:
信息和资源管理系统现在通常是基于 Web 的系统,用户接口是使用 Web 浏览器实现的。电子商务系统是一种基于互联网的资源管理系统。可以接受电子的商品或服务的订单,然后安排将商品或服务递送给客户。在电子商务系统中,应用相关层包括额外的功能,支持所谓的“购物车”,即用户可以在不同的事务中购买多个商品,然后在一个事务中完成对所有商品的支付。
服务器实现:
这些系统通常实现为多层的客户机/服务器体系结构:
- Web 服务器负责对所有用户的通信,用户接口是 Web 浏览器实现的;
- 应用服务器负责实现应用相关的逻辑,也包括信息存储和检索请求;
- 数据库服务器将信息从数据库中移入和移除,并负责事务管理。
6.6.4 语言处理系统
原话:
语言处理系统把自然语言或人工语言翻译成该类语言的其他表示。
编译器的组件:
- 词法分析器:将一个输入的语言记号转换为内部形式。
- 符号表:保持实体名字相关的信息。
- 语法分析器:检查正在翻译的语言的语法。
- 语法树:代表待编译程序的内部结构。
- 语义分析层:使用来自语法树和符号表的信息来检查输入的语言文本的语义正确性。
- 代码生成器:在语法树中穿行并生成抽象机代码。
6.7 要点
原话:
- 软件体系结构是有关关键系统如何组织的描述。
- 体系结构设计决策包括对应用类型的决策、系统分布的决策、所使用的体系结构风格的决策,以及对体系结构如何文档化和评估的决策。
- 体系结构可能从许多不同的视角和视图来文档化。可能的视图包括概念视图、逻辑视图、进程视图、开发视图和物理视图。
- 体系结构模式是复用通用的(一般的)系统体系结构知识的一种方法。它描述体系结构,解释这样的体系结构的使用时机以及它的优缺点。
- 应用较多的体系结构模式有 MVC、分层体系结构、容器结构、客户机-服务器结构、以及管道和过滤器结构。
- 应用系统体系结构的一般模型能帮助我们理解应用的运作,比较相同类型的应用,验证应用系统设计的有效性并能达到对大粒度组件的复用。
- 事务处理系统是交互式系统,允许数据库中的信息被很多远程用户访问和修改。
- 语言处理系统用来将文本从一种语言翻译成另一种语言,来执行输入语言所定义的指令。他们包括一个翻译器和一个执行生成语言的抽象机。
过渡:体系结构确定之后,接下来要细化到对象、类、接口和代码。面向对象设计原则和设计模式是高质量实现的核心工具。
七、设计与实现
本章问题链
- 面向对象设计的主要步骤是什么?
- SOLID 原则和迪米特法则分别解决什么问题?
- 设计模式如何分类?常见模式(工厂、抽象工厂、观察者、责任链)怎么用?
- 复用、配置管理、开源开发各需要注意什么?
学习框架
设计与实现 = 面向对象设计过程 → SOLID + LoD → 设计模式 → 实现问题(复用/配置/开源)一句话总结:好的设计让软件容易理解、修改、扩展和复用;SOLID 和模式是达成这一目标的常用工具。
7.1 设计和实现
原话:
软件设计和实现是软件工程过程中的一个阶段,在此阶段开发出可执行的软件系统。软件设计和实现活动总是交叉进行的。软件设计是创造性活动,基于客户的需求识别系统组件及其关系。实现是将设计转变为程序的过程。设计是关于如何解决问题的活动,因此通常有一个设计过程。
7.1.1 购买还是构建
原话:
广泛的应用领域可以购买商业现货系统(COTS),然后根据用户需求进行调整和裁剪。比如,准备开发一个医疗记录系统,那么可以购买医院中已经使用的程序包。这样开发成本会更低,相对于常规的用编程语言开发的方法,开发速度也更快。采用这种方式开发应用程序时,设计过程要集中考虑如何配置这些功能,使得系统可以满足现有需求。
7.2 利用 UML 进行面向对象设计
原话:
面向对象系统是由一组相互交互的对象所组成。对象设计过程包括设计对象类和这些类之间的关系。当设计被实现为一个执行系统的时候,对象从它们的类定义中动态创建出来。
面向对象的设计需要完成:
- 了解并定义上下文和与系统的外部交互
- 设计系统体系结构
- 识别出系统中的主要对象
- 开发设计模型
- 定义对象接口
7.2.1 系统上下文与交互
原话:
任何软件设计过程的第一阶段是去了解待开发软件和外部环境之间存在的关系。设置系统上下文边界有助于我们决定什么特征是在建系统中需要实现的,哪些特征是存在于其他关联系统中的。系统上下文模型和交互模型可以互为补充,呈现出系统和环境之间的关系。系统上下文模型是一个结构模型,描述开发中的系统所处环境中的其他系统。交互模型是一个动态模型,表明系统在运行中是如何与环境交互的。
7.2.2 体系结构的设计
原话:
一旦完成软件系统与系统所在环境之间交互的定义,就可以将这些信息作为系统体系结构设计的基础。结合有关体系结构设计的一般性知识和具体的领域知识。首先识别出组成系统的主要组件以及它们之间的交互。运用一种体系结构模式,来组织这些组件。
7.2.3 对象类识别
原话:
识别对象类是面向对象设计阶段比较难的一项工作。没有一个通用的方法对于任何系统开发都适用。靠的是系统设计者的技术、经验和领域知识。
方法:
- 名词提取法
- 试探法
- 对象识别过程需要进行反复迭代
7.2.4 设计模型
原话:
设计模型,表示系统中包含的对象或对象类,同时也描述了实体间的关联与关系。这些模型是系统需求和系统实现之间的桥梁。
两类设计模型:
- 结构模型:通过系统对象类及其之间的关系来描述系统的静态结构。
- 动态模型:描述系统的动态结构和系统对象之间的交互。
早期 3 种有用的模型:
- 子系统模型:给出对象的逻辑分组即子系统,子系统之间存在的联系。
- 时序模型:说明对象交互的序列。
- 状态机模型:说明单个对象如何响应事件来改变它们的状态。
时序模型:
时序模型描述每种交互模式下发生的对象交互序列。对象水平放置于时序图的顶部;时间垂直表示,模型从上到下读取;互动由带标记的箭头表示,不同风格的箭头表示不同类型的交互;对象生命线中的一个薄矩形表示当对象是系统中的控制对象时的时间。
状态图:
状态图用于显示对象如何响应不同的服务请求和这些请求触发的状态转换。状态图是系统或对象的运行时行为的有用的高级模型。通常不需要画出系统中所有对象的状态图。因为系统中的许多对象相对简单,所以状态模型会为设计添加不必要的细节。
7.2.5 接口描述
原话:
对不同组件之间的接口的描述。接口设计是关于定义一个或一组对象的详细接口信息。
7.3 面向对象设计原则(SOLID + LoD)
原话:
面向对象的设计原则是面向对象设计的基本指导思想,是评价面向对象设计的价值观体系,是设计模式的出发点和归宿,也是构造高质量软件的出发点。
设计目标:
构造出高质量软件,以保持系统稳定。设计目标包括可扩展性(Extensibility)、灵活性(Flexibility)、可插入性(Pluggability)等。
坏的设计:
糟糕的设计总是散发出臭味,让人不悦。判断一个设计的好坏,主观上能否让你的合作方感到心情愉悦,是最直观的标准。设计开发人员要培养嗅觉,当你看到 UML 图或者代码,感到杂乱、繁琐、郁闷的时候,你可能正面对一个糟糕的设计。坏的设计通常表现为:僵硬性(Rigidity)、脆弱性(Fragility)、牢固性(Immobility)、不必要的复杂性(Needless Repetition)、晦涩性(Opacity)。
好的设计:
好的设计容易理解、容易修改和扩展、容易复用、容易实现与应用,简单、紧凑、经济适用,是让人工作起来心情愉快的设计。
7.3.1 SRP:单一职责原则
原定义:
就一个类而言,应该仅有一个引起它变化的原因。
原话:
单一职责原则(Single Responsibility Principle):不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
违反 SRP 的案例:
Rectangle 类会因为两方面的原因而变化:计算几何方面的原因和用户界面设计方面的原因。其中一个发生变化后,必须修改 Rectangle 类,而这种修改则可能导致另一个应用程序出错。除此之外,违反 SRP 还会带来物理依赖的缺点。
解决方案:
增加新的类,使得每个类仅有一个职责。
换句话说:SRP = 一个类只负责一件事,变化原因只有一个。
7.3.2 OCP:开闭原则
原定义:
软件实体(类、模块、函数等)应该是可扩展的,但是不可修改的。
特征:
- 对于扩展是开放的(Open for extension):模块的行为可以扩展,当应用的需求改变时,可以对模块进行扩展,以满足新的需求。
- 对于更改是封闭的(Closed for modification):对模块行为扩展时,不必改动模块的源代码或二进制代码。
原话:
OCP 的关键在于抽象。抽象技术:abstract class, Interface。抽象预见了可能的所有扩展(闭)。由抽象可以随时导出新的类(开)。
原话:
OCP 是 OOD 中很多说法的核心。如果这个原则应用得有效,应用程序就会具有更多的可维护性、可重用性以及可健壮性。很多设计模式都是遵从这个原则而提出来的。LSP 是 OCP 成为可能的主要原则之一。正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。
手与门的案例:
- 初始设计:Hand 直接依赖 Door。新增 Drawer、Refrigerator 时需要修改 Hand。
- 符合 OCP 的设计:抽象出 Excutable 接口,Hand 依赖 Excutable。新增 Drawer、Refrigerator 时只需实现接口,不需要修改 Hand。
换句话说:OCP = 对扩展开放,对修改封闭;加功能靠新增,不靠改旧代码。
7.3.3 LSP:里氏替换原则
原定义:
若对于类型 S 的任一对象 o1,均有类型 T 的对象 o2 存在,使得在 T 定义的所有程序 P 中,用 o1 替换 o2 之后,程序的行为不变,则 S 是 T 的子类型。
如果在任何情况下,子类(或子类型)或实现类与基类都是可以互换的,那么继承的使用就是合适的。为了达到这一目标,子类不能添加任何父类没有的附加约束。
长方形与正方形案例:
- Square 类针对 height、width 添加了 Rectangle 所没有的附加约束(即要求 height=width)。
- 这样 Square 类(子类)不能完全替换 Rectangle(父类),违背了 LSP 原则。
- 使用 resize 方法时,子类会出错。
解决方案:
在可能的情况下,由抽象类(接口)继承。只要有可能,不要从具体类继承。行为集中的方向是向上的(抽象类),数据集中的方向是向下的(具体类)。
- 引入 Quadrangle(四边形)抽象类,Rectangle 和 Square 都继承它。
换句话说:LSP = 子类要能完全替换父类,不能增加父类没有的约束。
7.3.4 ISP:接口隔离原则
原定义:
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
原话:
客户不应该依赖他们用不到的方法,只给每个客户它所需要的接口。为了避免“肥接口(fat interface)”,应当以一个类实现多个接口,而各客户仅仅获知必须的接口。
本质:
使用多个专门的接口比使用单一的接口好。一个类对另一个类的依赖性应当是建立在最小的接口上的。避免接口污染(Interface Pollution)。
解决方案:
- 使用委托分离接口:Adapter 模式
- 使用多重继承(实现)分离接口
换句话说:ISP = 大接口拆小接口,类只依赖自己需要的方法。
7.3.5 DIP:依赖倒置原则
原定义:
高层模块不依赖低层模块,二者都依赖于抽象。抽象不依赖于细节,细节依赖于抽象。针对接口编程,不要针对实现编程。又称控制反转(IoC,Inversion of Control)、依赖注入。
启发式原则:
- 任何变量都不应该拥有指向具体类的指针或者引用。
- 任何类都不应该从具体类派生。
- 任何方法都不应该改写其任何基类中已经实现的方法。
DIP 经典案例 — Mark IV 咖啡机:
咖啡机描述:
Mark IV 咖啡机最多可以一次煮好 12 杯咖啡。使用者首先将滤网放入滤网支架中,将咖啡粉末放入滤网内,将滤网支架滑入托座中。然后使用者向烧水壶内加入最多 12 杯冷水,按下“加热(Brew)”键,水被加热至沸腾。蒸汽压力将迫使水漫过咖啡粉末,咖啡通过滤网的过滤,流入咖啡壶中。咖啡壶放在保温托盘上,从而可以在一段时间内保持温度。只当壶中有咖啡时,保温托盘才处于工作状态。如果将壶从保温托盘上拿开,水流将立刻停止,这样煮沸的咖啡就不会溢出到保温托盘上。
硬件设施:
- 用于烧水壶的加热部件。它可以被开启和关闭。
- 保温托盘的加热部件。它可以被开启和关闭。
- 保温托盘上的传感器。它有三个状态:warmerEmpty, potEmpty 和 potNotEmpty。
- 烧水壶中的传感器。它有两个状态:boilerEmpty 和 boilerNotEmpty。
- “加热”键。这个键指示加热过程。它上面有一个小指示灯,当加热过程结束,咖啡已经煮好之后,这个灯亮起来。
- 一个压力阀门,当它开启时,烧水壶中的压力降低。压力下降,则经过滤网的水流立刻停止。该阀门可以处于“开启”和“关闭”状态。
常见问题设计:
- 泡泡类(Vapor Classes):没有带来任何好处的类。例如 Light 类只是封装了 API 调用,没有实际价值。
- 无用的抽象:例如 Heater 接口、Sensor 接口,没有人会使用它们。
- 上帝类(God Classes):全部逻辑都集中在 CoffeeMaker 类中,其他类都是臆想出来的、没有用的、多余的。
真正的抽象:
面向对象的设计目标是提高系统的可维护能力,而如何找到系统的稳定状态呢?通过抽象把握现实世界的本质。
- 最简单的冲咖啡方式:人工冲泡。最基本的对象:HotWaterSource(提供热水)、ContainmentVessel(容纳冲好的咖啡)。
- 有了 UI 后,最基本的三个对象:UserInterface、HotWaterSource、ContainmentVessel。
用例分析:
- 用例 1:加热 - 基本路径:
- 用例起始于用户按下加热键
- 系统开始检查水源、咖啡壶已经准备好
- 系统开始加热
- 加热完成后,系统显示指示灯,提醒用户
- 异常路径:加热过程中,用户拿走咖啡壶,则停止加热,同时停止供水。
- “加热”步骤 4:加热结束:
- 加热结束后,指示灯应当点亮,停止供应热水。
- 加热结束的消息也应该让 ContainmentVessel 知道,因为当用户再次将空咖啡壶放到保温托盘上,它必须负责通知 UI 熄灭指示灯。
- HotWaterSource 和 ContainmentVessel 都有责任通知其他对象加热结束。
- 异常路径:加热中拿走咖啡壶:
- ContainmentVessel 应当立刻通知 HotWaterSource 停止继续供应热水。
通信图(Communication Diagram):
通信图描述对象之间消息的连接关系,侧重说明哪些对象之间有消息传递。通信图中对象用对象图符表示,箭头表示消息发送的方向,编号标明消息的执行顺序。顺序法:简单编号方案,从 1 开始,由小到大,顺序排列。层次法:小数点制编号方案。
与顺序图相比,通过编号来看消息的执行顺序比较困难,但通信图中对象间灵活的空间布局可以更方便地展示动态连接关系等有用信息。
通信图与顺序图对比:
- 功能完全相同,可利用工具进行互相转换。
- 侧重点不同,根据实际情况选择一种合适的图进行用例分析和设计。
- 顺序图:显示了消息的明确顺序;更适用于全部流的可视化;更适用于实时规约和复杂场景。
- 通信图:显示除交互之外的关系;更适用于协作模式的可视化;更适用于一个既定对象上的所有效果的可视化;更易于头脑风暴讨论使用。
由通信图产生类图:
这是一个非常理想的结构:责任被合理分配,各对象之间的消息平衡,没有泡泡类,协作图与类图相匹配。
利用泛化(实现)关系添加细节:
抽象类的抽象操作,保持该类的稳定性,满足 OCP。具体的代码实现交由具体类来实现。
我们得到的是“咖啡机”,所有这三个类与“Mark IV 咖啡机”这一特殊产品没有任何直接关系。这正是我们所期望的——我们得到了真正的抽象,把握住了问题的本质。下面要做的,是根据这个抽象来实现 Mark IV 咖啡机。
DIP 的本质:
通过抽象提取业务本质,并建立一个稳定的结构描述这个本质。对于具体的业务规则的处理是在这个本质的基础上的扩展。技术、工具、意识形态等的发展可能使业务规则不断变化,但本质不变;而 DIP 则帮助我们轻松的适应这些变更。
换句话说:DIP = 高层和低层都依赖抽象;面向接口编程,不要面向实现编程。
7.3.6 迪米特法则(Law of Demeter)
原定义:
只与你直接的朋友通信,而避免和陌生人通信。众所周知类(或模块)之间的通信越少,耦合度就越低,从而更有利于我们对软件的宏观管理。
换句话说:LoD = 只和直接朋友通信,少跟陌生人说话,降低耦合。
7.4 设计模式
原定义:
设计模式是关于问题及其解决方案的重用抽象知识的一种方式。模式是对问题的描述及其解决方案的本质。应该足够抽象,以便在不同的环境中重复使用。模式描述通常利用面向对象的特性,如继承和多态。
设计模式的分类:
从目的来看:
- 创建型(Creational)模式:将对象的部分创建工作延迟到子类或者其他对象,从而应对需求变化为对象创建时具体类型实现引来的冲击。
- 结构型(Structural)模式:通过类继承或者对象组合获得更灵活的结构从而应对需求变化为对象的结构带来的冲击。
- 行为型(Behavioral)模式:通过类继承或对象组合来划分类与对象间的职责,从而应对需求变化为多个交互的对象带来的冲击。
从范围来看:
- 类模式:处理类与子类的静态关系。
- 对象模式:处理对象间的动态关系。
优点:面向界面编程、降低耦合性、增加灵活性。
设计模式的元素:
- 名字:模式的一个有意义的指代。
- 对问题的描述:解释什么时候模式可以应用。
- 对部分设计的解决方案:描述设计方案的各个部分及其之间的关系和职责。它不是一个具体的设计描述,而是一个设计方案的模板。
- 结果陈述:说明应用该模式的结果和副作用。
7.4.1 工厂方法模式(Factory Method)
原定义:
定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
工厂方法模式四要素:
- 工厂接口:工厂方法模式的核心,与调用者直接交互用来提供产品。
- 工厂实现:决定如何实例化产品,是实现扩展的途径。
- 产品接口:定义产品的规范,所有的产品实现都必须遵循。
- 产品实现:实现产品接口的具体类,决定了产品在客户端中的具体行为。
适用场景:
- 作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。
- 简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。
- 工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。
- 当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。
7.4.2 抽象工厂模式(Abstract Factory)
原话:
抽象工厂模式是工厂方法模式的升级版本,它用来创建一组相关或者相互依赖的对象。它与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类。也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。
产品族概念:
所谓的产品族,是指位于不同产品等级结构中功能相关联的产品组成的家族。抽象工厂模式所提供的一系列产品就组成一个产品族;而工厂方法提供的一系列产品称为一个等级结构。
优点:
抽象工厂模式除了具有工厂方法模式的优点外,最主要的优点就是可以在类的内部对产品族进行约束。所谓的产品族,一般或多或少的都存在一定的关联,抽象工厂模式就可以在类内部对产品族的关联关系进行定义和描述,而不必专门引入一个新的类来进行管理。
缺点:
产品族的扩展将是一件十分费力的事情,假如产品族中需要增加一个新的产品,则几乎所有的工厂类都需要进行修改。所以使用抽象工厂模式时,对产品等级结构的划分是非常重要的。
适用场景:
当需要创建的对象是一系列相互关联或相互依赖的产品族时,便可以使用抽象工厂模式。说的更明白一点,就是一个继承体系中,如果存在着多个等级结构(即存在着多个抽象类),并且分属各个等级结构中的实现类之间存在着一定的关联或者约束,就可以使用抽象工厂模式。假如各个等级结构中的实现类之间不存在关联或约束,则使用多个独立的工厂来对产品进行创建,则更合适一点。
7.4.3 观察者模式(Observer)
原定义:
定义对象间一种一对多的依赖关系,使得当每一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。
适用场景:
在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。比如,我们要设计一个自动部署的功能,就像 Eclipse 开发时,只要修改了文件,Eclipse 就会自动将修改的文件部署到服务器中。一个对象要时刻监听着另一个对象,只要它的状态一发生改变,自己随之要做出相应的行动。
观察者模式的结构:
- 被观察者:类中有一个用来存放观察者对象的容器,另外还有 attach、detach、notify 方法。
- 观察者:一般是一个接口,它只有一个 update 方法,在被观察者状态发生变化时触发。
- 具体的被观察者:便于扩展,可以在此角色中定义具体的业务逻辑。
- 具体的观察者:观察者接口的具体实现,定义被观察者对象状态发生变化时所要处理的逻辑。
优点:
观察者与被观察者之间是属于轻度的关联关系,并且是抽象耦合的,这样,对于两者来说都比较容易进行扩展。
缺点:
观察者模式是一种常用的触发机制,它形成一条触发链,依次对各个观察者的方法进行处理。但同时,这也算是观察者模式一个缺点,由于是链式触发,当观察者比较多的时候,性能问题是比较令人担忧的。并且,在链式结构中,比较容易出现循环引用的错误,造成系统假死。
设计问题(何时使用模式):
要在自己的设计中使用模式,要相信所面临的任何设计问题都可以用某种相关模式来解决。例如:
- 告诉一些对象某个其他对象的状态已经发生改变(观察者模式)。
- 将接口整理到一些相关对象中,这些对象通常是增量式开发出来的(外观模式)。
- 提供一些访问集合元素的标准方法,无论该集合是如何实现的(迭代器模式)。
- 允许在运行时对已有类进行功能扩展(装饰器模式)。
7.4.4 责任链模式(Chain of Responsibility)
原定义:
使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
if…else…的问题:
- 代码臃肿:实际应用中的判定条件通常比较复杂,如果判断条件比较多,if…else…语句就没法看了。
- 耦合度高:如果想继续添加处理请求的类,就要继续添加 else if 判定条件;条件判定的顺序也是写死的。
责任链模式的结构:
- 抽象处理类:主要包含一个指向下一处理类的成员变量 nextHandler 和一个处理请求的方法 handRequest。
- 具体处理类:对具体的处理逻辑和处理的适用条件进行实现。
优点:
责任链模式与 if…else…相比,它的耦合性要低一些。因为它把条件判定都分散到了各个处理类中,并且这些处理类的优先处理顺序可以随意设定。
缺点:
责任链模式也有缺点,这与 if…else…语句的缺点是一样的,那就是在找到正确的处理类之前,所有的判定条件都要被执行一遍。当责任链比较长时,性能问题比较严重。
适用场景:
假如使用 if…else…语句来组织一个责任链时感到力不从心,代码看上去很糟糕时,就可以使用责任链模式进行重构。责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中。这样做的优点是比较灵活了,但同样也带来了风险。比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。
7.5 实现问题
7.5.1 复用
原话:
从 20 世纪 60 年代到 90 年代,大部分新软件都是从头开始开发的,用高级语言编写全部的代码。唯一重要的复用是对函数和编程语言库中对象的复用。但是,预算和进度的压力使得这种方法越来越不实用,特别是商业和基于互联网的系统。因此,出现了一种基于复用现有软件的开发方式,并逐步用于商业系统、科学软件、以及嵌入式系统工程。
复用层次:
- 抽象层:运用软件设计中的成功抽象。代表方式是体系结构模式和设计模式。
- 对象层:可直接复用库中的对象,代替自己编写代码。
- 组件层:组件是一组通过相互合作实现相关功能和服务的对象及对象类集合。
- 系统层:复用整个应用系统。
复用成本:
- 寻找可复用的软件并评估其是否符合需求的时间花费。
- 找到合适的可复用软件后,还要有购买此软件的花费。
- 调整和配置可复用软件组成或系统使其满足待开发系统需求的花费。
- 集成各个可复用软件(如果使用的是不同来源的软件)及新代码的花费。
7.5.2 配置管理
原话:
配置管理指的是管理变更中软件系统的一般过程。配置管理的目标是支持系统集成过程,使得每个开发人员可以在管理中访问工程代码和文档,查找变更,编译连接组件并生成系统。
配置管理活动:
- 版本管理:对软件组件不同版本的追踪提供支持。版本管理系统包括协调多个程序员开发的机制,可以防止一个程序员覆盖其他人提交到系统中的代码。
- 系统集成:提供支持帮助开发人员定义在创建每个版本时所用的组件版本。这些描述可以用于编译连接需要的组件以自动构建一个系统。
- 问题追踪:提供支持允许用户报告缺陷及其他问题,并允许开发人员查看谁在修复这些问题,以及何时完成修复。
7.5.3 宿主机-目标机开发
原话:
多数软件是在一台计算机(宿主机)上开发,但在另一台机器(目标机)上运行。一般的,称之为开发平台和运行平台。平台不只是指硬件设备,还包括安装的操作系统及其他支持软件,如数据库管理系统,或者开发平台中的集成开发环境。通常,开发平台和运行平台安装有不同的软件,且这些平台拥有不同的系统结构。
开发平台工具:
- 集成编译器和句法导向的编辑系统,能够创建、编辑和编译代码。
- 编程语言调试系统。
- 图形编辑工具,例如编辑 UML 模型的工具。
- 测试工具,如 JUnit,可以在新版本的程序上自动运行一组测试。
- 项目支持工具,能够帮助我们为不同的开发项目组织代码。
集成开发环境(IDEs):
软件开发工具集成在一起形成了集成开发环境(IDE)。IDE 是一系列支持不同方面软件开发的软件工具,包括一些常用的框架和用户界面。通常,IDE 是针对某个编程语言(如 Java)而开发出来的。
组件/系统部署因素:
- 组件的软硬件需求:如果一个组件是为特定硬件体系结构而设计的,或者依赖于一些其他软件系统,那么显然需要部署在一个提供所需软硬件支持的平台上。
- 系统可用性需求:高可用性系统需要组件部署在多个平台上。当一个平台出现故障,还可以用到备用的组件。
- 组件通信:如果组件间有高层通信,通常需要将它们部署在同一个平台,或者距离比较近的平台上。这样可以减少通信的延迟。
7.6 开源开发
原话:
开源开发是软件开发的一种方法,它将软件系统的源代码公布出来,并允许志愿开发人员加入到开发中来。该方法源于自由软件基金会,该组织主张源代码不应该是专有的,而应始终可供用户根据需要进行检查和修改。开源软件利用互联网发展了大量志愿开发人员,由此扩充了这个理念,多数人同时也是代码的用户。
著名开源产品:Linux 操作系统、Java、Apache 网络服务器、mySQL 数据库管理系统。
开源问题:
- 待开发的产品是否应该使用开源组件?
- 软件开发过程是否应该采用开源方法?
开源业务:
越来越多的公司采用开源方法进行开发。他们的业务模型不是依赖于软件产品的销售,而是在卖对该产品的支持。他们相信让开源社区参与进来可以使得软件开发变得更经济、更迅速、且可以为软件创造一个用户团体。
常见许可证:
| 许可证 | 说明 |
|---|---|
| GPL | GNU 通用公共许可,也被称作“互惠”许可。如果用 GPL 许可证的开源软件,那么我们必须将自己的软件开源。 |
| LGPL | GNU 宽松的通用公共许可。可以编写链接到开源代码的组件,而无需公布自己的组件源码。但是如果更改了这些许可证下的组件,那么就需要开源。 |
| BSD | Berkley 标准分布许可,非互惠许可。不会被强制要求公开对开源代码的任何改动。可以在用于商业用途的系统中使用这些代码。但是必须知道代码的原作者。 |
许可证管理:跟踪组件、了解许可证、制定路线图、培训、审计、参与社区。
7.7 要点
原话:
- 软件设计和实现时相互交织的活动。设计的细节层次取决于待开发系统的类型以及是否使用计划驱动或敏捷开发方法。
- 面向对象的设计过程包括系统体系结构的设计、识别系统中的对象、运用不同的对象模型描述设计、以及文档化组件接口。
- 面向对象的设计过程中会产生不同的模型。这些模型包括静态模型(类模型、泛化模型、关联模型)和动态模型(时序模型、状态机模型)。
- 要对组件的接口进行精确的定义,这样其他的对象才可以使用它们。可以使用 UML 接口模板来定义接口。
- LSP 要求子类不能添加任何父类没有的附加约束。
- OCP 的关键在于抽象。
- SRP 要求每个类仅有一个职责。
- ISP 要求一个类对另一个类的依赖性应当建立在最小的接口上。
- DIP 要求高层模块不依赖低层模块、二者都依赖于抽象,抽象不依赖于细节、细节依赖于抽象。
过渡:代码写出来之后,必须验证它是否符合需求、是否存在缺陷。测试是软件质量的重要保障。
八、软件测试
本章问题链
- 测试能证明什么、不能证明什么?
- Verification 和 Validation 有什么区别?
- 审查和测试各有什么优势?
- 开发测试、发布测试、用户测试分别做什么?
- TDD 的基本流程是什么?
学习框架
软件测试 = 测试基础 → V&V → 审查 vs 测试 → 测试阶段 → 测试技术 → TDD → 发布/用户测试一句话总结:测试只能证明程序有错,不能证明程序无错;V&V 的核心是“做对的事”和“把事做对”。
8.1 测试基础
原话:
测试的目的是试图说明一个程序可以做我们期望它所做的工作和在投入使用之前发现程序的缺陷。当测试软件时,使用人工数据来执行这个程序。我们审查测试运行的结果,找出关于程序的非功能属性的错误、异常和其他信息。测试只能证明存在错误,而不能证明其不存在。测试是一个软件更广泛的检验和有效性验证过程的一部分。
测试的目标:
- 向开发者和用户展示软件满足了需求。
- 对于定制软件,这意味着对用户和系统需求文档中的每个需求至少要有一个测试。
- 对于通用软件产品,这意味着要对每个集成在产品发布版本中的所有系统特征以及这些特征的组合进行测试。
- 为了找出软件中的缺陷与不足,即软件的行为是不正确的、所不希望的或不符合它的描述的。
- 缺陷测试关心的是找出所有不希望出现的系统的行为根源。例如系统崩溃,与其他系统的不期望的交互,不正确的计算和数据毁坏。
8.2 V&V:验证与确认
Verification vs Validation 一句话记忆:
| Verification(验证/检验) | Validation(确认/有效性验证) | |
|---|---|---|
| 核心问题 | Are we building the product right? | Are we building the right product? |
| 关注点 | 是否符合规格说明 | 是否满足用户真正需求 |
| 本质 | 把事情做对 | 做对的事情 |
原话:
在软件测试中不仅要检查程序是否出错、程序是否和软件产品的设计规格说明书一致,而且还要检验所实现的正确功能是否就是客户或用户所需要的功能。两者缺一不可。这两部分活动构成了一个完整的测试活动。这就是软件测试中有名的 V&V,即 Verification 和 Validation。
Verification(验证/检验):
检验或验证软件是否已正确地实现了产品规格书所定义的系统功能和特性。检验过程提供证据表明,软件相关产品与所有生命周期活动(需求分析、设计、编程、测试等)的要求(如正确性、完整性、一致性、准确性等)相一致。检验是否满足生命周期过程中的标准、实践和约定;检验为判断每一个生命周期活动是否已经完成,以及是否可以启动其他生命周期活动建立一个新的基准。
在 ISO9000 中,“检验”的严格定义是:检验是通过检查和提供客观证据,表明规定要求已经满足的认可。“检验”强调的是“规定规格要求”。
Validation(确认/有效性验证):
这种有效性验证要求更高,要能保证所生产的软件可追溯到用户需求的一系列活动。验证过程提供证据,表明软件是否满足客户需求(指分配给软件的系统需求),并解决了相应问题。
在 ISO9000 中,“验证”的严格定义是:验证是通过检查和提供客观证据,表明一些针对某一特定预期用途的要求已经满足的认可。“验证”强调的是“预期用途的要求”。
区别与联系:
检验(Verification)是检验开发出来的软件产品和设计规格书的一致性,即是否满足软件厂商的生产要求。但设计规格书本身就可能有问题、存在错误,所以即使软件产品中某个功能实现的结果和设计规格书完全一致,但所设计的功能不是用户所需要的,依然是软件严重的缺陷。因为设计规格书很有可能一开始就对用户的某个需求理解错了,所以仅仅进行检验(Verification)测试还是不充分的,所以还需要进行性验证(Validation)测试。验证(Validation)就是检验产品功能的有效性,即是否满足用户的真正需求。
Boehm 名句:
- Verification:Are we building the product right?(是否正确地构造了软件?)
- Validation:Are we building the right product?(是否构造了正确的软件?)
V & V 的信心:
V & V 的最终目的是建立信心,即这个软件系统是“达到目的”的。这种对系统的信心水平取决于系统的目的、系统用户的期望和系统当前市场环境。
- 软件目的:信任程度取决于软件对组织的关键程度。
- 用户期望:用户对一些软件的质量抱有很低的期望。
- 市场环境:较早的投入市场比找到程序中的缺陷更重要。
8.3 审查 vs 测试
静态 V&V 技术目标:
系统需求、设计模型、程序的源代码、建议的系统测试。
原话:
| 方式 | 特点 |
|---|---|
| 软件审查 | 关注静态系统表示的分析发现问题。可以由基于工具的文档和代码分析进行补充。检查所有的可读的软件表示,目的是发现是否有异常和缺陷。审查不要求执行软件。分析和审查系统需求、设计模型、程序的源代码。对于发现程序错误是一个有效的技术。 |
| 软件测试 | 关注训练和观察产品行为(动态验证)。系统使用测试数据执行,并观察其操作行为。 |
审查优点:
- 在测试期间,一个错误可能会掩盖其他错误。审查是一个静态过程,我们不必关心错误之间的相互作用。所以,一个单个审查阶段可以发现系统中的许多错误。
- 审查一个系统的不完整版本不需要额外的代价。如果一个程序不完整,我们需要开发特殊测试工具来测试可用的部分。
- 除了搜索程序缺陷,审查可以考虑程序的其他质量属性,如可移植性和可维护性。
审查和测试的关系:
审查和测试是互为补充的,不是对立的验证技术。在 V&V 过程中,审查和测试都应该使用。审查可以检查是否和需求描述一致,但无法检查是否是用户真正的需求。审查无法检查非功能需求,例如性能、可用性等。
8.4 测试阶段
测试阶段一句话记忆:
| 阶段 | 谁来做 | 测什么 | 关键目标 |
|---|---|---|---|
| 开发测试 | 开发团队 | 单元、组件、系统 | 发现缺陷 |
| 发布测试 | 独立测试团队 | 完整系统 | 证明可发布 |
| 用户测试 | 用户/客户 | 真实环境 | 确认可用 |
原话:
| 阶段 | 说明 |
|---|---|
| 开发测试 | 即在开发中进行系统测试来发现故障和缺陷。在测试过程中,其很可能涉及系统设计师和程序员。 |
| 发布测试 | 即一个测试小组对一个系统的完整版本进行测试,然后发布给用户。发布测试的目的是审查系统是否满足系统信息持有者的需求。其涉及到测试人员。 |
| 用户测试 | 即系统的一个用户或潜在的用户在他们自己的环境中测试这个系统。 |
8.5 开发测试
原话:
开发测试包括系统开发团队所进行的所有测试活动。在开发过程中,可在 3 个粒度级别进行测试:
- 单元测试,即对单独的程序单元或对象类进行测试。单元测试应该着重测试对象或方法的功能。
- 组件测试,即将多个程序单元整合创建一个合成的组件。组件测试应该着重测试组件的接口。
- 系统测试,即集成系统中的一些和所有的组件作为一个整体进行测试。系统测试应该着重测试组件的交互。
8.5.1 单元测试
原话:
单元测试是测试程序组件的过程。测试程序组件:方法、对象类。单元测试是一个缺陷测试过程。单元可能是:对象内单个函数或方法;带有若干属性和方法的对象类;带有定义好的接口的组件。
对象类测试的完整覆盖:
- 测试与对象相关的所有操作。
- 设置和检查与对象相关的所有属性。
- 让对象处于所有可能的状态下,就是说要模拟所有能改变对象状态的事件。
- 泛化或继承使得对象类测试更加复杂,因为要测试的信息未被本地化。
气象站测试示例:
需要定义 reportWeather、Configuring、Testing、Running 和 Shutdown 测试用例。使用状态模型,识别要测试的状态转换序列和引起这些转换的事件序列。例如:
- Shutdown -> Running -> Shutdown
- Configuring -> Running -> Testing -> Transmitting -> Running
- Running -> Collecting -> Running -> Summarizing -> Transmitting -> Running
自动化测试:
只要有可能,单元测试应该是自动化的,以便在没有人为干预的情况下运行和检查测试。在自动化单元测试中,应该充分利用测试自动化框架(如 JUnit)来编写和运行程序测试。单元测试框架提供了一个通用的测试类,我们只需要扩展它来形成新的测试案例。可以运行已经实现的所有测试,然后通过一些图形用户界面来报告测试成功与否。
自动测试组件:
- 准备部分:用测试用例初始化系统,即输入和期望的输出。
- 调用部分:调用所要测试的对象或方法。
- 断言部分:比较调用的结果和预期的结果是否相同。如果断言取值为真,那么测试成功;如果断言取值为假,那么测试失败。
8.5.2 测试用例选择
原话:
测试非常昂贵和费时,所以应该选择有效的单元测试。有效性意味着:测试案例应该表明,当按照预期的方式使用时,所测试的组件能够像假设的那样去执行。如果在组件中有缺陷,这些缺陷应该能被测试案例发现。
两种类型的测试用例:
- 第一种应该能够反映一个程序的正常操作,并且应该能显示出组件工作正常。
- 另外一种测试用例应该建立在对通常问题的经验基础上。它应该使用非正常的输入来检测是否得到了正常处理,或者是否使得组件崩溃。
测试策略:
- 划分测试:识别具有共同特征和以同样的方法处理的一组数据。我们应该从这些组中选择测试数据。
- 基于准则的测试:使用测试准则来选择测试案例。这些准则反映了程序员在开发组件时对经常犯的各种错误的经验。
等价类划分:
程序的输入数据和输出结果总是落在几个不同且具有共同特征的类中。这些类中的每一个都是一个等价划分或域,其中程序以相同的方式对每个类成员进行操作。应从每个划分或域中选择测试用例。
示例:
假设某个程序描述声明程序接受 4~10 个输入值,它们都是五位的大于 10000 的整数。请使用上述信息来辨别输入划分和可能的测试输入值。
测试准则(以序列为例):
- 用一个只有单个值的序列来测试程序。
- 在不同的测试中使用不同规模的多个序列。
- 导出一个测试,让第一个、中间一个和最后一个元素得到测试。
- 用零长度序列测试。
Whittaker 等人的测试准则:
- 选择能够迫使系统产生所有错误信息的输入。
- 设计能够使系统的输入缓冲溢出的输入。
- 重复相同的输入或一系列输入很多次。
- 迫使产生无效的输出。
- 迫使输出结果太大或太小。
8.5.3 组件测试与接口测试
原话:
软件组件通常是由许多彼此交互的对象组合的复合组件。例如,在气象站系统中,再配置组件包括处理再配置各个方面的各对象。我们可以通过它们定义的接口来访问它们的功能。那么测试这些组件时,我们首先关心的就是测试组件的接口行为是否符合它们的描述。我们可以假定组件的单个对象的单元测试已经完成。
接口测试:
目的是检测由于接口错误或关于接口的无效假设引起的故障。
接口类型:
- 参数接口:主要是数据和函数指针,由一个组件传递到另一个组件。对象中的方法有一个参数接口。
- 共享内存接口:内存块由程序或函数所共享。
- 程序接口:子系统封装了一组由其他子系统调用的过程。
- 消息传递接口:子系统通过消息传递来请求其他子系统上的服务。
常见接口错误:
- 接口误用:调用者组件在调用其他组件时接口使用不当而产生接口错误,比如参数类型不对、传递顺序不对、参数数量不对等。
- 接口误解:调用者组件误解了被调用组件的接口描述而产生接口错误,对被调用组件行为进行了错误的假设。(如二分查找、调用的数组未排序)
- 时序错误:数据的生产者和消费者可能有不同的速度。由于生产者来不及更新共享接口信息造成消费者访问了过期的数据。
接口测试的一般准则:
- 设计一组测试集,使得对传给外部组件的参数的选择紧靠取回范围边缘的那些值。
- 当有指针从接口传递时,总用空指针参数来测试接口。
- 当组件通过程序接口被调用时,设计一些容易引起组件失败的测试。
- 在消息传递系统中进行强度测试。
- 在组件间通过共享内存来交互时,可以设计一种测试,使其对激活组件的次序有所改变。这些测试可以暴露程序员对共享数据的产生和消费顺序所进行的隐式的假设。
8.5.4 系统测试
原话:
系统测试包括集成组件来形成一个新版本的系统,然后测试集成后的系统。系统测试的重点是测试组件之间的相互作用。系统测试检查组件是否兼容、正确交互、并在正确的时间跨接口传输正确的数据。系统测试测试系统的紧急行为。
系统测试 VS 组件测试:
在系统测试期间,单独开发的可复用组件和现成的系统可能会与新开发的组件集成到一起,然后对完整的系统测试。不同小组成员和群组开发的组件可能在这个阶段集成。系统测试是一个集体的过程而不是一个独自的过程。在一些公司中,系统测试可能由一个单独的测试小组执行,没有设计人员和程序员的参与。
用例测试:
开发用于识别系统交互的用例可以用作系统测试的基础。每个用例通常涉及几个系统组件,因此测试用例会强制发生这些交互。与用例关联的序列图记录正在测试的组件和交互。
测试准则:
彻底的系统测试是不可能的,因此可以开发定义所需系统测试覆盖范围的测试策略。测试准则举例:
- 所有的能从菜单中得到的系统功能都应该被测试到。
- 可以从同一个菜单中访问的组合功能需要被测试。
- 在提供用户输入的地方,所有的功能都必须对正确的和不正确的输入进行测试。
综合测试准则(Myers):
- 在任何情况下都必须使用边界值分析方法,经验表明用这种方法设计出测试用例发现程序错误的能力最强。
- 必要时用等价类划分方法补充一些测试用例。
- 用错误推测法再追加一些测试用例。
- 对照程序逻辑,检查已设计出的测试用例的逻辑覆盖程度。如果没有达到要求的覆盖标准,应当再补充足够的测试用例。(覆盖度方法)
- 如果程序的功能说明中含有输入条件的组合情况,则一开始就可选用因果图法。
8.6 测试驱动开发(TDD)
原话:
测试驱动开发,英文全称 Test-Driven Development,简称 TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。
基本过程:
- 快速新增一个测试;
- 运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过;
- 做一些小小的改动,尽快地让测试程序可运行,为此可在程序中使用一些不合情理的方法;
- 运行所有的测试,并且全部通过;
- 重构代码,以消除重复设计,优化设计结构。
简单来说,就是不可运行/可运行/重构——这正是测试驱动开发的口号。
TDD 优势:
- 代码覆盖:原则上,我们所写的每个代码片段都至少有一个测试。
- 回归测试:随着程序的开发,测试套件也增量式的开发出来。
- 简化调试:当测试失败时,需要明确问题出在哪里。新编写的代码需要检查和修改。
- 系统文档:测试本身就表现为一种文档形式,它描述了代码应该做什么。
TDD 劣势:
- 开发者可能只完成满足了测试的代码,而忽略了对实际需求的实现。
- 会放慢开发实际代码的速度,特别对于要求开发速度的原型开发造成不利。
- 对于 GUI、资料库和 Web 应用而言,构造单元测试比较困难。如果强行构造单元测试,反而给维护带来额外的工作量。
- 使得开发更为关注用例和测试案例,而不是设计本身。
回归测试:
回归测试是一种系统测试,其检查更改没有“破坏”以前的工作代码。在手动测试过程中,回归测试是昂贵的,但通过自动化测试,它是简单直接的。每次对程序进行更改时,都会重新运行所有测试。测试必须在变更提交之前成功运行。
8.7 发布测试
原话:
发布测试是为开发组以外的用户使用的系统的一个特殊版本所做的测试过程。发布测试的主要目标是增加供应商对系统良好的使用信心。因此发布测试必须证明系统具有指定的功能、性能和可依赖性,在常规操作下不会出错。发布测试通常是一个黑盒测试过程,测试从系统描述导出。
发布测试 VS 系统测试:
发布测试是系统测试的一种形式。重要区别:一个未参与系统开发的独立团队,应负责发布测试。开发团队的系统测试应着重于发现系统中的缺陷(缺陷测试)。发布测试的目的是检查系统是否满足其需求,并且是否足可以对外销售(有效性验证测试)。
8.7.1 基于需求的测试
原话:
好的需求工程实践的一个总的原则是需求应该是可测试的。对于每一个需求,得到它的一组测试。有效性测试。
MHC-PMS 过敏警告示例:
- 需求:如果知道一个病人对任何特殊药物过敏,那么有该药物的处方将导致向系统用户发出警告消息。如果开处方的医生选择忽略过敏警告,那么医生应当提供一个忽略的原因。
- 测试设计:
- 建立一个没有已知过敏史的病历记录。开一个会出现某已知过敏症的处方。检查系统没有发出警告信息。
- 检查一个有已知过敏史的病历记录。开一个会出现病人对此过敏的处方,并检查系统是否发出警告信息。
- 建立一个病历记录,其中包括两个或两个以上过敏药物。开两个处方分别带有其中一种过敏药物,并检查系统为每种药物发出了正确的警告信息。
- 为病人开带有两个过敏药物的处方。
- 开一个带某种过敏药物的处方,这种药物发出警告,医生拒绝了这个警告。检查系统允许用户提供信息解释警告被拒的原因。
8.7.2 情景测试
原话:
设计典型的使用场景并使用这些来为系统开发测试用例。
MHC-PMS 场景示例:
- 通过登录到系统进行身份验证。
- 将指定病人记录下载并上传到笔记本电脑。
- 家庭出诊时间安排。
- 在移动设备上加密和解密患者记录。
- 记录检索和修改。
- 与维护副作用信息的药物数据库的链接。
- 电话提示系统。
8.7.3 性能测试
原话:
测试总体特性,如性能和可靠性。既关心对系统满足其需求的证明,也要关心发现系统的问题和缺陷。压力测试:超出边界。基于处理器网络的分布式系统。
8.8 用户测试
原话:
用户或客户测试是测试过程的一个阶段,在这个阶段,用户或客户在系统测试中提供输入和建议。用户测试是必不可少的,即使是进行了全面系统测试与发行测试的系统。其原因是用户工作环境的影响对系统的可靠性、性能、可用性和鲁棒性有重大影响。这些不能在测试环境中复制。
用户测试类型:
| 类型 | 说明 |
|---|---|
| Alpha 测试 | 软件的用户与开发小组一起在开发者的地点测试。 |
| Beta 测试 | 用户在真实环境中使用软件产品。 |
| 接收测试 | 定制系统开发的一个固有的部分。 |
接收测试过程的 6 个阶段:
- 定义接收准则
- 计划接收测试
- 导出接收测试
- 运行接收测试
- 协商测试结果
- 拒绝/接收系统
敏捷方法与接收测试:
在敏捷方法中,用户/客户是开发团队的一部分,负责决定系统的可接收性。测试由用户/客户定义,并与其他测试集成,因为它们在进行更改时自动运行。没有单独的接收测试过程。这里的主要问题是嵌入式用户是否“典型”,是否可以代表所有系统利益相关者的利益。
8.9 要点
原话:
- 测试只能证明系统中存在错误。它不能说明系统中不再有错误。
- 开发测试时软件开发小组的责任。独立的测试小组应该在系统发布给客户之前负起测试系统的责任。在用户测试过程中,客户或系统使用者提供测试数据,以及确定测试成功与否。
- 开发测试包括单元测试、即测试单个对象和方法;组件测试,即测试相关的一组对象;系统测试,即测试部分或完整的系统。
- 测试软件时,我们应该根据经验和原则把软件打散来选择测试用例的类型,选择那些在其他系统中已经证明是能有效地发现缺陷的那些测试用例类型。
- 有可能的话,我们应该编写自动化测试。将这个测试嵌入到程序中,这样的程序可以每次当系统一有变化时就运行。
- 测试优先开发时开发的一种方法在代码测试之前就已经写好测试。
- 情景(脚本)测试是非常有用的,因为它可以复制系统的使用。它包括设计一种典型的使用场景,并以此来导出测试用例。
- 接收测试是一个用户的测试过程,目的是决定这个软件是否足以进行部署,以及能否用在它的实际操作环境中。
过渡:软件交付给用户后,需求、环境、技术都在变化。软件进化研究如何让系统在生命周期内持续可用、可改、可维护。
九、软件进化
本章问题链
- 软件为什么必须不断进化?
- Lehman 定律告诉我们什么?
- 软件维护有哪些类型?维护成本为什么高?
- 再工程和重构有什么区别?
- 遗留系统应该如何管理?
学习框架
软件进化 = 变更不可避免 → Lehman 定律 → 维护 → 再工程/重构 → 遗留系统管理一句话总结:软件交付不是终点,维护往往比开发更贵;重构和再工程是控制进化成本的关键手段。
9.1 变更不可避免
原话:
当系统投入使用后,新的需求就会出现;业务环境变化;错误必须修补;新的设备要加入到系统中去;系统的性能或可靠性必须要加以提高。对所有机构来说,一个关键的问题是对现存系统的变更进行实现和管理。
9.2 进化的重要性
原话:
一些机构在软件系统上已经投入了很多,所以他们属于非常重要的资产。为了维护它们的资产价值,必须对之进行修改和更新。大多数大型公司在维护系统上的开支要比在系统开发上的多很多。
9.3 开发和进化的螺旋模型
软件开发与进化应当是一个集成的、完整的、增量式的过程,可用螺旋模型表示。
9.4 运行状态分类
| 状态 | 说明 |
|---|---|
| 进化(Evolution) | 软件正常工作,新需求提出,新功能实现 |
| 服务(Service) | 修正 Bug,维持正常工作,不再增加新功能 |
| 淘汰(Phase-out) | 能正常工作,但不再修改 |
9.5 软件进化过程
原话:
软件进化过程依赖于所维护的软件的不同类型、使用的开发过程、参加人员的技术和经验。变更建议都是系统进化的动力。这些变更建议包括在发布的版本中还没有实现的已有需求、新的需求请求、系统所有者的补丁要求和来自系统开发团队的改进软件的想法和建议。变更识别的过程和系统进化是循环和持续的,并且贯穿于系统的生命周期。
变更实现:
变更实现的过程可以看成是一个开发过程的迭代过程。在此迭代过程中完成对系统修改版本的设计、实现和测试。只是,这里的一个重要区别是变更实现的最初阶段是对程序的理解,特别是当源系统的开发者不承担变更实现的责任时。在这个阶段,你要了解程序是怎样构造和实现它的功能的。在实现一个变更时,我们要利用以上的理解来确保实现的变更不会对现有系统产生不利影响。
紧急变更请求:
在不经过软件工程过程的所有阶段的情况下,必须执行紧急更改。如果有严重的系统缺陷发生,那么必须修补而使正常的操作继续;如果操作系统环境的变更中有不期望的情况发生,那么就会破坏正常的操作;如果系统上运行的业务有未预料到的改变发生,比如有新的竞争对手出现或者有新的法律生效并影响了系统。
敏捷方法和进化:
敏捷方法基于增量开发,因此敏捷开发向交付后的进化转变是无缝衔接的。进化就是简单的继续进行基于快速发布的敏捷开发过程。自动回归测试在变更时特别有价值。变更可以表示为额外的用户故事。
移交问题:
- 开发团队运用了敏捷方法,但是进化团队却不熟悉敏捷方法而选择了一个计划驱动的方法。进化团队可能期望详细的文档来支持进化工作的进行,而这恰恰是敏捷方法所没有提供的。
- 当计划驱动的方法被用于开发时,而进化团队选择使用敏捷方法。这种情况下,进化团队可能不得不从头开发自动化的测试,而且不会有像敏捷开发过程中所期待的系统代码重构和简化。
9.6 Lehman 定律
原话:
程序的动态进化特性就是对系统变更的研究。Lehman 等人提出关于系统变更的 Lehman(雷曼)定律。Lehman 定律可能适用于所有类型的大型软件系统(他们称为 E 类系统)。
原话:
在系统正在开发中,系统的需求可能会发生变化,因为环境正在改变。因此交付的系统将不能满足其要求!系统与其环境紧密结合。当系统安装在环境中时,会更改环境,从而改变系统需求。如果要保持系统在环境中仍然有用,则必须更改系统。变更是不可避免的。
Lehman 定律:
- 持续变更:程序必须变化,否则会越来越没用。
- 复杂度增加:随着变更,结构会变得越来越复杂,需要额外资源。
- 大型程序进化规律:进化过程是自调节的,系统属性(大小、版本间隔、报告错误数)每版大致稳定。
- 组织稳定性:开发速率几乎恒定,与投入资源关系不大。
- 熟悉度守恒:每版增量变更量大致恒定。
- 持续增长:功能必须持续增长以维持用户满意度。
- 质量下降:除非系统适应变化,否则质量会下降。
- 反馈系统:进化是多主体、多回路的反馈系统,必须作为反馈系统来管理。
Lehman 定律的适用性:
Lehman 定律似乎普遍适用于由大型组织开发的大型定制系统。不清楚如何为以下系统修改它们:收缩软件产品;包含大量 COTS 组件的系统;小组织;中型系统。
9.7 软件维护
软件维护类型一句话记忆:
| 类型 | 触发原因 | 例子 |
|---|---|---|
| 改正性维护 | 发现缺陷 | 修复 Bug |
| 适应性维护 | 环境变化 | 操作系统升级、硬件更换 |
| 完善性维护 | 业务变化 | 增加新功能、优化体验 |
原话:
软件交付后,继续修改程序。软件维护通常用于用户定制软件。通用软件一般称为进化或发布新版本。维护一般不涉及系统架构的重大修改。通过改变现有组件或添加新组件的方式实现变更。
三种类型:
| 类型 | 说明 |
|---|---|
| 改正性维护 | 修改软件中的缺陷,包括代码错误、设计错误、需求错误 |
| 适应性维护 | 使软件适应不同的操作环境,包括硬件变化、操作系统变化、其他支持软件的变化 |
| 完善性维护 | 增加或修改系统功能,原因:机构因素或业务因素的改变 |
维护成本:
- 通常高于开发成本(2 到 100 倍)。
- 受到技术成本和非技术成本的影响。
- 成本随着维护的进行增加,维护会破坏软件结构,使得进一步维护更难。
- 敏捷软件维护成本高。
影响维护成本的因素:
- 团队稳定性:如果团队成员是相同的工作人员,那么维护成本就会降低。
- 糟糕的开发实践:一个系统的开发人员可能没有维护的合同责任,所以没有动力为未来的变化做出设计。
- 人员技术水平:维护人员一般都缺乏经验,而且不熟悉应用领域。
- 程序结构和年龄:随着程序年龄的增长,它们变得越来越不容易理解和变更。
9.8 维护预测
原话:
维护预测关注评估系统的哪部分可能会导致问题,并具有较高的维护成本。变更接受程度取决于变更影响因素的可维护性;实施变更会降低系统的性能并降低其可维护性;维护成本取决于变更的数量,变更的成本取决于可维护性。
变更预测:
预测变更的数目需要了解系统和外部环境之间的关系。环境变化时,紧耦合系统需要变更。要对系统与环境之间的关系作出判断,应该评估以下几点:
- 系统接口的数目和复杂性;
- 固有的易变性系统需求的数目;
- 系统所处的业务过程。
复杂性度量:
可以通过评估系统组件的复杂性来预测可维护性。研究表明,大多数维护工作花费在相对较少数量的系统组件上。复杂性取决于:控制结构的复杂性;数据结构的复杂性;对象、方法(过程)和模块大小。
过程度量:
- 请求纠正性维护的数目;
- 作用分析所需的平均时间;
- 实现一个变更请求的平均时间;
- 突出的变更请求的数目。
如果任何一个或所有指标在增加,则可能表明可维护性的下降。
9.9 再工程
原定义:
重构或重组遗留系统的部分模块,不改变其功能。并非所有子系统都需要频繁维护,只有部分子系统需要频繁维护。再工程包括对系统重建文档、重构系统体系结构、用一种更先进的程序设计语言转换系统、修改和更新系统的数据结构和系统的数据价值。
再工程的好处:
- 降低风险:新的软件开发风险很高。可能存在开发问题、人员配置问题和规格问题。
- 降低成本:再工程的成本通常远远低于开发新软件的成本。
再工程活动:
- 源代码转换:改变程序的设计语言。
- 反向工程:分析程序,找出系统的组织结构和功能。
- 程序结构改善:对程序的控制结构进行分析和修改,使它更容易阅读和理解。
- 程序模块化:对程序结构进行再组织。
- 数据再工程:清理和重组系统数据。
再工程成本因素:
- 再工程的软件的质量。
- 可用于再工程的工具支持。
- 所需数据转换的范围。
- 再工程的专家工作人员的可获取性。
9.10 重构
原定义:
重构是提升程序以减缓其由于更改而退化的过程。可以将重构视为“预防性维护”,以减少未来变更的问题。重构涉及修改程序以改善其结构,降低其复杂性或使其更容易理解。当您重构程序时,您不应该添加功能,而应专注于程序改进。
重构与再工程的区别:
- 再工程:发生在系统已经维护了一段时间,且维护费在不断上升的情况下。使用自动化工具来处理并再工程一个遗留系统,产生一个更具可维护性的新系统。
- 重构:是一个连续不断的改进过程,它贯穿于开发和进化的整个过程。重构是要避免导致成本上升和维护困难的结构以及代码的退化问题。
常见代码坏味道:
- 冗余代码:在程序的不同地方相似的代码可能重复出现很多次。可以通过删除和用一个方法或功能区实现。
- 长方法:如果方法太长了,那么这个方法应当被重新设计成几个较短的方法。
- 选择语句:这种情况常常牵扯到重复,因为选择语句 switch 依靠的是同一个值的不同类型。选择语句可能分散在程序的各个地方。在面向对象语言中,常常可以通过多态性来实现同一个事情。
- 数据聚集:当同样的一组数据项(类中的域、方法中的参数)在程序的不同地方重复出现时,可以通过用一个对象封装所有数据来解决。
- 假设的一般性:开发者为了以后万一使用到在程序中所包含的一般性。这通常可以简单地删除掉。
9.11 遗留系统管理
原话:
组织依赖于遗留系统,则必须选择一个策略来进化这些系统。策略的选择应该取决于系统的质量和它的商业价值。
管理策略:
| 质量\价值 | 高价值 | 低价值 |
|---|---|---|
| 高质量 | 继续维护 | 可以维护,也可抛弃 |
| 低质量 | 有待于转换,或用合适的系统代替 | 抛弃系统 |
评估维度:
-
业务价值评估:
- 应考虑不同的观点:系统终端用户、商务客户、线路经理、IT 经理、高级管理人员。
- 访问不同的利益相关者并整理结果。
-
业务价值评估所要讨论的问题:
- 系统的使用:如果系统只是偶尔被使用或被很少一部分人使用,那么其含有较低的业务价值。
- 支持的业务流程:如果强制使用低效率的业务处理系统,则系统可能具有较低的业务价值。
- 系统的可靠性:如果系统不可靠,而且问题直接影响到企业客户,则系统的业务价值很低。
- 系统的输出:若业务依赖于系统的输出,那么系统的业务价值较低。
-
系统质量评估:
- 业务流程评估:业务流程如何支持当前业务目标?
- 环境评估:系统的环境有多好?维护成本如何?
- 应用程序评估:应用软件系统的质量怎么样?
-
业务流程评估问题:
- 是否有定义的过程模型,是否遵循?
- 组织的不同部分对相同的功能使用不同的进程?
- 该过程如何适应?
- 与其他业务流程的关系是什么?
- 该流程是否得到遗留系统的有效支持?
-
应用评估定量数据:
- 系统变更请求的数量;
- 系统使用的不同用户界面的数量;
- 系统使用的数据量。
9.12 要点
原话:
- 软件开发与进化应当是一个集成的、完整的、增量式的过程,它可用螺旋模型表示。
- 对于定制系统来说,软件维护的费用一般超过了软件开发的费用。
- 软件进化过程是由变更请求驱动的,包括变更影响分析、版本规划和变更实现。
- Lehman 定律,例如变更是持续的观点,描述了许多来自于系统进化长期研究的领悟。
- 软件维护有 3 种基本类型:修改软件中的缺陷、使软件适应不同的操作环境,以及向系统中添加或修改功能。
- 软件再工程的目标在于改善系统结构和文档,使其更容易理解和变更。
- 重构是预防性维护的一种形式,其在保留系统功能的前提下更改程序。
- 应该对遗留系统的业务价值、应用软件的质量以及应用软件的环境进行评估,然后才能决定是否更换、转换和维护系统。
过渡:技术活动(需求、设计、实现、测试、维护)需要在时间、成本、质量、范围之间取得平衡。项目管理就是协调这些约束的学问。
十、软件项目管理
本章问题链
- 项目管理的四大约束是什么?风险如何识别、评估和应对?
- 成本估算有哪些方法?LOC、FP、COCOMO、PERT 怎么用?
- 进度管理有哪些工具和方法?关键路径、PERT、赶工、快速跟进有什么区别?
- 质量管理包括哪些活动?QA 和 QC 有什么区别?
- 质量成本如何度量?
学习框架
软件项目管理 = 风险管理 → 成本管理 → 进度管理 → 质量管理一句话总结:项目管理是在范围、时间、成本、质量之间做权衡,用计划和控制降低不确定性。
10.1 风险管理
10.1.1 风险概念
原话:
软件项目中的风险包括:不断变换的需求、低劣的计划和估算、不可信赖的承包人、欠缺的管理经验、人员问题、技术失败、政策的变化、性能欠佳等。
Rothfeder 1988:对 600 家成功的公司调查,35% 有项目失控的经历。Jones 1991:大型项目按时完成的概率几乎为 0,被取消的概率与赌博一样。Tom Gilb:如果你不主动地击败风险,他们就会主动击败你的。
原定义:
三要素:
- 一个事件
- 事件发生的概率
- 事件的影响
风险分类(按范围):
- 商业风险
- 惯例风险
- 人员风险
- 技术风险
- 开发环境风险
- 客户风险
- 产品风险
- 过程风险
风险分类(按可预测性):
- 已知-已知(Known-knowns)
- 已知-未知(Known-unknowns)
- 未知-未知(Unknown-unknowns)
风险的基本性质:
- 客观性
- 不确定性
- 不利性
- 可变性
- 相对性
- 同利益的对称性
10.1.2 风险管理过程
- 风险识别
- 风险评估
- 风险规划
- 风险控制
10.1.3 风险识别
原话:
风险识别是试图通过系统化地确定对项目计划的威胁,识别已知和可预测的风险。
方法:
- 德尔菲方法
- 头脑风暴法
- 情景分析法
- 面谈法
- 风险条目检查表
风险条目检查表:
检查表法是利用检查表作为风险识别的工具。检查表法是根据风险要素建立软件项目的风险条目列表。列表中列出所有与风险因素有关的提问。可以使管理者集中识别常见的类型中的已知和可预测的风险。有研究表明:IT 项目常常存在一些共同的风险源。
检查表风险识别类型域:
- 产品规模
- 商业影响
- 客户相关
- 过程定义
- 开发技术
- 开发环境
- 人员数目及经验
SEI 风险识别检查表三大领域:
-
Product Engineering:
- Requirements:Stability, Completeness, Clarity, Validity, Feasibility, Precedent, Scale
- Design:Functionality, Difficulty, Interfaces, Performance, Testability, Hardware Constraints, Non Developmental software
- Code and Unit Test:Feasibility, Testing, Coding/Implementation
- Integration and Test:Environment, Product, System
- Engineering Specialties:Maintainability, Reliability, Safety, Security, Human Factors, Specification
-
Development Environment:
- Management Process:Planning, Project Organization, Management Experience, Program Interfaces, Management Methods, Monitoring, Personnel Management, Quality Assurance, Configuration Management
- Development process:Formality, Suitability, Process Control, Familiarity, Product control
- Development System:Capacity, Suitability, Usability, Familiarity, Reliability, System Support, Deliverability
- Work Environment:Quality Attitude, Cooperation, Communication, Morale
-
Program Constraints:
- Resources:Schedule, Staff, Budget, Facilities
- Contract:Type of Contract, Restriction, Dependence
- Program Interfaces:Customer, Associate Contractors, Subcontractors, Prime Contractor, Corporate Management, Vendors, Politics
基于生存期的检查表:初始阶段、设计阶段、实施阶段、收尾阶段。
10.1.4 风险评估
原话:
确定风险发生概率的估计和评价,项目风险后果严重程度的估计和评价,项目风险影响范围的分析和评价,以及对于项目风险发生时间的估计和评价。
原话:
分析风险发生的概率,确定发生的可能性(P)。风险后果,发生后对项目目标的影响(I)。风险值,风险的严重程度 R = F(P, I)。
确定优先次序:按风险的严重性排序,确定最需要关注的 TOP 10 风险。
风险概率:
- 风险概率值:没有可能(0)到确定(1)
- 风险概率度量:高、中、低;极高、高、中、低、极低;不可能,不一定,可能和极可能等。
风险后果(影响):
- 风险后果:风险影响项目目标的严重程度,从无影响到无穷大。
- 风险后果度量:高、中、低;极高、高、中、低、极低;灾难,严重,轻微,可忽略等。
定性评估:高/中/低风险矩阵。
风险感知示例:
试验一:A:直接获得 100 美元;B:投掷一次硬币。如果是正面,你会获得 250 美元;如果背面,你必须支付 50 美元。
试验二:A:你原本有 500 美元,结果被别人强行拿走 100 美元;B:你原本有 400 美元,投掷一次硬币。如果正面,你会获得 200 美元;如果背面,你必须支付 200 美元。
定量评估方法:
- 盈亏平衡分析
- 模拟
- 访谈
- 决策树分析
- 量化风险条目检查表
访谈:
确定概率分布模型。领域专家访谈,信息采集。
正态分布进度风险评估:
- ±1SD ≈ 68.3%
- ±2SD ≈ 95.5%
- ±3SD ≈ 99.7%
- 例如:E = 13.5,SD = 1.07,T = E + 1SD = 14.5,P = 50% + 68.3%/2 = 85%
决策树分析与 EMV(期望货币价值):
损益期望值是决策树的一种计算值。根据风险发生的概率计算出一种期望的损益。例如:某行动方案成功的概率是 50%,收益是 10,EMV = 10 × 50% = 5。
EMV 公式:
EMV = Σ(概率 × 收益/损失)
课堂练习示例:
- 方案 1:随机投掷硬币两次,如果两次投掷的结果都是硬币正面朝上,获得 10 元;背面每朝上一次付出 1.5 元。EMV = 1。
- 方案 2:随机投掷硬币两次,付出 2 元;如果两次都是正面朝上,获得 10 元。EMV = 0.5。
10.1.5 风险规划策略
-
回避风险:
- 对所有可能发生的风险尽可能的规避,采取主动放弃或者拒绝使用导致风险的方案。
- 例如放弃采用新技术。
- 注意事项:对风险有足够的认识;当其他风险策略不理想的时候,可以考虑;可能产生另外的风险;不是所有的情况都适用的。
-
转移风险:
- 为了避免承担风险损失,有意识将损失或与损失有关的财务后果转嫁出去的方法。
- 例如:出售、分包、开脱责任合同、保险。
-
损失控制:
- 损失预防
- 损失抑制
-
自留风险:
- 由项目组织自己承担风险事故所致损失的措施。
- 类型:主动自留风险和被动自留风险;全部自留风险和部分自留风险。
人员频繁流动风险实例:
人员的频繁流动是一项风险,基于过去的历史和管理经验,频繁流动可能性的估计值为 70%,开发时间增加 15%,总成本增加 12%。为了缓解这一风险,项目经理应采取的策略:
- 与现有人员讨论人员流动的原因;
- 项目启动时,做好会出现人员流动的准备,采取一些技术以确保人员一旦离开后,项目仍然能继续;
- 建立良好的项目组织和通信渠道,以使大家能够了解每个有关的开发活动的信息;
- 指定文档标准并建立相应的机制,以保证文档能够及时建立;
- 对所有工作组织细致的评审,使大多数人能够按计划进度完成自己的工作。
10.1.6 风险管理计划
内容:
- 风险应对计划(top 10 清单)
- 岗位职责
- 时间
- 预算
- 追踪等等
风险管理推荐的措施:
- 软件项目计划包括风险管理计划。
- 任选风险管理负责人。
- 使用 TOP TEN 风险清单,主要的风险管理工具。
- 建立匿名风险汇报渠道。
风险、高风险、冒险:
风险范围的极限,计划过于紧张,就是冒险。风险计划可以帮助说明项目困境。避免冒险项目。
案例启发:
你是一个项目的项目经理,计划了一个项目需要 36 个月完成,然后提交领导,由于领导的原因导致项目的开始日期推延 2 个月,但是领导要求你按照计划完成,作为项目经理,你应该如何做?
10.2 成本管理
10.2.1 估算基础
原话:
估算不是很准确的,有误差的。经验(历史)数据非常重要。不要太迷信数学模型。
软件项目规模:
软件项目规模即工作量,是从软件项目范围中抽出的软件功能,然后确定每个软件功能所必须执行的一系列软件工程任务。包括:软件规划,软件管理,需求,设计,编码,测试,以及后期的维护等任务。
规模的单位:
- LOC(Line of Code):源代码程序长度的测量
- FP(Function Point):用系统的功能数量来测量
- 人月、人天、人年
软件项目成本:
完成软件规模相应付出的代价。待开发的软件项目需要的资金。人的劳动的消耗所需要的代价是软件产品的主要成本。
软件的规模和成本的关系:
规模是成本的主要因素,是成本估算的基础。有了规模就确定了成本。
10.2.2 估算输入与输出
估算输入:
- 项目需求、工作分解结构(WBS)
- 历史项目数据
- 资源要求(资源编制计划)
- 资源消耗率:如人员成本:100 元/小时
- 进度规划:项目总进度(一般是合同要求)
- 学习曲线
成本类型:
- 直接成本:与具体项目相关的成本。
- 间接成本:不能具体到某个项目中的成本,可以分摊到各个具体项目中的成本,例如:培训、房租水电、员工福利、市场费用、管理费、其他等等。
项目估算结果:
- 估算文件:资源、资源的数量、质量标准、估算成本等信息。单位:一般是货币单位。
- BAC(Budget At Completion,完成预算)
- 估算说明:工作范围、估算的基础和依据、估算的假设、估算的误差变动等。
估算说明:
- 预测所需要的总工作量的过程。
- 是一种量化的结果。
- 可以有一些误差。
- 成本估算不同于项目定价。
- 贯穿于软件的生存周期。
10.2.3 LOC 估算
原话:
代码行(LOC)从软件程序量的角度定义项目规模。要求功能分解足够的详细。有一定的经验数据(类比和经验方法)。与具体的编程语言有关。
优点:代码是所有软件开发项目都有的“产品”,而且很容易计算代码行数。
缺点:
- 对代码行没有公认的可接受的标准定义。
- 代码行数量依赖于所用的编程语言和个人的编程风格。
- 在项目早期,需求不稳定、设计不成熟、实现不确定的情况下很难准确地估算代码量。
- 代码行强调编码的工作量,只是项目实现阶段的一部分。
10.2.4 功能点(FP)估算
原话:
功能点(FP)用系统的功能数量来测量其规模。与实现产品所使用的语言和技术无关。两个评估:内部基本功能、外部基本功能。加权和量化。
公式:
FP = UFC × TCF
- UFC(未调整功能点计数):根据外部输入、外部输出、外部查询、外部文件、内部文件的数量和复杂度加权求和。
- TCF(技术复杂度因子):
TCF = 0.65 + 0.01 × ΣFi
其中 Fi 是 14 个技术因素(每个 0–5 分),TCF 范围 0.65–1.35。
14 个技术因素:
- 数据通信
- 分布式数据处理
- 性能
- 重度使用配置
- 事务率
- 在线数据输入
- 最终用户效率
- 在线更新
- 复杂处理
- 可复用性
- 安装容易程度
- 操作容易程度
- 多站点
- 变更容易程度
计算示例:
FP = UFC × TCF
UFC = 301
TCF = 0.65 + 0.01 × (14 × 3) = 1.07
FP = 301 × 1.07 = 322
10.2.5 其他估算方法
| 方法 | 说明 |
|---|---|
| 类比法(Analogy) | 估算人员根据以往的完成类似项目所消耗的总成本(或工作量),来推算将要开发的软件的总成本(或工作量),然后按比例将它分配到各个开发任务单元中。是一种自上而下的估算形式。 |
| 自下而上法(Bottom-up) | 利用任务分解结构图,对各个具体工作包进行详细的成本估算,然后将结果累加起来得出项目总成本。 |
| 参数法(Parametric) | 一种使用项目特性参数建立数据模型来估算成本的方法,是一种统计技术,如回归分析和学习曲线。 |
| 专家估算法 | 由多位专家进行成本估算,取得多个估算值,最后得出综合的估算值。 |
类比法使用情况:有类似的历史项目数据;信息不足(要求不是非常精确)的时候;在合同期和市场招标时。
类比法特点:简单易行,花费少;具有一定的局限性;准确性差,可能导致项目出现困难。
自下而上法使用情况:项目开始以后,WBS 的开发阶段;需要进行准确估算的时候。
自下而上法特点:相对比较准确,准确度来源于每个任务的估算情况;非常费时,估算本身也需要成本支持;可能发生虚报现象。
参数法使用情况:存在成熟的项目估算模型;应该具有良好的数据库数据为基础。
参数法特点:比较简单,而且也比较准确;如果模型选择不当或者数据不准,也会导致偏差。
Delphi 专家估算法:
- 组织者发给每位专家一份软件系统的规格说明和一张记录估算值的表格,请他们估算。
- 专家详细研究软件规格说明后,对该软件提出 3 个规模的估算值:最小 ai、最可能的 mi、最大 bi。
- 组织者对专家的表格中的答复进行整理,计算每位专家的 Ei = (ai + 4mi + bi) / 6。
- 综合结果后:E = (E1 + E2 + … + En) / N(N:表示 N 个专家)。
- 再组织专家无记名填表格,比较估算差,并查找原因。如果各个专家的估算差异超出规定的范围(例如:15%),则需重复上述过程,最终可以获得一个多数专家共识的软件规模。
Delphi 示例:
- 专家 1:1,8,9 => (1 + 9 + 4×8) / 6 = 7(万元)
- 专家 2:4,6,8 => (4 + 8 + 4×6) / 6 = 6(万元)
- 估算结果 = (6 + 7) / 2 = 6.5(万元)
估算方法总结:
- 初期:类比、专家估算
- 计划阶段:自下而上、参数模型
- 实施阶段(包括变更发生):自下而上、参数模型
成本估算方法综述:
主要考虑三种模型:类比法、自下而上法、参数法。自下而上法费时费力,参数法比较简单。自下向上法与参数法的估计精度相似。类比法通常用来验证参数法和自下而上法的结果。各种方法不是孤立的,应该注意相互的结合使用。
10.2.6 经验成本模型
通用形式:
E = A + B × S^C
其中 E 是以人月表示的工作量,A、B、C 是经验导出的系数,S 是主要的输入参数(通常是 LOC、FP 等)。
面向 LOC 驱动的模型:
- Walston-Felix(IBM):E = 5.2 × (KLOC)
- Balley-Basili:E = 5.5 + 0.73 × (KLOC)
- COCOMO:E = 3.2 × (KLOC)
- Doty:E = 5.288 × (KLOC)
面向 FP 驱动的模型:
- Albrecht and Gaffney:E = -12.39 + 0.0545FP
- Matson and Barnett:E = 585.7 + 15.12FP
建议掌握模型:IBM 模型、COCOMO 模型。
IBM 模型:
E = 5.2 × L^0.91 (L:KLOC,E:人月 PM)
D = 4.1 × L^0.36 (D:月)
S = 0.54 × E^0.6 (S:人)
DOC = 49 × L^1.01 (DOC:页)
IBM 模型示例:
采用 Java 完成项目,366 功能点,则
L = 366 × 46 = 16386 行 = 16.386 KLOC
E = 5.2 × 16.386^0.91 = 66 人月
DOC = 49 × 16.386^1.01 = 826 页
COCOMO 81 模型:
项目类型:
- 有机型(Organic):各类应用程序,例如数据处理、科学计算等。受硬件的约束比较小,程序的规模不是很大。
- 嵌入型(Embedded):系统程序,例如实时处理、控制程序等。紧密联系的硬件、软件和操作的限制条件下运行,软件规模任意。
- 半分离型(Semidetached):各类实用程序,介于上述两种软件之间,例如编译器(程序)。规模和复杂度都属于中等或者更高。
模型类别:
- 基本 COCOMO:静态单变量模型。
- 中等 COCOMO:基本模型基础上考虑影响因素,调整模型。
- 高级 COCOMO:中等 COCOMO 模型基础上考虑各个步骤的影响。
基本 COCOMO 公式:
E = a × (KLOC)^b
| 项目类型 | a | b |
|---|---|---|
| 有机型(Organic) | 2.4 | 1.05 |
| 半有机型 / 半分离型(Semidetached) | 3.0 | 1.12 |
| 嵌入型(Embedded) | 3.6 | 1.20 |
基本 COCOMO 示例:
一个 33.3 KLOC 的软件开发项目,属于中等规模、半有机型的项目:
a = 3.0,b = 1.12
E = 3.0 × 33.3^1.12 = 152 PM
中等 COCOMO 公式:
E = a × (KLOC)^b × 乘法因子
- a、b 是系数。
- 乘法因子是根据成本驱动属性打分的结果,对公式的校正系数。
- 成本驱动属性包括:产品属性、平台属性、人员属性、过程属性。
- 每个属性 Fi 的取值范围为:很低、低、正常、高、很高、极高,共六级。正常情况下 Fi = 1。
- 乘法因子 = F1 × F2 × … × Fn。
中等 COCOMO 示例:
一个 33.3 KLOC 的软件开发项目,半有机型:
a = 3.0,b = 1.12
乘法因子 = 0.70 × 0.85 × 1 × … × 1.15 = 1.09
E = 3.0 × 33.3^1.12 × 1.09 = 166 PM
高级(详细)COCOMO:
将项目分解为一系列的子系统或者子模型。在一组子模型的基础上更加精确地调整一个模型的属性。
10.2.7 实用软件估算模型
步骤:
- 对任务进行分解:1, 2, …, i, …
- 估算每个任务的成本 Ei
- 直接成本 = E1 + E2 + … + Ei + … + En
- 项目总估算成本 = 直接成本 + 间接成本
- 项目总报价 = 项目总估算成本 + 风险利润
- 风险利润 = 利润 + 风险基金 + 税
直接估算成本 Ei:
- 先估算规模 Qi,然后估算成本 Ei = Qi × 人力成本参数。
- 唯一估计值:Qi = Avg。
- PERT 算法:Qi = (Max + 4Avg + Min) / 6。
直接成本组成:
- 开发成本
- 管理成本
- 质量成本
例如:人力成本参数 = 2 万/人月,30 人月规模的项目的直接成本是 60 万。
开发(工作量)规模与管理、质量规模:
Scale(Dev)(单位:人月)
Scale(Mgn) = a × Scale(Dev) [a 为比例系数:例如 20%--25%]
直接成本 = Scale(Dev) + a × Scale(Dev)
间接成本估算:
间接成本 = 直接成本 × 间接成本系数
估算成本 = 直接成本 + 间接成本 = 直接成本 × (1 + 间接成本系数)
= 规模 × 人力成本参数 × (1 + 间接成本系数)
成本系数 = 人力成本参数 × (1 + 间接成本系数)
估算成本 = 规模 × 成本系数
例如:间接成本系数 = 0.3,成本系数 = 3 万/人月。
项目总报价:
项目利润 = 估算成本 × a%
风险基金 = 估算成本 × b%
税 = 估算成本 × c%(例如 c 为 5.5 左右)
项目总报价 = (a + b + c)% × 项目总估算成本 + 项目总估算成本
10.2.8 成本预算
原话:
成本预算是将项目的总成本按照项目的进度分摊到各个工作单元中去。成本预算将总的成本安排到各个任务中。成本预算的目的是产生成本基线。
分配类型:
- 分配资源成本:资源成本与资源的基本费率紧密相连。设置资源费率:标准费率、加班费率、每次使用费率等。
- 给任务分配固定资源成本:当一个项目的资源需要固定数量的资金时,用户可以向任务分配固定资源成本。例如:需要的硬件设备。
- 给任务分配固定成本:有些任务是固定成本的类型的任务,也就是说,用户知道某项任务的成本不变,不管任务的工期有多长,或不管任务使用了那些资源。在这种情况下,用户向任务直接分配成本。例如:培训任务。
成本基线:项目成本预算的结果。
10.2.9 估算精度与调整
估算准确度等级:
| 类型 | 精度 |
|---|---|
| 量级估算(Order-of-magnitude) | -25% ~ +75% |
| 预算估算(Budget) | -10% ~ +25% |
| 确定性估算(Definitive) | -5% ~ +10% |
估算不准的原因:
- 基础数据不足
- 缺乏经验的估算人员
- 签约前后不连贯
- 低劣的推测技术
- 估算对需求的敏感性
避免低劣估算:
- 避免无准备的估算
- 留出估算的时间,并做好计划
- 使用以前的项目数据
- 使用开发人员提供的数据为基础估算
- 分类法估算
- 详细的较低层次上的估算
- 使用软件估算工具
- 使用几种不同估算技术,并比较它们的结果
估算的表达方式技巧:
- 加减限定:6 个人月,+3 人月,-1 人月。
- 范围:5-9 人月。
- 风险量化。
- 情况说明。
真相与假象:
- 真相 8:造成软件项目失控最普遍的两个原因之一就是软件估算不足(另外的原因是需求不稳定)。
- 真相 9:往往在错误的时间,甚至在没有定义需求也即没有理解问题之前,就进行软件估算。
- 真相 10:往往由错误的人员,即不是由软件开发人员或者项目经理,而是由高层管理或者市场营销,进行软件估算。
- 真相 11:由错误的人员、在错误的时间做出的软件估算不但往往是错误的,而且很少随着项目的进行做必要的调整。
- 真相 12:虽然软件估算会发生如此错误,但是人们不去关心产生错误的原因,却还在努力按照错误估算的时间进度执行。
- 真相 13:管理人员和技术人员对软件估算的认识是完全隔绝的,因此也难以达到一致的目标。
- 真相 14:很少有真正的可行性研究。
- 假象 6:要估算软件项目开发时间和开发成本,必须首先估算软件项目的代码行数。
调整预算方法:
- 进度编制
- 资源调整
- 成本预算
- 计划优化调整
- 计划基线
资源冲突的表现:
- 分配给一个资源的工时总量大于它的最大可用工时量。
- 同一种资源被分配给时间上重叠的几个任务或项目中。
解决资源冲突的方法:
- 资源调配
- 推迟资源开始工作时间
- 替换资源
- 设置资源加班时间
- 调整资源日历
- 只使用资源的一部分工作时间
降低预算成本的方法:
- 降低资源的费率
- 减少任务的工时
- 减少加班
- 替换资源
- 减少任务的固定成本
- 删除任务
10.3 进度管理
10.3.1 进度基础
原话:
进度是对执行的活动和里程碑制定的工作计划日期表。
原话:
进度管理是为了确保项目按期完成所需要的过程。
原话:
按时完成项目是项目经理最大的挑战之一。时间是项目规划中灵活性最小的因素。进度问题是项目冲突的主要原因,尤其在项目的后期。
10.3.2 进度管理过程
- 活动定义(Activity definition)
- 活动排序(Activity sequencing)
- 活动资源估计(Activity resource estimating)
- 活动历时估计(Activity duration estimating)
- 制定进度计划(Schedule development)
- 进度控制(Schedule control)- 项目跟踪
活动定义:
确定为完成项目的各个交付成果所必须进行的诸项具体活动。
10.3.3 活动关系
任务(活动)之间的关系:
| 关系 | 说明 |
|---|---|
| FS(Finish-Start)结束-开始 | A 结束,B 才能开始 |
| SS(Start-Start)开始-开始 | A 开始,B 才能开始 |
| FF(Finish-Finish)结束-结束 | A 结束,B 才能结束 |
| SF(Start-Finish)开始-结束 | A 开始,B 才能结束 |
依赖类型:
- 强制性依赖关系
- 软逻辑关系
- 外部依赖关系
10.3.4 进度图表
网络图:
网络图是活动排序的一个输出。展示项目中的各个活动以及活动之间的逻辑关系。网络图可以表达活动的历时。
PDM(Precedence Diagramming Method,优先图法、节点法、单代号网络图):
- 构成 PDM 网络图的基本元素是节点(Box)。
- 节点(Box)表示活动(工序、工作)。
- 用箭线表示各活动(工序、工作)之间的逻辑关系。
- 可以方便地表示活动之间的各种逻辑关系。
- 在软件项目中 PDM 比 ADM 更通用。
ADM(Arrow Diagramming Method,箭线法、双代号网络图):
- ADM 也称为 AOA(activity-on-arrow)。
- 在 ADM 网络图中,箭线表示活动(工序、工作)。
- 节点 Node(圆圈)表示前一道工序的结束,同时也表示后一道工序的开始。
- 只适合表示结束-开始的逻辑关系。
虚活动:
- 为了定义活动。
- 为了表示逻辑关系。
- 不消耗资源。
甘特图:
- 显示基本的任务信息。
- 可以查看任务的工期、开始时间和结束时间以及资源的信息。
- 只有时标,没有活动的逻辑关系。
里程碑图:
- 里程碑显示项目进展中的重大工作完成。
- 里程碑不同于活动:活动是需要消耗资源的,里程碑仅仅表示事件的标记。
资源图:显示资源随时间的分配情况。
10.3.5 历时估算方法
定额法:
T = Q / (R × S)
- T:活动持续时间
- Q:活动的工作量
- R:人力或设备的数量
- S:产量定额,以单位时间完成的工作量表示
示例:
Q = 6 人月,R = 2 人,S = 1,则 T = 3 月
Q = 6 人月,R = 2 人,S = 1.5,则 T = 2 月
特点:方法比较简单,容易计算。适合规模比较小的项目,比如说小于 10000 LOC 或者说小于 6 个月的项目。
经验模型:
D = a × E^b
- D:月进度
- E:人月工作量
- a:取值范围是 2-4
- b:1/3 左右,依赖于项目的自然属性
建议掌握模型:
- Walston-Felix(IBM):D = 2.4 × E
- 基本 COCOMO:D = 2.5 × E
示例:
项目的规模 E = 152 PM,采用基本 COCOMO 模型估算的进度:
D = 2.5 × 152^0.35 = 14.5 月
专家估计方法:
通过专家依靠过去资料信息进行判断,以估算进度的方法。如果找不到合适的专家,估计结果往往不可靠和具有较大风险。
类推估计方法:
利用一个先前类似活动的实际时间作为估计未来活动时间的基础,这种方法常用于早期项目,掌握的信息不多。类推估计是专家判断的一种形式,以下情况类推估计是可靠的:先前活动和当前活动是本质上类似,而不仅仅是表面的相似;专家有所需专长。
模拟估计方法:
用不同的假设试验一些情形,以便计算相应的时间,最常见方法是蒙特卡洛分析技术。Monte Carlo 分析假设了各活动所用时间的概率分布以用来计算整个项目完成所需时间的概率分布。蒙特卡洛分析的结果比 PERT 和 CPM 方法结果更悲观。
基于承诺的进度估计:
从需求出发去安排进度。不进行中间的工作量(规模)估计。要求开发人员做出进度承诺,非进度估算。
优点:有利于开发者对进度的关注;有利于开发者在接受承诺之后提升士气。
缺点:开发人员的估计比较乐观;易于产生大的估算误差。
Jones 的一阶估算准则:
- 取得功能点的总和。
- 从幂次表中选择合适的幂次将其升幂。
示例:
FP = 350
平均水平的商业软件公司
粗略的进度 = 350^0.45 = 12 月
10.3.6 PERT 三点估算
原话:
工程评估评审技术(PERT)利用网络顺序图逻辑关系和加权历时估算来计算项目历时的技术。当估算项目中某项单独的活动、且存在很大的不确定性时,采用。它是基于对某项任务的乐观、悲观以及最可能的概率时间估计,采用加权平均得到期望值。
E = (O + 4M + P) / 6
- O:最小估算值,乐观(Optimistic)
- P:最大估算值,悲观(Pessimistic)
- M:最大可能估算(Most Likely)
示例:
PERT weighted average = (8 + 4 × 10 + 24) / 6 = 12 days
其中 8 = 乐观时间,10 = 最可能时间,24 = 悲观时间
标准差:
δ = (P - O) / 6
方差 = δ²
路径期望和方差:
E_path = ΣEi
方差_path = Σδi²
δ_path = √(方差_path)
置信区间:
- ±1δ ≈ 68.3%
- ±2δ ≈ 95.5%
- ±3δ ≈ 99.7%
10.3.7 关键路径法(CPM)
原话:
关键路径法根据指定的网络图逻辑关系和单一的历时估算,计算每一个活动的单一的、确定的最早和最迟开始、以及完成日期。计算浮动时间。计算网络图中最长的路径。确定项目完成时间。
网络图中任务进度时间参数:
- ES(最早开始时间,Early start)
- LS(最晚开始时间,Late start)
- EF(最早完成时间,Early finish)
- LF(最晚完成时间,Late finish)
- FF(自由浮动,Free Float)
- TF(总浮动,Total Float)
- Lead(超前)
- Lag(滞后)
浮动时间(Float):
浮动时间描述了一个活动的机动性,它是一个活动在不影响其它活动或者项目完成的情况下可以延迟的时间量。
总浮动(Total Float):
在不影响项目最早完成时间的情况下,本活动可以延迟的时间。LS - ES 或者 LF - EF。
自由浮动(Free Float):
在不影响后置任务最早开始时间的情况下,本活动可以延迟的时间。ES(后置任务) - EF(前置任务)- Lag。
关键路径:
关键路径是决定项目完成的最短时间。是时间浮动为 0(Float = 0)的路径。网络图中最长的路径。关键路径上的任何活动延迟,都会导致整个项目完成时间的延迟。
关键路径的其他说明:
- 明确关键路径后,你可以合理安排进度。
- 关键路径可能不止一条。
- 在项目的进行过程中,关键路径是可能改变的。
正向计算(正推法):
按照时间顺序计算最早开始时间和最早完成时间的方法。首先建立项目的开始时间;项目的开始时间是网络图中第一个活动的最早开始时间;从左到右,从上到下进行任务编排;当一个任务有多个前置时,选择其中最大的最早完成日期作为其后置任务的最早开始日期。
公式:
ES + Duration = EF
EF + Lag = ESs(后置任务)
反向计算(逆推法):
按照逆时间顺序计算最晚开始时间和最晚结束时间的方法。首先建立项目的结束时间;项目的结束时间是网络图中最后一个活动的最晚结束时间;从右到左,从上到下进行计算;当一个前置任务有多个后置任务时,选择其中最小最晚开始日期作为其前置任务的最晚完成日期。
公式:
LF - Duration = LS
LS - Lag = LFp(前置任务)
任务滞后 Lag:
A 完成之后 3 天 B 开始。公式:ES(S) = EF(P) + Lag。
任务超前 Lead:
A 完成之前 3 天 B 开始。作用:解决任务的搭接;对任务可以进行合理的拆分;缩短项目工期。
任务拆分:将大任务拆分为多个小任务,便于资源分配和进度优化。
同时浮动时间:
当多个并行活动共享同一段总浮动时间时,不能简单认为每个活动都能独立使用全部浮动。例如,若 B 和 C 共享 80 天的总浮动,B 的工期为 10、C 的工期为 5,则按比例分配后,B 可浮动约 53 天,C 可浮动约 27 天。如果由于 B、C 分别延误 80 天造成 100 万损失,则赔偿可按比例计算:B 赔偿 100 × (1 − 2/3),C 赔偿 100 × (1 − 1/3)。作为项目经理应该避免一些对项目不利的因素,严禁不应该的浮动,避免损失。
10.3.8 进度压缩
原话:
时间压缩法是在不改变项目范围的前提下缩短项目工期的方法。
赶工(Crashing):
赶工也称为时间-成本平衡方法。在不改变活动的前提下,通过压缩某一个或者多个活动的时间来达到缩短整个项目工期的目的。在最小相关成本增加的条件下,压缩关键路经上的关键活动历时的方法。
进度压缩单位成本方法:
进度压缩单位成本 = (压缩成本 - 正常成本) / (正常进度 - 压缩进度)
示例:
任务 A:正常进度 7 周,成本 5 万;压缩到 5 周的成本是 6.2 万
进度压缩单位成本 = (6.2 - 5) / (7 - 5) = 0.6 万元/周
如果压缩到 6 周的成本是:5.6 万
时间压缩例题:
路径 1:开始 -> A -> B -> 结束,Path: 16 周
路径 2:开始 -> C -> D -> 结束,CP Path: 18 周
总成本 20 万
任务 A:N:7 周:5 万,C:5 周:6.2 万
任务 B:N:9 周:8 万,C:6 周:11 万
任务 C:N:10 周:4 万,C:9 周:4.5 万
任务 D:N:8 周:3 万,C:6 周:4.2 万
将工期压缩到 17:压缩 C 到 9 周,总成本 20.5 万
将工期压缩到 16:压缩 C 到 9 周,压缩 D 到 7 周,总成本 21.1 万
将工期压缩到 15:压缩 A 到 6 周,B 到 7 周,C 到 9 周,D 到 7 周,总成本 22.3 万
Charles Symons 压缩因子:
进度压缩因子 = 压缩进度 / 正常进度
压缩进度的工作量 = 正常工作量 / 进度压缩因子
示例:
初始进度估算是 12 月,初始工作量估算是 78 人月
如果进度压缩到 10 月,进度压缩因子 = 10/12 = 0.83
则进度压缩后的工作量 = 78 / 0.83 = 94 人月
总结:进度缩短 17%,增加 21% 的工作量
研究表明:进度压缩因子 > 0.75,最多可以压缩 25%
快速跟进(Fast Tracking):
是在改变活动间的逻辑关系,并行开展某些活动。
10.3.9 关键链法
管理预留:
管理预留是一项加在项目末端的人为任务,不是加在每一个任务间隔上,即给项目增加一个储备时间。给储备所分配的时间一般是项目完成所有任务需要时间的 10%~15%。
缓冲:
- Project Buffer(项目缓冲):保护项目总工期。
- Feeding Buffer(汇入缓冲):保护非关键链汇入关键链的地方。
约束理论(TOC):
所有现实系统都存在约束。约束的存在表明系统存在改进的机会。“木桶效应”。
约束理论五大关键步骤:
- 找出系统中的约束因素;
- 决定如何挖掘约束因素的潜力;
- 使系统中所有其他工作服从于第二步的决策;
- 提升约束因素的能力;
- 若该约束已经转化为非约束性因素,则回到第一步,否则回到第二步,要注意不要让思维惯性成为新的主要约束因素。
10.3.10 资源平衡
资源调整尝试法:
资源优化配置。通过调整进度计划,形成平稳连续的资源需求。最有效的利用资源。使资源闲置的时间最小化。尽量避免超出资源能力。
方法:
- 维持工期不变,资源平衡,使资源强度尽可能平衡。
- 在满足资源约束条件下,使工期最短。
10.4 质量管理
10.4.1 质量概念
质量的多种定义:
- 符合目的或者用途(Joseph Juran)
- 用户的感觉就是质量(A V Feigenbaum)
- 符合顾客在其合理价格下对产品的要求(Sud Ingle)
- 产品或者服务满足明确和隐含需要能力的性能特性的总体(BS4778)
质量定义:
质量是满足要求的程度,包括符合规定的要求和满足顾客的需求。
软件质量:
软件质量是软件满足明确说明或者隐含的需求的程度。明确说明:查询功能。隐含说明:查询速度。
McCall 质量模型和 ICEDT 主观模型:
| 维度 | 说明 |
|---|---|
| Intuitive(直观) | 产品的使用是否合乎人们的通常想法? |
| Consistent(一致) | 产品的运行是否有一贯性? |
| Efficient(高效) | 产品是否可以让用户迅速和经济地完成其任务? |
| Durable(耐用) | 产品是否稳定和可靠? |
| Thoughtful(体贴) | 产品是否充分考虑了用户的需求? |
10.4.2 质量的重要性
原话:
质量管理是项目管理的最高统一(三大目标的统一)。低质量的软件就像定时炸弹。低质量的产品,增加成本。质量是生命也是信誉。
质量的形成:
质量形成于产品或者服务的开发过程中,而不是事后的检查(测试)把关等。
10.4.3 质量管理演进
演进过程:工匠自控 → 质量检查(1920)→ 质量控制(1940)→ 全面质量管理 TQM(1960)→ 质量保证(1970)。
软件质量管理的发展过程:
| 维度 | 至 50 年代末 | 至 70 年代初 | 至今 |
|---|---|---|---|
| 程序/产品 | 程序 | 软件 | 软件产品 |
| 开发组织 | 个体 | 项目设计组、软件作坊 | 软件机构(软件工厂) |
| 开发方式 | 靠开发者个人技巧 | 靠少数人协调把关 | 系统化、规范化、全员化 |
| 开发技术特点 | 机器代码、汇编语言;个人设计、个人使用、手工技巧、无维护观念、无系统化方法 | 程序设计理论深入、模块化、自顶向下、逐步求精,不重视维护问题 | 结构化设计理论和方法、面向对象方法、快速原型技术等 |
| 开发工具 | 无 | 高级语言、操作系统、数据管理系统 | 逐步产生与各种方法相应的工具、开发环境 |
| 质量管理特征 | 无 | 无 | 系统化、规范化、全员化 |
10.4.4 质量大师
戴明(Deming)十四点管理原则:
- 企业要有一个长期目标。
- 学习新的理论。
- 正确理解检验的意义。从一开始就要把质量考虑到产品的生产过程当中去,而不是依靠检验去保证产品质量。
- 成本不是决策时要考虑的惟一因素。
- 持续不断地改进。
- 学会培训雇员。
- 学会领导。
- 消除员工的恐惧。
- 团队协作。
- 停止训诫。
- 摒弃数量指标要求。
- 消除可能引起员工自卑心理的因素。
- 鼓励自我提高与教育。
- 采取行动。
朱兰(Juran):
约瑟夫·朱兰是世界著名的质量管理专家。朱兰的主要贡献是提出了质量的“适用性”理念,将人性尺度纳入质量范畴,以“大质量”促使质量管理从最初的统计方法向经营管理方向得以拓展,建立了质量管理的螺旋型提高模式。
朱兰第一个把帕累托分布引入质量管理,提出了“二八法则”并予以广泛应用。朱兰理论的核心:管理就是不断改进工作。他的“质量计划、质量控制和质量改进”三部曲,为企业的质量管理提出了一套完整的方法论。
帕累托原则:
帕累托原则代表一种存在于任何人群的普遍现象,即相对少数的人创造了绝大多数的价值。根据这个原则,质量损失呈不均匀分布状态,也就是说大部分后果是由少数几个原因造成的。这个原则使人们把注意力集中在几个能够产生重大影响的事物上,因此成为质量改进最有力的工具。
帕累托原则可以概括为:关键的少数原理。20 世纪 50 年代,朱兰首次把这个法则推广到许多工作领域,称为“关键的少数”原则。
克劳士比(Crosby):
克劳士比被誉为“最伟大的管理思想家”、“零缺陷之父”、“世界质量先生”。引发了全球源于生产制造业、继而扩大到工商业所有领域的质量运动。其中“零缺陷”,“符合要求”的质量定义以及“不符合要求的代价”等均出自克劳士比的笔端。
克劳士比质量管理五原则:
- 质量就是符合要求,而不是好。“好、卓越、美丽、独特”等描述,都是主观的和含糊的。
- 真正意义上的质量问题不存在。质量问题应归结到生产、设计等一系列问题上。如果能够预防,那么就不会出现质量问题。预防产生质量,而检验不能产生质量。
- 不需要什么质量经济,在第一时间把事情做对总是最廉价的方法。
- 对于工作表现的惟一衡量指标就是质量成本,即当要求没有符合时产生的额外的费用。
- 衡量工作表现的惟一标准就是“零缺陷”。它意味着要满足整个工作过程的全部要求。
10.4.5 质量成本(CoQ)
原话:
质量成本是由于产品的第一次工作不正常而衍生的附加花费,包括两部分:预防成本、缺陷成本。
CoQ = 预防成本 + 缺陷成本
质检/过失比:
质检/过失比 = 预防成本 / 缺陷成本
- 质检/过失比 < 1:隐藏很多错误。
- 质检/过失比 > 2:应该努力达到的程度。
10.4.6 QA vs QC
质量管理的对象:过程的质量、产品的质量。
软件质量管理过程:质量计划、质量保证、质量控制。
| 维度 | QA(质量保证) | QC(质量控制) |
|---|---|---|
| 关注点 | 过程 | 产品 |
| 问题 | Is it done right?(是否正确地做?) | Is it right done?(是否做了正确的事?) |
| 是否直接改进产品 | 否 | 是 |
| 活动 | 过程审计、产品审计 | 技术评审、代码走查、测试、返工、控制图、趋势分析、抽样统计 |
质量保证(QA):
通过评价项目整体绩效,建立对质量要求的信任。提供项目和产品可视化的管理报告。这个任务本身并不能提高产品的质量。一般由质量保证部门人员实施。
质量保证的要点:
- 对项目进行评价
- 推测能否达到质量指标
- 建立对项目的信心
质量保证活动 - 审计(Audit):
审计是对过程或者产品的一次独立评估。将审核的主体与为该主体以前建立的一组规程和标准进行比较。目的是确保真正的遵循了这一个过程,产生了合适的文档和精确反映实际项目的报告。可以预先规划的,也可以是临时决定的。
常用质量保证活动:
- 项目执行过程审计
- 项目产品审计
质量控制(QC):
确定项目结果与质量标准是否相符。同时,确定消除不符的原因和方法。控制产品的质量,及时纠正缺陷。这个任务本身提高产品的质量。一般由开发人员实施。
质量控制的要点:
- 检查工作结果
- 按照标准跟踪检查
- 确定措施消灭质量问题
QA 与 QC 关系:
- QA: Is to focus the process.
- QC: Is to control the quality before delivery.
10.4.7 质量规划
原话:
质量计划:确定项目应达到的质量标准。决定如何满足质量标准的计划安排和方法。
质量计划内容:
- 项目应达到的质量目标和所有特性的要求
- 确定项目中的质量活动和质量控制程序
- 项目不同阶段,职责,权限,交流方式以及资源分配
- 确定项目采用的控制手段,合适的验证手段和方法
- 确定和准备质量记录
质量规划方法:
- 试验设计:一种统计学方法,确定哪些因素可能会对特定变量产生影响。一个好的试验设计包含:明确衡量产品质量的可量化指标(响应变量/输出变量);寻找影响试验指标的可能因素(影响因子和输入变量),并初步确定因子水平的范围;选择适用的试验设计方法;科学地分析试验结果,包括直观分析、方差分析、回归分析等。
- 基准对照:寻找最佳实践的方法,是利用其他项目的实施情况作为当前项目性能衡量的标准。
- 质量成本分析:质量成本的综合分析,以便决定质量活动。
- 流程图方法:显示系统的各种成分是相互的关系,帮助我们预测在何处可能发生何种质量问题。
- 因果分析图:描述相关的各种原因和子原因如何产生潜在问题或影响,将影响质量问题的“人员、设备、参考资料、方法、环境”等各方面的原因进行细致的分解。
10.4.8 质量目标与度量
常见指标:
- 可用性(Availability)
- 初始故障率
- 随机故障率
- 平均无故障时间(MTTF)
- 平均故障间隔时间(MTBF)
- 缺陷密度(FD)
- 平均修复时间(MTTR)
10.4.9 质量计划模板
质量计划模板参照:
- 项目概述
- 实施策略
- 项目组织
- 质量保证对象分析及选择
- 质量保证任务划分
- 实施计划
- 资源计划
- 记录的收集、维护与保存
10.4.10 质量体系
原话:
质量体系:为实施质量管理所需的组织结构、程序、过程和资源。
质量体系与质量计划的区别:
- 质量体系是企业长期遵循和需要重复实施的文件,具有较强的标准性质。
- 质量计划是一次性实施的,一旦项目结束,质量计划的有效性就结束。
质量不同思路:
- 微软:紧包装
- 摩托罗拉:需要达到 6 Sigma
- 航天飞机控制软件:接近零缺陷、无故障
- 典型的政府合同:满足合同的要求和规格
软件质量改善的要求:
- 软件质量活动必须经过规划。
- 软件质量活动规划必须明文规定。
- 质量活动必须尽早开始。
- 质量小组必须独立存在。
- 应该经过训练。
- 必须有适当的经费。
10.5 要点
风险管理要点:
- 风险管理的四个过程:风险识别、风险评估、风险规划、风险控制。
- 风险管理计划:风险应对计划、岗位职责、时间、预算、追踪等。
- 风险管理的主要工具:TOP 10 风险清单。
成本管理要点:
- 成本估算的过程。
- 成本估算的方法。
- 掌握成本估算的技巧。
进度管理要点:
- 进度管理的基本概念。
- 进度估算的基本方法。
- 编制项目计划。
质量管理要点:
- 质量管理的过程:质量规划、质量保证、质量控制。
- 质量计划。
全课总结
核心记忆框架
软件工程 = 概念 + 过程 + 方法 + 管理
概念:软件、软件工程、软件属性、职业道德
过程:瀑布 / 增量 / 螺旋 / RUP / 敏捷
方法:需求工程 → 建模 → 体系结构 → 面向对象设计 → 测试 → 维护
管理:风险 / 成本 / 进度 / 质量高频易混点速记
- Verification vs Validation:验证 = 按规格做对;确认 = 满足用户真正需求。
- 瀑布 vs 增量:瀑布顺序推进,增量小步快跑。
- 重构 vs 再工程:重构是持续小改,再工程是大规模改造遗留系统。
- QA vs QC:QA 管过程,QC 管产品。
- 赶工 vs 快速跟进:赶工加资源,快速跟进改并行。
复习路径
- 先通读每章的“本章问题链”和“一句话总结”。
- 重点记忆所有
**原话**和**原定义**引用块。 - 用附录公式速查表和易混淆概念对照表做考前冲刺。
附录一:常用公式速查
| 公式 | 说明 |
|---|---|
FP = UFC × TCF | 功能点。UFC:未调整功能点计数;TCF:技术复杂度因子 |
TCF = 0.65 + 0.01 × ΣFi | 技术复杂度因子。Fi:14 个技术因素得分,每个 0–5 分 |
Ei = (a + 4m + b) / 6 | Delphi/三点估算。a:最小值,m:最可能值,b:最大值 |
E = (O + 4M + P) / 6 | PERT 期望历时。O:乐观,M:最可能,P:悲观 |
δ = (P - O) / 6 | PERT 标准差 |
E_path = ΣEi | 路径期望 |
σ²_path = Σδi² | 路径方差 |
E = a × (KLOC)^b | COCOMO 81 基本模型 |
E = 5.2 × L^0.91 | IBM 模型工作量。L:KLOC,E:人月 PM |
D = 4.1 × L^0.36 | IBM 模型工期。D:月 |
S = 0.54 × E^0.6 | IBM 模型人员。S:人 |
DOC = 49 × L^1.01 | IBM 模型文档页数。DOC:页 |
T = Q / (R × S) | 定额法历时估算。Q:工作量,R:资源数量,S:生产率 |
D = a × E^b | 经验模型进度估算。D:月,E:人月 |
EMV = Σ(概率 × 收益/损失) | 期望货币价值 |
TF = LS - ES = LF - EF | 活动总浮动时间 |
FF = ES(后置) - EF(前置) - Lag | 活动自由浮动时间 |
CoQ = 预防成本 + 缺陷成本 | 质量成本 |
质检/过失比 = 预防成本 / 缺陷成本 | 质量测量方法,目标 > 2 |
进度压缩单位成本 = (压缩成本 - 正常成本) / (正常进度 - 压缩进度) | 赶工成本分析 |
进度压缩因子 = 压缩进度 / 正常进度 | Symons 进度压缩 |
压缩进度工作量 = 正常工作量 / 进度压缩因子 | Symons 进度压缩 |
D = 2.5 × E^b | 基本 COCOMO 进度估算(b: 0.32–0.38) |
R = F(P, I) | 风险值 = 风险概率与影响的函数 |
附录二:易混淆概念对照
| 概念 A | 概念 B | 区别 |
|---|---|---|
| Verification(验证/检验) | Validation(确认/有效性验证) | Verification:Are we building the product right? 是否按规格说明做对。Validation:Are we building the right product? 是否满足用户真正需求。 |
| 审查(Inspection) | 测试(Testing) | 审查是静态 V&V,不执行代码;测试是动态 V&V,执行代码。 |
| 重构(Refactoring) | 再工程(Reengineering) | 重构是连续不断的预防性维护,不改变功能;再工程是用自动化工具处理遗留系统,产生新系统。 |
| QA(质量保证) | QC(质量控制) | QA 关注过程,问 Is it done right?;QC 关注产品,问 Is it right done?。 |
| 瀑布模型 | 增量式开发 | 瀑布是阶段分明、顺序执行;增量是描述、开发、验证交织进行。 |
| 瀑布模型 | 螺旋模型 | 瀑布是计划驱动线性模型;螺旋是风险驱动迭代模型,每圈都进行风险分析。 |
| Scaling up | Scaling out | Scaling up 是把敏捷方法应用到大型项目;Scaling out 是把敏捷方法推广到大机构。 |
| 包含关系(include) | 扩展关系(extend) | include 是提取公共步骤,被包含用例是必需的;extend 是在特定条件下插入新行为,基础用例可独立完整。 |
| 聚合(Aggregation) | 组合(Composition) | 聚合是弱整体-部分关系,生命周期独立;组合是强整体-部分关系,生命周期相关。 |
| 直接成本 | 间接成本 | 直接成本可直接归属到项目;间接成本由多个项目共享。 |
| 总浮动(TF) | 自由浮动(FF) | TF:不影响项目最早完成时间的可延迟时间;FF:不影响后置任务最早开始时间的可延迟时间。 |
| 赶工(Crashing) | 快速跟进(Fast Tracking) | 赶工是在关键路径上增加资源缩短历时;快速跟进是把原本顺序的活动改为并行。 |
| 增量开发 | 增量交付 | 增量开发是逐步开发并评估;增量交付是部署给用户使用。 |
| 抛弃式原型 | 进化式原型 | 抛弃式用于澄清需求后丢弃;进化式逐步演化为最终系统。 |
| 体系结构模式 | 设计模式 | 体系结构模式关注系统宏观结构;设计模式关注局部设计问题。 |
| 单元测试 | 组件测试 | 单元测试针对单个对象/方法;组件测试针对接口和交互。 |
| 阿尔法测试 | Beta 测试 | Alpha 在开发者现场进行;Beta 在用户真实环境中进行。 |
整理完成。复习时建议:先看每章“本章问题链”和“学习框架”建立整体认知,再精读
**原话**/**原定义**引用块,配合“换句话说”和对比表格加深理解,最后用两个附录做考前冲刺。