Как я могу наложить проверяющие ограничения на входные параметры метода?


Вот типичный способ достижения этой цели:

public void myContractualMethod(final String x, final Set<String> y) {
    if ((x == null) || (x.isEmpty())) {
        throw new IllegalArgumentException("x cannot be null or empty");
    }
    if (y == null) {
        throw new IllegalArgumentException("y cannot be null");
    }
    // Now I can actually start writing purposeful 
    //    code to accomplish the goal of this method

Я думаю, что это решение уродливо. Ваши методы быстро заполняются шаблонным кодом, проверяющим допустимые входные параметры контракта, скрывая сердце метода.

Вот что я хотел бы иметь:

public void myContractualMethod(@NotNull @NotEmpty final String x, @NotNull final Set<String> y) {
    // Now I have a clean method body that isn't obscured by
    //    contract checking

Если эти аннотации выглядят как JSR 303/Bean Validation Spec, то это потому, что я их позаимствовал. К сожалению, они, кажется, не работают таким образом; они предназначены для аннотирования переменных экземпляра, то запуск объекта через валидатор.

Какие из многих Java-фреймворков проектирования по контракту обеспечивают наиболее близкую функциональность к моему примеру" like to have"? Исключения, которые выбрасываются, должны быть исключениями времени выполнения (например, IllegalArgumentExceptions), чтобы инкапсуляция не нарушалась.

8 5

8 ответов:

Если вы ищете полноценный механизм проектирования по контракту, я бы взглянул на некоторые из проектов, перечисленных на странице Википедии для DBC.

Если вы ищете что-то более простое, вы можете посмотреть на класс Precediatures из коллекций google, который предоставляет метод checkNotNull (). Таким образом, вы можете переписать код, который вы отправили:

public void myContractualMethod(final String x, final Set<String> y) {
    checkNotNull(x);
    checkArgument(!x.isEmpty());
    checkNotNull(y);
}

Я видел технику Эрика Берка, которая примерно похожа на следующую. Это элегантное использование статического импорта. Код читается очень красиво.

Чтобы получить представление, вот класс Contract. Она минимальна здесь, но может быть легко заполнена по мере необходимости.
package net.codetojoy;

public class Contract {
    public static void isNotNull(Object obj) {
        if (obj == null) throw new IllegalArgumentException("illegal null");
    }
    public static void isNotEmpty(String s) {
        if (s.isEmpty()) throw new IllegalArgumentException("illegal empty string");
    }
}

И вот пример использования. Метод foo() иллюстрирует статический импорт:

package net.codetojoy;

import static net.codetojoy.Contract.*;

public class Example {
    public void foo(String str) {
        isNotNull(str);
        isNotEmpty(str);
        System.out.println("this is the string: " + str);
    }

    public static void main(String[] args) {
        Example ex = new Example();
        ex.foo("");
    }
}

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

Существует небольшой пакет проверки аргументов Java, реализованный как обычная Java. Он поставляется с несколькими стандартными проверками / проверками. И для тех случаев, когда кому-то нужны его собственные более конкретные проверки, он поставляется с некоторыми вспомогательными методами. Для проверок, которые происходят несколько раз, просто расширьте интерфейс ArgumentValidation, с вашим собственным и создайте реализующий класс, который расширяется от класса ArgumentValidationImpl.

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

if (x.isEmpty()) {
    throw new IllegalArgumentException("x cannot be empty");
}

И полагаться на Java, чтобы бросить NullPointerException, если x является null. Вам просто нужно изменить свой "контракт", чтобы сказать, что NPE бросается для определенных типов ситуаций" вы позвонили мне с незаконными параметрами".

Джаред указал вам на различные фреймворки, которые добавляют поддержку DBC в Java.
То, что я нашел, работает лучше всего: просто документируйте свой контракт в JavaDoc (или любой другой Documentationframework, который вы используете; Doxygen поддерживает теги DBC.)
Если ваш код запутан множеством бросков и проверок ваших аргументов, это не очень полезно для вашего читателя. Документация есть.

Я бы использовал аннотации параметров, отражение и универсальный класс валидатора, чтобы создать средство для всего приложения. например, вы можете закодировать метод класса, например:

.. myMethod( @notNull строка x, @notNullorZero строка y){

if (Validator.ifNotContractual(getParamDetails()) {
    raiseException..
    or 
    return ..
}

}

Методы класса "помечены", чтобы аннотировать свои требования к контракту. Используйте рефлексию для автоматического обнаружения параметров, их значений и аннотаций. Отправьте все это в статический класс для проверки и сообщите вам результат.

Не полностью рабочее решение, но JSR-303 предлагает расширение проверки на уровне метода . Поскольку это всего лишь предложение расширения, реализации JSR-303 могут его игнорировать. Найти реализацию немного сложнее. Я не думаю, что Hibernate Validator поддерживает его, но я считаю, чтоagimatec-validation имеет экспериментальную поддержку. Я не использовал ни то, ни другое для этой цели, поэтому я не знаю, насколько хорошо они работают. Мне было бы интересно узнать это. хотя, если кто-то даст ему ход.

Если вы используете Java 8, лямбды можно использовать для создания очень элегантного и читаемого решения для проверки:

public class Assert {

    public interface CheckArgument<O> {
        boolean test(O object);
    }

    public static final <O> O that(O argument, CheckArgument<O> check) {
        if (!check.test(argument))
            throw new IllegalArgumentException("Illegal argument: " + argument);
        return argument;
    }
}

Вы используете его как:

public void setValue(int value) {
    this.value = Assert.that(value, arg -> arg >= 0);
}

Исключение будет выглядеть следующим образом:

Exception in thread "main" java.lang.IllegalArgumentException: Illegal argument: -7
    at com.xyz.util.Assert.that(Assert.java:13)
    at com.xyz.Main.main(Main.java:16)

Первая приятная вещь заключается в том, что приведенный выше класс Assert-это все, что действительно нужно:

public void setValue(String value) {
    this.value = Assert.that(value, arg -> arg != null && !arg.trim().isEmpty());
}

public void setValue(SomeObject value) {
    this.value = Assert.that(value, arg -> arg != null && arg.someTest());
}

Конечно that() может быть реализовано несколькими способами: с помощью форматной строки и аргументов, для вызова других видов исключений и т. д.

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

Не то, чтобы вы не можете предварительно протестировать пакет, если хотите:

public static CheckArgument<Object> isNotNull = arg -> arg != null;

Assert.that(x, Assert.isNotNull);

// with a static import:

Assert.that(x, isNotNull);
Я понятия не имею, плохо ли это для производительности или не очень хорошая идея по какой-то другой причине. (Я сам только начал смотреть на лямбды, но код, похоже, работает так, как должен...) Но мне нравится, что Assert можно сократить (нет необходимости в зависимостях, которые не могут быть решающими для проекта) и что тесты очень заметны.

Вот метод для лучшей ошибки сообщение:

public static final <O> O that(O argument, CheckArgument<O> check,
    String format, Object... objects) 
{
    if (!check.test(argument))
        throw new IllegalArgumentException(
            String.format(format, objects));
    return argument;
}

Вы называете это так:

public void setValue(String value) {
    this.value = Assert.that(value, 
        arg -> arg != null && arg.trim().isEmpty(), 
        "String value is empty or null: %s", value);
}

И выходит:

Exception in thread "main" java.lang.IllegalArgumentException: String value is empty or null: null
    at com.xyz.util.Assert.that(Assert.java:21)
    at com.xyz.Main.main(Main.java:16)

Update: Если вы хотите использовать конструкцию x = Assert... с предварительно упакованным тестом, результат будет приведен к типу, используемому в предварительно упакованном тесте. Поэтому он должен быть возвращен к типу переменной... SomeClass x = (SomeClass) Assert.that(x, isNotNull)