5.1 可维护性的度量与构造原则
一. 软件的可维护性
软件维护:修复错误、改善性能。种类有:
- 纠错性:对
bug
进行修复 - 适应性:迁移相关。如把软件从电脑端迁移到手机端需要做一些改变。
- 完善性:对功能进行完善。
- 预防性:为了以后软件可能的变化,对现有软件的架构进行实现或改变。
软件演化指的是对软件进行持续的更新。软件的大部分成本来自于维护阶段。
提高软件可维护性的方法:
- 模块化
OO
设计原则:SOLID
GRASP
OO
设计模式- 基于状态的构造技术
- 表驱动的构造方式
- 基于语法的构造技术
二. 软件可维护性的度量
软件可维护性:软件是否容易被扩展、改变。在 Code review
层面:
- 设计结构是否足够简单?
- 模块之间是否松散耦合?
- 模块内部是否高度聚合?
- 是否使用了非常深的继承树,是否使用了
delegation
替代继承? - 代码的圈复杂度是否太高?
- 是否存在重复代码?
软件可维护性的度量指标:
- 圈复杂度:一种用来衡量软件中基本路径/可执行路径条数的上限。公式:
CC = E - N + 2
(E
指程序流程中边数,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
是关于如何为“类”和“对象”指派“职责”的一系列原则。