软件项目管理

第1章 软件开发概述

1.1 程序与软件

1.1.1 从程序到软件

1、软件

软件是有计算机程序和程序设计的概念发展演化而来,是程序和程序设计发展到一定规模后并且再逐步商品化的过程中形成的。

2、计算机程序的工程性和使用价值

  • 计算机程序具有复制价值
  • 计算机程序的生成是一种有多人合作、经历不同阶段的开发,且具有可复制和重复使用的器或件

3、计算机程序

(1)计算机程序简称程序,是为了解决某个特定问题而用程序设计语言描述适合计算机处理语句序列

(2)软件是能够完成预定功能和性能的可执行的程序和使程序正常执行所需要的数据,加上描述软件开发过程及其管理、程序的操作和使用的有关文档,即“软件=程序+数据+文档”。

4、举例

教学管理系统。

1.1.2 软件类型

1、按功能划分

  • 系统软件:如计算机操作系统、设备驱动程序、通信处理程序、 络管理程序。
  • 支撑软件:如IDE、编译程序、文件格式化程序、DBMS、应用框架与程序库。
  • 应用软件:包括商业数据处理软件、工程与科学计算机软件、管理信息系统、办公自动化软件、计算机辅助教学软件、游戏娱乐类软件、 交通信类软件等。

2、按各种方式划分

  • 实时处理软件
  • 分时软件
  • 交互式软件
  • 批处理软件

3、按服务对象范围划分

  • 项目软件
  • 产品软件

4、其他分类

  • 商业软件
  • 开源软件
  • 共享软件

5、软件开发

随着软件变得越来越大、越来越复杂,软件开发的关注点也发生了变化,相对于小规模的程序设计,提出了大规模的程序设计,即软件开发。

1.1.3 程序设计与软件开发

1、计算机程序的两种形式

  • 可执行程序
  • 可读源代码

2、程序设计

以某种程序设计语言为工具,编写源程序,然后由编译系统完成可执行代码的转换。

3、程序设计活动

程序设计活动包括分析、设计、编码、测试、排错等。

4、程序=算法+数据结构

数据结构指程序处理或应用的数据之间的逻辑关系。算法是指解决特定问题的步骤和方法。程序设计的核心是选择和设计适合特定问题的数据结构和算法,用编程语言编制为程序。

5、软件工程

随着计算机技术的发展,软件规模变得越来越大,软件已经不可能仅仅依靠个人才能去编写与开发了,而是需要团队。程序设计活动走向软件工程,软件工程把经时间考验而证明正确的管理技术和当前能够得到的最好技术方法结合起来,以系统性的、规范化的、可定量的过程化方法去开发和维护。

1.2 软件生存周期

1.2.1 使用角度的软件生存周期

从用户角度可分为三个阶段:

1、提出需求

根据用户需求,提出要解决的问题和需要的软件。

2、获取软件

对获取软件的最佳途径做出决策并选择最佳的供应商。软件的获取有三种主要途径:

  • 购买软件
  • 定制或开发软件
  • 租凭软件或租凭服务

3、使用软件

一旦获得软件之后,用户操作软件使之为其服务。

1.2.2 开发角度的软件生存周期

一般分为:定义软件、开发软件和维护软件。

1、需求定义

  • 功能性需求:定义软件再抽象级别应该提供的基本功能。
  • 非功能性需求(特性需求):软件应该具备的特性,如可用性、性能、安全性、可靠性等。
  • 需求定义阶段一个重要部分是建立一组系统应该满足的总体目标。

2、软件设计

如何实现需求的决策和方案,是将系统功能分配到不同组成元素的过程,包括一组活动:

  • 划分需求:对需求进行分析并合并成相关的组。
  • 确定子系统:识别出独立或集体满足需求的子系统。
  • 给子系统分配需求:原则上只要子系统的识别是需求划分驱动的,这个过程可省略。但在实践过程中,需求划分和子系统匹配从来都是不完全匹配的,使用外部系统或库函数可能会要求更改需求。
  • 定义子系统:确定每个子系统或模块的特殊功能。
  • 定义子系统的接口:定义每个子系统提供的或需要的接口。

经典的软件工程将软件设计分为:

  • 概要设计
  • 详细设计

3、软件实现

完成可运行程序及数据的软件开发过程。

4、软件维护

对已完成开发并发布、交付使用的软件产品进行完善、纠正错误、改进性能和其他属性,或使软件使用改变了的环境。软件维护分为四种类型:

  • 改正性维护
  • 适应性维护
  • 完善性维护
  • 预防性维护

1.3 软件开发过程

1.3.1 瀑布式开发过程

1、瀑布式开发过程

也叫软件生存期模型。它按照软件生命周期,把开发分为制定计划、需求分析、软件设计、程序编写、软件测试和运行维护这6个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。

2、核心思想

按工序将问题化简,将功能的实现与设计分开,便于分工协作,即采用结构化的分析与设计方法将逻辑实现与物理实现分开。

3、特性

强调文档的作用,并要求每个阶段都要仔细验证。

4、存在的问题

  • 阶段划分僵硬,每个阶段不能缺省,而且产生了大量文档,增加了工作量。
  • 开发是线性的,只有等到整个过程的末期才能见到开发成果——可运行软件,不利于快速响应变化的需求。
  • 早期的错误要等到开发后期的测试阶段才能发现,可能带来严重的后果,增加了开发的风险。

1.3.2 增量开发模型

1、增量开发

增量开发特指待开发的软件不是一次就能完成,而是把软件分成一系列增量,完成一部分就交付一部分。对每个增量的使用和评估都作为下一个增量发布的新特性和功能。这个过程在每个增量发布后不断重复,直到产生了最终的完善产品。

2、基本思想

让开发者能够从早期的开发、系统的增量、交付的版本中学到经验。从系统的开发和使用中学习一切可能学到的东西。过程中的关键是从系统需求的简单子集实现开始,通过迭代增强和进化后续版本,直到系统被实现。每次迭代中,对设计进行修改,并增加新的功能要求。

3、特性

