Streams vs. Decorators

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

sixnines availability badge   GitHub stars