软件工程
教材
Software Engineering: Theory and Practice (Fourth Edition)
Chapter 1 Why Software Engineering
1.1 什么是软件工程
软件工程的核心思想:把软件产品看作是一个工程产品来处理,把需求计划、可行性研究、工程审核、质量监督等工程化的概念引入到软件生产中,期望达到一般工程项目的三要素:进度、经费、质量。
问题求解
- 分析(analyzing):将问题分解成可以理解并能够处理的若干小的部分。
- 合成(synthesis):把小的构造块组合成一个大的结构。
- 方法(method)/技术(technique):产生某些结果的形式化过程。
- 工具(tool):用更好的方式完成某件事情的设备或自动化系统。
- 过程(procedure):把工具和技术结合起来,共同生产特定产品。
- 范型(paradigm):表示构造软件的特定方法或哲学。
软件工程师使用工具、技术、过程和范型来提高软件产品的质量。
软件工程师的角色
以计算机科学理论和计算机功能为基础,通过对要解决问题的本质的了解,采用相应的工具和技术,实现设计方案,推出高质量的软件产品。
1.2 软件工程的进展
描述“bug”的术语
- 错误(error):a human made mistakes in software producing. 即人们在进行软件开发活动的过程中出错(如误解需求,代码出错)
- 故障(fault):problem in function implementation. 功能实现中的问题(one error leads to several faults. static exist)
- 失效(failure):failure in running software. 软件(系统)运行中的错误(系统违背了它应有的行为),由于faults(dynamic exist)
零缺陷软件(Zero-defect software)
由于市场压力(market forces)促使软件开发人员快速交付产品,几乎没有时间进行完全的测试。所以零缺陷软件是不可能的(换句话说不能保证?)
关于"bugs"的讨论
- 有时改正(fixing)比重写(rewriting)更加困难
- 纠错是代价高昂的,越在系统生命周期后越高昂
- 复审(review)是非常重要的,包括自我审查(Self check)和同行评审(Peer review)。Fagan的研究中指出前者只能找出开发阶段1/5的故障,而后者为4/5
1.3 什么是高质量软件
Kitchenham和Pfleeger指出应该以三种方式来考虑质量:产品的质量、生产该产品的过程的质量、在产品将使用的商业环境背景下产品的质量。
产品的质量(Quality of the product)
- 用户(users)角度:功能充足,容易学习且容易使用
- 开发人员(developers)角度:关注产品的内部特性(internal characteristics)
McCall的质量模型: 左侧为外部质量因素,右侧为产品质量因素
过程的质量(Quality of the process)
即软件开发和维护过程的质量。
对于过程,我们可能有以下问题:
- 在什么时间、什么地点,我们可能发现某种特定类型的故障?
- 如何能够在开发过程的更早期阶段发现故障?
- 如何构建容错机制以便把故障演变为失效的可能性降到最低?
- 是否有一些其他的做法能够在确保质量的前提下使我们的过程更加高效或有效?
针对这些问题,我们只有对过程进行建模(量化)才能够解答。
一些模型:
Capability Maturity Model(CMM):能力成熟度模型
ISO 9000:该标准影响较小
SPICE:软件过程改进和能力确定模型
补充:ISO 9000 由英国学院派一批没有做过复杂系统的人制定出来的标准(八股味渐浓)
CMM->CMMI,但是该模型强调以开发过程为中心,而不是以人为中心,在中国有些水土不服,且CMM评估耗资不菲,评估过程复杂,耗时长,常常导致效果也不太理想。不少认证流于形式。
商业环境背景下的质量(Business Value)
要考虑到技术价值与商业价值的联系与区别
技术价值(technical value):技术指标(速度、正确运行时间、维护成本等)
商业价值(business value):机构对软件是否与其战略利益相吻合的一种价值评估
技术价值不能自动地与商业价值等同
我们的目标是将技术价值与商业价值在软件中统一起来。
改进过程 的商业价值(Business value of process improvement):
传统规划(traditional formula):Return On Investment(ROI)= 资金 + 利润 + 风险金
但工业界通过项目工作量来看待投资,公司感兴趣的是节省时间和使用更少的人,而他们对投资回报的定义也体现出他们将重点放在减少工作量上。
这些公司的投资回报包含:
- 培训(training)
- 进度(schedule)
- 风险(risk)
- 质量(quality)
- 生产率(productivity)
- 过程(process)
- 客户(customer)
- 成本(costs)
- 业务(business)
目标是通过减少工作量(effort)来降低成本
1.4 软件工程的涉及人员
客户(customer)、开发者(developer)、用户(user)
客户、开发者可以是公司、机构或个人,而用户则是软件的使用者
客户、用户和开发人员这种简单的区分也越来越复杂,开发人员可能存在使用额外的开发人员——分包商(subcontractor)的情况(外包)
1.5 系统的方法
为项目作边界(boundary)规划是重要的。即你必须知道项目从哪开始,到哪结束,需要做什么。同样的问题可以应用于任何系统。一个系统是对象和活动的集合,再加上对象和活动之间关系的描述。
活动是发生在系统中的某些事情,通常描述为由某个触发器引发的事件,活动通过改变某一特性将一个事物转变成另一个事物。(input + actions + output)
对象/实体是活动中涉及的要素
关系是对实体和活动中数据项及动作的相互关系的描述
边界:系统中的项和处理的范围
综上系统就是由一组实体、一组活动、实体和活动之间关系的描述、系统边界组成的集合。
由于系统之间可能存在相互联系,因此刻画系统边界非常重要。
在由许多系统组成的较大型软件工程的前提下,框架规划设计是必要的。
增量式开发方法(Incremental development approach):包含一系列阶段,其中每一个阶段都使前面的系统不受当前系统约束的限制。
1.6 工程的方法
书中借助造房子的例子引出软件的开发包含下面的活动:
- 需求分析和定义(Requirement analysis and definition):
包括问题定义(problem definition)、可行性研究(feasibility research)、需求分析(requirement analysis)、复审(review by all anticipants)。其中需求分析中有软件需求规格说明书(Software Requirement Specification),简称SRS,包含边界、实体、活动等内容。 - 系统设计(System Design):
包括用户界面、系统结构图和报告、复审(review by developer and customer)。系统结构图(Software architecture diagrams),简称SAD,内容为怎么实现系统,即用到的拓扑、功能、数据架构 - 程序设计(Program Design):
包括模块功能算法与数据描述、复审(review by developers) - 编写程序/程序实现(Program Implementation):
包括编程和调试、复审(review by developer/coders) - 单元测试(Unit Testing):
包括模块功能测试与性能测试(生成测试报告)、复审(review by test team) - 集成测试(Integration Test):
根据SAD集成测试、复审(review by test team) - 系统测试(System Testing):
根据SRS对系统整体进行测试、复审(review by developer and customer) - 系统交付(System Delivery):
包括交付(有直接、并行、逐渐三种方式,还有用户培训)、复审(检查用户手册和操作手册?) - 维护(Maintenance):
包括修改软件(改错或满足新需求)的过程(撰写维护报告),复审(review by maintenance team)
上述阶段只是大致划分,实际执行时更为复杂,还有若干辅助性过程和阶段,同时有些阶段是相互重叠、相互影响的。比如测试阶段,实际上在需求分析和系统设计时就可能开始制定和完善测试计划了。
1.7 开发团队的成员
分为分析员、设计人员、程序员、测试人员、培训人员、维护人员、资料管理员、配置管理员
1.8 软件工程发生的变化及其起因
假如能轻易列出软件工程的开发步骤,那么,为什么软件工程师生产高质量软件如此艰难?
因为在实际开发过程是非常困难和复杂的。比如客户可能随时会更改需求;大多数系统并不是单独存在的,它们与其他的系统交互,或接受信息或提供信息。难以确保系统间接口的文档的精确性和完整性。
所以要以灵活的方式运用软件工程工具与技术,研究软件工程的较大规模分析设计技术。二者都是至关重要的。
变化的本质
早期的应用程序,系统用两种基本方式来设计:转换(Transformation)和事务(Transaction)。转换将输入转换为输出,事务则由输入决定哪个功能将被执行。
而现在的应用程序,通常运行在多个系统上,除了执行用户需要的功能,还有网络控制、安全性、用户界面表示和处理,以及数据或对象管理。
传统的瀑布模型,假定开发活动是线性的,即只有在一个活动的前一个活动完成以后才进行该活动。这种模型已经不适用于当今的系统了。
Wasserman总结了改变软件工程实践的七个关键要素:
- 商业产品投入市场时间的紧迫性
- 计算经济学的改变:更低的硬件成本,更高的开发、维护成本
- 功能强大的桌面计算平台的可用性
- 广泛的局域网和广域网
- 面向对象技术的出现和采用
- 使用窗口、图标、菜单和指针的图形用户界面
- 软件开发瀑布模型的不可预测性
Wasserman提出了软件工程中的八个基本概念来应对这一挑战,也构成了有效软件工程规范的基础:
- 抽象(Abstraction):
基于某种层次归纳水平的问题描述。它使我们将注意力集中在问题的关键方面而非细节。软件分析与设计要学会抽象,学会把问题逐步抽象成为类的集合,软件任务就是通过类之间的交互来完成。 - 分析和设计方法以及表示法(Analysis and design methods and notations):
采用标准的符号表示系统,利于交流、建模并检查完整性和一致性、易于对需求和设计部件进行重用。 - 用户界面原型化(User interface prototyping):
原型化意味着建立系统的小型版,通常具有有限的关键功能,以利于用户评价和选择。原型化通常用来设计一个良好的用户界面,当然在其他场合也可以使用原型。 - 软件体系结构(Software architecture):
定义:一组体系结构单元及其相互关系集来描述软件系统。单元越独立,体系结构越模块化,就越容易分别设计和开发不同的部分。
Wasserman指出至少有五种方法将系统划分为单元:
(1)模块化分解:基于指派到模块的功能。
(2)面向数据的分解:基于外部数据结构。
(3)面向事件的分解:基于系统必须处理的事件。
(4)由外到内的设计:基于系统的用户输入。
(5)面向对象的设计:基于标识的对象的类以及它们之间的相互关系。
一个系统可以由不同体系结构来组成。 - 软件过程(Software process):
定义:软件开发活动中的各种组织及规范方法。注意活动中的组织和规范对软件的质量和开发的速度有积极作用,但是Wasserman指出由于不同应用类型和组织文化之间的巨大差异使得对过程本身进行预先规定是不可能的。因此,软件过程不可能以抽象和模块化的方式作为软件工程的基础。 - 复用:
重复采用以前开发的软件系统中具有共性的部件,用到新的开发项目中去(这里的复用不仅是源代码的复用)。但想要制定一个长期、有效的可复用计划可能是很困难的,困难如下:
(1)有时候构建一个小构件比起在可复用构件库中搜索来得更快
(2)要使一个构件足够通用,可能需要花费很多额外的时间
(3)难以保证构件的质量和测试程度
(4)如果某个复用的构件失效或需要更新,不清楚谁对此负责
(5)理解和复用一个由他人编写的构件,通常是困难的。
(6)在通用性和专业性之间通常存在冲突 - 测度:
量化我们做了什么以及我们的目标是什么,从而用通用数学语言来描述我们的行动和结果。
两个方面:量化描述系统+量化审核系统 - 工具和集成环境
使用工具和标准化的集成开发环境来增强软件开发。Wasserman指出在工具集成中必须处理的五个问题:
(1)平台集成:工具在异构型网络中的互操作能力。
(2)表示集成:用户界面的共性。
(3)过程集成:工具和开发过程之间的链接。
(4)数据集成:工具共享数据的方式。
(5)控制集成:一个工具通知和启动另一个工具中的动作的能力。
Chapter 5 Designing the System
5.5 Issues in Design
- 可修改性
- 性能
- 安全性
- 可靠性
- 健壮性
- 易使用性
- 商业目标
设计中的协作关系
关于开发团队的几个任务
- 人员选择与分工(谁最适合设计系统的某个部分)
- 问题的表达语言和文档组织形式
- 设计的部件之间的协调与有效交互
设计过程崩溃的主要类型
- 缺少具体的设计方案
- 缺少设计过程的元方案
- 问题和解决方案的优先级选择不合理
- 约束不明
- 没有可验证的模拟设计
- 进度难以跟踪
- 从总体开始到底层,解决方法不够完整
执行协作设计的主要问题
- 解决个人经验,理解能力和爱好等方面的差异,妥善搭配
- 解决人们在团队的行为方式与单独的行为方式的不同:例如日本团队,上下级观念强
- 设计是一个协作和迭代的过程,共识需要充分讨论
设计用户界面
外观、感觉、模型的导航规则、思维模型、寓意/比喻
- 文化差异问题:考虑信仰,价值观,道德规范,传统,风俗,传说等
- 用户爱好问题
用户界面设计是否成功将直接影响着系统的质量,并最终影响着用户对系统的满意程度
用户界面的好坏直接影响软件的价值
图形用户界面的设计要求以用户为中心,应该使用用户术语实现与用户的交互
界面设计的原则:
- 能够及时提供信息反馈
- 对用户出错的宽容性
- 支持快捷方式的使用
- 尽量减少对用户记忆的要求
- 快速的系统响应
并发性
5.6 优秀软件设计所具备的特征(含6.2节)
高质量的设计应该满足以下特征:
- 易理解
- 易实现
- 易测试
- 易修改
- 对需求的正确理解
部件/模块独立性
模块的独立性程度可以分为内聚和耦合
耦合(Coupling)
两个软件模块之间的相互关联程度(忽略模块内)
高度耦合、松耦合、解耦合的
学习耦合的目的就是为了降低模块间的依赖性
- 非直接耦合:模块相互之间没有信息传递
- 数据耦合:字面意思,模块间传递的是数据
- 特征耦合:模块间传递的是数据结构
- 控制耦合:模块间传递的是控制量
- 公共耦合:不同模块访问公共数据
- 内容耦合:一个模块直接修改另一个模块(A模块直接调用B模块的私用数据或者直接转移到B模块去)
注意,面向对象的设计具有天然的低耦合,原因在于通常,每个对象都具有相对的独立性。
我们设计的目的是追求低耦合,但无法禁止/避免高耦合模块的出现/使用
内聚(Cohesion)
软件模块内部各组成成分的关联程度
目的是每个模块尽可能地凝聚
- 偶然性内聚:模块的各个部分互不相关,只是为了方便或偶然的原因处于同一个模块中
- 逻辑性内聚:模块的各个部分只通过代码的逻辑结构相关联
- 时间性内聚:模块各部分要求在同一时间完成
- 过程性内聚:各部分有特定次序
- 通讯性内聚:各部分访问共享数据
- 顺序性内聚:各部分有输入输出关系
- 功能性内聚:各部分组成单一功能
面向对象的设计带有较高程度的内聚
我们设计的目的是追求高内聚,但我们无法禁止/避免低内聚模块的出现/使用
内聚度和耦合度的质量是设计质量测度的重要指标之一
意外的识别和处理
SRS一般描述的是系统应该做什么,但通常不会显式描述系统不应该做什么
意外:不曾预料到的行为/事件
- 未能提供相应服务
- 提供了错误的服务或数据
- 数据崩溃
所以优秀的设计应该允许用户向系统报告异常,系统设计时应该嵌入故障树分析和失效模式分析技术,来表示和识别异常以进行最后的异常处理
故障树是一个倒置的树,不同的路径分支总会构成不同的故障。根节点表示想分析的故障/失效
最难处理的异常是不规律的异常
缺陷的预防和容错
- 主动的错误检测:做更多的预先检查和防范;一个功能有多个预留的实现途径
- 被动的错误检测:等待故障发生再处理
- 容错设计:当软件失败发生时,采取措施减少损失并将损害隔离开来,在用户接受的条件下使系统继续运行。
安全性分析(5.7.3)
把不安全因素看成风险,在设计系统体系结构或程序结构时,有关安全问题必须进行以下风险分析
- 软件特征化:深入理解分析系统的目标和实现方式
- 威胁分析:针对威胁来源和各种威胁活动
- 漏洞评估:寻找系统设计的各种问题
- 风险可能性分析:
- 风险影响决策
- 风险缓解计划:
成本效益分析(5.7.5)
Chapter 6 面对对象的思考
什么是面向对象?
面向对象是一种软件开发方法,它将问题及其解决方法组织成一系列独立的对象,数据结构和动作都被包括在内。
如何识别一个OO的表示?
要满足如下七个特征:
OO的开发过程
关于OO的几个问题
- OO开发的优势:语义表达的一致性;过程的一致性
- 三个角度描述:静态是
- OO过程的定义:OO的需求、OO的高层设计、OO的低层设计、OOP、OO的测试
潜台词OO的设计可以由粗到细,直到实现演化而成
OO开发过程的特征: - 跨越全过程的一致性
- OO的思想是一个方法论和一种解决方法的表达形式
设计模式
设计模式与具体的基于OO的语言没有关系,最重要的是建立面向对象的基本思想,尽可能在设计中实现抽象运用、面向接口编程、低耦合、高内聚、可复用、可扩展等概念
模式:是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。
设计模式:是一套被反复使用的代码设计经验的总结,使用设计模式目的是为了可复用代码、让代码更容易被他人理解并且保证软件质量。
说明:设计模式提供的是较低层次的设计决策,即针对单个软件模块或少量模块而给出的一般性解决方案,也是软件体系结构之下的应对各种软件需求的具体设计思考模式。
简单工厂
抽象工厂将一系列语义相互关联的产品设计到一个工厂类,当增加多个语义相关产品时,不能简单的增加工厂类来解决问题时,选择
创建者模式
关于薪资计算的抽象工厂模式带来的问题,例如计算一系列语义相关产品后,如薪资、社保、个税后,如果还要对这些结果进行整理,有时还要组装成新的对象。这不得不在客户端的代码中进行,这是不可接受的。一种方案是将零件组装放在工厂类里,但违背了工厂类单一职责的设计原则。所以针对比较复杂的软件产品,涉及复杂对象的组装与创建,将设计一个单独的类来负责产品的组装。
Chapter 8 测试
不能仅依赖开发人员测试,也不能仅依赖外部机构的测试,必须建立一个独立的测试部门。
测试的唯一目的是发现错误/缺陷
faults:静态的,就是个error(缺陷)
failure:软件的动作与需求描述的不相符,称之为失败
只涉及降低代码缺陷的设计技术
Types of faults
- 计算和精度缺陷:算法或公式在编程实现时出现错误或最终结果达不到精度要求
- 文档缺陷:文档描述与程序动作不相符
- 过载缺陷(overload faults):对定长数据结构的使用超出了规定的能力,如队列长度、缓冲区大小
- 能力/容量缺陷:当系统活动接近它的上限时性能可能无法接受(例如用户数量接近上限,产生延迟等)
- 时序性缺陷:当多个流程的协调或精心定义的时间顺序被打破时
- 性能缺陷:与能力缺陷的区别,在于性能缺陷为在正常条件下性能需求不能满足,可能组装后测试不充分导致
- 恢复性缺陷:
- 硬件和系统软件缺陷:
- 代码的标准和规程缺陷:
测试的组织 - Unit test: verifies the component functions
- integration test: verifies the system components work together by system design and program design
- function test: check function by srs
- performance test:check performance by srs
- acceptance test: check the customer’s requirement
- installation test: check the system in actual environment
测试步骤的关系
测试的观点/方法
- 黑盒:测试人员在完全不了解程序内部的逻辑结构和内部特性的情况下,只根据程序的需求规格及设计说明,检查程序的功能是否符合它的功能说明。被测模块完成一切应做的事情,拒绝一切不应做的事情。黑盒测试的参考文档是系统需求、主要文档是系统设计和程序设计阶段文档。若是可重用部件,则是类似系统文档。
- 白盒:知晓测试对象的结构。
Unit test:
- 检查代码:代码复审(面向提交)
- 编译代码:
- 设计测试用例做测试
测试程序模块
以测试程序为目的而挑选的输入数据,包括对应的期望结果。
软件测试无法穷尽所有的情况
所以软件测试是有风险的行为
软件缺陷的寄生虫性:找到的软件缺陷越多,就说明软件缺陷越多
原因:程序员的疲倦、程序员往往犯同样的错误,某些软件的缺陷其实是大灾难的征兆。
有趣的是,一支团队开发的程序,其缺陷也有其分布规律。
软件测试的杀虫剂现象:软件测试越多,其免疫力越强的现象(越难找到缺陷)
不断编写不同的新的测试程序
软件测试的不修复原则:并非所有软件缺陷都能修复
不需要修复的原因可能有:
- 没有足够的时间
- 不算真正的软件缺陷
- 修复的风险太大
- 不值得修复
微软软件测试中的BUG,BUG定义广泛,在软件使用过程中所出现的任何一个可疑问题,或者导致软件不能符合设计要求或满足消费者需要的问题都是BUG。即BUG有时候并不是程序错误,例如有时候软件没有按照一般用户的使用习惯来运行,也可以视为一个BUG。
微软Alpha测试和Beta测试
根据微软的经验,每修复三到四个BUG一般就会产生一个新的BUG。
从事软件测试需要兴趣、热情与耐心
- 测试人员必须且有能力怀疑一切
基本测试用例的设计
黑盒测试法
等价分类法:把被测程序的输入域划分为若干等价类。出发点是,每一个测试用例都代表了一类与它等价的其他例子。如果用这个例子未能发现程序的错误,则与它等价的其他例子一般也不会发现程序的错误。
设计等价类的测试用例一般分为两步进行,即首先划分等价类并给出定义;然后选择测试用例。
选择的原则是,有效等价类的测试用例尽量公用,以期进一步减少测试次数,无效等价类必须每类一例,以防漏掉本来可能发现的错误。
公用的前提是将输入域切分,并组合式输入
边界值分析法,在等价分类法中,代表一个类的测试数据可以在这个类的允许范围内任意选择。但如果把测试值选在等价类的边界上,往往有更好的效果。
错误猜测法:猜测被测程序中哪些地方容易出错,更多地依赖于测试人员的直觉与经验。
白盒测试法
白盒测试以程序的内部逻辑为依据。合理的白盒测试,就是要选取足够的测试用例,对源代码实现比较充分的覆盖,以便尽可能多地发现程序中的错误。
逻辑覆盖法
逻辑覆盖是一组覆盖方法的总称:
- 语句覆盖:使被测程序地每条语句至少执行一次
- 判定覆盖:使被测程序的每一分支至少执行一次,又称分支覆盖
- 条件覆盖(补充):要求判定中的每个条件均按真、假两种结果至少执行一次。如果判定中仅含一个条件,条件覆盖也就是判定覆盖
- 条件组合覆盖(补充):与条件覆盖的区别不是简单地要求每个条件都出现真、假两种结果,二是要求这些结果的所有可能都至少出现一次,即A and B条件组合有2*2四种情况,至少都要覆盖
- 路径覆盖