MIT_6.031之避免调试

避免调试的策略

调试是软件开发中不可避免的一部分,但通过一系列有效的策略,可以最大限度地减少调试的需求或在必要时保持调试的简单性。

第一道防线:消除错误

1. 通过设计消除错误

  • 静态检查:在编译时检测代码中的错误,如类型检查和未使用变量等。
  • 动态检查:在运行时进行的检查,确保程序的状态和行为符合预期。
  • 不变性(Immutable Types):使用不可变类型,确保一旦创建对象,其状态不可改变,从而防止错误。

2. 使用关键字 final

  • 关键字 final 可用于修饰类、方法或变量,确保它们只能分配一次。这有助于防止在程序中出现意外的状态更改。

第二道防线:本地化 Bug

1. 尝试将 Bug 本地化

  • 将 bug 限制在程序的一小部分,使其更易于追踪和修复。

2. 快速失败

  • 越早发现问题(越接近问题的起因),就越容易解决。使用断言和防御性编程来确保条件在执行时被检查。

示例

1
2
3
4
5
6
7
8
/**
* @param x requires x >= 0
* @return approximation to square root of x
*/
public double sqrt(double x) {
if (!(x >= 0)) throw new AssertionError(); // AssertionError
// 执行计算
}

断言

1. 防御性检查

  • 使用 assert 语句进行防御性检查,以记录程序状态的假设。
1
2
assert (x >= 0); // 仅检查条件
assert (x >= 0) : "x is " + x; // 带有错误信息的检查

2. 启用断言

  • 默认情况下,Java 中的断言处于关闭状态,因为它们会影响性能。可以通过在运行时传递 -ea 参数来启用断言。

3. 断言与 JUnit

  • 断言是实现内部检查的工具,而 JUnit 的 assertTrue()assertEquals() 用于测试结果的有效性。

断言的内容

1. 参数和返回值的自我检查

  • 使用断言确保方法参数和返回值满足预期条件。
1
2
3
4
5
6
7
public double sqrt(double x) {
assert x >= 0; // 检查参数
double r;
// 计算结果
assert Math.abs(r*r - x) < .0001; // 检查返回值
return r;
}

2. 覆盖所有情况

  • 使用断言防止非法情况出现。
1
2
3
4
5
6
7
8
switch (vowel) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u': return "A";
default: assert false; // 捕获未处理的情况
}

何时编写断言

  • 在编写代码时编写断言,而不是在完成后,以保持对不变性的记忆。
  • 避免琐碎的断言,不要在显而易见的情况下使用断言。
  • 不要使用断言测试程序外部的条件(如文件存在性、网络可用性等)。

断言练习

例:解方程组 ( ax^2 + bx + c = 0 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 解方程 ax^2 + bx + c = 0
* @param a 二次项系数,要求 a != 0
* @param b 一次项系数
* @param c 常数项
* @return 该方程的实根列表
*/
public static List<Double> quadraticRoots(final int a, final int b, final int c) {
List<Double> roots = new ArrayList<>();
assert a != 0; // A
// 计算根
assert roots.size() <= 2; // B
for (double x : roots) {
assert Math.abs(a*x*x + b*x + c) < 0.0001; // 确保是根
}
return roots;
}

增量开发

  • 一次只构建程序的一部分,确保在开发过程中持续进行测试。

单元测试和回归测试

  • 使用单元测试确保每个模块的正确性,并通过回归测试保证系统在修改后依然保持正确。

模块化与封装

1. 模块化

  • 将系统分为多个组件或模块,每个模块独立设计、实施和测试,促进重用。

2. 封装

  • 每个模块负责自身的内部行为,系统其他部分中的错误不会破坏其完整性。

方法与访问控制

1. 控制可见性

  • 使用 publicprivate 控制变量和方法的可见性,以保护内部实现。

2. 变量范围

  • 将变量范围保持得尽可能小,便于推断程序中可能存在错误的位置。
  • 在需要时声明变量,而不是在函数开始时声明所有变量,以减小作用域。

3. 避免全局变量

  • 使用局部变量而不是全局变量,以提高代码的可维护性和安全性。

总结

  • 避免错误:使用静态类型、动态检查和不可变类型来预防错误的发生。
  • 限制错误:通过断言和快速失败来本地化和限制错误的传播。
  • 增量开发:逐步构建和测试代码,使用单元测试和回归测试确保质量。
  • 变量范围最小化:减少需要检查的代码量,使程序更易于理解和维护。

代码质量度量

  • 避免错误:努力防止和消除错误。
  • 容易理解:使用静态类型、最终声明和断言作为文档,提高可读性。
  • 准备进行更改:通过断言和静态类型自动检查假设,确保在修改代码时能够及时发现问题。