「Software Engineering-2」Software Design & Realization

Abstract, Code, Test and then Iterate

Posted by Culaccino on January 13, 2021

软件开发阶段分为「软件设计」和「软件实现」两个部分,其中设计又能细分为总体设计&详细设计两部分,实现可以分为编码、单元测试、综合测试。开发阶段是软件生命周期中的重头,是迭代过程中主要的实践部分,也是技术与管理互相协作配合体现最多的部分。

1. 总体设计

在需求分析确定了”做什么“问题后,系统设计需要解决”怎么做“的问题。总体设计站在全局高度上,花较少成本,从较抽象的层次上分析对比多种可能的系统实现方案和软件结构,从中选出最佳方案和最合理的软件结构,并划分出组成系统的物理元素——程序、文件、数据库、人工过程和文档等(仍处于黑盒子级),从而用较低的成本开发出叫高质量的软件系统。

设计原理

模块化

使软件结构清晰,降低编程、测试和维护的复杂度。

抽象

层次构造。可行性研究时的抽象时,将软件作为系统的一个完整部件;需求分析时的抽象,将软件使用问题域中熟悉的语言描述;设计时的抽象,将软件抽象程度细化;最后编码时,达到抽象的最底层

逐步求精

自顶向下,逐步细化。

信息隐藏和局部化

隐藏(可参考C++类中的private),局部化(将逻辑关系密切的软件元素在物理上也放得彼此靠近)

模块独立

模块独立是模块化、抽象、信息隐藏和局部化的直接结果。我们希望软件中各个模块各司其职,各模块间又不会有太多的互相作用。我们可以用内聚耦合两个标准来衡量模块的独立程度。

耦合:不同模块间互连程度的度量,取决于模块接口的复杂度(隐藏程度)

  • 非直接耦合:没有直接关系,独立性最强
  • 数据:一个模块访问另一个模块的时候,彼此之间是通过数据参数来交换输入、输出信息的,这种耦合为数据耦合,模块间独立性较强;
  • 特征:一组模块通过参数传递记录信息,用户情况是个数据结构,所有模块都与此有关,引起了所有模块间产生依赖关系;
  • 控制:一个模块通过传送开关、标志、名字等控制信息,控制选择另一个模块的功能;
  • 外部:一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称为外部耦合;
  • 公共:若一组模块都访问同一个公共数据环境,则他们之间的耦合就称为公共耦合;
  • 内容:如果出现以下情况之一则为内容耦合:①一个模块访问另一个模块的内部数据 ②一个模块不通过正常入口而转到另一个模块的内部 ③两个模块有一部分程序代码重叠(只可能发生在汇编程序中)④一个模块有多个入口(即有几种功能)

img

内聚:一个模块内各个元素彼此结合的紧密程度(局部化程度)

  • (低)偶然内聚:一个模块完成一组任务,这些任务彼此间即使有关系,关系也是很松散的。
  • (低)逻辑内聚:一个模块完成的任务在逻辑上属于相同或相似的一类(例如一个模块产生各种类型的全部输出)。
  • (低)时间内聚:一个模块包含的任务必须在同一段时间内执行(例如将多个变量的初始化放在同一个模块中实现)。
  • (中)过程内聚:一个模块的处理元素是相关的,而且必须以特定次序执行。
  • (中)通讯内聚:一个模块中所有的元素都是用统一输出数据或产生同一个输出数据。
  • (高)顺序内聚:一个模块内的处理元素和同一个功能密切相关,而且这些处理必须顺序执行(通常一个处理元素的输出元素的输出数据作为下一个处理元素的输入数据)。
  • (高)功能内聚:一个模块内所有的元素属于一个整体完成一个单一的功能。

启发规则

  • 改进软件结构提高模块独立性
  • 模块规模应该适中(自身的规格)
  • 深度、宽度、扇出和扇入都应适当(与其他模块的连接不宜过多)
  • 模块的作用域应该在控制域之内
  • 力争降低模块接口的复杂度,尽量单出单进
  • 模块功能应该可以预测

面向数据流的设计方法

  • 变换流:传入(外部表示->内部表示)- 变换 - 传出(内部表示->外部表示)
  • 事务流:接收数据 - 判断类型 - 选择通路

