Статьи

Потоки против декораторов

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

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

1
Iterable<Double> probes;

Теперь нам нужно показать только первые 10 из них, игнорируя нули и единицы, и масштабировать их до (0..100) . Звучит как легкая задача, верно? Есть три способа сделать это: процедурный, объектно-ориентированный и способ Java 8. Начнем с процедурного пути:

01
02
03
04
05
06
07
08
09
10
11
12
int pos = 0;
for (Double probe : probes) {
  if (probe == 0.0d || probe == 1.0d) {
    continue;
  }
  if (++pos > 10) {
    break;
  }
  System.out.printf(
    "Probe #%d: %f", pos, probe * 100.0d
  );
}

Почему это процедурный путь? Потому что это обязательно. Почему это обязательно? Потому что это процедурно. Нет, я шучу

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

Теперь Java 8 предоставляет нам Streams API , который должен предложить функциональный способ сделать то же самое. Давайте попробуем использовать это.

Во-первых, нам нужно создать экземпляр Stream , который Iterable не позволяет нам получить напрямую. Затем мы используем потоковый API, чтобы сделать работу:

1
2
3
4
5
6
7
8
StreamSupport.stream(probes.spliterator(), false)
  .filter(p -> p == 0.0d || p == 1.0d)
  .limit(10L)
  .forEach(
    probe -> System.out.printf(
      "Probe #%d: %f", 0, probe * 100.0d
    )
  );

Это будет работать, но скажет Probe #0 для всех зондов, потому что forEach() не работает с индексами. В интерфейсе Stream нет такой вещи, как forEachWithIndex() как в Java 8 (и в Java 9 тоже ). Вот обходной путь с атомным счетчиком:

01
02
03
04
05
06
07
08
09
10
11
AtomicInteger index = new AtomicInteger();
StreamSupport.stream(probes.spliterator(), false)
  .filter(probe -> probe == 0.0d || probe == 1.0d)
  .limit(10L)
  .forEach(
    probe -> System.out.printf(
      "Probe #%d: %f",
      index.getAndIncrement(),
      probe * 100.0d
    )
  );

«Что случилось с этим?» Вы можете спросить. Во-первых, посмотрите, как легко мы попали в беду, когда мы не нашли правильный метод в интерфейсе Stream . Мы сразу же отказались от «потоковой» парадигмы и вернулись к старой доброй процедурной глобальной переменной (счетчику). Во-вторых, мы на самом деле не видим, что происходит внутри этих методов filter() , limit() и forEach() . Как именно они работают? В документации говорится, что этот подход является «декларативным», и каждый метод в интерфейсе Stream возвращает экземпляр некоторого класса. Какие они классы? Мы понятия не имеем, просто посмотрев на этот код.

Самая большая проблема с этим потоковым API — это сам интерфейс Stream, он огромен!

Эти две проблемы связаны. Самая большая проблема с этим потоковым API — это сам интерфейс Stream — он огромен. На момент написания статьи существует 43 метода. Сорок три в одном интерфейсе! Это противоречит каждому принципу объектно-ориентированного программирования, начиная с SOLID, а затем до более серьезных.

Что такое объектно-ориентированный способ реализации того же алгоритма? Вот как я мог бы сделать это с Cactoos , который является просто коллекцией примитивный простые классы Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
new And(
  new Mapped<Double, Scalar<Boolean>>(
    new Limited<Double>(
      new Filtered<Double>(
        probes,
        probe -> probe == 0.0d || probe == 1.0d
      ),
      10
    ),
    probe -> () -> {
      System.out.printf(
        "Probe #%d: %f", 0, probe * 100.0d
      );
      return true;
    }
  ),
).value();

Посмотрим, что здесь происходит. Во-первых, Filtered украшает наши итерируемые probes чтобы извлечь из него определенные элементы. Обратите внимание, что Filtered реализует Iterable . Затем Limited , также являющаяся Iterable , выводит только первые десять элементов. Затем Mapped преобразует каждый зонд в экземпляр Scalar<Boolean> , который выполняет печать строк.

Наконец, экземпляр And проходит через список «скаляров» и просит каждого из них вернуть boolean . Они печатают строку и возвращают true . Так как это true , And делает следующую попытку со следующим скаляром. Наконец, его метод value() возвращает true .

Но подождите, индексов нет. Давайте добавим их. Для этого мы просто используем другой класс, называемый AndWithIndex :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
new AndWithIndex(
  new Mapped<Double, Func<Integer, Boolean>>(
    new Limited<Double>(
      new Filtered<Double>(
        probes,
        probe -> probe == 0.0d || probe == 1.0d
      ),
      10
    ),
    probe -> index -> {
      System.out.printf(
        "Probe #%d: %f", index, probe * 100.0d
      );
      return true;
    }
  ),
).value();

Вместо Scalar<Boolean> теперь мы отображаем наши пробники на Func<Integer, Boolean> чтобы они могли принять индекс.

Прелесть этого подхода в том, что все классы и интерфейсы маленькие, и поэтому они очень сочетаемы. Чтобы сделать итеративность зондов ограниченной, мы украшаем ее Limited ; чтобы сделать его отфильтрованным, мы украшаем его Filtered ; чтобы сделать что-то еще, мы создаем новый декоратор и используем его. Мы не привязаны к одному единственному интерфейсу, как Stream .

Суть в том, что декораторы — это объектно-ориентированный инструмент для изменения поведения коллекций, в то время как потоки — это нечто другое, для чего я даже не могу найти название.

PS Кстати, вот как тот же алгоритм может быть реализован с помощью Iterables :

01
02
03
04
05
06
07
08
09
10
11
12
13
Iterable<Double> ready = Iterables.limit(
  Iterables.filter(
    probes,
    probe -> probe == 0.0d || probe == 1.0d
  ),
  10
);
int pos = 0;
for (Double probe : probes) {
  System.out.printf(
    "Probe #%d: %f", pos++, probe * 100.0d
  );
}

Это странная комбинация объектно-ориентированного и функционального стилей.

Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Streams vs. Decorators

Мнения, высказанные участниками Java Code Geeks, являются их собственными.