Как я могу наложить проверяющие ограничения на входные параметры метода?
Вот типичный способ достижения этой цели:
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 ответов:
Если вы ищете полноценный механизм проектирования по контракту, я бы взглянул на некоторые из проектов, перечисленных на странице Википедии для 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)