В 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), делает их в некотором смысле разумным выбором. В качестве предметно-ориентированного языка они создали идиомы, которые легко читать, например,
assertThat(theBiscuit, equalTo(myBiscuit)); assertThat(theBiscuit, is(equalTo(myBiscuit))); assertThat(theBiscuit, is(myBiscuit));
Когда вы прочитаете этот код, вы сразу «поймете», что утверждается, потому что API выглядит как проза. Но научиться писать код в этом API сложнее. Вы должны будете понять:
- Откуда берутся все эти методы
- Какие существуют методы
- Кто мог бы расширить подголовник с помощью пользовательских Matchers
- Каковы лучшие практики при расширении DSL
Например, в приведенном выше примере, в чем именно разница между тремя? Когда я должен использовать один, а когда другой? is()
Проверяется ли идентичность объекта? equalTo()
Проверяется ли на равенство объектов?
Урок Hamcrest продолжается такими примерами:
publicvoidtestSquareRootOfMinusOneIsNotANumber() { assertThat(Math.sqrt(-1), is(notANumber())); }
Вы можете видеть, что, по- notANumber()
видимому, это пользовательское сопоставление, реализованное в какой-то части утилиты:
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 тестирования:
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 с их функциональными эквивалентами:
// 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));
С лямбда-выражениями и хорошо разработанным assertThat()
API я уверен, что вы больше не будете искать правильный способ выражать свои утверждения с помощью сопоставлений.
Обратите внимание, что, к сожалению, мы не можем использовать Double::isNaN
ссылку на метод, поскольку она не совместима с Predicate<Double>
. Для этого нам нужно было бы применить магию примитивных типов в API утверждений, например
static void assertThat( double actual, DoublePredicate expected ) { ... }
Который затем можно использовать как таковой:
assertThat(Math.sqrt(-1), Double::isNaN);
Да, но…
… Вы можете услышать, как вы говорите: «но мы можем сочетать спички с лямбдами и ручьями». Да, конечно, мы можем. Я только что сделал это сейчас в интеграционных тестах jOOQ . Я хочу пропустить интеграционные тесты для всех диалектов SQL, которых нет в списке диалектов, предоставляемых в качестве системного свойства:
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)) );
… и это тоже здорово, правда?
Но почему бы мне просто не написать:
// 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. Hamcrest в Сличитель тип не функциональный интерфейс, хотя это было бы очень легко превратить его в единое целое. Фактически, CustomMatcher
логика Hamcrest должна быть перенесена в интерфейс 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?