Чи антипатерн використовувати peek () для зміни елемента потоку?


36

Припустимо, у мене є потік Речей, і я хочу "збагатити" їх середнім потоком, я можу використовувати peek()це, наприклад:

streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);

Припустимо, що мутація Речей у цьому пункті коду є правильною поведінкою - наприклад, thingMutatorметод може встановити поле "lastProcessed" на поточний час.

Однак peek()у більшості контекстів означає "дивись, але не чіпай".

Користуємося peek()для мутувати елементи потоку в антипаттерн або необачно?

Редагувати:

Альтернативним, більш традиційним, підходом було б перетворення споживача:

private void thingMutator(Thing thing) {
    thing.setLastProcessed(System.currentTimeMillis());
}

до функції, яка повертає параметр:

private Thing thingMutator(Thing thing) {
    thing.setLastProcessed(currentTimeMillis());
    return thing;
}

і використовувати map()замість цього:

stream.map(this::thingMutator)...

Але це вводить функціональний код (the return), і я не переконаний, що це зрозуміліше, тому що ви знаєте, peek()повертає той самий об'єкт, але з першого map()погляду навіть не зрозуміло, що це той самий клас об'єктів.

Далі, у peek()вас може бути лямбда, яка мутує, але з map()вами доведеться побудувати аварію поїзда. Порівняйте:

stream.peek(t -> t.setLastProcessed(currentTimeMillis())).forEach(...)
stream.map(t -> {t.setLastProcessed(currentTimeMillis()); return t;}).forEach(...)

Я думаю, що peek()версія зрозуміліша, а лямбда явно мутує, тому "загадкового" побічного ефекту немає. Аналогічно, якщо використовується посилання на метод, а назва методу чітко передбачає мутацію, це теж зрозуміло і очевидно.

В особистій записці я не цураюся використання peek()мутації - мені здається, що це дуже зручно.



1
Ви використовували peekпотік, який динамічно генерує його елементи? Це все ще працює, або зміни втрачені? Змінення елементів потоку для мене звучить ненадійно.
Себастьян Редл

@SebastianRedl Я вважаю це на 100% надійним. Я вперше використовував його для збору під час обробки: List<Thing> list; things.stream().peek(list::add).forEach(...);дуже зручно. Останнім часом. Я використовував його , щоб додати інформацію для публікації: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. Я знаю, що є інші способи зробити цей приклад, але я тут сильно надто спрощую. Використання peek()створює більш компактний і елегантний код IMHO. Читання убік, це питання справді стосується того, що ви підняли; це безпечно / надійно?
богемський


@Bohemian. Ви все ще практикуєте цей підхід peek? У мене є подібне запитання щодо stackoverflow, і сподіваюся, що ви могли це переглянути і дати свої відгуки. Спасибі. stackoverflow.com/questions/47356992 / ...
tsolakp

Відповіді:


23

Ви правильні, "заглянути" в англійському розумінні цього слова означає "дивись, але не чіпай".

Однак в JavaDoc стану:

заглянути

Потоковий погляд (Споживчі дії)

Повертає потік, що складається з елементів цього потоку, додатково виконуючи надану дію на кожен елемент, оскільки елементи споживаються з отриманого потоку.

Ключові слова: "виконання ... дії" та "спожито". У JavaDoc чітко зрозуміло, що слід очікувати, що ми зможемо peekзмінювати потік.

Однак JavaDoc також говорить:

Примітка API:

Цей метод існує головним чином для підтримки налагодження, де потрібно бачити елементи, коли вони проходять повз певну точку трубопроводу

Це вказує на те, що воно призначене більше для спостереження, наприклад, реєстрація елементів у потоці.

З усього цього я берусь за те, що ми можемо виконувати дії, використовуючи елементи в потоці, але повинні уникати мутуючих елементів у потоці. Наприклад, рухайтеся вперед і називайте методи на об'єктах, але намагайтеся уникати мутуючих операцій над ними.

Як мінімум, я додаю короткий коментар до вашого коду в наступних рядках:

// Note: this peek() call will modify the Things in the stream.
streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);

Думки різняться щодо корисності таких коментарів, але я б застосував такий коментар у цьому випадку.


1
Чому вам потрібен коментар, якщо ім'я методу чітко сигналізує про мутацію, наприклад thingMutator, або конкретніше resetLastProcessedі т.д. Якщо обрані хороші імена, чи цього недостатньо? Або ви хочете сказати, що, незважаючи на гарне ім'я, все, що peek()є, як сліпа пляма, до якої більшість програмістів "проскакували" (візуально пропустили)? Крім того, "головним чином для налагодження" - це не те саме, що "тільки для налагодження" - які призначені для використання, крім "в основному"?
богемський

@Bohemian Я б пояснив це головним чином тим, що назва методу не інтуїтивно зрозуміла. І я не працюю на Oracle. Я не в їх команді Java. Я поняття не маю, що вони задумали.

@Bohemian, тому що, шукаючи те, що модифікує елементи так, що мені не подобається, я перестану читати рядок на "заглянути", тому що "заглянути" нічого не змінить. Тільки пізніше, коли всі інші місця коду не містять помилку, яку я переслідую, я повернусь до цього, можливо, озброївшись налагоджувачем і, можливо, тоді перейду "омг, хто поставив туди цей оманливий код ?!"
Френк Хопкінс

@Bohemian в іспиті тут, де все в одному рядку, потрібно все одно читати, але можна пропустити вміст "заглянути", і часто заява потоку переходить через кілька рядків з однією операцією потоку на рядок
Френк Хопкінс,

1

Це може бути легко витлумачено неправильно, тому я б уникав цього використовувати. Напевно, найкращим варіантом є використання лямбда-функції для об'єднання двох необхідних вам дій у виклику forEach. Ви також можете розглянути можливість повернення нового об'єкта, а не мутацію існуючого - це може бути трохи менш ефективно, але це, ймовірно, призведе до більш читабельного коду та зменшує можливість випадкового використання зміненого списку для іншої операції, яка повинна отримали оригінал.


-2

Примітка API говорить нам про те, що метод доданий в основному для таких дій, як налагодження / реєстрація / запускання статистики тощо.

@apiNote Цей метод існує головним чином для підтримки налагодження, де потрібно * бачити елементи, коли вони проходять повз певну точку трубопроводу: *

       {@code
     * Stream.of ("один", "два", "три", "чотири")
     * .filter (e -> e.length ()> 3)
     * .peek (e -> System.out.println ("Відфільтроване значення:" + e))
     * .map (Рядок :: toUpperCase)
     * .peek (e -> System.out.println ("Зображене значення:" + e))
     * .collect (Collectors.toList ());
     *}


2
Ваша відповідь вже охоплена Сніговиком.
Вінсент Савард

3
І "головним чином" не означає "лише".
богем

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.