软件构造 5-1 Metrics and Construction Principles for Maintainability

5.1 可维护性的度量与构造原则

一. 软件的可维护性

  软件维护:修复错误、改善性能。种类有:

  • 纠错性:对 bug 进行修复
  • 适应性:迁移相关。如把软件从电脑端迁移到手机端需要做一些改变。
  • 完善性:对功能进行完善。
  • 预防性:为了以后软件可能的变化,对现有软件的架构进行实现或改变。

  软件演化指的是对软件进行持续的更新。软件的大部分成本来自于维护阶段


  提高软件可维护性的方法:

  • 模块化
  • OO 设计原则SOLID GRASP
  • OO 设计模式
  • 基于状态的构造技术
  • 表驱动的构造方式
  • 基于语法的构造技术

二. 软件可维护性的度量

  软件可维护性:软件是否容易被扩展、改变。在 Code review 层面:

  • 设计结构是否足够简单?
  • 模块之间是否松散耦合?
  • 模块内部是否高度聚合?
  • 是否使用了非常深的继承树,是否使用了 delegation 替代继承?
  • 代码的圈复杂度是否太高?
  • 是否存在重复代码?

  软件可维护性的度量指标:

  • 圈复杂度:一种用来衡量软件中基本路径/可执行路径条数的上限。公式:
    CC = E - N + 2E 指程序流程中边数,N 为结点数),CC=P+1(程序中判定的个数),CC= number of areas(流程图中由边组成区域的个数,包括外部区域)
  • 代码行数
  • 操作个数
  • 可维护性指数MI
  • 继承的层次数
  • 类之间的耦合度
  • 单元测试的覆盖度

三. 设计原则

  模块化编程使得可维护性更好,希望把程序划分为多个模块,每个模块实现不同的功能,模块之间关系松散。即高内聚,低耦合

  • 分离关注点:模块负责的职责要少
  • 信息隐藏:类具体的实现不能让用户知道。

1. 五种度量指标

  • 可分解性:软件是容易分出来不同的模块
  • 可组合性:软件多个模块组合在一起否能满足需求
  • 可理解性:模块的职责否清晰
  • 可持续性:发生变化时(如升级)受影响范围最小
  • 出现异常之后的保护:出现异常后受影响范围最小

2. 五个设计方法

  • 直接映射:使用模块时直接使用而不是跨模块使用(别用 Adapt 使得程序变得复杂)
  • 尽可能少的接口
  • 尽可能小的接口
  • 显式接口
  • 信息隐藏

3. 耦合与内聚

  耦合:类与类之间连接/联系的复杂性。
左耦合大于右耦合
  程序希望耦合性越低越好,但太低的耦合性会使程序/类规模变大。耦合性低到极致,内聚性也很低。


  内聚:模块内部的聚合性。每个模块的内部功能/操作仅仅是完成某个目标而实现。
在这里插入图片描述
  在一定限度内,高内聚很可能是低耦合的(当然,低内聚很可能是高耦合的)。

四. OO 设计原则:SOLID

  • (SRP) 单一责任原则
  • (OCP) 开放-封闭原则
  • (LSP) Liskov 替换原则
  • (DIP) 依赖转置原则
  • (ISP) 接口聚合原则

1. SRP 单一责任原则

  每一个类只需要包含一个用户所需要的操作即可。

  这是因为责任是变化的原因:

  • 不应有多于 1 个的原因使得一个类发生变化
  • 一个类,一个责任
    在这里插入图片描述

2. (OCP) (面向变化的)开放-封闭原则

  要求模块可以容易地增加需求,但不破坏现有的结构。

  对扩展性的开放

  • 模块的行为应是可扩展的,从而该模块可表现出新的行为以满足需求的变化

  对修改的封闭

  • 但模块自身的代码是不应被修改的
  • 扩展模块行为的一般途径是修改模块的内部实现
  • 如果一个模块不能被修改,那么它通常被认为是具有固定的行为

  关键的解决方案:抽象技术(可以使用策略模式),尽量用抽象类/接口。


  例:如果有多种类型的 Server ,那么针对每一种新出现的 Server ,不得不修改 Server 类的内部具体实现。如下图:
在这里插入图片描述
  通过构造一个抽象的 Server 类: AbstractServer ,该抽象类中包含针对所有类型的 Server 都通用的代码,从而实现了对修改的封闭;当出现新的 Server 类型时,只需从该抽象类中派生出具体的子类 ConcreteServer 即可,从而支持了对扩展的开放。


  例:源程序,一大堆复杂的 if else
/switch case 结构,维护起来非常麻烦:

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)
			{...}
	}
}

  根据 OCP 原则修改:

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();//delegation
	}
}
class Shape {
	abstract void draw();
}
class Rectangle extends Shape {
	public void draw() {
		// draw the rectangle
	}
}

3. (LSP) Liskov 替换原则

  子类型必须能够替换其基类型。

  派生类必须能够通过其基类的接口使用,客户端无需了解二者之间的差异。

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() {...}
}

5. (DIP) 依赖转置原则

  依赖即委托。调用时/使用时尽量依赖接口不是实现类
在这里插入图片描述

void Copy(OutputStream dev) {
	int c;
	while ((c = ReadKeyboard()) != EOF)
		if (dev == printer)
			writeToPrinter(c);
		else
			writeToDisk(c);
}

  这依赖于具体的打印机/键盘。修改:
在这里插入图片描述

interface Reader {
	public int read();
}
interface Writer {
	public int write(c);
}
class Copy {
	void Copy(Reader r, Writer w) {
		int c;
		while (c=r.read() != EOF)
			w.write(c);
	}
}

在这里插入图片描述
  换句话说:delegation 的时候,要通过 interface 建立联系,而非具体子类。

//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(...)
	}
}

五. OO 设计原则:GRASP

  全称:通用职责分配原则。程序包含多个类,每个类代表不同的职责。GRASP 是关于如何为“类”和“对象”指派“职责”的一系列原则。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值