貓頭鷹的深夜翻譯:在JVM上根據合約編程

前言

這周我準備介紹一個有趣的可是不多使用的方法面試

按照合約編程,又稱爲合約編程,是一種軟件設計的方法。它規定了軟件設計師應該爲軟件組件定義正式,精確和可驗證的接口規範,將常規的抽象數據類型擴展爲前置條件,後置條件和不變量。這些規則被稱爲合約,能夠比擬爲商業合同中的條件和義務。
— Wikipedia
https://en.wikipedia.org/wiki...

本質上它使得計算儘快的由於錯誤而失敗。若是從假設條件開始就不知足,那麼沒有必要繼續運行代碼。編程

讓咱們使用兩個銀行之間的轉帳操做做爲例子說明。如下是一些條件:安全

前置條件:微信

  • 轉帳的數額必須大於0

不變量:框架

  • 轉出的銀行帳號的餘額必須爲正

轉帳以後:工具

  • 源銀行帳戶餘額必須等於初始餘額減去轉帳金額
  • 目標銀行帳戶餘額必須等於初始餘額加轉移金額

簡單的實現

能夠手動實現前置條件後置條件:post

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...
}

寫起來很是麻煩,並且很難閱讀。ui

檢查不變式翻譯爲既檢查前提條件又檢查後置條件

Java語言實現

你可能已經經過assert關鍵字熟悉了前置條件和後置條件:spa

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> 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參數返回錯誤信息

全部的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類並支持大量的條件驗證方法。

clipboard.png

根據咱們本身簡單的實現,前置條件不符合會拋出IllegalArgumentException,然後置條件不符合會拋出IllegalStateException

維基百科頁面還列出了幾個專用於按合同進行編程的框架:

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

上面的框架大多數基於註解。

註解的優勢和缺點

讓咱們從優勢開始:註釋使條件更加明顯。

而另外一方面,它們也有如下缺陷:

  • 它們須要在編譯時或運行時進行字節碼操做
  • 它們要麼:

    • 範圍有限(好比@email
    • 或者委託給一個外部的語言,該語言被配置爲註釋字符串屬性,違背了類型安全

Kotlin的方法

Kotlin的合約編程基於簡單的方法調用,位於Preconditions.kt文件中

clipboard.png

  • 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都提供了這種實現。

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

相關文章
相關標籤/搜索