10 面向可维护性的构造技术
10.1 软件维护和演化
软件工程中的软件维护是软件产品交付后的修改,以纠正故障、提高性能或其他属性。
面向可维护性的构造的技术:
- 模块化
- OO设计原则
- OO设计模式
- 基于状态的构造技术
- 表驱动的构造技术
- 基于语法的构造技术
10.2 维修性指标
名词:
- 可维护性:“修改软件系统或组件以纠正故障、提高性能或其他属性,或适应变化的环境的容易程度”。
- 可扩展性:软件设计/实施考虑到未来的增长,并被视为对系统扩展能力和实现扩展所需努力水平的系统性度量。
- 灵活性:软件能够根据用户需求、外部技术和社会环境等进行轻松更改
- 可适应性:交互式系统(自适应系统)的一种能力,它可以根据所获取的有关其用户及其环境的信息,使其行为适应单个用户。
- 可管理性:如何高效、轻松地监控和维护软件系统,以保持系统的性能、安全性和平稳运行。
- 支持性:基于包括质量文档、诊断信息和知识渊博且可用的技术人员在内的资源,软件在部署后保持运行的有效性。
常见问题:
- 设计结构是否简单
- 模块之间是否松散耦合
- 模块内部是否高度聚合
- 是否用委托代替继承
- 是否存在许多循环
- 是否存在重复代码
一些常用的可维护性指标:
- 圈复杂度:具有复杂控制流的程序将需要更多的测试来实现良好的代码覆盖率,并且维护性较差
- 代码行数(直观但不准确)
- 可维护性指数:计算一个介于0和100之间的索引值,该值表示维护代码的相对容易程度,越高越好维护。
- 继承的层次数
- 类之间的耦合度
- 单元测试的覆盖度:指示自动单元测试覆盖了代码库的哪些部分
代码异味:代码中出现的不好的现象
10.3 模块化设计和模块化原则
模块化编程是一种设计技术,它强调将程序的功能划分为独立的、可互换的模块,以便每个模块都包含只执行所需功能的一个方面所需的一切。
模块化编程的特征(好处):高内聚、低耦合、分离关注点(将功能分配给将类似功能分组在一起的模块)、信息隐藏
内聚和耦合原则可能是评估设计可维护性的最重要的设计原则。
10.3.1 评估模块化的五个标准
- 可分解性
- 可组合性(较大组件由较小组件组成)
- 可理解性
- 可持续性:发生变化时受影响范围最小
- 出现异常之后的保护:出现异常后受影响范围最小
10.3.1.1 可分解性
将问题分解为各个可独立解决的子问题
目标:使模块之间的依赖关系显式化和最小化
10.3.1.2 可组合性
可容易的将模块组合起来形成新的系统
目标:使模块可在不同的环境下复用
10.3.1.3 可理解性
每个子模块都可被系统设计者容易的理解
10.3.1.4 可持续性
规格说明小的变化将只影响一小部分模块,而不会影响整个体系结构
10.3.1.5 出现异常之后的保护
运行时的不正常将局限于小范围模块内
10.3.2 模块化设计的五条规则
直接映射
直接映射:模块的结构与现实世界中问题领域的结构保持一致
直接映射会对持续性和可分解性产生影响
尽可能少的接口
模块应尽可能少的与其他模块通讯
对以下评价标准产生影响:可持续性、保护性、可理解性、可组合性
尽可能小的接口
如果两个模块通讯,那么它们应交换尽可能少的信息
对“可持续性”和“保护性”产生影响
显式接口
当A与B通讯时,应明显的发生在A与B的接口之间
受影响的评价标准:可分解性、可组合性、可持续性、 可理解性
信息隐藏
经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面
每个模块的设计者必须选择模块属性的子集(暴露在外的部分)作为模块的官方信息,以供客户端模块使用。
10.3.3 低耦合和高内聚
耦合是模块之间依赖性的度量。如果一个模块的更改可能需要另一个模块的更改,则两个模块之间存在依赖关系。
模块之间的耦合程度取决于:模块之间的接口数量(数量)和每个接口的复杂性
eg:
当耦合度较高时,内聚度往往较低,反之亦然。最好的设计在模块内具有高内聚性(也称强内聚性),模块之间具有低耦合性(也称弱耦合性)。
10.4 设计原则:SOLID
10.4.1 单一责任原则(SRP)
一个类只专注于一件事,责任是产生变化的原因。
如果一个类包含了多个责任,那么将引起不良后果:
- 引入额外的包,占据资源
- 导致频繁的重新配置、部署等
eg:(一个反例)
不要刻意将功能分离,当可以预见到变化(扩展)时,再采取SRP原则。
10.4.2 (面向变化的)开/关原则(OCP)
模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化。
但模块自身代码不应该被修改,扩展模块行为的一般途径是修改模块的内部实现,如果一个模块不能被修改,那它常备认为具有固定的行为。
解决方案:抽象技术,类的行为用继承和委托机制。
eg:(一个反例)
10.4.3 Liskov替换原则(LSP)
见第9章
10.4.4 接口隔离原则(ISP)
只提供客户必须的接口,避免接口污染、胖接口。
将胖接口分解为多个小接口,不同用户使用不同的接口,只能访问所需要的端口。
10.4.5 依赖转置原则(DIP)
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
抽象不应该依赖于实现细节,实现细节应该依赖于抽象。
委托的时候要通过interface建立联系。
核心是使用接口来缓冲,即将接口作为参数传入类中,作为临时变量或者成员变量。将接口的实现类和调用者隔离。
总结:让类保持责任单一、接口稳定。
抽象(abstraction):模块之间通过抽象隔离开来,将稳定部分和容易
变化部分分开
- LSP:对外界看来,父类和子类是“一样”的;
- DIP:对接口编程,而不是对实现编程,通过抽象接口隔离变化;
- OCP:当需要变化时,通过扩展隐藏在接口之后的子类加以完成,而不要修改接口本身。
分离(Separation):Keep It Simple, Stupid (KISS)
- SRP:按责任将大类拆分为多个小类,每个类完成单一职责,规避变化,提高复用度;
- ISP:将接口拆分为多个小接口,规避不必要的耦合。
10.5 语法驱动的构造
有一类应用,从外部读取文本数据,在应用中做进一步处理。具体而言,字节或字符序列可能是输入文件、网络信息、命令行指令、内存中的数据等。
对于这类序列,语法的概念是一个很好的设计选择:使用语法判断字符串是否合法,并解析成程序里使用的数据结构。
10.5.1 语法成分
一个语法定义了一类“字符串”,语法由一组产生式节点描述, 其中每个产生式定义一个非终结符。我们可以遵循特定规则,利用操作符、终止节点和其他非终止节点,构造新的字符串
终止符(叶节点,无法再往下扩展)、非终止符(内节点)
10.5.2 语法中的运算符
连接符、重复符、选择符
eg:
其他运算符:
“?”(选择)、“+”(至少一个)、“[]”(表示包含方括号中列出的任何字符的长度为1的字符串)、“[^]”(与前面取反)
eg:
- * ? +优先级最高,连接次之,| 最低
10.5.3 语法中的递归
10.5.4 分析树
将语法与字符串匹配可以生成一个解析树,该树显示字符串的各个部分如何与语法的各个部分相对应。解析树的叶子用终端标记,表示已解析的字符串部分,他们没有子节点,不能再扩大规模了。
如果我们将叶子连接在一起,我们将得到原始字符串。
eg:
可以发现,hostname
是二叉树,表示使用了递归;如果不使用递归 ,则如下图:
eg:
10.5.5 markdown和HTML
markdown和HTML都是可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式
10.5.6 正则语法和正则表达式
正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点.
url、markdown是正则的,但是html不是正则的。
终端和运算符的简化表达式可以写成更紧凑的形式,称为正则表达式。正则表达式去掉了引号以及空格,因此它只包含端子字符、分组括号和运算符字符,从而表达更简洁,但也更难懂。正则表达式也简称为regex。
正则表达式中的符号:
eg:
上下文无关文法左侧只含有一个非终结符,它定义的语法范畴(或语法单位)是完全独立于这种范畴可能出现的环境,无需考虑上下文
10.5.8 在Java中使用正则表达式
在本课程里,只需要能够熟练掌握正则表达式regex这种“基本语法”,并熟练使用JDK提供的regex parser进行数据处理即可.
regex在编程中被广泛使用。在Java中,可以使用正则表达式来操纵字符串(请参见String.split、String.matches、Java.util.regex.Pattern
)。它们是Python、Ruby和JavaScript等现代脚本语言的一流功能,您可以在许多文本编辑器中使用它们进行查找和替换。
Java.util.regex
包中有三个类:
- Pattern对象是对regex正则表达式进行编译之后得到的结果
- Matcher对象:利用Pattern对输入字符串进行解析
PatternSyntaxException
指示正则表达式模式中的语法错误
eg:
字符类:
预定义的字符类:
有三种匹配模式:
- Greedy :匹配器被强制要求第一次尝试匹配时读入整个输入串,如果
第一次尝试匹配失败,则从后往前逐个字符地回退并尝试再次匹配,直
到匹配成功或没有字符可回退。 - Reluctant:从输入串的首(字符)位置开始,在一次尝试匹配查找中只勉
强地读一个字符,直到尝试完整个字符串。 - Possessive: 直接匹配整个字符串,如果完全匹配就匹配成功,否则匹配
失败。效果相当于equals()。
边界匹配器:
模式匹配在java.lang.String
中的使用:
public boolean matches(String regex)
:告诉此字符串是否与给定的正则表达式匹配。调用了Pattern.matches(regex, str)
。public String [] split(String regex,int limit)
:围绕给定正则表达式的匹配项拆分此字符串。调用了Pattern.compile(regex).split(str, n)
public String [] split(String regex)
:围绕给定正则表达式的匹配项拆分此字符串。public String replace(CharSequence target,CharSequence replacement)