诊断Java代码: 在规范钢丝上行走

要构建可靠的软件,程序规范很关键。没有良好定义的规范,很难诊断软件系统的异常行为。但是很多软件系统的程序规范定义得很差劲。而且更糟的,是许多软件系统根本就没有规范。

直观的看,程序规范是对程序行为的一种描述。它可以采取许多形式,但无论采取何种形式,都有一条主线贯穿所有实例:必须有某种类型的系统规范,因为您得依靠它来判断系统是否运转正常。

规范可以形式化也可以松散地定义,这取决于开发中系统的稳定性和危险程度,还与开发完毕后修改系统的容易程度有关。

我们将通过讨论规范为什么重要、为什么会经常被忽略以及如何改善这种情况来开始我们的这次“旅程”。

平衡精度的成本和收益

在微处理器设计世界中,系统部署在各种规模的应用上,从个人计算机到担负重任的医疗和军事系统。在这个领域中有一个普遍的、牢不可破的规则:在部署之后修改芯片设计的成本是极其昂贵的。

因此,通常会将微处理器的规范形式化,这也就不足为奇了。一个正式的规范有巨大的好处,因为能够自动地解释和分析它。就微处理器而言,设计的许多方面可以自动地被验证无误。

软件类比:编程语言

在软件世界中,在部署和危险程度方面,最类似于微处理器的事物是编程语言。一种普及的编程语言被用来编写无数的程序,它们被用于各种危险程度级别不同的系统中。

和芯片一样,在投入使用之后修改语言设计的成本是相当昂贵的,因为所有现有的程序都必须修改和重新编译。因此,与其它软件系统相比,编程语言的规范通常是相当正式的。

对于语法,这种形式化尤其重要。事实上,所有现代编程语言都拥有正式定义的语法。大多数解析器是通过使用自动的解析器生成器构造的,生成器读入这些文法并产生完整的解析器作为输出。

不幸的是,语言语义并不倾向于作这样严格的规定。这并非因为不能做到这样严格。

象 ML 这样语言拥有形式化的语义,并因此证明了许多关于它们的法则,用于验证其正确性的某些方面(譬如,它们的类型系统的完善)。但是象 ML 这样的语言是特例。我们可以确定造成这种情况的两个原因。

首先,因为证明关于编程语言规范的特性实际上比硬件设计更难以驾驭,所以不太需要正式的规范。相反,许多语言是用叙述性文字规定的。这些文字规范对于大多数实际使用语言的人(譬如编译器作者)来说已经足够了。实际上,编译器作者通常着迷于不太正式的规范,因为它给了他们更多优化程序的空间。另外,有些时候语言的使用者是程序员,他们中的大多数很欣赏非正式规范,因为这样他们可以轻松地理解规范。

第二个理由是,许多语言是由单独的开发人员作为“业余爱好”开发的,而他们往往并不专长于编程语言领域。遗憾的是,这些开发人员常常并不了解为规定编程语言语义而开发的形式体系。

模棱两可的成本的示例

然而,语言规范中的模棱两可或不一致造成的成本可能是相当昂贵的,会导致可移植性、可靠性的降低,甚至会造成安全性漏洞。通过研究一些当前广泛使用的语言,可以发现它们的规范中相对的精确程度是如何影响它们的。

C++ 语言的规范有许多模棱两可的地方,甚至在语法级别也有。此外,规范的许多部分是变成依赖于实现的。结果是:C++ 程序通常很难在多种平台上按预期的那样运行。

Python 语言规范遗留了许多依赖于实现或未定义的细节。结果,诸如 Jython 和 CPython 之类的实现,在提供与另一方相同的行为这个方面,面临着巨大难题。如果不是因为 Python 语言相对比较简单(并非贬义),这一问题还会更糟。

尽管 Java 语言没有正式的规范(类似于 ML 那样的),但是在精确的非正式规范的开发上投入了很多努力。该语言通常编译成由 JVM 解释的字节码,而 JVM 本身具有良好的规范(尽管通过正式分析在该规范中发现了一些模棱两可的地方)。此外,Java API 都作为 JVM 的一部分规定。这使得 Java 代码具有空前程度的可移植性。

我们从中可以得出结论,一份尽可能精确的语言规范会非常有用。但即使在编程语言世界中,规范中的问题也是最昂贵的,精确的规范很少,部分原因在于预先制作一份精确规范很昂贵。

许多公司发现成本更低的做法是先交付产品,以后(或者很可能永远也不会)再充实规范细节。诚然,对于生命周期较短以及部署范围较窄的应用程序,预先定义精确规范确实是太昂贵了。有时可能竞争对手早已交付了系统,而开发团队还未正式确定其系统的规范。

此外,大型规范很少在用户需求更改时更新,并且因此被忽略了。但是如果预先定义规范太昂贵,开发团队应该采取什么方法来规定他们的软件呢?

在回答这个问题之前,让我们考虑一下一个常用的,但也确实是最糟糕的方法吧。

时间: 2024-10-04 12:53:51

诊断Java代码: 在规范钢丝上行走的相关文章

诊断Java代码

