通过合约在JVM上进行编程

本周,我想解决一个有趣的方法,这种方法我很少见过,但是非常有用。

按合同设计,也称为合同编程,按合同编程和按合同设计编程,是一种设计软件的方法。 它规定,软件设计人员应为软件组件定义形式,精确和可验证的接口规范,该规范应使用前提条件,后置条件和不变式来扩展抽象数据类型的普通定义。 根据对商业合同的条件和义务的概念隐喻,这些规范被称为“合同”。
—维基百科
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方法存在几个问题:

  1. 前置条件和后置条件之间没有区别
  2. 必须通过-ea启动标志将其激活

Oracle 文档明确声明:

虽然assert结构不是成熟的按合同设计工具,但是它可以帮助支持非正式的按合同设计样式的编程。

替代Java实现

从Java 8开始, Objects类提供了三种方法,它们通过合同方法提供了一些非常有限的编程:

  1. public static <T> T requireNonNull(T obj)
  2. public static <T> T requireNonNull(T obj, String message)
  3. 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类,该类提供了许多条件检查方法。

春天断言
图1. Spring断言

根据我们自己的幼稚实现,如果不满足条件,则先决条件检查将引发IllegalArgumentException ,而后继条件检查将引发IllegalStateException

上面的Wikipedia页面还列出了一些专门用于按合同编程的框架:

以上大多数框架都是基于注释的。

注释的优缺点

让我们从pro开始:注释使条件显而易见。

另一方面,它们遭受一些缺点:

  • 他们需要在编译时或运行时进行字节码操作
  • 他们要么:
    • 它们的范围非常有限( 例如 @Email
    • 或委托给外部语言,该语言被配置为注释字符串属性,与typesafe相反

Kotlin的方法

Kotlin的合同编程方法基于简单的方法调用,这些方法分组在Preconditions.kt文件中:

前提条件
图2. 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提供了它们。

翻译自: https://blog.frankel.ch/programming-by-contract-jvm/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值