前言
这周我准备介绍一个有趣的但是很少使用的方法
按照合约编程,又称为合约编程,是一种软件设计的方法。它规定了软件设计师应该为软件组件定义正式,精确和可验证的接口规范,将常规的抽象数据类型扩展为前置条件,后置条件和不变量。这些规则被称为合约,可以比拟为商业合同中的条件和义务。
— Wikipedia
https://en.wikipedia.org/wiki...
本质上它使得计算尽快的因为错误而失败。如果从假设条件开始就不满足,那么没有必要继续运行代码。
让我们使用两个银行之间的转账操作作为例子说明。以下是一些条件:
前置条件:
- 转账的数额必须大于0
不变量:
- 转出的银行账号的余额必须为正
转账之后:
- 源银行账户余额必须等于初始余额减去转账金额
- 目标银行账户余额必须等于初始余额加转移金额
简单的实现
可以手动实现前置条件后置条件:
public void transfer(Account source, Account target, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Amount transferred must be higher than zero (" + amount + ")";
}
if (source.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
source.transfer(target, amount);
if (source.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalState("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
// Other post-conditions...
}
写起来非常麻烦,而且很难阅读。
检查不变式翻译为既检查前提条件又检查后置条件
Java语言实现
你可能已经通过assert关键字熟悉了前置条件和后置条件:
public void transfer(Account source, Account target, BigDecimal amount) {
assert (amount.compareTo(BigDecimal.ZERO) <= 0);
assert (source.getBalance().compareTo(BigDecimal.ZERO) <= 0);
source.transfer(target, amount);
assert (source.getBalance().compareTo(BigDecimal.ZERO) <= 0);
// Other post-conditions...
}
Java语言实现有几个问题:
- 前置条件和后置条件没有区别
- 需要使用
-ea
标记启动
Oracle的文档明确说明:
虽然assert构造不是一个完整的合约编程工具,但它可以帮助支持非正式的按照合约设计的编程风格。
其它的Java语言实现
自从Java 8之后,Objects
类的三个方法提供了对合约式编程的部分支持:
- public static
T requireNonNull(T obj) - public static
T requireNonNull(T obj, String message) - public static
T requireNonNull(T obj, Supplier messageSupplier)
最后一个方法中的
Supplier
参数返回错误信息
所有的3个方法都会在obj为null的时候抛出NullPointerException
。更有意思的是,他们都会在obj不是null的时候返回该对象。从而导致了以下风格的代码:
public void transfer(Account source, Account target, BigDecimal amount) {
if (requireNonNull(amount).compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Amount transferred must be higher than zero (" + amount + ")";
}
if (requireNonNull(source).getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
source.transfer(target, amount);
if (requireNonNull(source).getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalState("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
// Other post-conditions...
}
不仅功能有限,而且并不能真正提高可读性,特别是如果添加错误消息参数的时候。
特定框架的实现
Spring框架提供了Assert
类并支持大量的条件验证方法。
根据我们自己简单的实现,前置条件不符合会抛出IllegalArgumentException
,而后置条件不符合会抛出IllegalStateException
。
维基百科页面还列出了几个专用于按合同进行编程的框架:
- OVal
- Contracts for Java
- Java Modeling Language
- Bean Validation
- valid4j
上面的框架大多数基于注解。
注解的优点和缺点
让我们从优点开始:注释使条件更加明显。
而另一方面,它们也有以下缺陷:
- 它们需要在编译时或运行时进行字节码操作
-
它们要么:
- 范围有限(比如
@email
) - 或者委托给一个外部的语言,该语言被配置为注释字符串属性,违背了类型安全
- 范围有限(比如
Kotlin的方法
Kotlin的合约编程基于简单的方法调用,位于Preconditions.kt
文件中
-
require
类型的方法会判断前置条件并且在不符合时抛出IllegalArgumentException
-
type
类型的方法会判断后置条件并且在不符合时抛出IllegalStateException
使用Kotlin重写后的方法如下:
fun transfer(source: Account, target: Account, amount: BigDecimal) {
require(amount <= BigDecimal.ZERO)
require(source.getBalance() <= BigDecimal.ZERO)
source.transfer(target, amount);
check(source.getBalance() <= BigDecimal.ZERO)
// Other post-conditions...
}
总结
在通常情况下,越简单越好。通过将检查和异常抛出指令包装到方法中,人们可以很容易地实现合约式编程。尽管在Java中没有这种即拆即用的封装,valid4j和Kotlin都提供了这种实现。