软件构造-第五章-第一节-可维护性的度量与构造原则


软件构造第五章 第一节 可维护性的度量与构造原则

一、软件的维护和演化

  1. 定义:
    • 软件可维护性是指软件产品被修改的能力,修改包括纠正、改进或软件对环境、需求和功能规格说明变化的适应。简而言之,软件维护:修复错误、改善性能。
  2. 类型:
    • 纠错性(25%)
    • 适应性(25%)
    • 完善性(50%)
    • 预防性(4%)
  3. 演化:
    • 软件演化是一个程序不断调节以满足新的软件需求过程。
  4. 演化的规律:
    • 软件质量下降,延续软件生命
  5. 软件维护和演化的目标:
    • 提高软件的适应性
    • 延续软件生命
  6. 意义:
    • 软件维护不仅仅是运维工程师的工作,而是从设计和开发阶段就开始了
    • 在设计与开发阶段就要考虑将来的可维护性 ,设计方案需要“easy to change”
  7. 基于可维护性建设的例子:
    • 模块化
    • OO设计原则
    • OO设计模式
    • 基于状态的构造技术
    • 表驱动的构造技术
    • 基于语法的构造技术

二、可维护性的常见度量指标

  1. 可维护性:
    • 可轻松修改软件系统或组件,以纠正故障,提高性能或其他属性,或适应变化的环境。
    • 除此之外,可维护性还有其他许多别名:可扩展性(Extensibility)、灵活性(Flexibility)、可适应性(Adaptability)、可管理性(Manageability)、支持性(Supportability)。总之,有好的可维护性就意味着容易改变,容易扩展。
  2. 软件可维护性的五个子特性:
    • 易分析性。软件产品诊断软件中的缺陷或失效原因或识别待修改部分的能力。
    • 易改变性。软件产品使指定的修改可以被实现的能力,实现包括编码、设计和文档的更改。如果软件由最终用户修改,那么易改变性可能会影响易操作性。
    • 稳定性。软件产品避免由于软件修改而造成意外结果的能力。
    • 易测试性。软件产品使已修改软件能被确认的能力。
    • 维护性的依从性。软件产品遵循与维护性相关的标准或约定的能力。
  3. 一些常用的可维护性度量标准:
    • 圈复杂度(CyclomaticComplexity):度量代码的结构复杂度。
    • 代码行数(Lines of Code):指示代码中的大致行数。
    • Halstead Volume:基于源代码中(不同)运算符和操作数的数量的合成度量。
    • 可维护性指数(MI):计算介于0和100之间的索引值,表示维护代码的相对容易性。 高价值意味着更好的可维护性。
    • 继承的层次数:表示扩展到类层次结构的根的类定义的数量。 等级越深,就越难理解特定方法和字段在何处被定义或重新定义。
    • 类之间的耦合度:通过参数,局部变量,返回类型,方法调用,泛型或模板实例化,基类,接口实现,在外部类型上定义的字段和属性修饰来测量耦合到唯一类。
    • 单元测试覆盖率:指示代码库的哪些部分被自动化单元测试覆盖。

三、模块化设计规范:聚合度与耦合度

3.1 模块化编程

  1. 模块化编程的含义:
    • 模块化编程是一种设计技术,它强调将程序的功能分解为独立的可互换模块,以便每个模块都包含执行所需功能的一个方面。
  2. 设计规范:
    • 高内聚低耦合
  3. 评估模块化的五个标准:
    • 可分解性:将问题分解为各个可独立解决的子问题
    • 可组合性:可容易的将模块组合起来形成新的系统
    • 可理解性:每个子模块都可被系统设计者容易的理解
    • 可持续性:小的变化将只影响一小部分模块,而不会影响整个体系结构
  4. 出现异常之后的保护:运行时的不正常将局限于小范围模块内
  5. 模块化设计的五条原则:
    • 直接映射:模块的结构与现实世界中问题领域的结构保持一致
    • 尽可能少的接口:模块应尽可能少的与其他模块通讯
    • 尽可能小的接口:如果两个模块通讯,那么它们应交换尽可能少的信息
    • 显式接口:当A与B通讯时,应明显的发生在A与B的接口之间
    • 信息隐藏:经常可能发生变化的设计决策应尽可能隐藏在抽象接口后面

3.2 内聚与耦合

  1. 内聚性:
    • 又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。
    • 所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。
  2. 耦合性:
    • 也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。
    • 对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分。这样有利于修改和组合。

四、SOLID原则

  单一职责原则告诉我们实现类要职责单一;
  里氏替换原则告诉我们不要破坏继承体系;
  依赖倒置原则告诉我们要面向接口编程;
  接口隔离原则告诉我们在设计接口的时候要精简单一;
  迪米特法则告诉我们要降低耦合;
  开闭原则是总纲(实现效果),它告诉我们要对扩展开放,对修改关闭。