引进了包的概念,无需等到所有需求明确,只要某个需求明确了,就可以进行开发。

4、优点

  • 短时间内交付一个可运行软件,解决一些急用功能。
  • 每次只交付一部分,用户有较充分的时间学习和适应新的产品。
  • 灵活使用需求的变化。
  • 有利于系统维护。

5、存在的风险

  • 由于各部件是逐步并入已有软件,必须确定每次增加的部件不破坏已构建好的系统,这需要软件具备开放式的系统结构,否则系统将失去稳定的结构。
  • 逐步增加不见的方式,很容易退化成边做边改模型,从而使软件过程的控制失去整体性。
  • 如何一致的定义“增量”何界定它的工作量、需求范围、功能或特性/li>

1.3.3 个体软件过程

1、PSP

  • PSP是一种可用于控制、管理和改进个人工作方式的自我持续改进过程。
  • 它是一个包括软件开发表格、指南和规程的结构化框架。
  • PSP 与具体的技术(程序设计语言、工具或设计方法)相对独立,其原则能够应用到任何软件工程任务之中。
  • PSP能够说明个体软件过程的原则。
  • 帮助软件工程师做出准确的计划。
  • 确定软件工程师为改善产品质量要采取的步骤。
  • 建立度量个体软件过程改善的基准。
  • 确定过程的改变对软件工程师能力的影响。

 2、个体软件过程

  • PSP0的目的是建立个体过程基线,学会使用PSP的各种表格采集过程的有关数据,执行的软件过程包括计划、设计、编码、编译和测试。按照选定的缺陷类型标准、度量引入的缺陷个数和排除的缺陷个数等,用在测量PSP过程的改进。PSP0.1增加了编码标准、程序规模度量和过程改善建议三个关键过程域。
  • PSP1的重点是个体计划,用自己的历史数据来预测新程序的大小和需要的开发时间,并使用线性回归方法计算估计参数,确定置信区间以评价预测的可信程度。PSP1.1增加了对任务和进度的规划。在PSP1阶段应该学会编制项目开发计划,这不仅对承担大型软件的开发十分重要,即使是开发小型软件,也必不可少。
  • PSP2的重点是个体质量管理,根据程序的缺陷建立检测表,按照检测表进行设计复查和代码复查(也称“代码走查”),以便及早发现缺陷,使修复缺陷的代价最小。PSP2.1则论述设计过程和设计模板,但并不强调选用什么设计方法,而强调设计完备性准则和设计验证技术。
  • PSP3的目标是把个体开发小程序所能达到的生产效率和生产质量延伸到大型程序;其方法是采用迭代增量式开发方法,首先把大型程序分解成小的模块,然后对每个模块按照PSP2.1所描述的过程进行开发,最后把这些模块逐步集成为完整的软件产品。在新一轮开发循环中,可以采用回归测试。

1.4 敏捷开发

1.4.1 概述

1、价值观和基本原则

(1)4个核心价值观

  • 个体和互动胜过流程和工具。
  • 工作的软件胜过详尽的文档。
  • 客户合作胜过合同谈判。
  • 响应变化胜过遵循计划。

(2)12条原则

  • 最优先要做的是通过尽早地、持续地交付有价值的软件满足客户需要。
  • 即使在开发后期也欢迎需求的变化,敏捷过程利用变化为客户创造竞争优势。
  • 经常交付可以工作的软件,从几星期到几个月,时间越短越好。
  • 业务人员和开发人员应该在整个项目过程中始终朝夕在一起工作
  • 要善于激励项目人员,给他们以所需要的环境和支持,并相信他们能够完成任务。
  • 在开发小组中最有效率、也最有效果的信息传达方式是面对面的交谈。
  • 工作的软件是进度的主要度量标准。
  • 责任人、开发者和用户应该维持长期、恒等的开发节奏。
  • 对卓越技术与良好设计的不断追求将有助于提高敏捷性。
  • 简单——尽可能减少工作量的艺术——至关重要。
  • 最好的架构、需求和设计都源于自组织的团队。
  • 每隔一定时间,团队都要总结、反省工作效率,然后相应地调整自己的行为。

2、基本技术

  • 敏捷方法可以视为一些最佳实践的集合,包括经典的软件开发技术和管理,也包括敏捷开发首创的技术和方法。
  • 敏捷开发遵循软件开发的基本原则,同时也总结出了11条面向对象设计的原则,如单一职责原则(模块内聚的体现)、(采纳的)Liskov替换原则等。
  • 敏捷开发主要采用了面向对象的开发技术,使用CRC卡( Class-Responsibility-Collaborator,类-责任-协作)、用户用例、设计模式及UML ( Unified ModellingLanguage,统一建模语言)。
  • 使用UML 的符 主要是类图和时序图,这两种符 有助于直接编写出代码。
  • 敏捷开发增强和推广了一些经典的实践,如意图导向编程,指的是先假设当前这个对象中已经有了一个理想方法,它可以准确无误地完成想做的事情,而不是直接盯着每一点要求来编写代码。

3、敏捷技术

  • 结对编程:两个程序员在一个计算机上共同工作。一个人输入代码,另一个人审查他输入的每一行代码。两个程序员经常互换角色。
  • 代码重构:指的是改变程序结构而不改变其行为,以便提高代码的可读性、易修改性等。例如,给变量重新命名,把一段代码提升为函数,把公共的属性和行为抽象成基类。
  • 测试驱动开发:在一个微循环开发中,首先确认并自动化进行一个失败的测试,然后编写足够的代码通过测试,在下一轮前重构代码。
  • 持续集成:微软等公司的软件开发方法包括每日构造产品,持续集成比它更进一步,只要可能,就把新代码或变更的代码合并到应用程序,然后测试,确保每一步的工作都正确。

1.4.2 Scrum方法

1、3个角色

  • 产品负责人
  • 产品经理
  • 团队

2、3个工件

  • 产品冲压工作
  • 冲刺清单
  • 燃尽图

3、5个活动

  • 冲刺计划会议
  • 每日站会
  • 冲刺评审会议
  • 冲刺回顾会议
  • 产品积压工作梳理会议

