Apakah ini merupakan antipattern untuk menggunakan peek () untuk memodifikasi elemen stream?

37

Misalkan saya memiliki aliran Things dan saya ingin "memperkaya" mereka mid stream, saya dapat menggunakannya peek()untuk melakukan ini, misalnya:

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

Asumsikan bahwa mutasi Hal pada titik ini dalam kode adalah perilaku yang benar - misalnya, thingMutatormetode ini dapat menetapkan bidang "lastProcessed" ke waktu saat ini.

Namun, peek()dalam sebagian besar konteks berarti "lihat, tapi jangan sentuh".

Apakah menggunakan peek()untuk mengubah elemen stream adalah antipattern atau keliru?

Edit:

Alternatifnya, yang lebih konvensional, adalah mengubah konsumen:

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

ke fungsi yang mengembalikan parameter:

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

dan gunakan map()sebagai gantinya:

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

Tapi itu memperkenalkan kode ala kadarnya ( returndan) dan saya tidak yakin itu lebih jelas, karena Anda tahu peek()mengembalikan objek yang sama, tetapi dengan map()itu bahkan tidak jelas sekilas bahwa itu adalah kelas objek yang sama.

Selanjutnya, dengan peek()Anda dapat memiliki lambda yang bermutasi, tetapi dengan map()Anda harus membangun bangkai kereta. Membandingkan:

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

Saya pikir peek()versinya lebih jelas, dan lambda jelas bermutasi, jadi tidak ada efek samping "misterius". Demikian pula, jika referensi metode digunakan dan nama metode jelas menyiratkan mutasi, itu juga jelas dan jelas.

Pada catatan pribadi, saya tidak menghindar dari penggunaan peek()untuk bermutasi - Saya merasa sangat nyaman.

Bohemian
sumber
1
Sudahkah Anda menggunakan peekaliran yang menghasilkan elemen-elemennya secara dinamis? Apakah masih berfungsi, atau perubahannya hilang? Mengubah elemen aliran terdengar tidak dapat diandalkan bagi saya.
Sebastian Redl
1
@SebastianRedl Saya telah menemukannya 100% dapat diandalkan. Saya pertama kali menggunakannya untuk mengumpulkan selama pemrosesan: List<Thing> list; things.stream().peek(list::add).forEach(...);sangat berguna. Belakangan ini. Aku sudah menggunakannya untuk menambahkan info untuk penerbitan: Map<Thing, Long> timestamps = ...; return things.stream().peek(t -> t.setTimestamp(timestamp.get(t))).collect(toList());. Saya tahu ada cara lain untuk melakukan contoh ini, tetapi saya sangat menyederhanakan di sini. Menggunakan peek()menghasilkan IMHO kode yang lebih kompak, dan lebih elegan. Selain keterbacaan, pertanyaan ini sebenarnya tentang apa yang Anda ajukan; apakah aman / dapat diandalkan?
Bohemian
2
Pertanyaannya Dalam aliran Jawa mengintip benar-benar hanya untuk debugging? mungkin menarik juga.
Vincent Savard
@ Bohemian. Apakah Anda masih mempraktikkan pendekatan ini peek? Saya punya pertanyaan serupa tentang stackoverflow dan berharap Anda bisa melihatnya dan memberikan tanggapan Anda. Terima kasih. stackoverflow.com/questions/47356992/…
tsolakp

Jawaban:

23

Anda benar, "mengintip" dalam arti bahasa Inggris dari kata itu berarti "lihat, tapi jangan sentuh."

Namun yang javadoc negara:

mengintip

Mengintip arus (Tindakan konsumen)

Mengembalikan aliran yang terdiri dari elemen-elemen aliran ini, selain itu melakukan tindakan yang disediakan pada setiap elemen saat elemen dikonsumsi dari aliran yang dihasilkan.

Kata kunci: "melakukan ... aksi" dan "dikonsumsi". JavaDoc sangat jelas bahwa kita diharapkan peekmemiliki kemampuan untuk mengubah aliran.

Namun JavaDoc juga menyatakan:

Catatan API:

Metode ini ada terutama untuk mendukung debugging, di mana Anda ingin melihat elemen saat mereka melewati titik tertentu dalam pipa

