软件构造 6-3 Assertions and Defensive Programming

6.3 断言与防御式编程

一. 断言 assert

  首先要遵循:以尽量不要引入 bug 的原则(防御式编程)编程。

  • 静态检查
  • 动态检查
  • 不可变类型
  • 不可变值
  • 不可变引用

  若无法避免,缩小 bug 的范围:

  • 限定在一个方法内部,不扩散
  • 尽快失败,就容易发现、越早修复

  断言就是一种 fail fast,避免 bug 的扩散。

1. assertion

  断言:在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。

public class AssertionTest {
	public static void main(String[] args) {
		int number = -5; // assumed number is not negative
		// This assert also serve as documentation
		assert (number >= 0) : "number is negative: " + number;
		// do something
		System.out.println("The number is " + number);
	}
}

  每个断言都包含一个您认为在程序执行时为布尔表达式

  • 若不为真将出现 AssertionError
  • 出现 AssertionError ,意味着内部某些假设被违反了
  • 增强程序员对代码质量的信心:对代码所做的假设都保持正确

  assert 具有以下两种方式之一:

  • assert condition
  • assert condition : messagemessage 可以描述错误原因、记录案发现场

  有时候为了安全性,防止错误对后续程序造成影响,release 版本使用 assert,但一般不使用。

2. assert 适用范围

   assert 可以使用在各种不变量中:

  • Internal Invariants 内部不变量
  • Rep Invariants 表示不变量checkRep()
  • Control Flow Invariants 控制流不变量
  • Pre conditions of methods 方法的前置条件
  • Post conditions of methods 方法的后置条件,如:
public double sqrt(double x) {
	assert x >= 0;
	double r;
	... // compute result r
	assert Math.abs(r*r - x) < .0001;
	return r;
}
  • Control-flow 控制流,如:不使用不分流;判断是否执行到不该执行的代码之后
switch (vowel) {
	case 'a':
	case 'e':
	case 'i':
	case 'o':
	case 'u': return "A";
	default: assert false ;
}
void foo() {
	for (...) {
		if (...)
			return;
	}
	assert false;
	// Execution should never reach this point!
}

  其实等价于使用语句如:

default: throw new AssertionError ("must be a vowel, but was: " + vowel);

  assert 还可以判断:

  • 指针是否为空
  • 传入方法的数组或其他容器至少可以包含 X 个数据元素
  • 一个表被初始化为实数值
  • 当方法开始执行(或完成)时,容器是空的(或满的)
  • 从一个高度优化的、复杂的方法得到的结果与从一个较慢但清晰编写的例程得到的结果相匹配

  实际上可以对程序执行时的某些条件进行验证。


  实际上, assert

  • 断言主要用于开发阶段,避免引入和帮助发现 bug
  • 实际运行阶段,不再使用断言,运行阶段对你编写的程序不可以使用 assert 以保证健壮性。但可以使用异常
  • 避免降低性能
  • 使用断言的主要目的是为了在开发阶段调试程序、尽快避免错误

  因此,程序之外的事,不受你控制,不要乱断言,注意:

  • 如:文件/网络/用户输入等不要检测。这是因为检测了也没办法
  • 断言只是检查程序的内部状态是否符合规约
  • 断言一旦 false ,程序就停止执行
  • 你的代码无法保证不出现此类外部错误 ,外部错误要使用 Exception 机制去处理。而 assert 一般检测程序内部的错误(开发阶段)。

  正常情况下,Java 缺省关闭断言,要使用 assert 时要记得打开 (-ea)。
  默认情况下, assert 功能是关闭的,这是因为

  • 断言不应该出现在工程的 release 版本。client 想要挽救 bug 而不是中止程序。
  • 断言非常影响运行时的性能

  开启 assert 需要使用相应的命令或者打开相应的按钮

3. 断言使用的指导

  assertException 的区别

  • 断言的目标是保证正确性,使用在开发阶段,针对程序员。使用断言处理“绝不应该发生”的情况
  • 错误/异常处理的目标是保证健壮性,使用在发布阶段,针对用户端。使用异常来处理你“预料到可以发生”的不正常情况。

  对于 pre-/post-condition ,是否使用 assert 有争议(前者通常用 Exception, 后者通常用 assert)。争议的理由有以下几点

  • 不管是否 -easpec 中的 pre-/post-conditions 都能够被保证
  • 即使 spec 被违反,也不应通过 assert 直接 fail ,而是应抛出具体的 RunTimeException

  我们使用如下原则:
  如果参数来自于外部(不受自己控制,如 public 方法的参数),使用异常处理;如果来自于自己所写的其他代码,可以使用断言来帮助发现错误(例如 post condition 就需要;如 private 方法)。

/**
* Sets the refresh rate.
*
* @param rate refresh rate, in frames per second.
* @throws
IllegalArgumentException if rate <= 0 or
* rate > MAX_REFRESH_RATE.
*/
public void setRefreshRate(int rate) {
	// Enforce specified precondition in public method
	if (rate <= 0 || rate > MAX_REFRESH_RATE)
		throw new IllegalArgumentException ("Illegal rate: " + rate);
	setRefreshInterval(1000/rate);
}
/**
* Sets the refresh interval (which must correspond to a legal rate).
*
* @param interval refresh interval in milliseconds.
*/
private void setRefreshInterval(int interval) {
	// Confirm adherence to precondition in nonpublic method
	assert interval > 0 && interval <= 1000/MAX_REFRESH_RATE :
	... // Set the refresh interval
}
/**
* Returns a
BigInteger whose value is (this 1 mod m).
*
* @param m the modulus.
* @return this -1 mod m.
* @throws ArithmeticException m <= 0, or this BigInteger
* has no multiplicative inverse mod m (that is, this BigInteger
* is not relatively prime to m).
*/
public BigInteger modInverse(BigInteger m) {
	if (m.signum <= 0)
		throw new ArithmeticException ("Modulus not positive: " + m);
	... // Do the computation
	assert this.multiply.(result).mod(m).equals(ONE) : this;
	return result;
}

  异常能做到断言能做的事。断言异常处理都可以处理同样的错误。但由于断言可以容易地开启关闭,故断言的使用仍然广泛。
  开发阶段断言尽可能消除 bugs发行版本里用异常处理机制处理漏掉的错误。

二. 防御性编程及工具

1. 阻止程序的非法输入

  正确性使得程序员只需对正确数据有正确反应,错误数据可以随意处理。但是从健壮性角度,仍然需对错误数据做出良好的反应,如无反应、提示错误信息或不允许输入错误数据。这时规约应该添加上对错误的处理方法
  对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等。

2. 设置路障

  不安全的方法(输入可能不合法的方法)通过检测手段检测数据输入是否有错误。路障可以使不正确的输入被过滤掉,使得不安全的方法变成安全的方法
  可以使用 publicprivate 方法判定,前者为不安全的方法,后者可能是安全的方法。
在这里插入图片描述
  类的 public 方法接收到的外部数据都应被认为是 dirty 的,需要处理干净再传递到private 方法——隔离舱方法、操作间技术。
  “隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用断言
  这种方法是代理模式Proxy 设计模式)。代理类可以做数据校验。

三. SpotBugs 工具

  SpotBugsJava 静态代码分析工具。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值