Sealed type dan switch yang tidak bisa lapuk
Kenapa himpunan tertutup layak diberitahukan ke compiler, apa yang sebenarnya kamu dapat dari exhaustiveness, dan kapan sealing justru jadi pajak perawatan.
Sebagian besar tambahan terbaik di Java modern punya satu sifat yang sama: mereka membuat compiler bisa memeriksa hal yang sebelumnya tidak bisa.
Sealing menyatakan himpunannya
public sealed interface PaymentResult { record Settled(String reference, Instant at) implements PaymentResult {} record Declined(String code, String reason) implements PaymentResult {} record Pending(Instant retryAfter) implements PaymentResult {}}sealed berarti: implementasi ini dan tidak ada yang lain. Menaruhnya di dalam
interface membuat himpunan yang diizinkan jadi implisit; mendeklarasikannya di
tempat lain butuh klausa permits eksplisit, dan implementasinya harus berada di
modul yang sama (atau package yang sama, di unnamed module).
Exhaustiveness itu inti persoalannya
String describe(PaymentResult result) { return switch (result) { case Settled s -> "settled " + s.reference(); case Declined d -> "declined: " + d.code(); case Pending p -> "retry after " + p.retryAfter(); };}Tanpa default, dan tetap compile. Compiler tahu himpunannya tertutup dan setiap
anggotanya sudah ditangani, jadi switch-nya exhaustive — default di sini akan
jadi kode mati, dan sebagian compiler akan mengatakannya.
Sekarang besok tambah kemungkinan keempat:
record Refunded(String reference) implements PaymentResult {}Setiap switch atas PaymentResult yang tidak punya case Refunded gagal
compile. Itu bukan gangguan, itu fiturnya. Alternatifnya adalah cabang
default yang diam-diam memperlakukan keadaan yang baru sebagai salah satu
keadaan lama — dan refund yang dilaporkan sebagai “declined” adalah jenis bug
yang sampai ke pelanggan lebih dulu daripada ke test.
Pattern, dan guard di tempat yang tepat
Pattern matching membongkar isinya dalam satu tarikan napas:
switch (result) { case Declined(String code, String reason) when code.startsWith("5") -> retryLater(reason); case Declined(String code, _) -> giveUp(code); case Settled s -> receipt(s); case Pending p -> schedule(p.retryAfter());}Dua hal yang perlu diketahui soal guard. Urutan penting: case dicoba dari
atas ke bawah, jadi case ber-guard harus berada sebelum case tanpa guard yang
juga akan cocok. Dan guard tidak melemahkan pemeriksaan exhaustiveness
selama ada case tanpa guard yang menutup tipenya — case Declined(String code, _)
itulah yang menjaga switch ini tetap exhaustive.
Kapan tidak perlu sealing
Sealing adalah janji bahwa himpunannya tertutup. Lakukan di tempat yang memang begitu:
| Tertutup, jadi seal | Terbuka, jadi jangan |
|---|---|
| apa yang bisa terjadi pada pembayaran | negara yang kamu dukung |
| state sebuah workflow | jenis laporan yang bisa dijalankan user |
| bentuk hasil parsing | implementasi plugin |
Sealed interface untuk “penyedia pembayaran yang kami integrasikan” terbaca bagus sampai hari Senin ketika kamu menambah satu dan harus menyentuh sembilan belas file. Itu pajak perawatan yang dibayar tanpa jaminan compiler yang kamu butuhkan — tidak ada yang akan diam-diam salah menangani satu provider, karena kode yang memilih provider memang selalu berupa satu lookup.
Optional itu tipe kembalian
Terkait, dan dari naluri yang sama — taruh faktanya di signature:
Optional<Customer> findByEmail(String email); // bagusrecord Customer(Optional<String> phone) {} // janganOptional sebagai field adalah anti-pattern yang sudah dikenal: tidak
serializable, menambah wrapper di setiap akses, dan membuat satu field punya tiga
keadaan (tidak ada, ada tapi kosong, ada) padahal maksudnya cuma dua. Sebagai
parameter, biasanya itu dua method yang memakai satu signature. Sebagai tipe
kembalian dia bekerja sesuai tujuannya: “mungkin tidak ada apa-apa di sini”
menjadi bagian dari kontrak, bukan komentar yang tidak dibaca siapa pun.
Versi panjangnya, dengan kasus-kasus di mana pola ini menggigit: Pola Java Modern untuk Produksi.
Diskusi
Memuat komentar…