После знакомства с новой парадигмой параллелизма в Java в моей предыдущей статье Новая модель параллелизма в Java я решил, что было бы полезно показать более реалистичный и практичный пример с CompletableFuture.

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

Так как это будет выглядеть? Давайте посмотрим на следующую диаграмму:

Как вы можете видеть на диаграмме, наша служба социальных сетей будет вызывать эти три API для получения последних сообщений с этих трех платформ для нашего пользователя.
Это можно делать последовательно, вызывая каждый API в определенном порядке. Например, предположим, что у нас есть интерфейс PostFetcher, и у нас есть реализация этого интерфейса для каждой платформы социальных сетей, с которой мы хотим получать сообщения.

Интерфейс будет выглядеть так:

И мы будем вызывать эти API последовательно следующим образом:

Хотя это сработает (пользователи будут получать свои сообщения, если все пойдет хорошо), это очень неэффективно и потенциально проблематично для нашего сервиса. Это хороший пример неправильного использования CompletableFuture и того, как мы можем потерять все преимущества использования CompletableFutures. Посмотрим почему!

Прежде всего, мы вызываем присоединиться после получения сообщений с каждой платформы. Мы должны помнить, что соединение блокирует наш поток, ожидающий завершения CompletableFuture.
Кроме того, мы не объединяем CompletableFutures для их параллельного запуска и выполнения каких-либо действий по завершении, мы вызываем их по отдельности последовательным образом.
Итак, в основном мы ждем каждого вызова API для завершить и оставаться в бездействии, пока мы не получим ответ. Мы будем делать что-то вроде того, что показано на следующей диаграмме:

Давайте теперь представим, что первый вызов API, которым в нашем примере является Twitter API, отвечает самым медленным из трех вызовов, например, 12 секунд. За эти 12 секунд вместо вызова двух других API-интерфейсов, чтобы работа была выполнена к тому времени, когда мы получим ответ от Twitter; мы все еще ждем начала второго вызова API. Это пустая трата времени, и мы должны избегать этого, поскольку время отклика нашей социальной сети всегда будет складываться из времени отклика трех вызовов API!
Например:

Sequential calls 
Twitter call -> 6.1 seconds 
Facebook call -> 2.5 seconds 
Instagram call -> 3.7 seconds 
Total response time -> 12.3 seconds

В приведенном выше примере общее время ответа составит 12,3 секунды.

Так чего же мы на самом деле хотим ?. Мы хотим объединить три вызова API с помощью CompletableFuture и запустить их параллельно; когда все три будут выполнены, мы объединим результаты трех в одну коллекцию и вернем ее пользователю. В этом случае время отклика будет наименьшим из трех; сколько это будет с таким же временем отклика из предыдущего примера?

Parallel calls 
Twitter call -> 6.1 seconds 
Facebook call -> 2.5 seconds 
Instagram call -> 3.7 seconds 
Total response time -> 6.1 seconds

Это займет 6,1 секунды, так как это самый медленный из трех вызовов; улучшение на 50% по сравнению с последовательными вызовами.

Итак, теперь, когда мы понимаем, почему мы собираемся реализовать это таким образом, давайте посмотрим, как мы можем отразить это в коде на Java с помощью CompletableFutures!

Реализация с использованием CompletableFuture

Первоначально мы собираемся позволить клиенту настраивать, какие платформы социальных сетей вызывать, задавая пользователя в этой конфигурации.
Исходя из этого, мы будем вызывать только те API, для которых мы знаем имя пользователя. из; тогда мы получим правильную реализацию интерфейса PostFetcher для получения сообщений для этого пользователя и платформы.
Определен интерфейс, представляющий точку входа в нашу службу, SocialMediaService :

Итак, первая часть будет выглядеть так:

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

Итак, теперь, когда у нас есть этот список, мы хотим объединить три CompletableFuture, чтобы вызывать их параллельно, и после их завершения возвращать одну единственную коллекцию сообщений.

Если вы читали последний пост, мы узнали, как объединить два CompletableFutures с помощью thenCombine и thenCombineAsync. Вспомним, что мы о них говорили:

thenCombine и thenCombineAsync
Используется, когда нам нужно запустить два CompletableFutures одновременно, и мы выдаем результат, когда оба CompletableFutures будут завершены.

Итак, объединение двух CompletableFutures кажется простым; мы уже видели это в прошлой статье, а как насчет трех и более?

Здесь это становится немного сложнее, особенно если мы хотим сделать это динамически. В нашем примере это было разработано таким образом, что если мы решили использовать дополнительную платформу социальных сетей, нам не нужно было изменять нашу реализацию нашей службы социальных сетей; это было сделано с соблюдением принципа открытости-закрытости.
Тогда наша реализация будет выглядеть так:

Мы использовали часть метода reduce Java Stream для создания нового CompletableFuture из каждых двух последовательных CompletableFuture и объединения их результатов. Затем метод combApiCalls выполняет следующие действия:

Обычно мы получаем каждую пару CompletableFuture (c1, c2), получаем их сообщения (posts1, posts2) и объединяем их, используя их потоки.
Чтобы было понятнее, скажем, что три вызова API возвращают По 3 сообщения в каждом, поэтому в первый раз сокращение будет объединять 3 + 3 сообщения; затем результат (6 сообщений) будет объединен с оставшимся результатом вызова API (3 сообщения), получив, наконец, набор из 6 + 3 = 9 элементов. Надеюсь это имеет смысл!

Что мы здесь делаем, так это объединяем все в один CompletableFuture, который клиент будет запускать на своей стороне при подписке на него, например, используя get или join.
Мы могли бы решить организовать наш CompletableFutures по-другому, например, используя thenCompose; однако из моего последнего поста мы должны помнить, что с помощью thenCompose мы ждем завершения предыдущего CompletableFuture. Это будет означать, что мы снова будем выполнять наши задачи последовательно, и это будет неэффективно.
Вот почему так важно понимать, как работает каждый метод.

Еще одна вещь, о которой стоит упомянуть о нашей реализации, заключается в том, что версия метода reduce, которую мы используем, возвращает Optional, поэтому мы возвращаем пустую коллекцию в случае отсутствия данных элегантным способом, используя orElse .

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

Итак, это будет конечный результат нашей реализации SocialMediaService:

Для реализации каждого вызова API я использовал несколько поддельных реализаций, которые возвращают CompletableFuture с фиксированным количеством сообщений после небольшой задержки. Вот одна из фальшивых реализаций:

И, наконец, чтобы убедиться, что все работает должным образом, у нас есть несколько модульных тестов:

Вот и все! Если вы хотите просмотреть весь пример, вы можете найти его на Github здесь.

Заключение

Как мы видели в этом более практическом примере, использование CompletableFuture дает нам более гибкий подход при вызове внешних API, имея возможность вызывать их и комбинировать их любым способом, который мы сочтем нужным.

Чтобы привыкнуть к этой новой парадигме, требуется немного времени, но как только мы получим полный контроль над API, мы сможем писать эффективный код намного проще и быстрее, чем мы когда-либо мечтали.

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







Большое спасибо за чтение !!!

Первоначально опубликовано на http://theboreddev.com 16 июня 2020 г.