您的当前位置:首页正文

软件构造

来源:个人技术集锦


《软件构造》课程复习纲要

参考教材《代码大全(第二版)》:以下简称CC2

复习所涉及的内容以PPT中出现的为准,具体细节参阅CC2中的解释。

1. 软件构造的概念

1) 测试和调试的区别

 测试是为了发现错误

 调试是为了改成错误

2) 软件开发包括哪些:

 问题定义

 需求分析

 实现计划

 总体设计

 详细设计

 编码实现

 系统集成

 单元测试

 系统测试

 校正性的维护

 功能强化

3) 软件构造活动主要是其中哪些部分?

1. 验证基础工作已经完成,可以进行创建工作;

2. 设计和编写子程序与模块

3. 创立数据类型并命名变量

4. 选择控制结构并组织语句块

5. 找出并修正错误

6. 评审其它小组的细节设计和代码,同时接受其它小组评审

7. 通过仔细地格式化和征集意见改进编码

8. 对分别完成的软件单元进行综合

9. 调整编码使其更小、更快

编码和调试,部分的详细设计和单元测试

4) 软件构造的重要性

 构造活动是开发软件的重要组成部分

 构造活动在开发软件中处于枢纽地位

 把主要精力集中于创建活动,可以极大地提高程序员的生产效率

 创建活动的产品,源代码,往往是软件的唯一精确描述

 创建活动是唯一一项必不可少的工作

5) 隐喻(Software Metaphors)

 常见的软件隐喻 (简单了解)

 软件书写:写代码(Writing Code)

 软件播种:生成系统(Growing a System)

 软件珍珠培植法:系统积累(System Accretion)

 软件创建:建造软件(building software)

 实用软件技术:智能工具箱(The Intellectual Toolbox)

 复合隐喻(Combing Metaphors)

隐喻可以帮助开发人员借助类比的力量加深对编程的理解

事实上,借助隐喻加深理解是各个研究领域都常用的方法

2. 软件构造的前期准备工作

1) 软件构造的前期准备工作有哪些

前期准备工作并非一成不变

 构建活动的准备工作要根据项目特点调整

 具体细节随项目的不同会有很大变化

构造开始前的准备工作有哪些

 辨明软件项目的类型

 明确问题定义

 明确需求规约

明确软件架构的个组成部分

什么是软件构造的前期准备工作

 软件构造的前期准备工作是复查性的活动

 明确前导性的工作结果有哪些

 重新审视前导性工作是否完成

 明确前导性工作完成的程度如何

 准备工作的中心目标是降低风险

 糟糕的需求分析

 糟糕的项目计划

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

根据CC2 Page59 核对表上的内容复习(只涉及ppt中有的内容)

1) 问题定义先决条件

2) 需求定义先决条件

3) 软件架构先决条件

3. 构造技术

1) 模块化设计 (简单了解相关概念)………………………………

这一部分的复习参阅CC2书上的第5章

2) 模块和子程序的区别

 子程序是具有一定功能的,可以调用的函数或过程

 模块是指数据及作用于数据的子程序的集合,或者

 模块也可能是指,可以提供一系列互相联系功能的子程序集合,而这些子程序

之间不一定有公共的数据

3) 模块化:内聚性与耦合性(重要)

ppt page55-page73

可以参阅CC2-7章子程序内聚性的相关内容

 耦合性( Coupling ):对一个软件结构内不同模块间互连程度的度量。

 内聚性(Cohesion):标志一个模块内各个处理元素彼此结合的紧密程度,理

想的内聚模块只做一件事情。

模块化

 模块化设计的目标是使每个子程序都成为一个“黑盒子”

 你知道进入盒子和从盒子里出来的是什么,却不知道里边发生什么

 通过接口的定义明确其功能

 “模块化”同时涉及到子程序设计和模块设计

 单独的子程序无法实现“黑盒子”的目标,因而需要模块

 但通过对子程序的控制可以达到模块化的效果

通过对一组子程序定义统一对外的接口,也完全有可能达到高度模块化这一目标

模块化:内聚性与耦合性

 模块独立程度的衡量标准

 耦合性( Coupling ):对一个软件结构内不同模块间互连程度的度量。

 内聚性(Cohesion):标志一个模块内各个处理元素彼此结合的紧密程度,理