4、5个价值

  • 承诺
  • 专注
  • 开放
  • 尊重
  • 勇气

1.5 软件构造

1.5.1 有关概念

通过程序设计(Programming)、编码(Coding)得到程序、语句(段),书写(Write)文档或程序,设计(Design)包括软件及其构件、类、函数、算法、数据结构等的软件工件,也通过开发(develope)得到软件或程序。伴随着程序成为软件、作为产品或系统,程序员或软件开发者使用可复用技术、集成技术等把一个软件的不同组成部分按照一定的结构、通过一系列步骤组装(Assemble)成可运行的软件。建造(Build)与程序的编译有关,它可以把一个或一组源程序文件翻译成可执行的指令序列;也可以把构成一个软件的所有源程序文件、配置文件、数据文件及它们需要的库文件等,按照一定的顺序编译并连接成一个可运行文件。Meyer:面向对象软件构造是一种软件开发方法,是运用面向对象技术开发具有结构的软件系统,其结构组成是类;类可以立即实现,也可以延迟实现(从而具有动态性)。因而,软件具有房屋建造的特点,是使用了预制的、可复用的建造部件,按照(设计的)结构和流程而完成的产品。

1.5.2 构造与开发过程

  • 构造在不同软件开发流程或模型中的地位也不一样。有些流程更加重视构造。
  • 从构造角度看,有些模型侧重于线性化过程——比如瀑布模型、阶段交付的生命周期模型。线性化开发方式更加重视构造之前的活动(需求和设计),并且在这些活动之间建立明确的任务划分。在这些模型中,构造工作主要就是编码。
  • 有些模型是迭代的——如Scrum、极限编程、进化式原型法。这些方式倾向于把构造视为与其他软件开发(包括需求、设计和计划)同时发生或重叠的活动。这些方式混合设计、编码和测试活动,把构造当成这些活动的集合体。
  • 如何考虑构造,在某种程度上依赖于采用的生存周期模型。一般地说,软件构造最主要是编码和调试,但也可以包含工作计划、详细设计、单元测试、集成测试,以及其他活动。

1.5.3 主要内容

  • 软件构造基础
  • 管理构造
  • 实际考虑
  • 构造技术
  • 软件构造工具

1.5.4 软件构造的重要性

  • 占据了软件开发的大部分工作(30%~80%的工作时间)
  • 是软件开发的中心活动
  • 重心放在构造上,能显著提升个体程序员的生产率
  • 构造的产品——程序源代码,常常是唯一标准的软件描述
  • 构造是确保唯一要完成的活动

1.6 为什么不直接编写软件

1.6.1 软件开发语言

1、按计算模型划分

  • 命令式语言
  • 冯·诺依曼式语言
  • 脚本语言
  • 面向对象语言
  • 声明式语言
  • 函数式语言
  • 逻辑式语言
  • 高级程序语言

2、按执行模式划分

高级语言的程序不能直接上计算机运行,需要转换成低级语言的指令后才能运行。

  • 编译型语言:C、C++、Ada
  • 解释型语言:Python、Basic及脚本语言等

1.6.2 编程工具与集成化开发环境

  • 软件开发还需要管理各种代码文件、检查程序质量、测试工具、管理bugs、软件打包工具等。这些基础开发工具通常是行式命令,直接在操作系统中输入相应的命令,如编译Java程序的编译命令javac,解释执行Java程序的命令java。也有一些语言直接提供可视化开发工具。
  • 可视化集成开发环境IDE,用图形用户界面(Graphical UserInterface , GUI)集成了代码编写、静态分析、编译、调试、连接、打包等功能的一体化软件开发套件。支持多种语言进行编程,还提供代码管理、代码分析、软件维护、软件测试及软件部署和交付等工具,同时支持多种形态应用软件(通用程序、Web应用、数据库应用、移动应用)的开发。如Delphi、Visual Studio、Eclipse和Netbeans。

1.6.3 软件运行环境

1、软件运行环境

软件运行环境,广义上说,是一个软件运行所要求的各种条件,包括软件环境和硬件环境。许多应用软件不仅仅要求特定的硬件条件,还对软件提出明确的支撑条件。操作系统将计算机的硬件细节屏蔽,将计算机抽象成虚拟资源。通常把计算机硬件和操作系统称为平台。

2、虚拟机

  • 为了能够使同一种编程语言的程序独立于操作系统,实现程序运行的独立性,即“一次编写程序、到处运行”,在操作系统层面提出并出现了语言虚拟机或运行容器。它为程序的运行提供所需的运行时资源,包括把程序翻译成计算机指令、分配内存、通过操作系统调用计算资源等。
  • 例如,Java 虚拟机 (Java Virtual Machine ,JVM)可以理解成一台运行Java程序的抽象的计算机。

3、支撑环境

  • 使用的数据库管理系统,如Oracle、MySQL、SQL Server。
  • Web 服务器,如Apache服务器、Tomcat服务器
  • 应用框架,如.NET Framework、Java程序的SSH 框架、Web 应用框架Ruby on Rails。
  • 第三方程序库、APIs,如大数据分析与处理的Panda,人工智能的TensorFlow, Caffe、Torch,等。

1.6.4 软件开发的最佳实践

1、最佳实践

  • 用户满意的、可以反复使用的软件开发的一切手段
  • 最佳实践认为存在某种技术、方法、过程、活动或机制可以使生产或管理实践的结果达到最优,并减少出错的可能性。

2、原则

一个已经接受或专业化的指导行动的最高准则或标准。人们总结、使用了软件开发的基本原则、面向对象原则等。原则必须通过某种途径体现出来,才具有指导作用。

3、机制

指的是有机体的构造、功能及其相互关系、工作原理,如可视化编程的事件响应机制、类型的多态机制。

4、技术

是科学原理的应用,是具有技能特点的特殊的步骤或途径。软件开发技术是运用了计算机科学、数学、系统科学、管理科学的基本原理,进行软件开发的方式方法。

5、方法

