[Из песочницы] Программирование согласно контракту на JVM

Привет, Хабр! Представляю вашему вниманию перевод статьи «Programming by contract on the JVM» автора Nicolas Fränkel.

На этой неделе я хотел бы заняться интересным подходом, который я редко видел, но он является очень полезным.

Wikipedia
Дизайн по контракту, также известный как контрактное программирование, является подходом к разработке программного обеспечения. Он предписывает, чтобы разработчики программного обеспечения определяли формальные, точные и проверенные спецификации интерфейса для программных компонентов, которые расширяют обычное определение абстрактных типов данных с предусловиями, постусловиями и инвариантами. Эти спецификации называются «контрактами», в соответствии с концептуальной метафорой с условиями и обязательствами деловых контрактов.
Wikipedia


По сути, условия быстро перестают работать. Нет смысла запускать код, если в конце, вычисление завершится неудачно из-за неправильного предположения.

Давайте рассмотрим пример операции передачи между двумя банковскими счетами. Вот некоторые условия:

Пред-условия:

  • Передаваемая сумма должна быть положительной.


Константы:

  • Исходный банковский счет должен иметь положительный баланс.


Пост-условия:

  • Баланс счета исходного банка должен быть равен начальному балансу, за вычетом суммы перевода.
  • Баланс целевого банковского счета должен быть равен начальному балансу плюс переведенная сумма.


Реализация «вручную»


Легко реализовать пред- и пост-условия «вручную»:

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-подхода:

  1. Разница между пред- и пост-условиями отсутствует
  2. Код должен быть запущен при помощи флага запуска -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 метода бросают NullPointerException, если obj равно null.

Более интересно то, что они возвращают, если 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 Framework предоставляет класс Assert, который предлагает множество методов проверки состояния:

ijce1skwzxw6wqcavydzfkxfu9m.png

В соответствии с собственными реализациями, проверки пред-условия вызывают исключение IllegalArgumentException, если условие не выполняется, тогда как проверки после состояния бросают исключение IllegalStateException.

На странице Википедии выше также перечислены несколько фрэймворков, посвященных программированию по контракту:

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


Большинство из вышеперечисленных фрэймворков основаны на аннотациях.

Плюсы и минусы аннотаций


Начнем с плюсов: аннотации делают условия очевидными.

С другой стороны, аннотации не лишены недостатков:

  • Они требуют манипуляции с байт-кодом либо во время компиляции, либо во время выполнения
  • Они довольно ограничены по своему охвату (например, Email)
  • Переводят на внешний язык, который настроен как атрибут строки аннотации


Kotlin-подход


Программирование на Kotlin по контракту основано на простых вызовах метода, сгруппированных в файле Preconditions.kt:

w7pquvpeuse82t9seloavpedip4.png

  • require методы реализуют пред-условия, а если их нет, то будет брошено IllegalArgumentException
  • check методы реализуют пост-условия, а если их нет, то будет брошено 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 предлагают их.

Спасибо за внимание, до новых встреч!

© Habrahabr.ru