《Java Bug模式》读书笔记

       Bug模式是程序中已发生的bug和潜在bug之间重复出现的相互关系。有了这些模式和bug现象的知识,程序员就能很快识别新发生的bug,还可以预防这些bug的发生。

       Bug模式与反模式有关,反模式是指被多次证明是失败的软件通用设计模式。这些设计的反面示例是传统正面设计模式的必要补充。虽然反模式也是一种设计模式,但Bug模式却是一种和编程错误相关的不正确的程序行为模式。这种关系与设计毫不相关,但是与编写代码和调试过程有关。

      下面分别对13中Bug模式进行详细了解。


1.Rogue Tile模式


起因通常是由在程序的不同部分之间复制和粘贴代码段产生的。

症状:您认为已经修正了导致错误行为的代码以后,程序还继续出现错误。

解决方法&预防措施

  1. 提取公共代码。
  2. 在封装代码和对每个功能元素保持单点控制两者之间做出折中。
  3. 静态类型语言会限制语言的可表达性,因而降低了预防Rogue Tile的效果。通用类型在一定程度上可以弥补Java语言的这一缺陷。
  4. 必须抛出所有已捕获或者已声明的检测过的异常,这样的要求意味着无法提取出方法中的某些代码。
  5. 面向方面的编程技术——通过在程序的类和函数中加入“方面(aspect)”来组织程序——有助于处理复制的代码(每个方面对应于程序的全局属性,例如,在方法中处理检查过的异常的方式)。

诊断清单

  1. 代码呈现出原先修正过的bug似乎仍然存在的症状。
  2. 在一项有很多剪切-粘贴代码的工作中出现bug。
  3. 某个类的数值字段类型改变引起了bug。
  4. 修改过的方法在编译后,返回一个表面合理但实际错误的数值。
  5. 单独构建TreeVisitor接口,分别为所需返回的每个类型创建一个不同的accept()。
  6. 在运行时,尽管对所有事情都使用了相同的TreeVisitor接口,并在每个Visitor方法调用时插入的类型转换,但结果还是出现了ClassCastException异常。


2.Dangling Composite模式


起因:在使用递归定义的数据类型的代码报告一个空指针异常。

症状:在定义递归数据类型时,没有为一些基本类型定义其所属类。因此,空指针被插入到各种复合数据类型中去。然后客户端代码处理基本类型的方式不一致。

解决方法&预防措施:为保证一致性,确保基本类型已经过正确地表示和检查,为每个基本类型分配各自的类。

诊断清单

  1. 出现了一个NullPointerException异常,而且不知道它代表什么错误!
  2. 在使用递归方式定义数据类型的代码中出现了NullPointerException异常。
  3. 在定义递归数据类型时是否存在问题,使得某些基本类型未确定其所属类?
  4. 在将空指针插入不同的符合数据类型时是否存在问题?
  5. 出去Leaf类,并将Leaf节点用Branch中left和right字段的控制来表示是否可以修复ClassCastException异常,并减免类型转换的操作?


3.Null Flag模式


起因:没有检查调用的方法是否将空指针作为返回值。

症状:使用空指针作为异常条件标志的代码段报告一个NullPointerException。

解决方法&预防措施:通过抛出异常报告异常条件。

诊断清单

  1. 出现了一个NullPointerException异常,而且不知道它代表什么错误!
  2. 出去Leaf类,并将Leaf节点用Branch中left和right字段的控制来表示是否可以修复ClassCastException异常,并减免类型转换的操作?


4.Double Descent模式


起因:部分代码在每次方法调用中向下访问了两层,并且在第二次时调度不当。

症状:程序在执行数据结构的递归下访问抛出ClassCassExceptions异常。

解决方法&预防措施:把执行强制类型转换的代码分解到每个类的单独方法中去。另一种方法是,检查不变量以确保强制类型转换成功执行。