是获得一个客体(对象)的步骤或过程。作为一个系统的步骤、技术活动被特定的专业或艺术采纳,是技能或技术的全部。研究方法及其知识的活动称为方法学,如面向对象方法学

6、工具

1.6.5 开发过程与管理

软件开发不像计算机、手机的制造等可以使用机器设备进行大规模的自动化生产。软件开发主要是人的智力活动,而且很多时候是一群人的开发活动。

  • 首先,软件开发是做出决策、权衡和选择的过程。
  • 对于人员管理,首先要识别出参与软件开发、与软件开发相关、受软件影响的人员,并分析每个相关人员对软件的诉求、期望、影响和作用。
  • 软件开发作为项目要评估成本、质量、人员等因素,预先做出项目计划。

第2章 模块化软件构造

2.1 分解与模块化

2.1.1 分解的含义

  • 把问题分解成两个或多个更小的问题
  • 分别解决每个小问题
  • 然后把各个小问题的解答结合起来,即可得到原问题的解答。

2.1.2 模块化与结构化

1、模块化

(1)模块化

  • 模块化是把问题分解成容易理解、便于控制、便于实现的子问题的一个重要手段,是实现控制复杂性的方式。
  • 模块化把一个程序分解成简单独立、互相作用的模块,对不同的模块设定不同的功能,来实现大型、复杂的程序。
  • 在程序系统的结构中,模块是可组合、可更换的程序单元。
  • 良好的模块设计只完成一个特定的或一组相关的子功能。
  • 所有模块按某种方式组装起来,成为一个整体,完成整个系统所要求的功能。

(2)软件模块

  • 具有相对独立性、由数据说明、执行语句等程序对象构成的代码集合。
  • 程序中的每个模块都需要单独命名,通过名字可实现对指定模块的访问。
  • 一个模块具有输入/输出(接口)、功能、内部数据、程序代码4个特征。

(3)模块的三大特征

  • 独立性:可以对模块单独进行设计、编码、调式、修改和存储。
  • 互换性:模块具有标准化的接口,容易实现模块间的互换。
  • 通用性:有利于实现系列产品间的模块的通用,实现跨系列产品间的模块的通用。

(4)模块化的好处

  • 有助于了解软件设计,使结构清晰,容易阅读和理解。
  • 使软件容易测试和调试。
  • 提高软件的可修改性。
  • 有助于软件开发工程的组织管理,一个复杂的大型程序可以有许多程序员分工编写不同的模块,并且可以进一步分配技术熟练的程序员编写困难的模块。

2、结构化

  • 图灵奖获得者Wirth提出的“结构化程序设计”(Structured Programming)的方法,可以简化为“算法+数据结构=程序”。
  • 该方法的重点是:不要求一步就编写成可执行的程序,而是分若干步进行,逐步求精。
  • 第一步编写出的程序抽象程度最高,第二步编出的程序抽象程度有所降低……最后编出的程序即为可执行的程序。
  • 优点:使程序易读、易写、易调试、易维护,也易于保证程序的正确性及验证其正确性。
  • 这种结构化设计方法又称“自顶向下”或“逐步求精”法,在程序设计领域引发了一场革命,成为程序开发的一个标准方法。

2.2 数据结构与算法

2.2.1 数据结构与算法的关系

1、数据结构

  • 数据结构是计算机存储、组织数据的方式,是指相互之间存在的一种或多种特定关系的数据元素的集合。
  • 数据结构为数据集提供独立于计算机内存的数据组织,并提供被视为一种抽象工具来访问。
  • 常见的数据结构有数组、集合、栈、队列、堆、树、图、散列表等。

2、计算机算法

  • 计算机算法以一步一步的方式来详细描述计算机如何将输入转化为所要求的输出的过程。
  • 描述算法的方式可以采用自然语言、程序设计语言,也可以两种语言混合使用。
  • 用计算机程序语言实现并在计算机上运行的算法就是程序,它是一个解决实际问题方法的程序语言的指令序列。
  • 基本的算法类型包括查找(顺序查找、二分查找)、排序(冒泡排序、快速排序、插入排序、归并排序等)、二叉树的遍历(前序遍历、中序遍历、后序遍历)、图的遍历(广度优先遍历、深度优先遍历)、最短路径算法。

3、两者关系

  • 一种数据结构、一种算法:如计算树的高度、树节点的层级。
  • 一种数据结构、多种算法:数组支持的算法有排序算法、查找算法、图类算法、矩阵类算法等。
  • 多种数据结构、一种算法:折半查找,使用的基本数据结构有数组、二叉树,也可以使用链表。
  • 多种数据结构、多种算法:针对数据结构数组和二叉树,基本的算法有遍历类、查找类、求最大值。

2.2.2 选择与设计数据结构

1、算式与习题的基本数据结构

(1)设计1:

包含两个运算数,一个运算符及结果的数据结构

需要注意:

1)命名:见名知意

2)一个结构类型是否包含非独立变量,应该考虑下列因素:

  •  获取非独立变量值的难易程度:比较简单的话不需要在结构体中定义,比较复杂,消耗资源则可用一个变量记录该值。
  • 使用非独立变量的频繁程度:使用频繁且值比较稳定,考虑包含非独立变量;否则不包含。

(2)设计2

用一个数组[operand,operand2,operator]表示算式Equation。

2、比较

3、算法分析与其它数据结构

  • 算法分析:产生有正整数n个不同算式的习题,算法的复杂度是O(n^2)。
  • 其它数据结构:集合

2.2.3 选择与设计算法

1、习题与算式的分离

把习题和算式明确地从代码中抽出,并分别用合适的数据结构表示,有助于各自的设计与实现,也能实现不同的算式和习题的任意组合。

2、算式产生与其约束条件的分离

分别定义运算数生成函数与约束条件检测函数,对满足一定条件的运算数才生成算式。这样,约束条件的任意变换都不影响算式生产函数,也支持用户灵活设置约束条件。

3、加减法算式的分离

便于生产混合运算。

2.3 模块化设计理论初步

2.3.1 模块化原则

