猫头鹰的深夜翻译:在JVM上根据合约编程

前言

这周我准备介绍一个有趣的但是很少使用的方法

按照合约编程,又称为合约编程,是一种软件设计的方法。它规定了软件设计师应该为软件组件定义正式,精确和可验证的接口规范,将常规的抽象数据类型扩展为前置条件,后置条件和不变量。这些规则被称为合约,可以比拟为商业合同中的条件和义务。
— 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类的三个方法提供了对合约式编程的部分支持:

  1. public static T requireNonNull(T obj)
  2. public static T requireNonNull(T obj, String message)
  3. 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类并支持大量的条件验证方法。

猫头鹰的深夜翻译:在JVM上根据合约编程_第1张图片

根据我们自己简单的实现,前置条件不符合会抛出IllegalArgumentException,而后置条件不符合会抛出IllegalStateException

维基百科页面还列出了几个专用于按合同进行编程的框架:

  • OVal
  • Contracts for Java
  • Java Modeling Language
  • Bean Validation
  • valid4j

上面的框架大多数基于注解。

注解的优点和缺点

让我们从优点开始:注释使条件更加明显。

而另一方面,它们也有以下缺陷:

  • 它们需要在编译时或运行时进行字节码操作
  • 它们要么:

    • 范围有限(比如@email
    • 或者委托给一个外部的语言,该语言被配置为注释字符串属性,违背了类型安全

Kotlin的方法

Kotlin的合约编程基于简单的方法调用,位于Preconditions.kt文件中

猫头鹰的深夜翻译:在JVM上根据合约编程_第2张图片

  • 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都提供了这种实现。

猫头鹰的深夜翻译:在JVM上根据合约编程_第3张图片
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

你可能感兴趣的:(java,jvm,programming)