诊断清单

  1. 当对数据进行递归下访时,代码会抛出ClassCastException异常。
  2. 当递归下访数据时,在一次递归调用中发生了多级下访。
  3. 创建同一个类的多个不同实例时是否会出现问题?
  4. 使用instanceof和equals来检查类和对象的标识符;还有更好的方法么?
  5. 怎么做才能消除连续节点中的0值?
  6. 在进行了一次方法调用后,无法正常调度代码。
  7. 出去Leaf类,并将Leaf节点用Branch中left和right字段的控制来表示是否可以修复ClassCastException异常,并减免类型转换的操作?
  8. 为什么需要在代码中对所有不变量进行注释?
  9. 使用instanceof检查来避免ClassCastException异常的缺点是什么?


5.Liar View模式


起因:测试只是直接检查了模型的不同方面。

症状:GUI程序通过了测试集的测试,但是随后却表现出本该在那些测试中就已消除的行为。

解决方法&预防措施:在整体测试的基础上为模型和视图编写独立的测试程序。

诊断清单

  1. 进行GUI设计时是否存在异常?
  2. GUI通过了所有测试,但是现在客户打电话告诉我出现了问题。这是什么原因?
  3. 单元测试的确是一种辅助调试的好方法,但真的需要写那么多单元测试吗?
  4. 测试和代码的运行行为为什么不匹配?
  5. 测试和代码的运行时未获得一致的结果。测试有错吗?
  6. GUI测试是如何工作的?
  7. 程序中模型-视图控制器(MVC)结构可能出错吗?
  8. 通过视图检查模型是否存在一个最佳时机?
  9. 如果无法轻易地实现自动测试GUI,那么怎样才能使用测试来检查代码?
  10. 是否应该持续重构方法?
  11. 要对代码进行有效的、永久性重构,是否存在其他关键要素?
  12. 据称Java的Robot类对GUI测试很有效。它对每种实例都有效吗?
  13. 非GUI的数据显示软件也可以显示出测试和运行时结果之间的差异么?


6.Saboteur Data模式


起因:一些内部数据被破坏,可能是语法上或者是语义上的破坏。

症状:用于存储和处理复杂的输入数据的程序在执行某项任务时意外地崩溃,而在执行其他类似任务时却安好无事。

解决方法&预防措施:对输入数据作尽可能多的完整性检查,而且越早越好。对于已经损坏的持久数据,研究这些数据并检查其完整性。

诊断清单

  1. 数据输入代码本可工作正常,但一段时间后,尽管执行同样的任务,却崩溃了。问题出在哪里?
  2. 引起崩溃的数据来自于网络上的大型数据结构。这是否是问题所在?
  3. 可能引发数据毁坏的原因是什么——句法部分还是语义部分?
  4. 可能引发数据毁坏的原因使什么——手工编辑或自动生成文件?
  5. 通过分析数据来检查数据是否已被毁坏,这似乎是一项很大的工程!这难道不是编译器应该做的工作?
  6. 类型检查是否可以帮助从语义上确定已损坏的数据?
  7. 如果无法在输入时检查数据,那么能对现有数据做哪些操作?
  8. 对数据执行迭代操作是否是清除毁坏数据的有效方法?在什么情况下是?
  9. 是否应该对数据执行迭代操作,访问所有数据(像在已部署的应用程序中一样)?
  10. 是否总可以在被毁坏的数据引起问题之前发现它么?如果不能,为什么?
  11. 将一行文本分为两个String是否会引起难以发现的、被毁坏的数据?


7.Broken Dispatch模式


起因:重载过程使得未经修改的方法激活了其他方法,而不是您所希望调用的方法。

症状:重载另一个方法之后,在测试从未修改过的代码时突然出现错误。

解决方法&预防措施:插入显式的向上类型强制转换;或者,重新考虑在不同类中提供的方法集。

诊断清单

  1. 我重载了一个方法,然后另外一个的方法发生了中断。这说明什么?
  2. 是否有可能因为方法参数不相匹配?
  3. 添加新的方法是否会引起其他方法的中断?
  4. 如果一个通用的方法的构造函数重载了一个更特殊的方法,会发生什么情况?
  5. 是否应该在我的代码中“布满”测试?


8.Impostor Type模式


起因:程序针对各种类型的数据使用带标记的字段,而不是独立的类。