想的内聚模块只做一件事情。

模块内聚性

 模块的内聚性

 内聚性表示了在一个模块中,各种操作及数据之间互相联系的紧密程度

 内聚性的强弱程度用“强度”表示

 应尽量提高内聚程度

 强调“强内聚”的目的

 每一个模块只需作好一项工作,而不必过分考虑其它任务

模块内聚性分类

 偶然内聚:一组任务关系松散(低)

 逻辑内聚:一组任务在逻辑上同属一类,例如均为输出(低)

 时间内聚:一组任务必须在同一段时间内执行(低)

子程序的内聚性

 模块的强内聚性并不说明子程序内聚性强

 子程序的内聚性影响到模块的实现质量

 子程序内聚性分类

 可取的内聚性

不可取的内聚性

可取的内聚性

 功能内聚性(最高)

 顺序内聚性(较高)

 通讯内聚性(中)

 临时内聚性(低)

功能内聚性:

 功能内聚性是最强也是最好的一种内聚

当程序执行一项并且仅仅是一项工作时,属于功能内聚

顺序内聚性

子程序内包含需要按特定顺序进行的、逐步分享数据而又不形成一个完整 功能的操作

通讯内聚性

 在一个子程序中,两个操作只是使用相同数据,而不存在其它任何联系时产生的

临时内聚性

 因为同时执行的原因才被放入同一个子程序里,这时产生临时内聚性

不可取的内聚性

 不可取的内聚性往往导致一些组织混乱而又难以调试和改进的代码

 应尽量避免不可取的内聚性的出现

 过程内聚性

 逻辑内聚性

偶然内聚性

过程内聚性

 当子程序中的操作是按某一特定顺序进行的,就是过程内聚性

 过程内聚性和顺序内聚性的区别

 顺序内聚性中的顺序操作使用的是相同数据

 过程内聚性中的操作使用的并不是相同数据

逻辑内聚性

 当一个子程序中同时含有几个操作,而其中一个操作又被传进来的控制标志所选择时,就产生了逻辑内聚性

 其内部操作仅仅是因为控制流,或者说“逻辑”的原因才联系到一起

偶然内聚性

 当同一个子程序中的操作之间无任何联系时,为偶然内聚性

 偶然内聚性也叫作“无内聚性”

模块耦合性

 模块耦合性

 耦合性表示相互作用的模块之间的互连程度

 应尽量降低模块之间的耦合性

 强调“松耦合”的目的

 用

模块应被设计成可以提供一整套功能,以便程序的其它部分与它清楚地相互作

耦合分类

 无任何连接:两个模块中的每一个都能独立地工作而不需要另一个的存在(最低耦合)。

 数据耦合:两个模块彼此通过参数交换信息,且交换的仅仅是数据(低耦合)。

 控制耦合:两个模块之间传递的信息有控制成分(中耦合)。

 公共环境耦合:两个或多个模块通过公共环境相互作用

 一个存数据,一个取数据(低耦合)

 都存取数据(低--中之间)

 内容耦合

 一个模块访问另一个模块的内部数据

 两个模块有一部分程序代码重叠

常见的OO耦合

 简单数据参数耦合

 两个对象之间通过参数传递数据,并且为简单数据类型

 简单对象耦合

 一个对象实例化另一个对象所形成的耦合关系

 对象参数耦合

 两个对象之间通过对象参数传递数据

 语义上的耦合

信息隐蔽

 信息隐蔽有称为“封装”

 使外部的可见部分和内部的不可见部分相互隔离

 信息封装是设计模块和子程序的一种重要方法

 对于模块而言,封装的部分称为内部信息

公开的部分称为模块的公开信息或接口

4) 结构化设计vs.面向对象设计

从模块的角度理解结构化设计和OO设计的区别

 面向对象设计中的模块与结构化设计中模块的含义是一致的

– 数据和功能的封装

– 满足信息隐蔽的要求

– 追求高内聚低耦合

 结构化设计中模块和模块之间的关系,被紧紧局限于信息流(结构化设计是面

向功能)

 面向对象设计方法,充分挖掘了“关系”的表达方式,可以尽可能的将事物之

