The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
API Streams было представлено в Java 8 вместе с лямбда-выражениями всего несколько лет назад. Я, как дисциплинированный знаток Java, попытался использовать эту новую функцию в нескольких своих проектах, например, здесь и здесь. Мне это не очень понравилось, и я вернулся к добрым старым декораторам. Более того, я создал Cactoos, библиотеку декораторов, чтобы заменить Guava, которая не так хороша во многих местах.
Вот простой пример. Допустим, у нас есть коллекция измерений, поступающих из некоторого источника данных, все они являются числами от нуля до единицы:
Теперь нам нужно показать только первые 10 из них, игнорируя нули и единицы, и изменить их масштаб до (0..100)
. Звучит как простая задача, верно? Есть три способа сделать это: процедурный, объектно-ориентированный и способ Java 8. Давайте начнем с процедурного способа:
Почему это процедурный способ? Потому что это императивный. Почему он императивный? Потому что это процедурный. На самом деле, я шучу.
Он императивный, потому что мы даем инструкции компьютеру о том, какие данные куда поместить и как их обрабатывать. Мы не объявляем результат, а строим его императивным образом. Это работает, но на самом деле не масштабируется. Мы не можем взять часть этого алгоритма и применить его к другой ситуации. Мы не можем легко его изменить, например, чтобы использовать числа из двух источников вместо одного и т. д. Это процедурный способ. Достаточно сказано. Не делайте так.
Теперь, Java 8 предоставляет нам Streams API, который, как предполагается, предлагает функциональный способ сделать то же самое. Давайте попробуем использовать его.
Сначала нам нужно создать экземпляр Stream
, который Iterable
не позволяет получить непосредственно. Затем мы используем API потоков для выполнения задачи:
Это будет работать, но для всех зондов будет выводиться Probe #0
, потому что forEach()
не работает с индексами. В интерфейсе Stream
в Java 8 (и также в Java 9 тоже) нет такого метода, как forEachWithIndex()
. Вот здесь есть обходное решение с использованием атомарного счетчика:
“Что не так с этим?” - вы можете спросить. Во-первых, посмотрите, как легко мы попали в беду, когда не нашли правильный метод в интерфейсе Stream
. Мы сразу же отошли от “потоковой” парадигмы и вернулись к доброму старому процедурному глобальному переменному (счётчику). Во-вторых, мы на самом деле не видим, что происходит внутри этих методов filter()
, limit()
и forEach()
. Как именно они работают? Документация говорит, что этот подход является “декларативным” и каждый метод в интерфейсе Stream
возвращает экземпляр некоторого класса. Какие это классы? Мы не имеем ни малейшего представления, просто взглянув на этот код.
Эти две проблемы связаны. Самая большая проблема с этим потоковым API - это сам интерфейс Stream
- он огромен. На момент написания в нем 43 метода. Сорок три, в одном интерфейсе! Это противоречит каждому принципу объектно-ориентированного программирования, начиная с SOLID и далее до более серьезных.
Каким образом можно реализовать тот же алгоритм в объектно-ориентированном стиле? Вот как я бы сделал это с помощью Cactoos, который является просто набором простых Java-классов:
Давайте посмотрим, что здесь происходит. Сначала Filtered
декорирует нашу перечисляемую probes
, чтобы убрать из нее определенные элементы. Обратите внимание, что Filtered
реализует интерфейс Iterable
. Затем Limited
, также являющийся Iterable
, берет только первые десять элементов. Затем Mapped
преобразует каждое исследование в экземпляр Scalar<Boolean>
, который выполняет печать строки.
Наконец, экземпляр And
проходит по списку “скаляров” и просит каждый из них вернуть boolean
. Они печатают строку и возвращают true
. Поскольку это true
, And
делает следующую попытку с следующим скаляром. Наконец, его метод value()
возвращает true
.
Но подождите, здесь нет индексов. Давайте добавим их. Для этого мы просто используем еще один класс, называемый AndWithIndex
:
Вместо Scalar<Boolean>
теперь мы отображаем наши зонды на Func<Integer, Boolean>
, чтобы они могли принимать индекс.
Преимущество такого подхода заключается в том, что все классы и интерфейсы являются небольшими, поэтому их можно очень просто комбинировать. Чтобы сделать ограниченный итератор зондов, мы его декорируем с помощью Limited
; чтобы отфильтровать его, мы декорируем его с помощью Filtered
; чтобы сделать что-то еще, мы создаем новый декоратор и используем его. Мы не ограничены одним интерфейсом, как Stream
.
В конечном счете, декораторы - это объектно-ориентированный инструмент для изменения поведения коллекций, в то время как потоки - это нечто другое, для чего я даже не могу найти название.
P.S. Кстати, вот как можно реализовать тот же алгоритм с помощью Iterables
из Guava:
Это странное сочетание объектно-ориентированного и функционального стилей.
Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 10:45