2. 详细设计

详细设计阶段的根本目标是确定应该怎样具体地实现所要求的系统,从而在下一阶段(编码)可以直接把该描述翻译成程序设计语言。

结构程序设计

只用3中基本的控制结构就能实现任何单入单出口的程序:顺序、选择(IF_THEN_ELSE)、循环(DO_WHILE)。而顺序和循环可以完全实现选择,故可以进行替换改写。

结构程序设计的经典定义:仅由顺序、选择、循环连接,且单入单出口。

人机界面设计

设计模型——>实现原型——>用户使用并评估——>修改。 (即迭代过程:设计实现第一级原型——>用户提意见——>设计实现下一级原型——>……>用户满意——>停止)

过程设计的工具

程序流程图

建议尽量减少使用

  1. 诱使程序员过早地考虑程序的控制流程,不去考虑程序的全局结构
  2. 箭头即控制流不能体现约束
  3. 不易表示数据结构

盒图(N-S图)

  • 特点

    • 功能域明确
    • 控制转移受限
    • 容易确定数据的作用域
    • 容易确定模块的层次结构
  • 基本符号

判定表

实质上是n*m的全列举方法。当算法中包含多重嵌套时,可以将状态转移清晰地表示出来。但当数据元素的值变多时,其简洁程度也会下降。

【结构】 左上:所有条件;左下:所有可能做的动作;右上:(与左上)各种条件组合的一个矩阵;右下:(与左下)每种条件组合对应的动作。示例如下:

判定树

判定表的变种,易懂。示例如下:

伪码(过程设计语言PDL)

  • 优点
    • 可作为注释
    • 工具简单:文字编辑器即可
    • 已有配套的PDL处理器,可以自动生成程序代码
  • 确定:不够清晰直观,需要有一定的程序阅读能力

面向数据结构的设计方法

由于在信息处理的过程中,输入数据、内存存储的信息、以及输出数据都有其对应的数据结构,数据结构又会影响程序处理的算法和复杂度,故而在详细设计中,使用面向数据结构的设计方法,得出对程序处理过程的描述。即总体设计时重视模块间划分和设计,详细设计时更在乎每个模块的具体处理。

Jackson方法

  • (a) 顺序结构,A由BCD组成,顺序为BCD
  • (b)选择结构,A由BCD之一构成
  • (c) 可选结构,注意B右上应当加上小圆圈,A或由B组成,或不出现
  • (d) 重复结构,A由B重复组成

复杂度

MaCabe方法

  • 环形复杂度:根据程序控制流计算的复杂度
  • 流程
    • 画出流图:程序流程图中仅保留符号信息,且结点全部变成圆圈,箭头可变弯,文字信息全部丢弃。即仅展示程序流程的结构,不在乎内容。
    • 计算环形复杂度,三种通用,可以相互利用:
      • V(G)=线性无关的区域数(包括外侧未被围起来的)
      • V(G)=E-N+2(E为边数,N为点数)
      • V(G)=P+1(P为判定节点数,即原程序流程图中菱形/判断语句的个数)
    • 评估:V(G)<=10为宜

Halstead方法

根据程序中运算符和操作数的总数来度量程序的复杂程度。预测程序长度H的计算公式如下:

\[H=n_1log_2n_1+n_2log_2n_2\]

3. 实现(编码与测试)

至此,软件开发进入至底层实现部分。编码时所选用的语言特点、及程序员的编程风格会对程序的可读性、可靠性、可测试性和可维护性产生深远的影响;而测试是能够发现设计阶段”逻辑蛀虫“的唯一手段,其主要分为单元测试和综合测试,分别由开发者(编码者)和测试人员承担。

编码

  • 语言选择
    • 用户优先
    • 高级语言
  • 编码风格
    • 文档
      • 标识符,注解和视觉组织结构(层次结构)
    • 数据说明
      • 按字母顺序
      • 数据结构:注解说明
    • 语句
      • 避免压行
      • 避免复杂条件判断和逻辑嵌套
      • 使用括号等使运算直观
    • 输入输出
      • 输入检查
      • 使用结束符结束而不是让用户指定数目
      • 交互输入要有明确提示
      • 设计良好的输出报表
      • 给输出数据加标志
    • 效率:时间空间输入输出复杂度等

