Одной из многих причин, почему мне нравится работать именно с функциональным программированием, является высокий уровень абстракции. Это связано с тем, что в конечном итоге мы имеем дело с более читаемым и лаконичным кодом, что, несомненно, способствует сближению с логикой предметной области.
В данной статье большее внимание уделяется на четыре вещи, представленные в Java 8, которые помогут вам овладеть новым уровнем абстракции.
1. Больше никаких циклов
Я уже говорил это ранее, и скажу снова. Попрощайтесь с циклами, и приветствуйте Stream API. Дни обсуждения Java о циклах элементов подходят к концу. Со Stream API в Java мы можем сказать что мы хотим получить, вместо того чтобы говорить как этого можно добиться.
Давайте рассмотрим следующий пример.
Мы имеем список статей, каждая из которых имеет свой список тегов. Теперь мы хотим получить первую статью, содержащую тег «Java».
Взглянем на стандартный подход.
public Article getFirstJavaArticle() {
for (Article article: articles) {
if (article.getTags().contains("Java")) {
return article;
}
}
return null;
}
Решим задачу, используя Stream API.
public Optional<Article> getFirstJavaArticle() {
return articles.stream()
.filter(article -> article.getTags().contains("Java"))
.findFirst();
}
Довольно круто, правда?
Сначала мы используем filter, чтобы найти все статьи, которые содержат тег Java, далее с помощью findFirst получаем первое включение.
Возникает вопрос: почему мы должны фильтровать весь список, если нам необходимо только первое включение? Так как потоки… ленивы и filter возвращает поток, вычисления происходят до тех пор, пока не будет найдено первое включение.
Я уже ранее посвятил статью о замене циклов на stream API. Прочтите его, если вам нужно больше примеров.
2. Избавьтесь от null-проверок
Можно заметить, что в предыдущем примере мы можем вернуть Optional<Article>.
Optional — это контейнер объекта, который может содержать или не содержать ненулевое значение.
Этот объект имеет некоторые функции высшего порядка, избавляющие от добавления повторяющихся if null/notNull проверок, что позволяет нам сфокусироваться на том, что мы хотим сделать.
Теперь усовершенствуем метод getFirstJavaArticle. Если не найдётся Java-статья, мы будем рады получению последней статьи.
Давайте рассмотрим, как выглядит типичное решение.
Article article = getFirstJavaArticle();
if (article == null) {
article = fetchLatestArticle();
}
А теперь решение с использованием Optional<T>.
getFirstJavaArticle()
.orElseGet(this::fetchLatestArticle);
Отлично выглядит, не правда ли?
Ни лишних переменных, ни if-конструкций, ни каких-либо упоминаний null. Мы просто используем Optional.orElseGet, чтобы сказать, что мы хотим получить, если не будет найдено значение.
Взглянем на другой пример использования Optional. Предположим, мы хотим получить название первой Java-статьи, если она будет найдена.
Опять же, используя типичное решение, нам пришлось бы добавлять null-проверку, но… знаете что? Optional здесь, чтобы спасти этот день.
playGround.getFirstJavaArticle()
.map(Article::getTitle);
Как вы можете видеть, Optional реализует функцию высшего порядка map, помогая нам применить функцию к результату, если он есть.
Для большей информации об Optional смотрите документацию.
3. Создайте свои функции высшего порядка
Как видите, Java 8 поставляется с уже готовым набором функций высшего порядка, и вы можете с ними творить чудеса. Но зачем останавливаться? И почему бы не создать свои собственные функции высшего порядка?
Единственное, что нужно, чтобы сделать функцию высшего порядка, — это взять один из функциональных интерфейсов Java, или интерфейса SAM-типа в качестве аргумента и / или возвращать одно из них.
Чтобы проиллюстрировать это, рассмотрим следующий сценарий.
У нас есть принтер, который может печатать различные виды документов. Перед печатью принтер должен прогреться, а после печати перейти в спящий режим.
Теперь мы хотим получить возможность посылать команды на принтер без заботы о его процедурах включения и выключения. Это может быть решено путём создания функции высшего порядка.
public void print(Consumer<Printer> toPrint) {
printer.prepare();
toPrint.accept(printer);
printer.sleep();
}
Как видите, мы используем Consumer<Printer>, являющийся одним из функциональных интерфейсов, в качестве аргумента. Затем мы выполняем данную функцию в качестве шага между процедурами запуска и отключения.
Теперь мы можем с легкостью использовать наш принтер, не заботясь ни о чём другом, кроме того, что мы хотим распечатать.
// Распечатать одну статью
printHandler.print(p -> p.print(oneArticle));
// Распечатать несколько статей
printHandler.print(p -> allArticles.forEach(p::print));
Для более подробного примера прочтите мою статью о том, как создать TransactionHandler.
4. Опасайтесь дублирования. Принцип DRY
Написание функций — дело легкое и быстрое. Тем не менее, с лёгким написанием кода приходит желание дублирования.
Рассмотрим следующий пример.
public Optional<Article> getFirstJavaArticle() {
return articles.stream()
.filter(article -> article.getTags().contains("Java"))
.findFirst();
}
Этот метод нам отлично послужил единожды, но он не является универсальным. Нам нужен метод, который будет в состоянии найти статьи, основанные на других тегах и требований в целом.
Весьма заманчивым является создавать новые потоки. Они настолько малы и их так просто можно сделать, как же это может навредить? Напротив, небольшие участки кода должны мотивировать принцип DRY дальше.
Давайте реорганизуем наш код. Для начала сделаем нашу функцию getFirstJavaArticle более универсальной — она будет принимать предикат в качестве аргумента, чтобы отфильтровать статьи в соответствии с тем, что нам нужно.
public Optional<Article> getFirst(Predicate<Article> predicate) {
return articles.stream()
.filter(predicate)
.findFirst();
}
Попробуем теперь воспользоваться этой функцией, чтобы получить несколько различных статей.
getFirst(article -> article.getTags().contains("Java"));
getFirst(article -> article.getTags().contains("Scala"));
getFirst(article -> article.getTitle().contains("Clojure"));
И, тем не менее, мы всё ещё имеем дело с повторяющимся кодом. Вы можете видеть, что используется один и тот же предикат для различных значений. Давайте попробуем удалить эти дублирования посредством интерфейса Function. Теперь наш код выглядит следующим образом.
Function<String, Predicate<Article>> basedOnTag = tag -> article -> article.getTags().contains(tag);
getFirst(basedOnTag.apply("Java"));
getFirst(basedOnTag.apply("Scala"));
Отлично! Я бы сказал, что этот код соответствует принципу DRY, не так ли?
Я надеюсь, что эта статья оказалась для вас полезной и придала несколько идей относительно того, что бы вы могли сделать на более высоком уровне абстракции с использованием Java 8 и функциональных особенностей.
Автор: kalterfive