1、Meyer提出了5条标准来评价一种设计方法是否定义了有效的模块系统能力:

  • 可分解性
  • 可组装性
  • 可理解性
  • 连续性
  • 保护性

2、模块的独立程度可以由两个定性标准来度量:

  • 内聚:衡量一个模块内部各个元素之间相互结合的紧密程度
  • 耦合:衡量不同模块彼此之间相互依赖(连接)的紧密程度

2.3.2 模块的内聚性

内聚性越高,相对地,它与其它模块之间的耦合度就会降低,模块越独立。内具有7种,由弱到强排列如下:

  • 偶然内聚
  • 逻辑内聚
  • 时间内聚
  • 过程内聚
  • 通信内聚
  • 顺序内聚
  • 功能内聚

2.3.3 模块间的耦合性

开发中尽量追求松耦合,耦合度从低到高可分为7级:

  • 非直接耦合
  • 数据耦合
  • 标记耦合
  • 控制耦合
  • 外部耦合
  • 公共耦合
  • 内容耦合

2.4 测试程序

2.4.1 测试需求

  • 首要任务就是分析用户需求与设计,梳理含糊不清、模棱两可、互相矛盾的需求,明确、细化和罗列出需求,并且将每个需求表示成可以检测的测试需求。
  • 其次,测试需求要求程序的预期结果和实际运行结果都要明确、合理、可观察并可比较。
  • 找到隐含需求或隐含的不确定因素也需要进行测试。

2.4.2 测试设计与测试用例

测试设计包括测试用例的设计,此外还包括是否实施所有层次的测试,是否采用测试工具或自动化测试框架、哪些测试采用哪些工具,如何组织人员等等。

程序的运行结果可以分成以下三类:

  • 产生的值
  • 状态变化
  • 必须一起解释为输出才有效的一个序列或一组值

测试数据全部通过,说明待测试程序在一定程度上满足需求或功能要求。测试结果的判定可能会存在误判和漏判。

待测程序使用测试用例的3种方式:

  • 程序员每次从键盘输入一个测试数据,观察测试结果并和预期值比较,记录测试通过与否。
  • 程序员通过编写测试程序,先存储测试用例,然后让待测程序逐个读取测试数据、运行、比较预期结果,同时记录测试结果。
  • 使用测试工具完成测试程序的操作及其他更多操作。

2.5 调式测试

2.5.1 缺陷的相关术语

软件Bug的准确术语是缺陷(Defect),就是软件产品中所存在的问题,最终表现为用户所需要的功能没用完全实现,不能满足或不能全部满足用户的需求。从产品内部看,软件缺陷是软件产品开发或维护过程中所存在的错误、毛病等各种问题;从产品外部看,软件缺陷是系统所需要实现的某种功能的失效或违背。

软件缺陷源自人的过失活动产生的不正确结果,导致在软件产品、模块中出现了缺陷或故障。

所谓错误就是导致不正确结果的全部。它展示了某个故障的不正确的内部状态。可以理解Bug是程序中引起错误的具体位置,因此,debug就是找出并更改程序中的错误。

2.5.2 调式基础

  • 科学的调式过程
  • 定位程序缺陷
  • 更正缺陷

2.6 讨论与提高

2.6.1 软件质量

ISO定义的6个独立的质量特性:

  • 功能性:程序是否满足了用户需求
  • 可靠性:程序保持规定的性能水平的能力
  • 可用性:程序有多容易使用
  • 效率:与程序运行时消耗的物理资源有关
  • 可维护性:是否容易修改程序
  • 可移植性:是否容易把程序移植到一个新的环境

2.6.2 软件测试的其它观点

  • 正面观:证明软件是正确的。
  • 负面观:证明程序有错误。
  • 风险角度:对软件系统中潜在的各种风险进行评估的各种活动。
  • 经济角度:以最小的代价获得高质量的软件。

2.6.3 测试设计

1、覆盖测试

(1)覆盖测试

测试所包含的软件的特征、元素、成分等方面的程度或范围。

(2)原则

  • 多覆盖域原则:满足一个测试覆盖不能为软件的正确程度提供充足的保证。
  • 测试覆盖原则:度量测试覆盖率并针对不断增强的覆盖率来改进测试数据,就能改进待测软件。

2、基于等价类划分的测试

等价类划分的两种不同情况:

  • 有效等价类
  • 无效等价类

划分原则:

  • 按区间划分
  • 按数据集合划分
  • 按限制条件划分
  • 按限制规则划分
  • 按输入方式划分
  • 细分等价类

3、基于边界值分析的测试

基本策略

  • 若输入条件指定了以a和b为边界的范围,则测试数据应该包括a,b,略大于a,略小于b,刚刚小于a,刚刚大于b的值作为非法的测试数据。
  • 若输入条件规定了输入值的个数,则用最大个数,最小个数,比最小个数少一,比最大个数多个的数作为测试数据。
  • 如果程序的规格中说明给出的输入域或输出域是有序集合,则应该选取集合的第一个元素和最后一个元素作为测试用例。
  • 如果程序中使用了一个内部数据结构,则应该选择这个内部数据结构的边界上的值作为测试用例。
  • 分析用户需求和软件设计,找出其它可能的边界条件。

2.6.4 编程风格

  • 标识符的命名域使用
  • 注释
  • 排版与布局:一致、符合常规、简明

第3章 面向对象的软件构造

3.1 抽象域封装

3.1.1 模块产生与合成

  • 函数簇包含了对核心数据的产生、变更和使用的操作函数。函数簇与其之外的其他函数通常不能产生、变更函数簇内的核心数据,仅仅使用。从模块化理解,一个函数簇实现了一组围绕核心数据的功能,具备通信内聚和功能内聚,属于强内聚。函数簇与其他函数、函数簇、模块的连接方式主要是数据的传递及对理解函数簇数据的函数的调用。
  • 局部化,如函数和复合语句的内部变量,是实现模块化的一种重要机制。函数调用可以视为模块组合的一种机制。通过函数调用把函数联系起来,构成更大规模的程序。
  • 数据结构,如数组或C的结构体为组成更大、更复杂的数据提供了构建具有层次结构的组织方式。单纯的数据结构不含对数据的操作,也不能保护其中的数据元素。可以在数据结构和数据类型的基础上实现模块化机制,这就需要抽象与封装。