间复杂的关系予以体现

5) 抽象数据类型(ADT)

了解基本概念,能够构造出简单的ADT CC2 – 6.1节

 类的基础:抽象数据类型

 类是一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、

明确定义的职责。

 ADT是指一组数据以及对这些数据所进行的操作的集合

 以上两段描述有何本质区别?

 从数据的角度来看类和ADT没有区别

 从OO的角度来看,类还涉及到面向对象中的概念

ADT中的数据

 抽象数据类型中的“数据”

 ADT中“数据” 的概念很随意

 ADT中的“数据”可以指任意类(Class)所能描述的实体

需要用到ADT的例子:文本编辑程序中对字体控制的部分

6) ADT与类的区别

体会ADT和类在概念上有什么区别。 (主要根据ppt上的介绍)

给你一个ADT,构造出相应的类(Java或C++)

体会ADT与类的区别

 使用ADT的益处

 可以隐藏实现细节

 改动不会影响到整个程序

 让接口提供更多信息

 更容易提高性能

 让程序的正确性更显而易见

 程序更具自我说明性

 无需在程序内到处传递数据

 可以在像现实世界中那样操作实体

 在类中对应的体现

 隐藏私有信息

 封装性

 真正意义的接口

 更易于修改

 易于识别错误

 可读性高

 封装数据和操作

更接近人的思维

从ADT到类:完整的理解

 类是以面向对象概念做支撑的ADT

 封装的概念

 继承的概念

 多态的概念

 ADT中的封装与类的封装有本质区别

 ADT是弱封装

 类通过可见性实现真正意义的封装(即模块化的黑盒)

7) 良好的类接口

参阅CC2-6.2节 给你不良好的类接口,能够指出其中的问题,并加以改进

(难度限于书中例子的难度)

8) 包含和继承的选择 (了解)

类之间的包含(Containment)关系

 描述了一个类拥有另外一个类的关系

 包含可以理解为“有一个……”的关系

类之间的继承(Inheritance)关系

 描述了一个类是另一个类的特化的关系

 继承可以理解为“是一个……”的关系

4. 防御式编程

1) 防御式编程的主要思想

ppt page3

 防御式编程的主要思想

 防御式编程是防御式设计(defensive design)的一种表现形式,它是为了保

证对程序的不可预见的使用不会造成之后程序功能的破坏。

 防御式编程的思想可以看成是为了尽量消除或降低墨菲法则(Murphy‘s

Law)带来的影响。

 防御式编程技术主要使用在那些容易发生错误输入以及错误的使用会带来灾

难性后果的程序片段上。

 隐喻:防御式驾驶

9) 墨菲法则 (了解)

能够使用Java、墨菲法则是指“任何有可能出错的事终将出错。(Anything that can go wrong will go wrong.)”。引申为“所有的程序都有缺陷”,或“若缺陷有很多个可能性,则它必然会朝往令情况最坏的方向发展”。

墨菲法则在设计理念上的使用

 3.5吋的软碟设计上,就设计成只有一种方法可以插进机中,而现代的光碟机也只

有一种放光碟的摆法,把出错的机会减少

c++、c#等任意一种语言建立自己的断言机制

10) 断言部分(重要)★

基本概念、使用方式

 断言是一种布尔表达式,用于在程序中表明该处所要满足的条件

 断言是指在开发期间使用的、让程序在运行时进行自检的代码

 一个断言通常含有两个参数

 一个描述假设为真时的情况的布尔表达式

 一个断言为假时需要显示的信息

 常见的两种形式(以Java为例)

 assert Expression1

 assert Expression1:Expression2

a) 建立自己的断言机制

常见的高级语言都支持断言

 如C++ 、Java 、C#和Microsoft Visual Basic等

如果不直接支持断言也可以自己实现

 J2SE 1.3及早期版本没有内建的断言机制

 C++ 中标准的assert宏并不支持文本信息

b) 使用断言的指导原则/建议

用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况

避免把需要执行的代码放到断言中

用断言来注解并验证前条件和后条件

对于高健壮性的代码,应该先使用断言再处理错误

