Czy to antipattern, aby użyć peek () do zmodyfikowania elementu strumienia?

36

Załóżmy, że mam strumień Rzeczy i chcę je „wzbogacić” w połowie strumienia, mogę peek()to zrobić, np .:

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

Załóżmy, że mutowanie Rzeczy w tym punkcie kodu jest poprawnym zachowaniem - na przykład thingMutatormetoda może ustawić pole „lastProcessor” na bieżący czas.

Jednak peek()w większości kontekstów oznacza „patrz, ale nie dotykaj”.

Używa peek()się mutować elementów Stream antywzorzec projektowy lub nierozważne?

Edytować:

Alternatywnym, bardziej konwencjonalnym podejściem byłoby przekonanie konsumenta:

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

do funkcji, która zwraca parametr:

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

i użyj map()zamiast tego:

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

Ale to wprowadza kod obłędny ( return) i nie jestem przekonany, że jest bardziej przejrzysty, ponieważ wiesz, że peek()zwraca ten sam obiekt, ale map()na pierwszy rzut oka nie jest nawet jasne, że jest to ta sama klasa obiektu.

Co więcej, peek()możesz mieć mutację lambda, ale wraz z map()tobą musisz zbudować wrak pociągu. Porównać:

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

Myślę, że peek()wersja jest wyraźniejsza, a lambda wyraźnie mutuje, więc nie ma „tajemniczego” efektu ubocznego. Podobnie, jeśli używane jest odwołanie do metody, a nazwa metody wyraźnie implikuje mutację, to również jest jasne i oczywiste.

Osobiście nie waham się używać peek()mutacji - uważam to za bardzo wygodne.

Czech
źródło
1
Czy korzystałeś peekze strumienia, który dynamicznie generuje jego elementy? Czy to nadal działa, czy zmiany zostały utracone? Modyfikowanie elementów strumienia wydaje mi się niewiarygodne.
Sebastian Redl
@SebastianRedl Uważam, że jest w 100% niezawodny. Po raz pierwszy użyłem go do zbierania podczas przetwarzania: List<Thing> list; things.stream().peek(list::add).forEach(...);bardzo przydatny. Ostatnio. Używałem go dodać informacje do publikacji: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. Wiem, że istnieją inne sposoby na zrobienie tego przykładu, ale tutaj nadmiernie upraszczam. Korzystanie peek()tworzy bardziej zwarty i bardziej elegancki kod IMHO. Pomijając czytelność, to pytanie naprawdę dotyczy tego, co poruszyłeś; czy to jest bezpieczne / niezawodne?
Bohemian
2
Pytanie W strumieniach Java zerknięcie jest naprawdę tylko do debugowania? może być również interesujące.
Vincent Savard
@Czech. Czy nadal ćwiczysz to podejście peek? Mam podobne pytanie na temat przepełnienia stosu i mam nadzieję, że mógłbyś na nie spojrzeć i wyrazić swoją opinię. Dzięki. stackoverflow.com/questions/47356992/…
tsolakp

Odpowiedzi:

23

Masz rację, „zerknięcie” w angielskim znaczeniu oznacza „patrz, ale nie dotykaj”.

Jednak gdy javadoc stany:

zerkać

Stream peek (akcja konsumencka)

Zwraca strumień składający się z elementów tego strumienia, dodatkowo wykonując podaną akcję na każdym elemencie, gdy elementy są konsumowane z wynikowego strumienia.

Słowa kluczowe: „wykonywanie ... akcji” i „pochłonięty”. JavaDoc jest bardzo jasne, że powinniśmy oczekiwać peekmożliwości modyfikacji strumienia.

Jednak JavaDoc stwierdza również:

Uwaga API:

Ta metoda istnieje głównie w celu obsługi debugowania, w którym chcesz zobaczyć elementy przepływające przez określony punkt w potoku

Wskazuje to, że jest przeznaczony bardziej do obserwacji, np. Rejestruje elementy w strumieniu.

Biorę z tego wszystko, że możemy wykonywać działania przy użyciu elementów w strumieniu, ale powinniśmy unikać mutowania elementów w strumieniu. Na przykład idź dalej i wywoływaj metody na obiektach, ale staraj się unikać mutowania na nich operacji.

Przynajmniej dodam krótki komentarz do twojego kodu w następujący sposób:

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

Opinie na temat przydatności takich komentarzy są różne, ale użyłbym takiego komentarza w tym przypadku.


źródło
1
Dlaczego potrzebujesz komentarza, jeśli nazwa metody wyraźnie sygnalizuje mutację, np. thingMutatorLub bardziej konkretną resetLastProcesseditp. O ile nie ma ważnego powodu, komentarze takie jak twoja sugestia zazwyczaj wskazują na zły wybór nazw zmiennych i / lub metod. Jeśli wybierzesz dobre imię, czy to nie wystarczy? A może mówisz, że pomimo dobrego imienia, wszystko peek()jest jak martwy punkt, który większość programistów „przeskakuje” (wizualnie pomija)? Ponadto „głównie do debugowania” nie jest tym samym, co „tylko do debugowania” - jakie zastosowania inne niż te „głównie” były zamierzone?
Bohemian
@ Bohemian Wyjaśniłbym to głównie dlatego, że nazwa metody nie jest intuicyjna. I nie pracuję dla Oracle. Nie należę do ich zespołu Java. Nie mam pojęcia, co zamierzali.
@ Bohemian, ponieważ szukając tego, co modyfikuje elementy w sposób, który mi się nie podoba, przestanę czytać wiersz w „peek”, ponieważ „peek” niczego nie zmienia. Dopiero później, gdy wszystkie inne miejsca kodu nie będą zawierały błędu, którego ścigam, wrócę do tego, być może uzbrojonego w debugger, a może potem „omg, kto umieścił ten wprowadzający w błąd kod ?!”
Frank Hopkins
@ Bohemian na przykładzie tutaj, gdzie wszystko jest w jednym wierszu, i tak trzeba czytać, ale można pominąć treść „podglądania” i często instrukcja stream przechodzi przez wiele wierszy z jedną operacją strumienia na wiersz
Frank Hopkins
1

Może być łatwo źle zinterpretowany, więc unikałbym używania go w ten sposób. Prawdopodobnie najlepszą opcją jest użycie funkcji lambda do połączenia dwóch potrzebnych działań w wywołanie forEach. Możesz również rozważyć zwrócenie nowego obiektu zamiast mutowania istniejącego - może być nieco mniej wydajny, ale może skutkować bardziej czytelnym kodem i zmniejsza ryzyko przypadkowego użycia zmodyfikowanej listy do innej operacji, która powinna otrzymałem oryginał.

Jules
źródło
-2

Nota API mówi nam, że metoda została dodana głównie do akcji takich jak debugowanie / rejestrowanie / odpalanie statystyk itp.

@apiNote Ta metoda istnieje głównie w celu obsługi debugowania, w której chcesz * widzieć elementy przepływające przez określony punkt w potoku: *

       {@kod
     * Stream.of („jeden”, „dwa”, „trzy”, „cztery”)
     *. filtr (e -> e.length ()> 3)
     * .peek (e -> System.out.println („Filtrowana wartość:” + e))
     * .map (String :: toUpperCase)
     * .peek (e -> System.out.println („Wartość odwzorowana:” + e))
     * .collect (Collectors.toList ());
     *}

javaProgrammer
źródło
2
Twoja odpowiedź jest już ułożona przez Bałwana.
Vincent Savard
3
A „głównie” nie oznacza „tylko”.
Bohemian