症状:程序以相同的方式来处理不同类型的数据,或者数据与任何指定的类型都不匹配。

解决方法&预防措施:尽可能将概念上不同的数据类型分为几个独立的类。

诊断清单

  1. 为什么程序把不同类型的数据作为相同类型处理?
  2. 代码无法识别某些数据类型。
  3. 在特殊的字段中使用标记来区分数据类型,这是否会出现问题?
  4. 为什么应该使用静态类型系统来区分数据类型?
  5. 为避免类型不匹配而采用如下解决方法:使用if-then-else语句模块来调度合适的类型。这种方法会起作用吗?


9.Split Cleaner模式


起因:程序的一些执行路径没有完成它们应该做的工作:程序未能在适当时刻一次性释放资源。

症状:程序未能正确地管理资源,而是泄露或过早地释放了这些资源。

解决方法&预防措施:把负责释放资源的代码移到获得资源的同一方法中。

诊断清单

  1. 代码发生内存泄露。可能的原因是什么?
  2. 为什么程序过早地释放资源(比如数据库连接)?
  3. 要释放资源,是否应该在同一个方法中获得并释放资源?
  4. 为了确保资源已经释放,是否应该跟踪代码可能执行的每条路径?
  5. 为什么某些代码执行路径并不是在相关时刻一次性释放资源?
  6. 是否应该对程序可能扩展的所有方式进行预测并为其编写代码?
  7. 发现没有包括正确的释放代码的执行路径,是否应该向此路径添加释放资源的代码?


10.Fictitious Implementation模式


起因:接口中包括大量实现方案无法满足的不变量。

症状:当使用某个接口的特定实现方案时,处理此接口的客户端类发生中断。

解决方法&预防措施:修改接口实现方案,以包括这些不变量。如果这些不变量尚未写入接口文档,则在其中作明确说明。

诊断清单

  1. 如何正式地规范定义接口?
  2. 在实现某个接口时,负责处理此接口的客户类为什么发生终端?
  3. 是否可以不在接口中显式记录不变量?
  4. 加载带有其他不变量的代码时存在哪些缺陷?
  5. 作为安全保障,是否应指定可全部被静态检查的不变量?
  6. 是否可以将接口不变量的定义限制为类型签名?
  7. 什么是断言?是否存在不同类型的断言?
  8. 断言应该被包含在接口实现方案的什么位置?
  9. 包括断言是否会严重增加程序执行的开销?
  10. 仅有断言是否足够捕获我希望在接口上定义的所有规则?
  11. 是否可以用单元测试为其他的接口不变量提供限制规范?
  12. 单元测试集是否能检测实现方案中所有的输入?
  13. 对于类型签名和单元测试,哪一种具备更强的表述性?


11.Orphaned Thread模式


起因:多个程序线程一直等待来自某个线程的输入,而该线程在抛出一个未被捕捉的异常后就退出程序了。

症状:多线程程序被锁定,可以或者无法将堆栈跟踪打印到标准错误。

解决方法&预防措施:把异常处理代码放到主线程中,告知依赖于该线程的其它线程已出现异常情况。另一种方法是,在退出的线程中放入处理程序,使它向其客户端传递相关信息。

诊断清单

  1. 多线程代码被锁定,它将堆栈跟踪打印到标准错误。
  2. 如果只使用单线程的设计会怎样?
  3. 对线程的stop()方法是否出现问题?
  4. 在什么类型的编程中,更有可能遇到被废弃的第2个线程?


12.Run-on Initializatier模式


起因:某个类的构造函数并未直接初始化所有的字段。

症状:在访问未被初始化的字段处抛出了一个NullPointerException异常。

解决方法&预防措施:在一个构造函数中初始化所有的字段。当没有更好的值可以使用时,使用特殊类作为默认值。对于有更好的值可以使用的情况,要包含多个构造函数。当受到其他因素的限制时,请至少包含一个isInitialized()方法。