c) 错误处理和断言

了解两者的主要区别

 错误处理代码(error-handling code) 是用来检查不太可能经常发生的非正常情况

 如,检查有害的输入数据

 错误处理属于程序运行的“正常”情况

 断言是用来检查永远不该发生的情况

 用于检查代码中的bug

 触发了断言则应该修改程序的源代码并重新编译,然后发布软件的新版本

d) 避免把需要执行的代码放到断言中

给出代码示例,能够指出此类问题

e) 用断言来验证前置和后置条件

给出代码示例,能够指出其中断言属于哪种应用

 前置条件是子程序或类的调用方代码在调用子程序或实例化对象之前要确保为真的属性

 前条件是调用方代码对其所调用的代码要承担的义务

 后置条件是子程序或类在执行结束后要确保为真的属性

 后置条件是子程序或类对调用方代码所承担的责任

11) 错误处理技术 (了解)

错误处理技术用来处理那些预料中可能要发生的错误情况

 返回中立值(如数值返回0)

 换用下一个正确的数据

 返回与前次相同的数据

 换用最接近的合法值 (如经度设置为(-180,180)之间)

 把警告信息记录到日志文件中

 用语言内建的异常机制抛出一个异常

……

 返回中立值

 继续执行操作并简单地返 回一个没有危害的数值

 例如:

– 数值计算可以返回0

– 字符串操作可以返回空字符串

– 指针操作可以返回一个空指针

 换用下一个正确的数据

 在处理数据流的时候,返回下一个正确的数据即可

 返回与前次相同的数据

 这种错误处理的思想就是“重用上一次正确的结果”

 如windows系统崩溃后用上一次配置重启动

 换用最接近的合法值

 常出现在数值超出其正常设定的上下界的时候

 把警告信息记录到日志文件中

 在使用这种方法的时候需要对错误信息进行标示,或者将警告信息单独存放,以便快速查询定位

 返回一个错误码

 当只有系统的某些部分处理错误,其他部分则不在本地(局部)处理错误时

 通知系统的方法

– 设置一个状态变量的值

– 用状态值作为函数的返回值

– 用语言内建的异常机制抛出一个异常

 调用错误处理子程序或对象

 优点:能把错误处理的职责都集中到一起

 代价:错误处理代码与整个程序紧密耦合

 在局部处理错误

 这种方法的思想是希望将错误问题限制在一个特定的区域中,而不要扩散

 缺点是会掩盖错误,而是其他部分无法获知

 具体错误处理的选择是一种平衡选择的结果

 例如局部处理错误和设计全局性错误处理子程序或对象就是相互对立的设计思想,但没有绝对的好坏

12) 异常处理机制(重要)

a) 异常的基本结构

 子程序使用throw 抛出一个异常对象,再被调用链上层其他子程序的try-catch 语

句捕获

b) 异常处理机制所涉及的要素

 异常对象

 对异常对象的抛出

 对所抛出异常的捕捉

c) finally一般被用来进行清理工作

try-catch-finally与try-catch的区别在于finally

 finally保证了在程序执行时无论有没有异常被抛出、捕捉,finally块都会被执行

finally一般被用来进行清理工作

 在异常处理时通过finally块来执行任何清除操作

 最常见的就是关闭流、关闭连接、释放或销毁资源等

d) 改进使用不当的异常情况

能够识别出代码中使用不当的异常(如,ppt P40,41 )

e) 隔离可以划分错误处理和断言

理解 通过隔离代码将错误处理和断言两种技术合理应用到自己的系统中。

 隔离部分包含了“脏数据”

 隔离部分的程序应使用错误处理技术,在那里对数据做的任何假定都是不安全的

 通过隔离部分之后的是“干净数据”

 隔栏内部的程序里就应使用断言技术,因为传进来的数据应该己在通过隔栏时被清理过了

13) 产品版和开发版

了解两者的区别

 不要自动地把产品版的限制强加于开发版之上

 产品级的软件要求能够快速地运行,而开发中的软件则允许运行缓慢

 产品级的软件要节约使用资源,而开发中的软件在使用资源时可以比较奢侈

 产品级的软件不应向用户暴露可能引起危险的操作,而开发中的软件则可以提

