资讯专栏INFORMATION COLUMN

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

whatsns / 2197人阅读

摘要:前言这周我准备介绍一个有趣的但是很少使用的方法按照合约编程,又称为合约编程,是一种软件设计的方法。这些规则被称为合约,可以比拟为商业合同中的条件和义务。通过将检查和异常抛出指令包装到方法中,人们可以很容易地实现合约式编程。

前言

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

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


想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68749.html

相关文章

  • 头鹰深夜翻译:JDK Vs. JRE Vs. JVM之间区别

    摘要:什么是为执行字节码提供一个运行环境。它的实现主要包含三个部分,描述实现规格的文档,具体实现和满足要求的计算机程序以及实例具体执行字节码。该类先被转化为一组字节码并放入文件中。字节码校验器通过字节码校验器检查格式并找出非法代码。 什么是Java Development Kit (JDK)? JDK通常用来开发Java应用和插件。基本上可以认为是一个软件开发环境。JDK包含Java Run...

    blair 评论0 收藏0
  • 头鹰深夜翻译:理解javaclassloader

    摘要:它们是通过来自远程的服务器的连接发送字节码并在本地运行,这一点令人兴奋。中有一个自定义的,它不是从本地文件系统加载类文件,而是从远程服务器上获取,通过加载原始字节码,再在中转化为类。它将字节码解析为运行时的数据结构,检查其有效性等。 前言 Java ClassLoader是java运行系统中一个至关重要但是经常被忽略的组件。它负责在运行时寻找并加载类文件。创建自定义的ClassLoad...

    Eminjannn 评论0 收藏0
  • 头鹰深夜翻译:JAVA中异常处理最佳实践

    摘要:无需检查的异常也是的子类。从低层抛出的需检查异常强制要求调用方捕获或是抛出该异常。当前执行的线程将会停止并报告该异常。单元测试允许我在使用中查看异常,并且作为一个可以被执行的文档来使用。不要捕获最高层异常继承的异常同样是的子类。 前言 异常处理的问题之一是知道何时以及如何去使用它。我会讨论一些异常处理的最佳实践,也会总结最近在异常处理上的一些争论。 作为程序员,我们想要写高质量的能够解...

    W_BinaryTree 评论0 收藏0
  • 头鹰深夜翻译:Volatile原子性, 可见性和有序性

    摘要:有可能一个线程中的动作相对于另一个线程出现乱序。当实际输出取决于线程交错的结果时,这种情况被称为竞争条件。这里的问题在于代码块不是原子性的,而且实例的变化对别的线程不可见。这种不能同时在多个线程上执行的部分被称为关键部分。 为什么要额外写一篇文章来研究volatile呢?是因为这可能是并发中最令人困惑以及最被误解的结构。我看过不少解释volatile的博客,但是大多数要么不完整,要么难...

    Lionad-Morotar 评论0 收藏0
  • 头鹰深夜翻译:核心JAVA并发(一)

    摘要:简介从创建以来,就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的开发人员理解核心的并发概念以及如何使用它们。请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。 简介 从创建以来,JAVA就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的JAVA开发人员理解核心的并发概念以及如何使用它们。 (博主将在其中加上自己的理解以及自己想出的例子作为补充) 概念 ...

    Richard_Gao 评论0 收藏0

发表评论

0条评论

whatsns

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<