3.1.2 抽象与封装

1、抽象

(1)抽象

  • 抽象是指对一个过程或者一件事物的某些细节有目的地隐藏,以便把其他方面、细节或结构表达的更清楚。
  • 抽象是从众多的事务中抽取共同的、本质性的特征,而舍弃非本质的特征。
  • 进行抽象的主要目的是希望通过把相关的属性和其它不相关的属性分开,分离关注点。
  • 抽象是处理复杂问题的一个手段。
  • 抽象是分离对象特性、限制对它们在当前环境关注的一个机制。
  • 抽象的使用者不必理解所有细节就可以使用对象,而仅仅理解对当前任务或问题的相关。

(2)分类

  • 过程抽象:使用一个函数或方法时知道它是干什么,而不知道它是如何完成的。
  • 数据抽象:将一个数据类型的特征预期实现分离。

2、封装

(1)两个含义

  • 把描述一个事务的性质和行为结合在一起成为构件,对外形成该事务的一个界限,封装能够集中而完整地对应并描述具体的事物,体现了事务相对独立性。
  • 信息隐蔽,及外界不能直接存取构件的内部信息(属性)及隐藏起来的内部操作,外部也不必知道操作的内部实现细节才能使用这些操作;信息隐藏,强制封装。

(2)优势

  • 构件外部只能通过外部可用的操作来访问内部数据和操作,降低了构件间的耦合度。
  • 构件内部修改对外部的影响变小,减少了修改引起整个程序范围的“波动效应”。
  • 更容易快速开发正确的程序,一旦程序员们确定了构件间的交互,每个人都可以独立地开发和测试分配的构件。
  • 改善通用性和维护性。

3.1.3 抽象数据类型

  • 数据类型是一个值的集合和定义在这个值上一组操作的总称。
  • 数据类型明显或隐含地规定了数据的取值范围、存储方式及允许进行的运算。
  • 数据可分为两类:原子类型、结构类型。
  • 基本数据类型:整型、浮点型、布尔型、字符、数组、结构体等。
  • 抽象数据类型(Abstract Data Type, ADT):是表示无关的数据类型,是一个数据模型及定义在该模型上的一组操作。定义有个ADT时,必须给出它的名字及各操作的名称,并且规定这些函数的参数性质。一旦定义了有个ADT及其具体实现,程序设计中就可以像使用基本数据类型那样,十分方便的使用ADT。

3.2 认识面向对象

3.2.1 设计类

1、抽象是设计类的基本方法

  • 抽象是在某个东西周围画上一个白盒子的动作:识别出它做什么、不做什么。
  • 抽象是对某个东西定义接口的动作。
  • 抽象告诉我们做什么。但是,抽象不告诉我们是如何做到这些的。

2、封装

  • 封装处理如何将这些特性模块化。封装解决的是如何划分一个系统的功能的设计问题。
  • 封装是在某个东西周围画上一个黑盒子的动作:它明确某事能完成,但是不告诉是怎样做到的。换句话说,封装对类的使用者隐藏了实现细节。
  • 为了使应用程序容易维护,要限制访问类的数据和操作。基本思路是:如果一个类想要另一个类的信息,要请求它,而不是取它。

3、模块化原则

  • 紧内聚、松耦合仍然适用于评价面向对象程序。
  • 包括类的内聚、方法的内聚;不同类之间的耦合、同一个类不同对象之间的耦合及同一个类内函数之间的耦合。
  • 类的模块化准则要求一个类应当是完整的、原始的、充分的。

3.2.2 设计操作

1、类的设计

  • 要尽量使其所有的操作都是原始的,每个操作仅提供简单、良好定义的行为。
  • 根据松散耦合的原则,也倾向于分离操作、减少它们之间的沟通。

2、平衡矛盾

  • 把复杂的行为集中在一个方法中,简化了接口,但其实现复杂了。
  • 反之,方法行为和实现简单了,但方法多了,接口复杂了。

3、建议

通常在面向对象开发中,把类的方法作为整体来设计,这是因为所有这些方法的合作构成了抽象的全部协议。设计时考虑以下建议:

  • 复用:这个行为在更多的环境中更有意义吗/li>
  • 复杂:实现这个行为有多难/li>
  • 适应︰这个行为与其置身的类有多少关系/li>
  • 实现知识∶实现这个行为要依赖于类的内部细节吗/li>

4、多态

  • 多态是类型理论的一个概念,一个名字可以表示多个不同类的实例,只要它们具有某个共同的超类而且相关。所以,被这个名字表示的任何对象都能以不同的方式对—组某些相同的操作做出响应。
  • 由于多态,一个操作就能在层次结构中的所有类以不同方式实现。这样,子类就能扩展超类的能力或者覆盖超类的操作。
  • 很多类具有相同协议时,多态最有用。如果没有多态,程序中会出现大量的if 或switch语句。

3.2.3 分类

类的使用会增加更多的类,类需要划分。分类是对整理知识的一致手段。就是试图把具有共同结构或表现出共同行为的事情分为一组。识别类和对象是面向对象开发的一个挑战。识别包含发现和发明。通过发现,我们认识到构成问题域词语的关键抽象和机制。通过发明,设计出一般化的抽象和机制,说明对象是如何协作的。发现和发明都是分类问题,其核心就是发现问题的共性。

1、类之间的关系

(1)面向对象中三种类关系

  • 普通与特殊(继承),即“是一种”。例如,加法算式是一种二元算式。
  • 整体-部分(聚合),即“是成员”。例如,算式是习题的一部分。
  • 关联,表示没有其他关系的类之间的某种语义依赖。如,“学生”和“教师”是两个独立的类,但是它们都和“课程”相关。

