软构学习——面向可维护性的编程
文章目录
1.软件维护和演化
1.1什么是软件维护?
软件工程中的软件维护是软件产品交付后的修改,以纠正故障、提高性能或其他属性。
1.1.1运维工程师
运维是软件开发中最困难的工作之一,涉及到其他所有环节,它处理来自用户报告的故障/问题。
维护工程师必须具备高超的调试技能。故障可能位于产品的任何地方,而故障的原始原因可能在于目前不存在的规范或设计文档(缺陷/问题本地化)。因而需要高超的诊断能力、测试能力和文档能力(测试、修复和记录更改)。
大多数运维任务并非需要涉及代码修改。但是有时候不得不回到代码层面…
修复代码后有更多步骤:
- 测试修改是否正确工作:使用专门构造的测试用例。
- 检查回归故障:使用存储的测试数据,并向存储的测试数据中添加专门的测试用例,以便将来进行回归测试。
- 记录变化。
如何最小化回归错误
- 参考详细的文档并使用构建的测试用例。
最大的问题:没有足够的文档记录和测试用例
- 运维工程师必须从源代码本身推断出避免引入回归故障所需的所有信息。
1.1.2软件维护的分类
-
纠错性维护——25%
交付后对软件产品进行反应性修改,以纠正发现的问题。
-
适应性维护——21%
交付后对软件产品进行的修改,以使软件产品在变化或变化的环境中可用。
-
完善性维护——50%
在交付后对软件产品进行增强,以提高性能或可维护性。
-
预防性维护——4%
在交付后对软件产品进行修改,以便在软件产品中的潜在故障变为有效故障之前对其进行检测和纠正。
1.2软件演化
- 软件演化是软件维护中使用的一个术语,指的是最初开发软件,然后由于各种原因重复更新的过程。
- 一个典型系统90%以上的成本发生在维护阶段,任何成功的软件都将不可避免地需要维护。
软件生命周期中有各种各样的版本
软件维护不仅仅是运维工程师的工作,而是从设计和开发阶段就开始了。在设计与开发阶段就要考虑将来的可维护性。因此,综合考虑灵活和可扩展的设计/构造,换句话说,“易于更改/扩展”。这就是所谓的软件构造的“可维护性”、“可扩展性”和“灵活性”。
1.2.1面向可维护性的技术
-
模块化
低耦合、高内聚
-
OO设计原则
-
OO设计模式
-
基于状态的构造技术
-
表驱动的构造技术
-
基于语法的构造技术
2.软件可维护性指标
2.1可维护性的别称
-
可维护性
修改软件系统或组件以纠正故障、提高性能或其他属性,或适应变化的环境的方便性。
-
可扩展性
软件设计/实施考虑到未来的增长,并被视为对系统扩展能力和实现扩展所需努力水平的系统性度量。
-
灵活性
软件能够根据用户需求、外部技术和社会需求轻松更改。
-
可适应性
交互式系统(自适应系统)的一种能力,它可以根据所获取的有关其用户及其环境的信息,使其行为适应单个用户。
-
可管理性
如何高效、轻松地监控和维护软件系统,以保持系统的性能、安全性和平稳运行。
-
支持性
基于包括质量文档、诊断信息和知识渊博且可用的技术人员在内的资源,软件在部署后保持运行的有效性。
2.2关于可维护性的问题
- 设计结构是否足够简单?
- 模块之间是否松散耦合?
- 模块内部是否高度聚合?
- 是否使用了非常深的继承树,是否使用了 委派替代继承?
- 代码的圈/环复杂度是否太高?
- 是否存在重复代码?
2.2.1圈/环复杂度 - 度量代码的结构复杂性
- 它是通过计算程序流中不同代码路径的数量创建的。独立路径的数量。
- 具有复杂控制流的程序将需要更多的测试来实现良好的代码覆盖率,并且维护性较差。
2.2.2代码行数 - 指示代码中的大致行数
- 非常高的计数可能表示某个类型或方法试图做太多的工作,应该进行拆分。
- 这也可能表明类型或方法可能难以维护。
2.2.3Halstead体积
- 基于源代码中(不同的)运算符和操作数数量的复合度量
2.2.4可维护性指数( Maintainability Index MI)
- 计算一个介于0和100之间的索引值,该值表示维护代码的相对容易程度。
2.2.5 继承的层次数
- 指示扩展到类层次结构根的类定义数。层次结构越深,就越难理解特定方法和字段的定义或/或重新定义位置。
2.2.6类之间的耦合度
通过参数、局部变量、返回类型、方法调用、泛型或模板实例化、基类、接口实现、外部类型上定义的字段和属性修饰来度量与唯一类的耦合。
- 良好的软件设计要求类型和方法应具有高内聚性和低耦合性。
- 高耦合表示由于与其他类型的许多相互依赖性,难以重用和维护的设计。
2.2.7单元测试的覆盖度
- 指示自动单元测试覆盖了代码库的哪些部分。
3.模块化设计和模块化原则
模块化编程是一种设计技术,它强调将程序的功能划分为独立的、可互换的模块,以便每个模块都包含只执行所需功能的一个方面所需的一切。在结构化编程和面向对象编程中,将整个程序的代码分解为多个部分。
设计的目标是将系统划分为模块,并以以下方式在组件之间分配责任:
- 高内聚
- 低耦合
模块化降低了程序员在任何时候都必须处理的总复杂性
- 将功能相似函数分给同一模块,功能不同的分给不同的模块——分离关注点
- 模块之间有小型、简单、定义良好的接口——信息隐藏
内聚和耦合原则可能是评估设计可维护性的最重要的设计原则
3.1评估模块化的五个标准
-
可分解性
较大的组件是否分解为较小的组件?
-
可组合性
较大的组件是否由较小的组件组成?
-
可理解性
组件是否可以单独理解?
-
可持续性——发生变化时受影响范围最小
对规范的微小更改是否会影响本地化且数量有限的组件?
-
保护(出现异常之后的保护) ——出现异常后受影响范围最小
运行时异常的影响是否仅限于少数相关组件?
3.1.1可分解性
将问题分解为各个可独立解决的子问题。
- 目标:使模块之间的依赖关 系显式化和最小化。
- 例如:自顶向下结构设计。
3.1.2可组合型
可容易的将模块组合起来形成新的系统。
-
目标:使模块可在不同的环境下复用
-
例如:UNIX利用管道连接命令
3.1.3可理解性
每个子模块都可被系统设计者容易的理解。
- 例如:UNIX命令。
3.1.4可持续性
规格说明小的变化将只影响一小部分模块,而不会影响整个体系结构。
例如:模块提供的所有服务应该通过统一标识提供。
3.1.5保护
运行时的不正常将局限于小范围模块内。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEYU2TdB-1654662294080)(https://s2.loli.net/2022/06/06/qp4yBlEsbKi3gGJ.png)]
3.2模块设计的五条准则
- 直接映射
- 尽可能少的接口
- 尽可能小的接口
- 显式接口
- 信息隐藏
3.2.1直接映射
模块的结构与现实世界中问题领域的结构保持一致。
对以下评价标准产生的影响:
- 可持续性
更容易评估和限制变化的影响
- 可分解性
3.2.2更少的接口——”不要对太多人讲话…”
模块应尽可能少的与其他模块通讯。
对以下评价标准产生影响:可持续性、保护性、可理解性、可组合性。
3.2.3更小的接口——”不要讲太多…”
如果两个模块通讯,那么它们应交换尽可能少的信息。
对**“可持续性”和“保护性”**产生影响。
3.2.4显示接口——”公开的大声讲话…不要私下嘀咕…”
当A与B通讯时,应明显的发生在A与B的接口之间。也就是说直接连接,而不是依靠第三方。
受影响的评价标准 :可分解性、可组合性、可持续性、 可理解性。
3.2.5隐藏信息
经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面。
影响“可持续性”
每个模块的设计者必须选择模块属性的子集作为有关模块的正式信息,以供客户端模块使用。
3.3高内聚与低耦合
3.3.1耦合
耦合是模块之间依赖性的度量。如果一个模块的更改可能需要另一个模块的更改,则两个模块之间存在依赖关系。
模块之间的耦合程度取决于:
- 模块之间的接口数量(数量)
- 每个接口的复杂性(由通信类型决定)(质量)
低耦合的例子
精心设计的web应用程序围绕以下方面进行模块化:
- 指定数据和语义的HTML文件
- 指定HTML数据外观和格式的CSS规则
- 定义页面行为/交互性的JavaScript
3.3.2内聚
内聚性是衡量模块的功能或职责之间的关联程度。
如果模块的所有元素都朝着相同的目标努力,则模块具有很高的内聚性。
最好的设计是高内聚并且低耦合,它们并不矛盾,一般做到高内聚,自然也就低耦合了。
4.OO设计准则:SOLID
SOLID(五个首字母)
- 单一责任原则(SRP)
- 开放-封闭原则(OCP)
- Liskov替换原则(LSP)
- 接口聚合原则(ISP)
- 依赖转置原则(DIP)
4.1单一责任原则(SRP)
ADT中不应该有多于1个原因让其发生变化,否则就拆分开。
责任:变化的原因。
- 不应有多于1个的原因使得一个类发生变化。
- 一个类,一个责任。
如果一个类包含了多个责任,那么将引起不良后果:
- 引入额外的包,占据资源 。
- 导致频繁的重新配置、部署等。
最简单的原则,却是最难做好的原则
example:
反例
interface Modem {
public void dial(String pno);
public void hangup();
public void send(char c);
public char recv();
}
修正后
interface DataChannel {
public void send(char c);
public char recv();
}
interface Connection {
public void dial(String phn);
public char hangup();
}
4.2开放-封闭原则(OCP)
对扩展性的开放
模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化。
对修改的封闭
- 但模块自身的代码是不应被修改的。
- 扩展模块行为的一般途径是修改模块的内部实现。
- 如果一个模块不能被修改,那么它通常被认为是具有固定的行为。
关键的解决方案:抽象技术
“软件实体(类、模块、函数等)应开放以供扩展,但应关闭以供修改”,即使用继承和组合/委派更改类的行为。
example
反例
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 - 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)
{....}
}
修正后
// 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
}
}
4.3Liskov替换原则(LSP)
“使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象”,即子类在替换基类时应该表现良好。
- 子类型必须能够替换其基类型。
- 派生类必须 能够通过其基类的接口使用,客户端无需了解二者之间的差异。
之前的博客已经介绍过LSP原则了。这里就不再讨论了。
4.4接口聚合原则(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();
}
ManWorker implements Workable, Feedable {
void work() {…};
void eat() {…};
}
RobotWorker implements Workable {
void work() {…};
}
4.5依赖转置原则(DIP)
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
抽象不应该依赖于实现细节,实现细节应该依赖于抽象。
//DIP - bad example
public class EmployeeService {
private EmployeeFinder emFinder; //concrete class, not abstract.
//Can access a SQL DB for instance
public Employee findEmployee(…) {
emFinder.findEmployee(…)
}
}
//DIP - fixed
public class EmployeeService {
private IEmployeeFinder emFinder
//depends on an abstraction, no an implementation
public Employee findEmployee(…) {
emFinder.findEmployee(…)
}
}
委派的时候要通过接口建立联系,而非具体子类。
4.6小结
抽象(abstraction):模块之间通过抽象隔离开来,将稳定部分和容易变化部分分开。
- LSP:对外界看来,父类和子类是“一样”的;
- DIP:对接口编程,而不是对实现编程,通过抽象接口隔离变化;
- OCP:当需要变化时,通过扩展隐藏在接口之后的子类加以完成,而不要修改接口本身。
分离(Separation): Keep It Simple, Stupid (KISS)
- SRP:按责任将大类拆分为多个小类,每个类完成单一职责,规避变化,提 高复用度;
- ISP:将接口拆分为多个小接口,规避不必要的耦合。
归纳起来:让类保持责任单一、接口稳定。
5.语法驱动的构造
有一类应用,从外部读取文本数据, 在应用中做进一步处理。
具体来说,字节或字符序列可能是:
- 输入文件有特定格式,程序需读取文件并从中抽取正确的内容。
- 从网络上传输过来的消息,遵循特定的协议。
- 用户在命令行输入的指令,遵循特定的格式。
- 内存中存储的字符串,也有格式需要。
对于这些类型的序列,语法的概念是一个很好的设计选择:
- 使用grammar判断字符串是否合法,并解析成程序里使用的数据结构。
- 通常是递归的数据结构。
正则表达式
对于许多需要反汇编字符串、从中提取信息或转换字符串的字符串处理任务,它是一种广泛使用的工具。
由于工大软构就考正则表达式基本语法使用,本文也就不介绍更多了。而且正则表达式已经在“形式语言与自动机”学过(这门课我错的好惨。。。)。了解更多相关知识可以看菜鸟教程。
分为多个小类,每个类完成单一职责,规避变化,提 高复用度;
- ISP:将接口拆分为多个小接口,规避不必要的耦合。
归纳起来:让类保持责任单一、接口稳定。
5.语法驱动的构造
有一类应用,从外部读取文本数据, 在应用中做进一步处理。
具体来说,字节或字符序列可能是:
- 输入文件有特定格式,程序需读取文件并从中抽取正确的内容。
- 从网络上传输过来的消息,遵循特定的协议。
- 用户在命令行输入的指令,遵循特定的格式。
- 内存中存储的字符串,也有格式需要。
对于这些类型的序列,语法的概念是一个很好的设计选择:
- 使用grammar判断字符串是否合法,并解析成程序里使用的数据结构。
- 通常是递归的数据结构。
正则表达式
对于许多需要反汇编字符串、从中提取信息或转换字符串的字符串处理任务,它是一种广泛使用的工具。
由于工大软构就考正则表达式基本语法使用,本文也就不介绍更多了。而且正则表达式已经在“形式语言与自动机”学过(这门课我错的好惨。。。)。了解更多相关知识可以看菜鸟教程。