本周,我想解决一个有趣的方法,这种方法我很少见过,但是非常有用。
按合同设计,也称为合同编程,按合同编程和按合同设计编程,是一种设计软件的方法。 它规定,软件设计人员应为软件组件定义形式,精确和可验证的接口规范,该规范应使用前提条件,后置条件和不变式来扩展抽象数据类型的普通定义。 根据对商业合同的条件和义务的概念隐喻,这些规范被称为“合同”。
https://zh.wikipedia.org/wiki/Design_by_contract
本质上,条件允许快速失败。 如果最后由于错误的假设而使计算失败,则运行代码是没有用的。
让我们以两个银行帐户之间的转帐操作为例。 以下是一些条件:
-
前提条件
-
- 转账金额必须为正
不变量
-
- 来源银行帐户必须有正余额
转移后
-
- 来源银行帐户余额必须等于初始余额减去转帐金额
- 目标银行账户余额必须等于初始余额加上转账金额
天真的实现
一个人可以轻松地“手动”实现前置条件和后置条件:
publicvoidtransfer(Accountsource,Accounttarget,BigDecimalamount){
if(amount.compareTo(BigDecimal.ZERO)<=0){
thrownewIllegalArgument("Amount transferred must be higher than zero ("+amount+")";
}
if(source.getBalance().compareTo(BigDecimal.ZERO)<=0){
thrownewIllegalArgument("Source account balance must be higher than zero ("+source.getBalance()+")";
}
source.transfer(target,amount);
if(source.getBalance().compareTo(BigDecimal.ZERO)<=0){
thrownewIllegalState("Source account balance must be higher than zero ("+source.getBalance()+")";
}
// Other post-conditions...
}
编写起来很麻烦,而且很难阅读。
检查不变量转换为同时检查前置条件和后置条件
Java实现
您可能已经通过assert
关键字熟悉前置条件和后置条件:
publicvoidtransfer(Accountsource,Accounttarget,BigDecimalamount){
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> T requireNonNull(T obj)
-
public static <T> T requireNonNull(T obj, String message)
-
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier)
最后一个方法中的Supplier参数返回错误消息
如果obj
为null,则所有3个方法都将抛出NullPointerException
。 更有趣的是,如果不是,它们都会返回obj
。 这导致了这种代码:
publicvoidtransfer(Accountsource,Accounttarget,BigDecimalamount){
if(requireNonNull(amount).compareTo(BigDecimal.ZERO)<=0){
thrownewIllegalArgument("Amount transferred must be higher than zero ("+amount+")";
}
if(requireNonNull(source).getBalance().compareTo(BigDecimal.ZERO)<=0){
thrownewIllegalArgument("Source account balance must be higher than zero ("+source.getBalance()+")";
}
source.transfer(target,amount);
if(requireNonNull(source).getBalance().compareTo(BigDecimal.ZERO)<=0){
thrownewIllegalState("Source account balance must be higher than zero ("+source.getBalance()+")";
}
// Other post-conditions...
}
这不仅非常有限,而且并不能真正提高可读性,特别是如果您添加错误消息参数。
特定于框架的实现
Spring框架提供了Assert
类,该类提供了许多条件检查方法。
根据我们自己的幼稚实现,如果不满足条件,则先决条件检查将引发IllegalArgumentException
,而后继条件检查将引发IllegalStateException
。
上面的Wikipedia页面还列出了一些专门用于按合同编程的框架:
以上大多数框架都是基于注释的。
注释的优缺点
让我们从pro开始:注释使条件显而易见。
另一方面,它们遭受一些缺点:
- 他们需要在编译时或运行时进行字节码操作
- 他们要么:
- 它们的范围非常有限( 例如
@Email
) - 或委托给外部语言,该语言被配置为注释字符串属性,与typesafe相反
- 它们的范围非常有限( 例如
Kotlin的方法
Kotlin的合同编程方法基于简单的方法调用,这些方法分组在Preconditions.kt
文件中:
-
require
类型方法实现前提条件,如果不满足则抛出IllegalArgumentException
-
check
类型方法实现后置条件,如果不满足则抛出IllegalStateException
用Kotlin重写上面的代码片段非常简单:
funtransfer(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...
}
列出的valid4J项目具有相同的方法。
结论
通常(总是?)情况越简单越好。 通过仅将检查和异常抛出指令包装到一种方法中,就可以轻松使用合同概念进行编程。 虽然在Java中没有可用的此类包装器,但valid4j和Kotlin提供了它们。