诊断清单

  1. 出现了一个NullPointerException异常,而且不知道它代表什么错误!
  2. 当访问一个未被初始化的字段时,出现NullPointerException异常。
  3. 类中不是所有字段都被初始化。是不是未正确构建构造函数?
  4. 需要客户类分多个步骤来初始化实例的构造函数是否存在问题?
  5. 某个类已经多次添加新的字段。这样添加字段是否可能会引起不正确的初始化?
  6. 语句的执行顺序是否对程序会产生影响?
  7. 是否应该说服客户抛掉旧代码,重新编写新代码?
  8. 若正在使用原有代码。是否可以修改构造函数签名?
  9. 当使用旧代码时,控制NullPointerException异常错误的最好方法是什么?
  10. 在类中使用isInitialized()方法会起到怎样的效果?
  11. 如何确保类的实例一直处于正确定义的状态?
  12. 如果使用空值填充类字段,是否会对初始化有所帮助?
  13. 确保某个实例是否已被初始化的快速方法是什么?
  14. 应如何避免在新的上下文中出现初始化bug?
  15. 表示默认值的最好方法是什么?
  16. 使用特殊的类来表示默认值是否会引起性能的下降?如果是,为什么?
  17. 是否可直接进行初始化检查,而无需在运行时执行类型转换,也不会引起性能的下降?
  18. 有人认为包含始终可抛出异常的方法才是正确的编程方法,但这看起来是否过于笨拙?


13.Platform-Dependent模式


13.1与供货商相关的bug模式


起因:JVM规范中留有某些内容未被指定,例如,未对尾调用的优化作出要求。较之与版本相关的bug而言,这类bug相对少一些。

症状:某些JVM出现了bug,而其他JVM上则没有。

解决方法&预防措施:因问题而异。


13.2与版本相关的bug模式


起因:因特定供应商JVM的某些版本中的bug而引起。较之供应商相关的bug而言,这类bug更为常见。

症状:JVM的某些版本可能出现bug,但其他版本则没有。

解决方法&预防措施:因问题而异。


13.3与操作系统相关的bug模式


起因:不同操作系统中的系统行为规则可能有所不同。例如,在UNIX中,可以删除已打开的文件;而在Windows上则不能。

症状:某些操作系统中可能出现,但是其他操作系统则没有。

解决方法&预防措施:因问题而异。



诊断清单
  1. 为什么代码只能运行在某些Java虚拟机上,而不能运行在其他机器上?
  2. 为什么代码只能运行在JVM的某些版本上,而不能运行在其他版本上?
  3. 为什么代码只能运行在某些操作系统上,而不能运行在其他操作系统上?
  4. 与规范相关的bug和与实现方案相关的bug有什么不同之处?
  5. 是否存在Java规范和实现方案引起的bug的综合叙述?


14.用于调试的设计模式


14.1最大化静态类型检查


1、尽可能设计final字段。
2、将不可能被改写的方法设为final。
3、包括作为默认值的类。
4、对异常情况进行检查,以确保所有的客户端程序都能够处理异常情况。
5、定义新的异常类型来精确区分各种异常情况。
6、当某个类的实例将一个状态或固定数目的状态用于Composite层次结构中的不同子类中时,就要中断这个类。
7、清除所有可能涉及平台相关性的行为。
8、在尽可能多的平台上进行测试。
9、将类型转换和instanceof测试降至最少。
10、使用Singleton设计模式帮助最小化instanceof的作用。
11、使用额外的方法和动态调度帮助最小化instanceof的作用。


14.2将引入bug的可能性降至最低


1、提取通用代码。
2、尽可能实现纯功能性方法。
3、在构造函数中初始化所有字段。
4、出现异常情况时立即抛出异常。
5、出现错误时立刻报告错误消息。
6、通过语法分析、类型检查等过程尽早发现bug。
7、通过类型转换、assertTrue()方法、文档和文档形式的参数在代码中置入断言。
8、尽可能在用户可观察到的状态下测试代码。



       使用上述原则上会有助于减少代码中的bug的发生几率。您可能会发现,此处讨论的部分bug模式不是最经常出现的那些bug;坦言之,本书讨论的很多bug模式问题多多,不要指望可一劳永逸地消除这些bug。我们只能学会更快地诊断它们,并使用正确的预防手段清除它们。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。