供一些额外的、没有安全网的操作

 调试模式的使用

14) 进攻式编程

了解什么是进攻式编程

 进攻式编程是主动暴露可能出现错误的态度

 在开发阶段让它显现出来,而在产品代码运行时让它能够自我恢复

 常用的进攻式方法

 确保断言语句使程序终止运行

 完全填充分配到的所有内存

 完全填充己分配到的所有文件或流

 止程序)

确保每一个case 语句中的default分支或else 分支都能产生严重错误(如终

 在删除一个对象之前把它填满垃圾数据

5. 语句编写的技巧

1) 条件语句 (重要)

a) 优先考虑正常代码路径

能够根据此原则对代码进行优化调整(难度限于CC2和PPT)

把正常情况的处理放在if 后面而不要放在else后面

思想:把决策的结果代码放在尽可能靠近决策位置

所谓正常的处理情况指的是程序运行的最主要路径

将注意力集中在正常路径上便于设计分支的逻辑

以上示例代码的问题在于:

从理解代码的角度来看,难以辨识程序的正常路径

从编写代码的角度来看,难以设计分支逻辑

调整的策略:

正常的路径一致地写在前面,即if语句后面

所有的错误情况都写在后面,即else语句后面

b) 利用布尔函数调用简化复杂的检测

能够根据此原则对代码进行优化调整(难度限于CC2和PPT)

2) 循环语句(重要)(难度限于CC2和PPT)

a) 循环的种类

了解常见循环语句分类各自的特点

计数循环( counted loop )执行的总次数是一定的

连续求值的循环( continuously evaluated loop )预先并不知道将要执行多少次,它会在每次送代时检查是否应该结束

无限循环(end less loop)一旦启动就会一直执行下去

迭代器循环( iteratorloop)对容器类里面的每个元素执行一次操作

b) 带退出的循环和半循环

能够使用带退出循环对带半循环的代码进行改写

c) while循环和for循环

能够对while循环和for循环代码片段进行相互改写

d) 避免依赖于循环下标最终取值

能够对依赖循环下标最终取值的代码进行改进

e) 用防卫子句( guard clause) ( 早返回或早退出) 来简化复杂的错误处理

能够用防卫子句对代码进行改进

3) 无条件跳转语句:GOTO

了解goto机制的背景和应用原则

GOTO是一种无条件跳转语句

GOTO可能是目前程序设计语言中最受争议的机制之一

目前主流的高级语言基本上都支持GOTOC、C++、Java、VB甚至Ada都(部分)支持GOTO

注:Java中goto只是保留字,但实际上可以通过break加label实现部分功能

最早的批评Dijkstra写了一封信,结论是“GOTO是有害的”

EdsgerW. Dijkstra, Go To Statement Considered Harmful, Communications of the ACM. Vol. 11, No. 3, March 1968.

该文中指出:一个程序的易读性和易理解性同其中所包含的无条件转移控制的个数成反比关系,也就是说,转向语句的个数越多,程序就越难读、难懂。

“GOTO是有害的”观点启发了结构化程序设计的思想

含有goto的代码很难安排好格式

使用goto也会破坏编译器的优化特性

使用goto会使运行速度更慢,而且代码也更大Donald Knuth (1974). Structured

Programming with go to Statements. Computing Surveys 6 (4): 261–301

如果使用位置恰当,goto可以减少重复的代码

goto在分配资源、使用资源后再释放资源的子程序里非常有用

在某些情况下,使用goto会让代码的运行速度更快,体积更小

大部分论断都反对随意使用goto

4) 表驱动法(重要)(难度限于CC2和PPT)

掌握直接访问、索引访问和阶梯访问的概念,能够:

a) 判断代码中使用的表驱动技术属于哪一种

所谓“直接访问”是指通过索引值(如下标)可以直接从表中找到对应的条目

当无法直接从表中查询需要的条目时,就需要借助其他办法先获取表键值

索引访问的方法就是先用一个基本类型的数据从一张索引表中查出一个键值,然后再用这一键值查出你感兴趣的主数据

索引表是一种间接访问的技术

阶梯访问方法不像索引结构那样直接,但是它要比索引访问方法节空间

