¿Es un antipatrón usar peek () para modificar un elemento de flujo?

37

Supongamos que tengo una secuencia de Cosas y quiero "enriquecerlas" a mitad de la secuencia, puedo usar peek()para hacer esto, por ejemplo:

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

Suponga que mutar las cosas en este punto del código es un comportamiento correcto; por ejemplo, el thingMutatormétodo puede establecer el campo "lastProcessed" en la hora actual.

Sin embargo, peek()en la mayoría de los contextos significa "mirar, pero no tocar".

¿Está utilizando peek()para mutar elementos de la corriente un antipatrón o mal aconsejado?

Editar:

El enfoque alternativo, más convencional, sería convertir al consumidor:

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

a una función que devuelve el parámetro:

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

y usar map()en su lugar:

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

Pero eso introduce código superficial (the return) y no estoy convencido de que sea más claro, porque sabes que peek()devuelve el mismo objeto, pero con map()ni siquiera es claro de un vistazo que es la misma clase de objeto.

Además, con peek()usted puede tener una lambda que muta, pero con map()usted tiene que construir un choque de trenes. Comparar:

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

Creo que la peek()versión es más clara y la lambda está claramente mutando, por lo que no hay ningún efecto secundario "misterioso". Del mismo modo, si se usa una referencia de método y el nombre del método implica claramente una mutación, eso también es claro y obvio.

En una nota personal, no evito usar peek()para mutar, me parece muy conveniente.

bohemio
fuente
1
¿Has utilizado peekuna secuencia que genera sus elementos dinámicamente? ¿Sigue funcionando o se pierden los cambios? Modificar los elementos de una transmisión no me parece confiable.
Sebastian Redl
1
@SebastianRedl Lo he encontrado 100% confiable. Lo usé por primera vez para recolectar durante el procesamiento: List<Thing> list; things.stream().peek(list::add).forEach(...);muy útil. Últimamente. Lo he utilizado para agregar información de la publicación: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. Sé que hay otras formas de hacer este ejemplo, pero estoy simplificando demasiado aquí. El uso peek()produce un código más compacto y elegante en mi humilde opinión. Dejando a un lado la legibilidad, esta pregunta es realmente sobre lo que ha planteado; ¿es seguro / confiable?
Bohemio
2
La pregunta en Java streams es realmente solo para depurar? puede ser interesante también
Vincent Savard
@Bohemio. ¿Todavía estás practicando este enfoque peek? Tengo una pregunta similar sobre stackoverflow y espero que puedas verla y dar tu opinión. Gracias. stackoverflow.com/questions/47356992/…
tsolakp

Respuestas:

23

Tienes razón, "mirar" en el sentido inglés de la palabra significa "mirar, pero no tocar".

Sin embargo los JavaDoc estados:

ojeada

Stream peek (Acción del consumidor)

Devuelve una secuencia que consta de los elementos de esta secuencia, además de realizar la acción proporcionada en cada elemento a medida que los elementos se consumen de la secuencia resultante.

Palabras clave: "realizando ... acción" y "consumido". El JavaDoc tiene muy claro que debemos esperar peektener la capacidad de modificar la secuencia.

Sin embargo, JavaDoc también establece:

Nota de API:

Este método existe principalmente para admitir la depuración, donde desea ver los elementos a medida que fluyen más allá de cierto punto en una tubería

Esto indica que está destinado más a la observación, por ejemplo, elementos de registro en la secuencia.

Lo que deduzco de todo esto es que podemos realizar acciones utilizando los elementos en la secuencia, pero debemos evitar la mutación de elementos en la secuencia. Por ejemplo, siga adelante y llame a métodos en los objetos, pero trate de evitar las operaciones de mutación en ellos.

Como mínimo, agregaría un breve comentario a su código a lo largo de estas líneas:

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

Las opiniones difieren sobre la utilidad de tales comentarios, pero usaría ese comentario en este caso.


fuente
1
¿Por qué necesita un comentario si el nombre del método indica claramente una mutación, por ejemplo thingMutator, o más concreto, resetLastProcessedetc. A menos que haya una razón convincente, los comentarios de necesidad como su sugerencia generalmente indican malas elecciones de nombres de variables y / o métodos. Si se eligen buenos nombres, ¿no es eso suficiente? ¿O estás diciendo que a pesar de un buen nombre, algo en el interior peek()es como un punto ciego que la mayoría de los programadores "echarían un vistazo"? Además, "principalmente para la depuración" no es lo mismo que "solo para la depuración": ¿qué usos distintos de los "principalmente" estaban destinados?
Bohemio
@Bohemian Lo explicaría principalmente porque el nombre del método no es intuitivo. Y no trabajo para Oracle. No estoy en su equipo de Java. No tengo idea de lo que pretendían.
@Bohemian porque cuando busque lo que modifica los elementos de una manera que no me gusta, dejaré de leer la línea en "peek" porque "peek" no cambia nada. Solo más tarde, cuando todos los demás lugares del código no contengan el error que estoy persiguiendo, volveré a esto, posiblemente armado con un depurador y tal vez luego "omg, ¡¿quién puso este código engañoso allí ?!"
Frank Hopkins
@Bohemian en el examen aquí donde todo está en una línea, uno necesita seguir leyendo de todos modos, pero uno puede omitir el contenido de "peek" y, a menudo, una declaración de flujo pasa por varias líneas con una operación de flujo por línea
Frank Hopkins
1

Podría malinterpretarse fácilmente, por lo que evitaría usarlo así. Probablemente la mejor opción es utilizar una función lambda para fusionar las dos acciones que necesita en la llamada forEach. También puede considerar devolver un nuevo objeto en lugar de mutar el existente; puede ser un poco menos eficiente, pero es probable que genere un código más legible y reduzca el potencial de usar accidentalmente la lista modificada para otra operación que debería Han recibido el original.

Jules
fuente
-2

La nota de API nos dice que el método se ha agregado principalmente para acciones como depuración / registro / estadísticas de disparo, etc.

@apiNote Este método existe principalmente para admitir la depuración, donde desea * ver los elementos a medida que fluyen más allá de cierto punto en una tubería: *

       {@código
     * Stream.of ("uno", "dos", "tres", "cuatro")
     * .filter (e -> e.length ()> 3)
     * .peek (e -> System.out.println ("Valor filtrado:" + e))
     * .map (String :: toUpperCase)
     * .peek (e -> System.out.println ("Valor asignado:" + e))
     * .collect (Collectors.toList ());
     *}

javaProgrammer
fuente
2
Tu respuesta ya está cubierta por Snowman.
Vincent Savard
3
Y "principalmente" no significa "solo".
Bohemio