Pull to refresh

Программирование согласно контракту на JVM

Reading time4 min
Views7K
Привет, Хабр! Представляю вашему вниманию перевод статьи "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> 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 метода бросают 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, который предлагает множество методов проверки состояния:



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

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


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

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


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

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

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

Kotlin-подход


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



  • 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 предлагают их.

Спасибо за внимание, до новых встреч!
Tags:
Hubs:
+15
Comments8

Articles

Change theme settings