阶梯结构的基本想法通过确定每项命中的阶梯层次确定其归类

b) 理解代码中“表”的定义和使用

其中重点掌握直接访问表、索引访问表

5) 短路求值

了解短路求值的概念

6. 代码改善

1) 软件质量的特性

a) 了解外在特性和内在特性

软件质量的特性

软件同时拥有外在的和内在的质量特性

外在特性

指的是该产品的用户所能够感受到的部分

质量的外在特性是用户关心的唯一软件特性,指软件是

否容易使用和正确运行等

内在特性

软件内部结构、组织所体现出来的特性

软件在代码层次上具有的特性

2) 常见的测试

了解单元测试、组件测试、集成测试、回归测试以及系统测试的概念

单元测试(Unit testing)

将一个程序员或者一个开发团队所编写的,一个完整的类、子程序或者小程序,从完整的系统中隔离出来进行测试

组件测试(Component testing)

将一个类、包、小程序或者其他程序元素,从一个更加完整的系统中隔离出来进行测试

这些被测代码涉及到多个程序员或者多个团队

集成测试(Integration testing)

是对两个或更多的类、包、组件或者子系统进行的联合测试,这些组件由多个程序员或者开发团队所创建

回归测试(Regression testing)

是指重复执行以前的测试用例,以便在原先通过了相同测试集合的软件中查找缺陷

系统测试(System testing)

在最终的配置下(包括同其他软硬件系统的集成)运行整个软件

3) 测试的特殊性

理解所谓测试的特殊性

测试的目标与其他开发活动背道而驰,测试的目标是找出错误

测试永远不可能彻底证明程序中没有错误

测试本身并不能改善软件的质量

测试时要求你假设会在代码里面找到错误

4) 不完整的测试

理解为什么不可能穷尽测试以及测试的基本出发点

穷尽测试

对程序的每种可能的情况都进行测试

对程序的每一种可能的输入值,以及它们之间的所有可以想象的组合进行测试

假设你有一个程序,它取一个姓名、一个地址及一个电话号码,并将这

些数据保存到文件当中。假设,其中的姓名和地址都是20 个字符长度,

每一26 个可选的字母,那么是可的入数

量:

个字符有下面能输名字: 2620 (20 个字符,每个字符有26 种选择)

地址: 2620 (20 个字符,每个字符有26 种选择)

电话号码: 1010 (10 个数字,每一个数字有10 种选择)

总共可能出现的情况: = 2620 * 2620 * 1010 ≈ 1066

测试的基本出发点

由于进行完全测试实际上是不可能的,因此测试的窍门就在于选择那些最有可能找到错误的测试用例

当规划测试的时候,要去除那些不会告诉你任何新情况的测试用例

使用不同的方法来有效地覆盖程序基本情况

总共可能出现的情况: = 2620 * 2620 * 1010 ≈ 1066

在上面的1066个可能的测试用例当中只有极少数有可能,把错误揪出来,剩下的则不会。

5) 结构化的基础测试(重要)

掌握基本概念以及基础测试用例最少数量的简单计算方法,能够根据代码设计相应的测试用例

基本思想

需要去测试程序中的每一条语句至少一次

要确保已经覆盖了所有的基础情况

以最小数量的测试用例覆盖所有路径

基本方法

最简单的方法就是算一算有多少条通过程序的路径,然

后据此开发出能通过程序里每条路径的最少数量的测试

用例

基础测试用例最少数量的简单计算方法:

1. 对通过子程序的直路,开始的时候记1

2. 遇到下面的每个关键字或者其等价物时加1

– if、while 、repeat 、for、and以及or。

3. 遇到每一个case 语句就加1, 如果case 语句没有缺省

情况,则再加1

对上例分析

要覆盖所有的基本情况至少需要6个测试用例 这并不意味着任意6 个测试用例都

能覆盖所有的基本情况。

技巧

注意那些导致你用例数量增加的关键字

– 代码中的每一个关键字都描述了非真即假的情况

– 因此,需要确保对每一种真的情况至少有一个测试用例,对每一种假的情况也至少有一个测试用例

因篇幅问题不能全部显示,请点此查看更多更全内容