4.1 SRP 单一责任原则(The Single Responsibility Principle)

  1. 含义:
    • 需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。
    • 如果一个类包含了多个责任,那么将引起不良后果:引入额外的包,占据资源;导致频繁的重新配置、部署等。
  2. SRP是最简单的原则,却是最难做好的原则。
  3. SRP的一个反例:
图4-1 SRP原则反例

4.2 OCP 开放封闭原则(The Open Closed Principle)

  1. 软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。
    • 模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化。
    • 模块自身的代码是不应被修改的
    • 扩展模块行为的一般途径是修改模块的内部实现
    • 如果一个模块不能被修改,那么它通常被认为是具有固定的行为。
  2. 关键解决方案:抽象技术。 使用继承和组合来改变类的行为。
  3. OCP的一个反例:
图4-2 OCP原则反例一
图4-3 OCP原则反例二

上面代码存在的问题:

  • 不可能在不修改GraphEditor的情况下添加新的Shape
  • GraphEditor和Shape之间的紧密耦合
  • 不调用GraphEditor就很难测试特定的Shape

改正后代码如下:

图4-4 OCP原则反例二改正

4.3 LSP 里氏替换原则(The Liskov Substitution Principle)

  Liskov’s 替换原则意思是:”子类型必须能够替换它们的基类型。”或者换个说法:”使用基类引用的地方必须能使用继承类的对象而不必知道它。” 这个原则正是保证继承能够被正确使用的前提。通常我们都说,“优先使用组合(委托)而不是继承”或者说“只有在确定是 is-a 的关系时才能使用继承”,因为继承经常导致”紧耦合“的设计。
  详细见第四章第二节

4.4 ISP 接口分离原则(The Interface Segregation Principle)

  1. 含义:
    • 客户端不应依赖于它们不需要的方法。换句话说,使用多个专门的接口比使用单一的总接口总要好。
  2. 客户模块不应该依赖大的接口,应该裁减为小的接口给客户模块使用,以减少依赖性。如Java中一个类实现多个接口,不同的接口给不用的客户模块使用,而不是提供给客户模块一个大的接口。
  3. 缺点:
    • 胖接口可分解为多个小的接口;
    • 不同的接口向不同的客户端提供服务;
    • 客户端只访问自己所需要的端口。
  4. 模型展示:
图4-5 ISP原则模型
  1. ISP原则反例:
    图4-6 ISP原则反例,右侧为更改方法

4.5 DIP 依赖转置原则(The Dependency Inversion Principle)

  1. DIP原则定义:

    • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
    • 抽象不应该依赖于细节,细节应该依赖于抽象
  2. 这个设计原则的亮点在于任何被DIP框架注入的类很容易用mock对象进行测试和维护,因为对象创建代码集中在框架中,客户端代码也不混乱。有很多方式可以实现依赖倒置,比如像AspectJ等的AOP(Aspect Oriented programming)框架使用的字节码技术,或Spring框架使用的代理等。

  3. 特点:

    • 高层模块不要依赖低层模块
    • 高层和低层模块都要依赖于抽象
    • 抽象不要依赖于具体实现
    • 具体实现要依赖于抽象
    • 抽象和接口使模块之间的依赖分离
  4. DIP原则反例及改正方法

    图4-7 DIP原则反例
    图4-8 更改方法

4.5 SOLID原则总结

  1. 一个对象只承担一种责任,所有服务接口只通过它来执行这种任务。
  2. 程序实体,比如类和对象,向扩展行为开放,向修改行为关闭。
  3. 子类应该可以用来替代它所继承的类。
  4. 一个类对另一个类的依赖应该限制在最小化的接口上。
  5. 依赖抽象层(接口),而不是具体类。

文章作者: Demerzel Sun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Demerzel Sun !
评论
 上一篇
不能使用泛型的几种情况 不能使用泛型的几种情况
软件构造笔记-Java 中不能使用泛型的几种情况 一、前言Java 1.5 引入了泛型来保证类型安全,防止在运行时发生类型转换异常,让类型参数化,提高了代码的可读性和重用率。但是有些情况下泛型也是不允许使用的,总结一下编码中不能使用
2020-04-28
下一篇 
软件构造-第四章-第三节-面向复用的设计模式 软件构造-第四章-第三节-面向复用的设计模式
软件构造第四章 第三节 面向复用的设计模式 一、结构型模式:Structural patterns1.1 适配器模式(Adapter) 目的:将某个类/接口转换为用户期望的其他形式。 含义:适配器模式是作为两个互不相容的接口的桥梁,
2020-04-20
  目录