文章目录
前言
这篇文章是我对软件构造课程的面向可维护性的软件构造技术章节的学习总结,以供未来使用。文章中的图片均来自课程教师的讲义。
主要内容包括:
- 软件的可维护性与进化
- 可维护性的指标
- 模块化编程和模块性准则
一、软件的可维护性与进化
软件维护
软件工程中的软件维护(Software maintenance)是指在交付后对软件产品进行修改,以纠正故障、提高性能或其他属性。
运维并非简单的修补,它反而是软件构造过程中最困难的工作之一。因为在这个阶段需要处理来自用户报告的故障和问题,这需要维护者有较高的debug和修复故障的能力。
软件维护也不仅仅包括对代码的修正,其步骤一般为:
- 根据用户报告修正代码
- 对做出的修改进行测试
- 进行回归测试
- 记录所有的变化
这里出现了回归测试(regression testing),用于确保对软件进行修改或更新后,旧的功能仍然正常运行,没有引入新的错误或导致已有功能出现问题。
软件进化
软件进化(software evolution)是软件维护中使用的一个术语,指的是最初开发软件,然后由于各种原因反复更新的过程。
软件维护所占的成本可达总成本的90%。几乎可以断定,即便是最成功的软件,也需要软件维护进行完善。也就是说,软件维护是一个不可避免且成本高的阶段。
这种“变化”是不可避免的,不光是成本,“变化”极有可能增加软件的复杂度,甚至反而造成了软件质量的下降。
所以,我们不得不“未雨绸缪”,尽量在设计与开发阶段增强软件的可维护性,使得软件"easy to change"。
由此可见,软件的可维护性是衡量软件优劣的一个重要指标。
二、可维护性的指标
概念
- 可维护性
- 可扩展性
- 灵活性
- 可适应性
- 可管理性
- 支持性
以上名词都可看做可维护性的近义词。
量化指标
- 圈复杂度(Cyclomatic Complexity)
- 它是通过计算程序流中不同代码路径的数量来创建的。
- 具有复杂控制流的程序将需要更多的测试来实现良好的代码覆盖,并且不太可持续。
- 代码行数(Lines of Code)
- 代码行数很多的程序可能表明一个类型或方法试图做太多的工作,应该进行拆分。
- 它还可能表明该类型或方法可能难以维护。
- 霍尔斯特德体积(Halstead Volume)是一种软件度量方法,由 Maurice H. Halstead 在 1977 年提出。它用于衡量软件代码的复杂性,基于程序源代码中的运算符和操作数的数量来计算。这个度量方法的目标是提供关于程序大小、复杂性和理解难度的指标。
- 可维护性指数(Maintainability Index,MI):计算一个介于0和100之间的指数值,表示代码维护的相对容易程度。值越大,表示代码越容易维护。
- 继承的层次数(Depth of Inheritance )
- 类间耦合度(Class Coupling)
- 单元测试的覆盖度(Unit test coverage)
三、模块化编程和模块性准则
概念
模块化编程(Modular programming)是一种设计技术,它强调将程序的功能划分为独立的、可互换的模块,使得每个模块都包含执行所需功能的一个方面所需的一切。
- 设计的目标是将系统划分为多个模块,并以这样的方式在组件之间分配责任:
- 高内聚
- 低耦合
- 模块化降低了程序员在任何时候都必须处理的总复杂性,如果:
- 分离关注点
- 信息隐藏
内聚性和耦合性原则可能是评估设计可维护性的最重要的设计原则。
评估模块性的五个标准
模块化设计的五条规则
耦合与内聚
耦合(Coupling)是衡量模块之间依赖性的指标。如果一个模块中的更改可能需要另一个模块的更改,则两个模块之间存在依赖关系。
模块之间的耦合程度由以下因素决定:
- 模块之间的接口数量
- 每个接口的复杂性
内聚性(Cohesion)是衡量一个模块的功能或职责之间有多紧密联系的指标。
如果一个模块的所有元素都朝着同一目标努力,那么它就具有很高的内聚性。
最好的设计在模块内具有高内聚性(也称为强内聚性),在模块之间具有低耦合性(也称为弱耦合)。
OO设计原则:SOLID
五大类设计原则
- (SRP) The Single Responsibility Principle 单一责任原则
- (OCP) The Open-Closed Principle 开放-封闭原则
- (LSP) The Liskov Substitution Principle Liskov替换原则
- (DIP) The Dependency Inversion Principle 依赖转置原则
- (ISP) The Interface Segregation Principle 接口聚合原则
SRP-单一责任原则
一个类改变的原因永远不应该超过一个,即一个类应该专注于做一件事,只做一件事情。
所谓的责任,在这里指变化的原因。
一个类应该只对应一个责任。若包含多个责任,可能会引入额外的包,占用资源;导致频繁的重新配置、部署等等。
一个反例:
OCP-(面向变化的)开放/封闭原则
OCP包含两点:
- 对扩展性的开放(Open for extension)
这意味着可以扩展模块的行为。随着应用程序需求的变化,或者为了满足新应用程序的需求,我们可以使模块以新的、不同的方式运行。 - 对修改的封闭(Closed for modification)
- 这样一个模块的源代码是不可侵犯的。任何人都不允许对其进行源代码更改。
- 扩展模块行为的一般途径是修改模块的内部实现。
- 不能更改的模块通常被认为具有固定的行为。
应用OCP的关键是抽象技术(abstraction)。
软件实体(类、模块、函数等)应开放用于扩展,但关闭用于修改,即使用继承和组合/委派来更改类的行为。
一个反例:
LSP-Liskov替换原则
该原则已经在面向复用的软件构造技术中详细说明,这里不在赘述。
ISP-接口隔离原则
- 不要强迫类实现它们不能实现的方法(Swing/Java)
- 不要用很多方法污染接口
- 避免“肥胖”接口
也就是说, 不能强迫客户端依赖于它们不需要的接口:只提供必需的接口。
胖接口具有很多缺点,最显著的就是低聚合。、
因此,可以将胖接口分解为多个小的,聚合度高的接口。
客户端根据自己的需求只选择需要的接口。
DIP-依赖转置原则
高级模块不应依赖于低级模块。两者都应该依赖于抽象。
抽象不应依赖于具体,而是具体依赖于抽象。
举例:
第一种方案:Copy方法直接调用下层的方法。
第二种方案:Copy方法调用接口提供给的方法,而接口则由不同的实现类进行实现。
显然,第二种方案符合DIP。因为第二种方案中,上层与下层的具体实现全部依赖于抽象。
上层client的代码面向抽象接口编程,可以隔离对下层具体实现机制的直接接触。
总结来说,就是使用委托时,要通过接口来建立联系,而非具体的子类。
这样的话,如果要更换下层方法,只需要在初始化时更换实现类的名称即可。