É um antipadrão para usar peek () para modificar um elemento de fluxo?

37

Suponha que eu tenha um fluxo de coisas e que queira "enriquecê-los" no meio do fluxo, posso usar peek()isso, por exemplo:

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

Suponha que a alteração das coisas neste momento no código seja um comportamento correto - por exemplo, o thingMutatormétodo pode definir o campo "lastProcessed" para o horário atual.

No entanto, peek()na maioria dos contextos significa "olhe, mas não toque".

O uso peek()para alterar elementos de fluxo é um antipadrão ou é desaconselhável?

Editar:

A abordagem alternativa, mais convencional, seria converter o consumidor:

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

para uma função que retorna o parâmetro:

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

e use map():

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

Mas isso introduz código superficial (the return) e não estou convencido de que seja mais claro, porque você sabe que peek()retorna o mesmo objeto, mas com map()isso nem fica claro à primeira vista que é a mesma classe de objeto.

Além disso, peek()você pode ter um lambda que sofre mutação, mas com map()você tem que construir um acidente de trem. Comparar:

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

Eu acho que a peek()versão é mais clara e o lambda está claramente mudando, então não há efeito colateral "misterioso". Da mesma forma, se uma referência de método for usada e o nome do método implicar claramente uma mutação, isso também será claro e óbvio.

Em uma nota pessoal, não evito usar o peek()mutate - acho muito conveniente.

boêmio
fonte
11
Você já usou peekcom um fluxo que gera seus elementos dinamicamente? Ainda funciona ou as alterações são perdidas? Modificar os elementos de um fluxo parece pouco confiável para mim.
Sebastian Redl 02/02
11
@SebastianRedl Eu achei 100% confiável. Eu o usei pela primeira vez para coletar durante o processamento: List<Thing> list; things.stream().peek(list::add).forEach(...);muito útil. Recentemente. Eu usei-o para adicionar informações para a publicação: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. Eu sei que existem outras maneiras de fazer esse exemplo, mas estou simplificando bastante aqui. Usar peek()produz código IMHO mais compacto e mais elegante. Legibilidade à parte, esta questão é realmente sobre o que você levantou; é seguro / confiável?
Bohemian
2
A pergunta Nos fluxos Java é espiada realmente apenas para depuração? pode ser interessante também.
Vincent Savard
@Boêmio. Você ainda está praticando essa abordagem peek? Tenho uma pergunta semelhante sobre o stackoverflow e espero que você possa ver e dar seu feedback. Obrigado. stackoverflow.com/questions/47356992/…
tsolakp

Respostas:

23

Você está correto, "espiar" no sentido inglês da palavra significa "olhar, mas não toque".

No entanto, o JavaDoc afirma:

olhadinha

Visualização de stream (ação do consumidor)

Retorna um fluxo que consiste nos elementos desse fluxo, executando adicionalmente a ação fornecida em cada elemento à medida que os elementos são consumidos no fluxo resultante.

Palavras-chave: "realizando ... ação" e "consumido". O JavaDoc é muito claro que devemos esperar peekter a capacidade de modificar o fluxo.

No entanto, o JavaDoc também declara:

Nota da API:

Esse método existe principalmente para oferecer suporte à depuração, em que você deseja ver os elementos à medida que eles fluem após um certo ponto em um pipeline

Isso indica que se destina mais à observação, por exemplo, elementos de registro no fluxo.

O que retiro de tudo isso é que podemos executar ações usando os elementos no fluxo, mas devemos evitar a mutação de elementos no fluxo. Por exemplo, vá em frente e chame métodos nos objetos, mas tente evitar operações mutantes neles.

No mínimo, gostaria de adicionar um breve comentário ao seu código ao longo destas linhas:

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

As opiniões divergem sobre a utilidade de tais comentários, mas eu usaria esse comentário neste caso.


fonte
11
Por que você precisa de um comentário se o nome do método sinaliza claramente uma mutação, por exemplo thingMutator, ou mais concreta resetLastProcessedetc. A menos que haja um motivo convincente, os comentários necessários, como a sua sugestão, normalmente indicam más escolhas de nomes de variáveis ​​e / ou métodos. Se bons nomes são escolhidos, não é suficiente? Ou você está dizendo que, apesar de um bom nome, qualquer coisa peek()é como um ponto cego que a maioria dos programadores "roçava" (pula visualmente)? Além disso, "principalmente para depuração" não é a mesma coisa que "apenas para depuração" - que outros usos além dos "principalmente" foram destinados?
Bohemian
@ Bohemian Eu explicaria isso principalmente porque o nome do método não é intuitivo. E eu não trabalho para Oracle. Não estou no time de Java deles. Não tenho ideia do que eles pretendiam.
@ Bohemian, porque ao procurar o que modifica os elementos de uma maneira que não gosto, pararei de ler a linha em "peek" porque "peek" não muda nada. Só mais tarde, quando todos os outros locais de código não contiverem o bug que estou perseguindo, voltarei a isso, possivelmente armado com um depurador e talvez então "omg, quem colocou esse código enganoso lá ?!"
Frank Hopkins
@Bohemian no exemplo aqui, onde tudo está em uma linha, é preciso ler de qualquer maneira, mas pode-se pular o conteúdo de "espreitar" e muitas vezes uma instrução de fluxo passa por várias linhas com uma operação de fluxo por linha
Frank Hopkins
1

Poderia ser facilmente mal interpretado, para evitar o uso assim. Provavelmente, a melhor opção é usar uma função lambda para mesclar as duas ações necessárias na chamada forEach. Você também pode considerar devolver um novo objeto em vez de alterar o existente - ele pode ser um pouco menos eficiente, mas provavelmente resulta em um código mais legível e reduz o potencial de usar acidentalmente a lista modificada para outra operação que deve receberam o original.

Jules
fonte
-2

A nota da API informa que o método foi adicionado principalmente para ações como estatísticas de depuração / registro / disparo etc.

@apiNote Esse método existe principalmente para oferecer suporte à depuração, onde você deseja * ver os elementos à medida que eles fluem após um certo ponto em um pipeline: *

       {@código
     * Stream.of ("um", "dois", "três", "quatro")
     * .filter (e -> comprimento e ()> 3)
     * .peek (e -> System.out.println ("Valor filtrado:" + e))
     * .map (String :: toUpperCase)
     * .peek (e -> System.out.println ("Valor mapeado:" + e))
     * .collect (Collectors.toList ());
     *}

javaProgrammer
fonte
2
Sua resposta já está coberta pelo Snowman's.
Vincent Savard
3
E "principalmente" não significa "apenas".
Bohemian