(2)面向对象软件还有一种常见的关系——依赖。

  • 依赖表示关系一端的成员以某种方式依赖于关系另一端的成员。它告诉开发者,如果这些元素发生变化,会影响其他成员。
  • 例如,以几何图形的显示display为例,除了显示图形的形状,还可以显示图形的颜色和线条,类Geometry则依赖类Color和Style (粗细、实线、虚线)。

2、接口与实现

(1)接口

  • Meyer认为程序设计本质上是契约:一个较大问题的功能通过把它们分包到不同元素的设计,而分解成若干较小的问题。
  • 类的接口提供了外部视角,重在抽象,同时隐藏了它的结构和组成。接口主要包括声明。
  • 类的实现是其内部视角,包含其行为,主要由所有定义在类接口操作的实现组成。

(2)类接口可进一步分成4类

  • 公共的:对所有用户可访问的声明;
  • 保护的∶仅允许类本身及其子类访问的声明;
  • 私有的∶仅允许类本身访问的声明;
  • 包∶仅允许和类在用一个包的声明。

接口是一些面向对象语言的基本元素,如Java语言的接口Interface,必须有具体的类才能实现接口定义的操作。

3.3 面向对象设计

3.3.1 面向对象的设计符

软件设计语言或符 ,如可视化图形设计符 。在面向对象设计中,普遍采用的包括描述程序静态结构的类图、描述程序动态行为的交互图。它们掩藏了类中方法的实现细节,突出了类的组成和类之间的关系,简洁清晰地表达设计意图和内容。复杂的算法、数据结构等操作的实现,仍然使用代码和伪代码补充说明。

UML不仅能使软件建模可视化,还有助于分析、评估和验证软件设计,支持从UML自动产生部分代码,指导产生测试用例。

UML类图:

  • 类用一个带有类名、属性和操作的矩形表示。分隔线用来分离类名、属性和操作。类名在矩形的最上方,其次是属性,然后是操作。
  • 类名∶具体类的名称正常书写;抽象类名加abstract 前缀,或用斜体书写;接口名加前缀interface,属性空着。
  • 属性:类似程序语言的声明——可见性、变量名、类型、初始值;前缀表示可见性:“+”公用,“-”私有,“#”保护静态变量或常量用大写字母的标识符,成员变量允许具有初始值。
  • 操作:用签名表示——可见性、返回类型、操作名称、参数及类型,可见性符 与属性的相同。

类之间的关联用一根线表示,包括每个关联类的角色名、数目、方向和约束:

  • 泛化是一端带空心三角形的连线,从子类到父类,空心三角形一端是父类。
  • 聚合用来描述一个元素(整体)包含另外的元素(部分),部分可以脱离整体作为一个独立的个体存在。聚合的整体端用空心菱形表示。
  • 组合是一种语义更强的聚合,部分组成整体,不可分割,整体消失部分也跟着消失,部分不能脱离整体而单独存在。组合的整体端用实心菱形表示。

3.4 调试的基本技术

3.4.1 单步调试源程序

1.设置断点

在要检查的语句上设置断点,程序执行到断点处会暂停。通过窗口观察此刻变量的值、检查程序运行情况;可以输入改变变量或表达式的值,然后让程序继续运行。Eclipse中在代码行左边的页边空白处双击设置断点。

2.单步调试

  • 利用单步命令,从断点处开始一次处理一条语句。变量值有助于程序员仔细观察程序的执行流程、了解程序变量值的变化、调查可疑代码。
  • 在debug视图中有三种方式:执行视图中的图标、右击出现的选择(含图标)、快捷键。

 3 .临时断点

  • 有时需要在显示的代码中临时设置有期限的断点,以便细致观察程序。
  • 在Eclipse 中,突出显示源码窗口中要设置临时断点的代码行,然后右击并选择Run to Line。

3.4.2 检查/更改变量的值

  • 当调试器暂停了程序运行后,可以执行一些调试命令来显示、改变程序变量的值。
  • 不使用调试器,在程序中增加打印语句显示程序变量值的变化也可以。
  • 在图(a)选中变量右击选Change Primitive Value,出现图(b)所示的对话框。

3.4.3 设置监视点观察变量

  • 监视点( watchpoint)结合了断点和变量检查的概念。每当指定变量的值发生变化时,都暂停程序的运行。
  • 监视点对局部变量的用途一般没有对作用域更宽的变量的用途大,因为一旦变量超出作用域(如函数结束),在局部变量上设置的监视点就会被取消。
  • main()中的局部变量例外,因为其中的变量要等到程序执行结束时才会被释放。

在Eclipse中设置监视点的方法︰

  • 在源码窗口中右击,选择Watch,然后在对话框中填写适当的表达式,如图3.11所示。代码的执行结果将显示在表达式窗口中。
  • 选中一句或一段代码右击,选择Inspect(检查)项,可以直接显示表达式的值。

3.4.4 上下移动调用栈

  • 与函数关联的运行时信息存储在称为帧的内存区域中。帧中包含了函数局部变量的值、形参,以及调用该函数的位置。
  • 系统为每个调用函数创建一个帧,并将其放在一个运行栈上;运行栈最上面的帧表示当前正在运行的函数,当函数退出时,这个帧就退出运行栈并释放所占的内存。

3.5 软件自动化测试

3.5.1 初始JUnit

1、使用JUnit进行测试的基本步骤:1-2

  • 建立测试类,命名规则∶待测类名+Test,如:BinaryOperation Test。在该类的前面用@RunWith 指定测试运行器,默认JUnit4。
  • 在用@Before注解的setUp()中为测试做必要准备(测试装置fixture )。
  • 为待测类的成员方法/函数编写测试方法,命名规则:test+待测方法,以@Test注解这个待测方法,其中务必包含测试断言。
  • 运行测试,查看运行结果,更改代码。
  • 增加方法或修改代码时,重复(3、4)。

