Java Modern untuk Produksi
EN
Materi 2 dari 9 · 13m

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); // bagus
record Customer(Optional<String> phone) {} // jangan

Optional 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.