MIT_6.031之避免调试
避免调试的策略
调试是软件开发中不可避免的一部分,但通过一系列有效的策略,可以最大限度地减少调试的需求或在必要时保持调试的简单性。
第一道防线:消除错误
1. 通过设计消除错误
静态检查 :在编译时检测代码中的错误,如类型检查和未使用变量等。
动态检查 :在运行时进行的检查,确保程序的状态和行为符合预期。
不变性(Immutable
Types) :使用不可变类型,确保一旦创建对象,其状态不可改变,从而防止错误。
2. 使用关键字 final
关键字 final
可用于修饰类、方法或变量,确保它们只能分配一次。这有助于防止在程序中出现意外的状态更改。
第二道防线:本地化 Bug
1. 尝试将 Bug 本地化
将 bug 限制在程序的一小部分,使其更易于追踪和修复。
2. 快速失败
越早发现问题(越接近问题的起因),就越容易解决。使用断言和防御性编程来确保条件在执行时被检查。
示例
1 2 3 4 5 6 7 8 public double sqrt (double x) { if (!(x >= 0 )) throw new 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 public static List<Double> quadraticRoots (final int a, final int b, final int c) { List<Double> roots = new ArrayList <>(); assert a != 0 ; assert roots.size() <= 2 ; for (double x : roots) { assert Math.abs(a*x*x + b*x + c) < 0.0001 ; } return roots; }
增量开发
一次只构建程序的一部分,确保在开发过程中持续进行测试。
单元测试和回归测试
使用单元测试确保每个模块的正确性,并通过回归测试保证系统在修改后依然保持正确。
模块化与封装
1. 模块化
将系统分为多个组件或模块,每个模块独立设计、实施和测试,促进重用。
2. 封装
每个模块负责自身的内部行为,系统其他部分中的错误不会破坏其完整性。
方法与访问控制
1. 控制可见性
使用 public
和 private
控制变量和方法的可见性,以保护内部实现。
2. 变量范围
将变量范围保持得尽可能小,便于推断程序中可能存在错误的位置。
在需要时声明变量,而不是在函数开始时声明所有变量,以减小作用域。
3. 避免全局变量
使用局部变量而不是全局变量,以提高代码的可维护性和安全性。
总结
避免错误 :使用静态类型、动态检查和不可变类型来预防错误的发生。
限制错误 :通过断言和快速失败来本地化和限制错误的传播。
增量开发 :逐步构建和测试代码,使用单元测试和回归测试确保质量。
变量范围最小化 :减少需要检查的代码量,使程序更易于理解和维护。
代码质量度量
避免错误 :努力防止和消除错误。
容易理解 :使用静态类型、最终声明和断言作为文档,提高可读性。
准备进行更改 :通过断言和静态类型自动检查假设,确保在修改代码时能够及时发现问题。