Ini menunjukkan bahwa ini lebih ditujukan untuk mengamati, misalnya mencatat elemen dalam aliran.

Apa yang saya ambil dari semua ini adalah bahwa kita dapat melakukan tindakan menggunakan elemen-elemen dalam stream, tetapi harus menghindari mutasi elemen dalam stream. Misalnya, lanjutkan dan panggil metode pada objek, tetapi cobalah untuk menghindari operasi bermutasi pada objek.

Paling tidak, saya akan menambahkan komentar singkat ke kode Anda di sepanjang baris ini:

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

Pendapat berbeda tentang kegunaan komentar tersebut, tetapi saya akan menggunakan komentar seperti itu dalam kasus ini.


sumber
1
Mengapa Anda memerlukan komentar jika nama metode dengan jelas menandakan mutasi, misalnya thingMutator, atau lebih konkret resetLastProcesseddll. Kecuali ada alasan kuat, komentar kebutuhan seperti saran Anda biasanya menunjukkan pilihan variabel yang buruk dan / atau nama metode. Jika nama baik dipilih, bukankah itu cukup? Atau apakah Anda mengatakan bahwa meskipun memiliki nama baik, semua yang ada di peek()dalamnya seperti titik buta yang oleh sebagian besar programmer akan "meluncur" (secara visual dilewati)? Juga, "terutama untuk debugging" bukan hal yang sama dengan "hanya untuk debugging" - penggunaan apa selain yang "terutama" dimaksudkan?
Bohemian
@ Bohemian Saya akan menjelaskannya terutama karena nama metode tidak intuitif. Dan saya tidak bekerja untuk Oracle. Saya tidak di tim Java mereka. Saya tidak tahu apa yang mereka maksudkan.
@ Bohemian karena ketika mencari apa yang memodifikasi elemen dengan cara yang saya tidak suka, saya akan berhenti membaca baris di "mengintip" karena "mengintip" tidak mengubah apa pun. Baru kemudian ketika semua tempat kode lain tidak mengandung bug yang saya kejar, saya akan kembali ke sini, mungkin dipersenjatai dengan debugger dan mungkin kemudian pergi "omg, siapa yang menaruh kode menyesatkan ini di sana ?!"
Frank Hopkins
@Bohemian dalam contoh di sini di mana semuanya berada dalam satu baris, kita harus tetap membaca, tetapi orang mungkin melewatkan konten "mengintip" dan seringkali pernyataan aliran melewati beberapa baris dengan satu operasi aliran per baris
Frank Hopkins
1

Itu bisa dengan mudah disalahartikan, jadi saya akan menghindari menggunakannya seperti ini. Mungkin opsi terbaik adalah menggunakan fungsi lambda untuk menggabungkan dua tindakan yang Anda perlukan ke dalam panggilan forEach. Anda mungkin juga ingin mempertimbangkan untuk mengembalikan objek baru daripada bermutasi yang sudah ada - mungkin sedikit kurang efisien, tetapi kemungkinan menghasilkan kode yang lebih mudah dibaca, dan mengurangi potensi secara tidak sengaja menggunakan daftar yang dimodifikasi untuk operasi lain yang seharusnya telah menerima yang asli.

Jules
sumber
-2

Catatan API memberitahu kita bahwa metode ini telah ditambahkan terutama untuk tindakan seperti debugging / logging / firing stats dll.

@apiNote Metode ini ada terutama untuk mendukung debugging, di mana Anda ingin * melihat elemen ketika mereka melewati titik tertentu dalam pipa: *

       {@kode
     * Stream.of ("satu", "dua", "tiga", "empat")
     * .filter (e -> e.length ()> 3)
     * .peek (e -> System.out.println ("Nilai yang difilter:" + e))
     * .map (String :: toUpperCase)
     * .peek (e -> System.out.println ("Nilai dipetakan:" + e))
     * .collect (Collectors.toList ());
     *}

javaProgrammer
sumber
2
Jawaban Anda sudah dicakup oleh Snowman.
Vincent Savard
3
Dan "terutama" tidak berarti "hanya".
Bohemian