2、JUnit4使用注解Annotation简化了测试编程。下面是用到的部分JUnit注解:

  • @Test:定义之后的方法是测试方法,否则后面的方法不是测试代码。测试方法必须是public void,即公共、无返回值的;可以抛出异常。
  • @Before:使用该注解的方法在每个测试方法执行之前都要执行一次。主要用于一些独立于用例之间的准备工作,比如创建一个对象,或者打开文件,或者连接数据库。
  • @After:使用了该注解的方法在每个测试方法执行之后要执行一次,与@Before对应,但不是必需的。
  • @Runwith :执行测试的运行器。放在测试类名之前,用来确定测试类是怎么运行的。不指定则使用默认Runner来运行测试代码。
  • @Ignore:该注解标记的测试方法在测试中会被忽略。

3.5.2 编写JUnit测试代码

1.基本测试——建立测试

  • JUnit把任何用@Test注解的方法当成一个测试用例。测试方法或测试函数可以随意命名,它们既没有参数,也没有返回值。
  • 测试方法代码的核心是调用待测程序得到实际的运行结果,用断言assertEquals比较待测程序的运行结果与预期结果是否相等。
  • 通过记录断言的真假,统计测试运行的成功与失败次数。

2.追踪失败的测试

  • 如果测试没有通过,Failures显示失败的测试数及测试函数。
  • 在Failure Trace下面可以查看错误。
  • 还可以深入探究失败的原因:选择一个失败的测试,右击,选择“Compare Result”,可以查看预期结果与实际结果的比较。

3.6 讨论与提高

3.6.1 对调用的进一步认识

1.调试与测试

  • 调试与测试都分析程序代码、选择性地运行程序,并观察程序的结果或运行过程。
  • 测试与调试的目标不同,采用了不同的技术、方法和工具。
  • 在软件构造过程中,开发者交替进行测试与调试∶测试发现程序可能存在错误,然后通过调试来修改错误,之后再通过测试确认程序错误得到了修改。

2.不调试就是最好的调试

精通程序调试不仅要掌握专门的调试器,还要充分利用其他编程辅助工具。最好的调试方法就是一开始就不要错误地编程:

  • 其次,充分利用编译器。
  • 第三,使用静态代码检查器。

3.6.2 设计原则与设计模式

1、设计模式的含义

  • 面向对象技术在软件开发过程中出现了一些可反复使用、解决实际问题的解决方案,称为设计模式。
  • 一个设计模式针对一个具体问题,用抽象方式描述解决一类特殊问题的、通用的设计方案元素。
  • 核心元素包括∶标示模式名称,描述适用的场景,刻画设计的模板,给出代码示例。

2、在面向对象技术中,用类图描述设计模式的结构,用交互图描述设计模式的行为。设计模式已经超出面向对象,设计并应用在其他的软件开发范式。设计模式也延伸到软件的其他组成成分,如算法模式和软件架构模式。从问题分析、设计思路、设计结构、模式特点、案例研究5个方面,介绍∶策略模式和迭代器模式。

3.6.3 面向对象的设计原则

1.策略模式

(1)问题分析。算法多种多样、经常改变,其他类不使用的算法。如何在运行时根据需要透明地更改对象的算法算法与对象本身解耦,从而避免上述问题/p>

(2)解决思路。使用一个抽象的策略算法,用子类继承这个公共的抽象类,定义每个具体的策略算法,把它们封装起来,通过实现抽象类的抽象算法使它们可互相替换。该模式使得算法可独立于使用它的应用客户而变化和扩展。

(3)设计结构

(4)模式特点。策略类及其子类提供了一系列可重用的封装算法,通过面向对象的多态、动态绑定技术,对象在运行时根据需要在各个算法之间进行切换。

注意:策略模式容易造成很多的策略类。 

2.迭代器模式

( 1)问题分析

在软件构建过程中要处理集合数据,集合对象的内部结构变化各异。对于这些集合对象,希望在不暴露其内部结构的同时,让外部客户代码透明地顺序访问其中的每个成员对象;同时也为同一种算法在多种集合对象上进行操作提供可能。

(2)解决思路

提供一种方法有效地按顺序访问聚合中各个成员对象,而又不暴露该聚合对象的内部表示。

(3)设计结构

 (4)模式特点

  • 迭代抽象——访问一个聚合对象的内容而无须暴露它的内部表示。
  • 迭代多态——为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
  • 健壮性考虑——遍历的同时有可能更改迭代器所在的集合结构,导致问题。

3、面向对象的5个基本原则

(1)单一职责原则(Single Responsibility Principle ,SRP )。一个类只有一种功能。面向对象程序对低耦合、高内聚原则的实践。

(2)开放封闭原则(Open Closed Principle , OCP)。软件实体(模块、函数、类)应该可以扩展,但不可修改。对扩展开放,对修改封闭。它是面向对象所有原则的核心。

  • 开放封闭原则是松散耦合的具体体现:允许程序通过类的继承、合成而扩展,但是不允许或尽量减少改变已经编译好的类。
  • 解决程序修改问题的核心是模块化。通过修改模块(类)而实现程序的开放封闭原则。
  • 例如,案例目前的要求是100以内的加减法运算,如果想把数据扩大到500,或者要求加法是200以内、减法是100以内。

(3)依赖倒转原则(Dependency-Inversion Principle, DIP)。抽象不应该依赖细节,细节应该依赖于抽象。该原则与传统的结构化分析与设计方法对立。

(4)里氏代换原则( Liskov Substitution Principle,LSP )。子类型能够替换基类型。这是保证继承复用的基础。违反了里氏代换原则必然导致违反开放封闭原则。

(5)接口隔离原则( Interface Segregation Principle,ISP )。不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。

第4章 数据处理的软件构造

4.1 数据及其持久性

保存在程序之外(如文件、 络)的数据称为持久数据。文件处理是编程语言支持应用程序存储和处理大量持久数据的一个最重要能力。Java等编程语言通常都提供了文件处理和输入/输出流的功能。

1、从比特到文件

  • 字符由比特组成。
  • 一组字符或字节组成字段,一个字段是传递含义的一组字符或字节。
  • 若干字段构成记录,记录是一组有关系的字段。
  • 一个文件可以是一组相关的记录。

2、组织文件中

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年9月10日
下一篇 2022年9月10日

相关推荐