1.软件维护与演化
软件维护(Software maintenance) 是在软件产品交付后对其进行的修改,以纠正故障、提高性能或其他属性
修复完代码之后还要测试所做的修改、进行回归测试、记录发生了什么变化
软件维护分为几类:
- 纠错性维护:交付后对软件产品进行的反应性修改,以纠正发现的问题
- 适应性维护:交付后对软件产品进行的修改,以保持软件产品在变化或变化的环境中可用
- 完善性维护(最主要):在交付后增强软件产品,以提高性能或可维护性
- 预防性维护:在交付后对软件产品进行修改,以便在软件产品中的潜在故障变为有效故障之前检测并纠正这些故障。
软件演化(Software evolution):最初开发软件,然后由于各种原因不断更新的过程。软件的大部分成本来自于维护阶段
2.维修性指标
一些常用的可维护性度量:
-
圈/环复杂度 :
用来度量代码的结构复杂性
计算程序独立路径的数量创建的
越复杂,越难以实现高代码覆盖率 -
代码行数:
指示代码中的大致行数
表示某个类型或方法做了太多的事情,要进行方法拆分
也可以表示类型或方法可能很难维护 -
Halstead Volume:
基于源代码中(不同的)运算符和操作数数量的复合度量 -
可维护性指数:
计算一个介于0和100之间的指数值,表示代码维护的相对容易程度 -
继承的层次数
指示扩展到类层次结构根的类定义数 -
类之间的耦合度
-
单元测试的覆盖度
自然,代码越复杂,越难以维护
3.模块化设计与模块化原则
模块化编程(Modular programming): 是一种设计技术,它强调将程序的功能分离为独立的、可互换的模块,使每个模块都包含执行所需功能的一个方面所必需的一切。
设计的目标是将系统划分为模块,并以以下方式在组件之间分配责任:
- 高内聚
视类与类之间的关系而定,高,意思是他们之间的关系要简单,明了,不要有很强的关系,不然,运行起来就会出问题。一个类的运行影响到其他的类。
- 低耦合
耦合:是对模块间关联程度的度量。
模块化降低了程序员在任何时候必须处理的总复杂性:
- 分离关注点
- 信息隐藏
内聚和耦合原则可能是评估设计可维护性的最重要的设计原则
3.1 评价模块性的五个标准
- 可分解性:较大的组件是否分解为较小的组件
目标:使模块之间的依赖关系显式化和最小化 - 可组合性:较大的部件是由较小的部件组成的吗
目标:使模块可在不同的环境下复用 - 可理解性:组件是否可以单独理解
- 可持续性:发生变化时受影响范围最小
- 出现异常之后的保护:出现异常后受影响范围最小
3.2 模块化设计的五个原则
-
直接映射
模块的结构与现实世界中问题领域的结构保持一致
对以下评价标准产生影响:可持续性、可分解性 -
尽可能少的接口
模块应尽可能少的与其他模块通讯
对以下评价标准产生影响:可持续性、保护性、可理解性、可组合性 -
尽可能小的接口
如果两个模块通讯,那么它们应交换尽可能少的信息
对以下评价标准产生影响:可持续性、保护性 -
显式接口
当A与B通讯时,应明显的发生在A与B的接口之间
受影响的评价标准:可分解性、可组合性、可持续性、 可理解性 -
信息隐藏
经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面
受影响的评价标准:可持续性
3.3 低耦合和高内聚
耦合(Coupling): 是模块之间依赖性的度量。如果一个模块中的更改可能需要另一个模块中的更改,则两个模块之间存在依赖关系
模块之间的耦合程度由以下因素决定:模块间的接口数目,每个接口的复杂度
比如:HTML、CSS与Javascript间的耦合
指定数据和语义的HTML文件
CSS指定HTML数据的外观和格式
定义页面行为/交互性的JavaScript
内聚(Cohesion): 是衡量模块的功能或职责有多紧密相关的一个指标
如果模块的所有元素都朝着同一个目标工作,那么模块就具有很高的内聚性
最好的设计在模块内具有高内聚力,而在模块之间具有低耦合
4.面向对象设计原则:SOLID
SOLID:5类设计原则
- 单一责任原则(SRP)
- 开放-封闭原则(OCP)
- Liskov替换原则(LSP)
- 依赖转置原则(DIP)
- 接口聚合原则(ISP)
4.1 单一责任原则
ADT中不应该有多于1个原因让其发生变化,否则就拆分开
如果一个类包含了多个责任,那么将引起不良后果:
- 引入额外的包,占据资源
- 导致频繁的重新配置、部署等
4.2 (面向变化的)开放/封闭原则
对扩展性的开放:模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化
对修改的封闭:但模块自身的代码是不应被修改的,扩展模块行为的一般途径是修改模块的内部实现,如果一个模块不能被修改,那么它通常被认为是具有固定的行为
关键的解决方案:抽象技术 使用继承和组合/委托来更改类的行为
4.3 Liskov替换原则
子类在代替基类使用时应该表现良好
派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异
具体的之前介绍过
4.4 接口隔离原则
不能强迫客户端依赖于它们不需要的接口:只提供必需的接口
不要强迫类实现它们无法实现的方法
避免接口污染(派生类必须实现某些它用不到的功能)
避免胖接口(接口中定义了不是所有实现类都需要的方法)
4.5 依赖转置原则
高层模块不应该依赖于低层模块,二者都应该依赖于抽象
抽象不应该依赖于实现细节,实现细节应该依赖于抽象
应该使用大量的接口和抽象!
4.6 小结
OO设计的两大武器:抽象和分离
归纳起来:让类保持责任单一、接口稳定
5.语法驱动的构造
一些程序模块以字节序列或字符序列的形式接收输入或产生输出,称为IO流
一般来说这些字符序列都按一定的规律排列:
- 输入文件有特定格式,程序需读取文件并从中抽取正确的内容
- 从网络上传输过来的消息,遵循特定的协议
- 用户在命令行输入的指令,遵循特定的格式
- 内存中存储的字符串,也有格式需要
因此,使用grammar判断字符串是否合法,并解析成程序里使用的数据结构,通常是递归的数据结构。一般使用正则表达式(Regular expression)
5.1 语法成分
为了描述一个符号串,不管它们是字节、字符还是从固定集合中提取的其他类型的符号,我们使用一种称为语法的紧凑表示法
用语法定义一类“字符串”,就比如:URL的语法将指定HTTP协议中合法URL的字符串集
语法中的文字字符串称为终端(terminals):
它们是表示字符串结构的解析树的叶子
我们通常用引号写终端,比如“http”或“:”
语法由一组产生式节点描述,其中每个产生式节点定义一个非终结节点
遵循特定规则,利用操作符、终止节点和其他非终止节点,构造新的字符串
非终结节点是表示字符串的树的内部节点。
语法中的产生式节点有:非终结符::=终结符、非终结符和运算符的表达式
语法中的一个非终结节点被指定为根。
语法识别的字符串集与根非终结节点匹配,此非终结节点通常称为根或开始
5.2 语法中的运算符
产生表达式中最重要的三个运算符是:
- 连接(Concatenation):用一个空格
- 重复(Repetition): *
- 选择(Union):|
例如:
它能匹配空字符串或一个链接
另外,我们介绍一些附加的运算符
附加运算符只是语法上的糖分(即,它们等价于三大运算符的组合):
- Optional:出现0次或1次,?
- 1次或更多次的出现:+
- 字符类[…]:例如小写字母[a-z]
- 倒排字符类[^…] :表示包含括号中未列出的任何字符的长度为1的字符串,例如 [^a-c] 意味着小写字母排除abc
后缀运算符*、?和+具有最高优先级,这意味着它们将首先应用;选择运算符(|)的优先级最低,这意味着它是最后应用的;我们可以用括号来提高优先级
5.3 语法树
将语法与字符串匹配可以生成一个解析树,显示字符串的各个部分如何对应于语法的各个部分
- 解析树的叶子用终端标记,表示已解析的字符串部分
- 没有子节点,不能再扩张了
- 如果我们把叶子连接在一起,我们就得到原来的字符串
5.4 Markdown 和 HTML
Markup: 一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式
Markdown: Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档
HTML: 超文本标记语言是一种用于创建网页的标准标记语言
Markdown不支持标记的嵌套,而HTML支持
有一些语言能使编辑文档像编程一样: Markdown, LaTeX
TeX是由著名的计算机科学家Donald E.
Knuth(高德纳)发明的排版系统,利用TeX可以很容易地生成高质量的dvi文件,打印输出。利用dvips,dvipdfmx,pdfLaTeX等程序生成pdf,ps文件,LaTeX2html生成html文件。
它在学术界十分流行,特别是数学、物理学、统计学与计算机科学界。TeX被普遍认为是一个很好的排版工具,特别是在处理复杂的数学公式时。利用诸如是LaTeX等终端软件,TeX就能够排版出精美的文本。通过CTAN上的宏包可以扩展其功能,可以作幻灯片,定义模板。中文支持可以由CCT、CJK、ctex等来完成。
5.5 正则语法和正则表达式
正则语法有一个特殊的属性: 通过将每个非终结符(根除外)替换为其右侧,您可以将其缩减为根的一个产生式,只在右侧使用终结符和运算符
正则表达式(Regular Expressions/regex): 终端和运算符的简化表达式可以写成更紧凑的形式
正则表达式去掉了端子周围的引号以及端子和运算符之间的空格,因此它只包含端子字符、用于分组的圆括号和运算符字符
正则表达式的可读性远不如原始语法,因为它缺少记录每个子表达式含义的非终结名,但是regex实现起来很快,而且许多编程语言中都有支持正则表达式的库
正则表达式中的一些特殊算子:
- . :代表任意字符
- \d:相当于[0-9]
- \s:任何空格字符,包括空格、制表符、换行符
- \w:任何单词字符,包括下划线
- 转义运算符或特殊字符,使其符合字面意思\+,\- ……
上下文无关语言(context-free/CFG): 可以用我们的语法系统表达的语言,不是所有CFG都是正则的。
5.6 分析器(Parsers)
parser: 输入一段文本,与特定的语法规则建立匹配,输出结果
Parser通常会生成一个解析树,该树显示如何将语法生成扩展为与字符序列匹配的句子
抽象语法树(AST): 表示语言表达式的递归抽象数据类型
Parser generator: 是一种读取语法规范并将其转换为能够识别与语法匹配的Java程序的工具;输入可以是一个文本文件,其中包含用BNF(巴克斯范式)或EBNF编写的语法,该语法定义了编程语言的语法;输出是Parser generator的一些源代码
Grammar定义语法规则(BNF格式的文本),Parser generator根据语法规则产生一个parser,用户利用parser来解析文本,看其是否符合语法定义并对其做各种处理
5.7 在Java中使用正则表达式
java.util.regex包主要由三个类组成:
- Pattern对象: 是对regex正则表达式进行编译之后得到的结果
- Matcher对象: 利用Pattern对输入字符串进行解析
- PatternSyntaxException对象: 是未检查的异常,它指示正则表达式模式中的语法错误
量词:
- Greedy : 匹配器被强制要求第一次尝试匹配时读入整个输入串,如果第一次尝试匹配失败,则从后往前逐个字符地回退并尝试再次匹配,直到匹配成功或没有字符可回退
- Reluctant: 从输入串的首(字符)位置开始,在一次尝试匹配查找中只勉强地读一个字符,直到尝试完整个字符串
- Possessive: 直接匹配整个字符串,如果完全匹配就匹配成功,否则匹配失败。效果相当于equals()
边界匹配器:
Pattern方法:
public boolean matches(String regex)
:字符串是否匹配给定的正则表达式public String[] split(String regex,int limit)
:围绕给定正则表达式的匹配项拆分此字符串- 更多的翻手册去吧