诊断Java代码: Broken Dispatch错误模式 诊断Java代码: Double Descent错误模式 诊断Java代码: Impostor Type错误模式 诊断Java代码: Java编程中的断言和时态逻辑 诊断Java代码: Liar View错误模式 诊断Java代码: Repl提供交互式评价 诊断Java代码: 单元测试与自动化代码分析协同工作 诊断Java代码: 将时态逻辑用于错误模式 诊断Java代码: 进行记录器测试以正确调用方法 诊断Java代码: 空标志错误模式

诊断Java代码: 进行记录器测试以正确调用方法

用 JUnit进行单元测试是一个功能强大的方法,它可以确保您的代码基础的完整性,但是一些不变量比其他(方法调用序列是其中一种)更难测试.在诊断Java 代码这一部分,Eric Allen描述了怎样在您的单元测试中使用记录器(一种特殊的侦听器),来确保一个方法调用序列按恰当的顺序发生.请点击文章顶部和底部的 讨论,与作者和其他读者在论坛上分享您关于本文的看法. 随着时间的推移,当系统开发人员,维护人员甚至是系统详细说明改变时,JUnit 框架提供一个很好的方法来改善系统的坚固性.通过测试,您可以检

诊断Java代码: Impostor Type错误模式

当使用字段中特殊的标记来区别对象类型时,可能会产生标记对相关数据误贴标签的错误 ― 通称为 Impostor Type 错误模式.在诊断 Java 代码的这一部分中,Eric Allen 对这个错误的症状和起因进行了分析,详细说明了预防错误发生的方法,并讨论了一种吸引人的混合实现方法,这种方法不使用 impostor type,但最后,还是有很多相同的缺点产生.请在 讨论论坛与作者及其他读者分享您对本文的看法. 程序中除了最无关紧要的部分外都要对某些数据类型进行操作.静态类型系统提供了一种方法,

诊断Java代码: 设计可扩展应用程序,第3部分

对应于我们上一篇" 诊断 Java 代码"中所讨论的透明盒可扩展性,黑盒可扩展性是指,在源代码既不能查看也不能修改时,可以扩展软件系统的方法.通常通过系统配置或使用特定于应用程序的脚本语言来进行这样的扩展.在本专题中,Eric Allen 讨论了何时设计黑盒可 扩展性的系统是有意义的,并提供了如何有效地实现这一设计的一些想法.阅读了本文后,您将知道何时使用黑盒并掌握如何实现它的一些技巧. 我已在以前的文章中谈到了代码重用设计策略的重要性(主要是因为各种信息处理任务的差异和相应费用的增加

诊断Java代码: 设计可扩展的应用程序,第2部分

玻璃箱可扩展性是指这样一种方式:软件系统可在源代码可以查看而不可以修改时被扩展 ― 它是黑箱设计(在这里构建扩展时,不查看原始代码)和开放箱设计(扩展代码直接写入到基础代码)的折衷.因为新的扩展直接建立在原始代码基础上,但不改动原始代码,所以,玻璃箱设计或许是扩展一个软件系统最有效.最安全的方法.在 诊断 Java 代码的这一部分中,Eric Allen 详述了上个月谈及的玻璃箱可扩展性主题.读完本文后,您将知道什么时候使用玻璃箱,并将获得一些如何实现它的提示. 随着信息处理任务(和与之相关的成

诊断Java代码:孤线程(Orphaned Thread)错误模式

在多线程代码中,使用驱动其它线程所负责的动作的单个主线程是常见的.这个主线程发送消息,通常是通过把它们放到一个队列中,然后其它线程处理这些消息.但是如果主线程抛出一个异常,那么剩余的线程会继续运行,等待更多输入到该队列,导致程序冻结.在诊断 Java 代码的这一部分中,专职 Java 开发者兼兼职捉虫者 Eric Allen 讨论检测.修复和避免这一错误模式. 用多线程编写代码对程序员大有好处.多线程能使编程(和程序)进行得快得多,而且代码能有效得多地使用资源.然而,跟生活中的很多事情一样,多线

css 、js-网页上的java代码格式像eclipse上输出

问题描述 网页上的java代码格式像eclipse上输出 就是要做到这种效果,是怎么实现的? 解决方案 用〈pre〉标签..... 解决方案二: 看过页面的源码了,直接用 标签就可以吗 解决方案三: 看过页面的源码了,直接用 标签就可以吗

Java代码注释规范详解_java

代码附有注释对程序开发者来说非常重要,随着技术的发展,在项目开发过程中,必须要求程序员写好代码注释,这样有利于代码后续的编写和使用. 基本的要求: 1.注释形式统一 在整个应用程序中,使用具有一致的标点和结构的样式来构造注释.如果在其它项目中发现它们的注释规范与这份文档不同,按照这份规范写代码,不要试图在既成的规范系统中引入新的规范. 2.注释内容准确简洁 内容要简单.明了.含义准确,防止注释的多义性,错误的注释不但无益反而有害. 3.基本注释(必须加) (a) 类(接口)的注释 (b) 构造函

诊断Java代码: 臆想实现错误模式,第2部分

臆想实现重温 回想一下 上次接口的 臆想实现是一个合法的实现,但不满足接口规范的某些未经检查的方面.我们考虑一下下面的堆栈接口,以及许多未被其单独的类型签名捕获的不变量: 清单 1. 一个堆栈接 public interface Stack { public Object pop(); public void push(Object top); public boolean isEmpty(); } 例如,请考虑我们希望任意堆栈实现都遵守的下列规则: 如果一个对象 o 被压进堆栈 s ,且在堆栈