È un antipattern usare peek () per modificare un elemento stream?


37

Supponiamo di avere un flusso di cose e che voglio "arricchirle" a metà flusso, che posso usare peek()per fare questo, ad esempio:

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

Supponiamo che la mutazione delle cose a questo punto nel codice sia un comportamento corretto - ad esempio, il thingMutatormetodo può impostare il campo "lastProcessed" sull'ora corrente.

Tuttavia, peek()nella maggior parte dei contesti significa "guardare, ma non toccare".

L'uso peek()per mutare gli elementi del flusso è un antipattern o sconsiderato?

Modificare:

L'approccio alternativo, più convenzionale, sarebbe quello di convertire il consumatore:

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

a una funzione che restituisce il parametro:

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

e usa map()invece:

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

Ma questo introduce il codice superficiale (il return) e non sono convinto che sia più chiaro, perché sai peek()restituisce lo stesso oggetto, ma con map()un colpo d'occhio non è nemmeno chiaro che è la stessa classe di oggetti.

Inoltre, con peek()te puoi avere una lambda che muta, ma con map()te devi costruire un disastro ferroviario. Confrontare:

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

Penso che la peek()versione sia più chiara e che la lambda sia chiaramente mutante, quindi non c'è alcun effetto "misterioso". Allo stesso modo, se viene utilizzato un riferimento al metodo e il nome del metodo implicava chiaramente una mutazione, anche questo è chiaro e ovvio.

Su una nota personale, non esito a usare peek()per mutare - lo trovo molto conveniente.



1
Hai usato peekun flusso che genera dinamicamente i suoi elementi? Funziona ancora o si perdono le modifiche? La modifica degli elementi di un flusso mi sembra inaffidabile.
Sebastian Redl,

1
@SebastianRedl L'ho trovato affidabile al 100%. L'ho usato per la prima volta per raccogliere durante l'elaborazione: List<Thing> list; things.stream().peek(list::add).forEach(...);molto utile. Ultimamente. L'ho usato per aggiungere informazioni per la pubblicazione: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. So che ci sono altri modi per fare questo esempio, ma sto semplificando eccessivamente qui. L'utilizzo peek()produce un codice IMHO più compatto ed elegante. Leggibilità a parte, questa domanda riguarda davvero ciò che hai sollevato; è sicuro / affidabile?
Boemia

2
La domanda negli stream Java è davvero solo per il debug? potrebbe anche essere interessante.
Vincent Savard,

@Bohemian. Stai ancora praticando questo approccio con peek? Ho una domanda simile su StackOverflow e spero che tu possa guardarlo e dare il tuo feedback. Grazie. stackoverflow.com/questions/47356992/...
tsolakp

Risposte:


23

Hai ragione, "sbirciare" nel senso inglese della parola significa "guarda, ma non toccare".

Tuttavia le JavaDoc stati:

sbirciare

Stream peek (azione del consumatore)

Restituisce un flusso costituito dagli elementi di questo flusso, eseguendo inoltre l'azione fornita su ciascun elemento quando gli elementi vengono consumati dal flusso risultante.

Parole chiave: "performing ... action" e "consumed". JavaDoc è molto chiaro che dovremmo aspettarci peekdi avere la possibilità di modificare il flusso.

Tuttavia JavaDoc afferma anche:

Nota API:

Questo metodo esiste principalmente per supportare il debug, in cui si desidera vedere gli elementi mentre passano oltre un certo punto in una pipeline

Ciò indica che è destinato maggiormente all'osservazione, ad esempio la registrazione di elementi nel flusso.

Ciò che prendo da tutto ciò è che possiamo eseguire azioni usando gli elementi nel flusso, ma dovremmo evitare di mutare gli elementi nel flusso. Ad esempio, vai avanti e chiama i metodi sugli oggetti, ma cerca di evitare operazioni di mutazione su di essi.

Come minimo, aggiungerei un breve commento al tuo codice seguendo queste linee:

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

Le opinioni differiscono sull'utilità di tali commenti, ma vorrei usare un commento del genere in questo caso.


1
Perché hai bisogno di un commento se il nome del metodo segnala chiaramente una mutazione, ad esempio thingMutator, o più concreta, resetLastProcessedecc. A meno che non ci sia una ragione convincente, i commenti sulla necessità come il tuo suggerimento in genere indicano scelte sbagliate di nomi di variabili e / o metodi. Se vengono scelti buoni nomi, non è abbastanza? O stai dicendo che, nonostante un buon nome, qualsiasi cosa peek()è come un punto cieco che la maggior parte dei programmatori "sfiorerebbe" (saltando visivamente)? Inoltre, "principalmente per il debug" non è la stessa cosa di "solo per il debug" - quali usi diversi da quelli "principalmente" erano previsti?
Bohemian,

@Bohemian Lo spiegherei principalmente perché il nome del metodo non è intuitivo. E non lavoro per Oracle. Non faccio parte del loro team Java. Non ho idea di cosa intendessero.

@Bohemian perché quando cerco cosa modifica gli elementi in un modo che non mi piace smetterò di leggere la riga "peek" perché "peek" non cambia nulla. Solo più tardi, quando tutti gli altri posti di codice non contenessero il bug che sto inseguendo, tornerò su questo, possibilmente armato con un debugger e forse poi andrò "omg, chi ha messo questo codice fuorviante lì ?!"
Frank Hopkins,

@Bohemian nell'esempio qui in cui tutto è in una riga, bisogna leggere comunque, ma si potrebbe saltare il contenuto di "sbirciatina" e spesso un'istruzione stream va su più righe con un'operazione di flusso per riga
Frank Hopkins

1

Potrebbe essere facilmente frainteso, quindi eviterei di usarlo in questo modo. Probabilmente l'opzione migliore è usare una funzione lambda per unire le due azioni necessarie nella chiamata forEach. Puoi anche considerare di restituire un nuovo oggetto piuttosto che mutare quello esistente: potrebbe essere leggermente meno efficiente, ma è probabile che si traduca in un codice più leggibile e riduce il potenziale dell'utilizzo accidentale dell'elenco modificato per un'altra operazione che dovrebbe ho ricevuto l'originale.


-2

La nota API ci dice che il metodo è stato aggiunto principalmente per azioni come il debug / registrazione / attivazione statistiche ecc.

@apiNote Questo metodo esiste principalmente per supportare il debug, in cui si desidera * vedere gli elementi mentre passano oltre un certo punto in una pipeline: *

       {@codice
     * Stream.of ("uno", "due", "tre", "quattro")
     * .filter (e -> e.length ()> 3)
     * .peek (e -> System.out.println ("Valore filtrato:" + e))
     * .map (String :: toUpperCase)
     * .peek (e -> System.out.println ("Valore mappato:" + e))
     * .collect (Collectors.toList ());
     *}


2
La tua risposta è già coperta da Snowman.
Vincent Savard,

3
E "principalmente" non significa "solo".
Boemia,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.