单元测试

  • 重点:接口,数据结构,执行通路,出错处理通路,边界条件
  • 流程
    • 人工测试(可以一次检查多个错误):普通法(多人),预排法(人+计算机,根据方案模拟运行)
    • 计算机测试:驱动程序(测试数据-打印结果),存根程序(代替测试模块调用其他模块-打印结果)

集成测试

渐增式:把程序划分成小段来构造测试,以便于错误定位;可以对接口更进一步测试;可以使用系统化的测试方法 非渐增式:把所有模块放到一起,作为一个整体进行测试,测试、排查非常复杂。

  • 渐增式测试

    • 自顶向下:DFS或BFS策略,利用存根程序

      1. 对主控模块进行测试,用存根程序代替所有直接附属的模块
      2. 根据选定的策略,每次用一个实际模块代换存根模块
      3. 每并入一个模块就进行一次测试
      4. 加入模块后可能需要进行回归测试。

      从②开始不断重复循环,直到构造起完整的软件结构为止。自顶向下可能会导致测试集中在高层次的测试处理上,而不得不将低层部分全部用存根程序代替,因此没有重要的数据自下往上流,故而失去了在特定的测试和组装特定的模块之间的精确对应关系,导致在确定错误的位置和原因时发生困难。

    • 自底向上:利用驱动程序

      1. 把低层模块组合成一个子功能族
      2. 利用驱动程序测试输出正确性
      3. 对模块的子功能进行测试
      4. 去掉驱动程序,把子功能组组合起来成为更大的子功能组

      从②开始不断循环,直到回溯到最顶层模块。

白盒(结构)测试

白盒测试多用于软件开发早期,可用于测试过程量,反推程序结构的正确性。

  • 逻辑覆盖
    • 语句覆盖:让程序中每个语句至少执行一次
    • 判定覆盖:每个语句、每个分支都至少执行一次
    • 条件覆盖:每个语句、每个条件都符合过
    • 判定/条件覆盖:同时满足判定和条件
    • 条件组合覆盖:所有可能组合(2^n),最全面
    • 点覆盖:等同语句覆盖(图形化思想)
    • 边覆盖:等同判定覆盖(图形化思想)
  • 控制结构测试
    • 基本路径测试
      • 画出流图
      • 计算环形复杂度n=V(G)
      • 找出n条线性独立的路线(即不能包含与其他路径中)
      • 根据路线设计测试用例
    • 条件路径测试
    • 循环路径测试

黑盒(功能)测试

黑盒测试多用于软件开发的中后期,测试员的关心点不在于程序结构是否正确,而是通过大量测试数据,根据输出的正确性,测试程序的鲁棒性是否强。

  • 等价类划分

    把所有可能的输入数据划分为若干等价类,每一类中的一个典型值在测试中的作用与这一类中所有其他值的作用相同。

    可以根据输入约束划分相应的有效等价类和无效等价类。

  • 边界值分析

    选取刚好等于、稍大、稍小于等价类边界的值。往往在边界值上存在需要特判的情况。

调试

  • 蛮干法:最低效
  • 回溯法:调试小程序较有用
  • 原因排除法:对分查找、归纳、演绎

软件可靠性/可用性

可靠性:程序在给定的时间间隔(0-t时间)内,按照规格说明说的规定成功地运行的概率。

可用性:程序在给定的时间点(t时刻),按照规格说明书的规定成功地运行的概率。

估算平均无故障时间:

  • 符号
    • Et - 测试前程序中错误总数
    • It - 程序长度(机器指令总数)
    • τ - 测试(包括调试)时间
    • Ed(τ) - 在0至τ期间发现的错误数
    • Ec(τ) - 在0指τ期间改正的错误数
  • 基本假定
    • 经验数据:0.5e-2 <= Er/Ir <= 2e-2
    • 平均故障时间MTTF=1/K(Et/It-Ec(τ)/It),K取200为宜