0chapter10:面向可维护性的软件构造技术
可维护性:软件能否以很小的代价适应变化
软件的维护一般是在修复错误/改善性能,会涉及到软件开发过程其他所有环节,主要目的是解决用户反馈的程序存在的错误或是应该改进的地方。难点是部分表现出来的错误难以准确定位,当然我们修复这个错误的时候不应该引入新的错误。最大的问题是没有足够详细的文档和测试用例。
修复完错误代码之后,应该要对修复部分进行测试,之后对修改完的整个程序系统进行回归测试,并记录修改前后的变化。
维护分为以下四种类型:
1.Corrective Maintenance:纠错性,修复代码错误
2.Adaptive Maintenance:适应性,提高适应环境能量
3.Perfective Maintenance:完善性,提升性能、扩充功能
4.Preventive Maintenance:预防性,提前解决可预见的但未发生的错误
软件演化:是软件维护的重要部分,对软件进行持续更新
软件大部分成本花费在维护阶段
因此我们可以在软件的最初版本中多下功夫,考虑并应付之后可能的变化,以免届时多次进行修复,提高了软件的复杂度,降低了软件性能和质量。所以,软件维护从设计时就开始了。
实现这种目的的技术:
模块化设计、面向对象设计原则、面向对象设计模式、基于状态的构造技术、基于表驱动的构造技术、基于语法的构造技术
可维护性几种名词:
1.可维护性:适应变化的能力
2.可扩展性:扩展功能的能力
3.灵活性:是否易于变化
4.可适应性:能否适应环境的变化
5.可管理性:维护的难度和效率
6.支持性:维护之后能否有效维持
需要考虑的几个问题:
1.设计结构是否足够简单
2.模块之间是否松散耦合
3.模块内部是否高度聚合
4.内部是否有很深的继承树,有无使用委托替换继承
5.代码的圈/环复杂度是否过高
6.代码的重复度
圈/环复杂度:Cyclomatic Complexity(CC)
CC=结构区域数=判断逻辑数+1
也可以用代码行数作为参考,但是并不一定准确
可以基于运算符、操作数的数量判断:
可维护性指数
MI=171-5.2ln(HV)-0.23*CC-16.2ln(LOC)+50*sin((2.46*COM)^0.5)
HV见上图,CC上面也解释过,LOC是代码行数,COM是模块平均注释行数
此外还可以考虑继承层次数、类之间的耦合度、单元测试覆盖度
模块化编程:
好处:高内聚(按照功能关联度区分开)、低耦合、分离关注点、隐藏信息
耦合度:模块间接口数和接口复杂度
内聚性:模块内方法之间联系的紧密程度
一般来说,耦合度和内聚性是反向增长的,内聚性越高越好,耦合度越低越好
划分模块的原则:
1.可分解性:将问题划分为各个可独立解决的子问题,使模块之间关系显示化、最小化
2.可组合性:可以容易将模块组合成新的系统,使模块在不同环境下复用
3.可理解性:易于理解,设计清楚
4.可持续性:规格小的变化只影响一小部分模块,不会影响整个体系结构,例如符号型变量/模块提供的所有服务应通过统一标识提供
5.保护性:运行时的异常只发生在小范围模块内
设计原则:
1.直接映射:模块结构与现实世界中问题领域的结构保持一致。影响可持续性、可分解性
2.模块应该与尽可能少的接口联系,影响可持续性、保护性、可理解性、可组合性
3.若模块之间存在联系,应该尽可能交换少信息(可以通过限制模块间通讯带宽实现)。影响可持续性、保护性
4.显示接口:A与B发生交互时,应该明显地发生在AB之间,不应该还有第三方的介入。影响可分解性、可组合性、可持续性、可理解性
5.信息隐藏:经常可能发生变化的设计决策应该尽可能隐藏在抽象接口后,变化较少的部分可以暴露给客户端。影响可持续性
面向对象设计原则
SOLID原则:
OCP:类应该对扩展性开放,对修改封闭。即行为可扩展,通过修改模块内部实现,但是模块自身的代码不应该被修改。这样我们才能认为模块有固定的功能。关键的解决方案是运用抽象技术,但是很难完全实现。
例子如下
// Open-Close Principle - Bad example
class GraphicEditor {
public void drawShape(Shape s) {
if (s.m_type==1)
drawRectangle(s);
else if (s.m_type==2)
drawCircle(s);
}
public void drawCircle(Circle r)
{....}
public void drawRectangle(Rectangle r)
{....}
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type=1;
}
}
class Circle extends Shape {
Circle() {
super.m_type=2;
}
}
// Open-Close Principle - Good example
class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
} }
class Shape {
abstract void draw();
}
class Rectangle extends Shape {
public void draw() {
// draw the rectangle
}
}
LSP:第九章中详细谈过,子类型必须能够替换基类型,派生类必须能够通过基类的接口使用,客户端无需知道二者的差异。
ISP:不能强迫客户端依赖于他们不需要的接口,只提供必须的接口,以免构造出胖接口、接口污染。
例子:
//bad example (polluted interface)
interface Worker {
void work();
void eat();
}
ManWorker implements Worker {
void work() {…};
void eat() {…};
}
RobotWorker implements Worker {
void work() {…};
void eat() {//Not Appliciable
for a RobotWorker};
}
interface Workable {
public void work();
}
interface Feedable{
public void eat();
}
//分成两个接口
interface Workable {
public void work();
}
interface Feedable{
public void eat();
}
ManWorker implements Workable, Feedable {
void work() {…};
void eat() {…};
}
RobotWorker implements Workable {
void work() {…};
}
DIP:高层模块不应该依赖于底层模块,底层模块不影响高层模块,二者依赖于抽象而不相互依赖,抽象不应该依赖于实现细节,但是实现细节应该依赖于抽象。委托时用接口建立联系而非具体的实现类。
如图,下面的实现方法更为安全,利用抽象类关联,不会干扰高层
综上,SRP、ISP负责分离,OCP\LSP\DIP负责抽象,让类保持责任单一、接口稳定
语法驱动构造
有一类应用,从外部读取文本数据再进行下一步处理,其中,对于输入文件有格式要求,从网上传过来的信息需要遵守一定协议,用户输入的指令也有格式要求,内存存储字符串格式要求
对此,我们一般用语法判断某个字符串是否合法并将之解析成程序中使用的数据结构(通常是递归的)
字符串结构划分为:
终止节点、叶节点、终结符:就是语法解析树中的叶子节点,无法再往下扩展,通常是用引号引起的字符串
产生式节点/非终止节点:遵循特定原则,利用操作符、终止节点、其他非终止节点构造新的字符串
根节点:所有节点的公共祖先,简单点讲是字符串开头
操作符分为:
连接:用一个空格符标识,x::=y z
重复:*?+均可以作为标识符,x::= y*(y? y+)
其中*表示重复0次以上的任意次,?表示重复0或1次,+表示1或更多次
选择:|,也可以理解为逻辑或。x::= y | z
优先级:重复>连接>选择
也可以用[]和其他符号表示内容:
x::=[a-z]表示a到z任意一个,还可以在这基础之上加运算符,如[a-z]+等等
也可以直接列举:x::=[abcde]表示abcde任意一个
还可以有取否形式:x::=[^a-c]表示不包含abc的任意一个
后面就是结合形式语言的各种构造方法实现:一个简单的小例子
url ::= 'http://' hostname (':' port)? '/'
hostname ::= word '.' hostname | word '.' word
port ::= [0-9]+
word ::= [a-z]+
正则语法:通过化简只有一个产生式构成而不包含任何非终止节点
正则表达式(regex):例
markdown ::= ( normal | italic ) *
italic ::= '_' normal '_'
normal ::= text
text ::= [^_]*
也可以用语法树视角来构造/分析:
regex中的一些特殊操作符:
操作符 | 作用 |
. | 表示任意字符 |
\d | 表示0-9单个数字 |
\s | 空白符,包括空格、tab、换行 |
\w | 数字、字母、下划线构成的词 |
\. | 各种符号,包括\( \) \+ \*等等 |
上下文无关文法:左侧只含有一个非终结符,它定义的语法范畴(或语法单位)是完全独立于这种范畴可能出现的环境,无需考虑上下文。
程序语言大多是CFG
Java语法如下:
Parser:将输入的文本与特定语法规则匹配,并输出结果/转化成语法树
构造依据: