Статьи

Java 8 пятница: большинство внутренних DSL устарели

В Data Geekery мы любим Java. И так как мы действительно входим в свободный API jOOQ и запросы DSL , мы абсолютно взволнованы тем, что Java 8 принесет в нашу экосистему.

Ява 8 Пятница

Каждую пятницу мы показываем вам пару замечательных новых функций Java 8 в виде учебника, в которых используются лямбда-выражения, методы расширения и другие замечательные вещи. Вы найдете исходный код на GitHub .

Большинство внутренних DSL устарели

Это довольно убедительное заявление одного из самых продвинутых внутренних DSL на рынке . Позволь мне объяснить:

Языки сложны

Изучать новый язык (или API) сложно. Вы должны понимать все ключевые слова, конструкции, операторы и типы выражений и т. Д. Это верно как для внешних DSL, так и для внутренних DSL и «обычных» API, которые, по сути, являются внутренними DSL с меньшей беглостью.

При использовании JUnit люди привыкли использовать спички подколенного сухожилия . Тот факт, что они доступны на шести языках (Java, Python, Ruby, Objective-C, PHP, Erlang) делает их в некотором смысле разумным выбором. В качестве предметно-ориентированного языка они создали идиомы, которые легко читать, например,

1
2
3
assertThat(theBiscuit, equalTo(myBiscuit));
assertThat(theBiscuit, is(equalTo(myBiscuit)));
assertThat(theBiscuit, is(myBiscuit));

Когда вы прочитаете этот код, вы сразу «поймете», что утверждается, потому что API выглядит как проза. Но научиться писать код в этом API сложнее. Вы должны будете понять:

  • Откуда берутся все эти методы
  • Какие существуют методы
  • Кто мог бы расширить подголовник с помощью пользовательских Matchers
  • Каковы лучшие практики при расширении DSL

Например, в приведенном выше примере, в чем именно разница между тремя? Когда я должен использовать один, а когда другой? Проверяет ли is() идентичность объекта? equalTo() проверяет равенство объектов?

Урок Hamcrest продолжается такими примерами:

1
2
3
public void testSquareRootOfMinusOneIsNotANumber() {
    assertThat(Math.sqrt(-1), is(notANumber()));
}

Вы можете видеть, что notANumber() видимому, является пользовательским сопоставителем, реализованным в некотором месте в утилите:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class IsNotANumber
extends TypeSafeMatcher<Double> {
 
  @Override
  public boolean matchesSafely(Double number) {
    return number.isNaN();
  }
 
  public void describeTo(Description description) {
    description.appendText("not a number");
  }
 
  @Factory
  public static <T> Matcher<Double> notANumber() {
    return new IsNotANumber();
  }
}

Несмотря на то, что этот вид DSL очень прост в создании и, возможно, немного забавен, начинать копировать и улучшать пользовательские DSL опасно по простой причине. Они ничем не лучше, чем их универсальные, функциональные аналоги, но их сложнее поддерживать. Рассмотрим приведенные выше примеры в Java 8:

Замена DSL с функциями

Предположим, у нас очень простой API тестирования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static <T> void assertThat(
    T actual,
    Predicate<T> expected
) {
    assertThat(actual, expected, "Test failed");
}
 
static <T> void assertThat(
    T actual,
    Predicate<T> expected,
    String message
) {
    assertThat(() -> actual, expected, message);
}
 
static <T> void assertThat(
    Supplier<T> actual,
    Predicate<T> expected
) {
    assertThat(actual, expected, "Test failed");
}
 
static <T> void assertThat(
    Supplier<T> actual,
    Predicate<T> expected,
    String message
) {
    if (!expected.test(actual.get()))
        throw new AssertionError(message);
}

Теперь сравните выражения соответствия Hamcrest с их функциональными эквивалентами:

01
02
03
04
05
06
07
08
09
10
11
12
// BEFORE
// ---------------------------------------------
assertThat(theBiscuit, equalTo(myBiscuit));
assertThat(theBiscuit, is(equalTo(myBiscuit)));
assertThat(theBiscuit, is(myBiscuit));
 
assertThat(Math.sqrt(-1), is(notANumber()));
 
// AFTER
// ---------------------------------------------
assertThat(theBiscuit, b -> b == myBiscuit);
assertThat(Math.sqrt(-1), n -> Double.isNaN(n));

С лямбда-выражениями и хорошо разработанным API assertThat() я уверен, что вы больше не будете искать правильный способ выражения своих утверждений с помощью сопоставлений.

Обратите внимание, что, к сожалению, мы не можем использовать Double::isNaN метод Double::isNaN , так как это не совместимо с Predicate<Double> . Для этого нам нужно было бы применить магию примитивных типов в API утверждений, например

1
2
3
4
static void assertThat(
    double actual,
    DoublePredicate expected
) { ... }

Который затем можно использовать как таковой:

1
assertThat(Math.sqrt(-1), Double::isNaN);

Да, но…

… Вы можете услышать, как вы говорите: «но мы можем сочетать спички с лямбдами и ручьями». Да, конечно, мы можем. Я только что сделал это сейчас в интеграционных тестах jOOQ . Я хочу пропустить интеграционные тесты для всех диалектов SQL, которых нет в списке диалектов, предоставляемых в качестве системного свойства:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
String dialectString =
    System.getProperty("org.jooq.test-dialects");
 
// The string must not be "empty"
assumeThat(dialectString, not(isOneOf("", null)));
 
// And we check if the current dialect() is
// contained in a comma or semi-colon separated
// list of allowed dialects, trimmed and lowercased
assumeThat(
    dialect().name().toLowerCase(),
 
    // Another matcher here
    isOneOf(stream(dialectString.split("[,;]"))
        .map(String::trim)
        .map(String::toLowerCase)
        .toArray(String[]::new))
);

… и это тоже здорово, правда?

Но почему бы мне просто не написать:

1
2
3
4
5
6
7
8
9
// Using Apache Commons, here
assumeThat(dialectString, StringUtils::isNotEmpty);
assumeThat(
    dialect().name().toLowerCase(),
    d -> stream(dialectString.split("[,;]"))
        .map(String::trim)
        .map(String::toLowerCase())
        .anyMatch(d::equals)
);

Хамкрест не нужен, просто старые лямбды и ручьи!

Конечно, читаемость — это дело вкуса. Но приведенный выше пример ясно показывает, что больше нет необходимости в устройствах сравнения Hamcrest и в DSL Hamcrest. Учитывая, что в течение следующих 2-3 лет большинство всех разработчиков Java будут очень привыкать к использованию Streams API в повседневной работе, но не очень привыкли к использованию Hamcrest API, я призываю вас, сопровождающих JUnit, отказаться от использование Hamcrest в пользу API Java 8.

Хамкрест теперь считается плохим?

Что ж, в прошлом оно служило своей цели, и люди к этому привыкли. Но, как мы уже указывали в предыдущем посте о сопоставлении исключений Java 8 и JUnit , да, мы верим, что мы, Java, за последние 10 лет поряли неправильное дерево.

Отсутствие лямбда-выражений привело к появлению множества полностью раздутых, а теперь и немного бесполезных библиотек . Многие внутренние DSL или маги-аннотации также затрагиваются. Не потому, что они больше не решают проблемы, к которым они привыкли, а потому, что они не готовы к Java-8. Тип Matcher Hamcrest не является функциональным интерфейсом, хотя было бы довольно легко преобразовать его в один. Фактически, логика CustomMatcher CustomMatcher должна быть перенесена в интерфейс Matcher, в методы по умолчанию.

Вещи не улучшаются с альтернативами, такими как AssertJ , которые создают альтернативный DSL, который теперь представляется устаревшим (с точки зрения многословия кода сайта вызова) через лямбды и Streams API.

Если вы настаиваете на использовании DSL для тестирования, тогда, вероятно, Спок был бы гораздо лучшим выбором.

Другие примеры

Hamcrest — только один пример такого DSL. В этой статье показано, как его можно почти полностью удалить из стека с помощью стандартных конструкций JDK 8 и нескольких служебных методов, которые, возможно, появятся в JUnit в ближайшее время.

Java 8 принесет много нового в дискуссии о DSL в последнее десятилетие, а также Streams API значительно улучшит наш взгляд на преобразование или построение данных. Но многие современные DSL не готовы к Java 8 и не были разработаны функционально. У них слишком много ключевых слов для вещей и понятий, которые трудно выучить, и это было бы лучше смоделировать с помощью функций.

Исключением из этого правила являются DSL, такие как jOOQ или jRTF , которые моделируют фактические ранее существующие внешние DSL в формате 1: 1, наследуя все существующие ключевые слова и элементы синтаксиса, что в первую очередь облегчает их изучение.

Что вы берете?

Как вы относитесь к вышеизложенным предположениям? Какой ваш любимый внутренний DSL, который может исчезнуть или полностью трансформироваться в течение следующих пяти лет, потому что он устарел в Java 8?

Ссылка: Java 8, пятница: большинство внутренних DSL устарели от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .