Pada Bagian 1 kita membangun kerangka yang mengenali perintah. Sekarang kita menghadapi kenyataan: pengguna sering memberi input yang tidak valid. Judul tugas bisa kosong, terlalu pendek, atau ID bisa berisi teks. Pada bagian ini kita membangun lapisan validasi yang membedakan jenis galat secara terstruktur dan menyampaikan pesan yang ramah pengguna.
Mengapa Validasi Terpisah #
Memvalidasi input langsung di dalam fungsi perintah cepat berantakan. Cabang if bercampur dengan logika bisnis, dan pesan galat tersebar di banyak tempat. Solusinya: pisahkan validasi menjadi fungsi khusus yang mengembalikan Result<T>, sehingga pemanggil dapat menangani kasus sukses dan galat dengan pola yang konsisten.
Sealed Class untuk Galat #
Kotlin menyediakan sealed class yang membatasi subkelas hanya pada file yang sama. Pembatasan ini membuat when exhaustif: kompilator memaksa kita menangani setiap kasus galat, sehingga tidak ada galat yang terlewat:
// Bagian 2: Validasi input dan penanganan error yang user-friendly.
// Mendemonstrasikan sealed class untuk galat, validasi terprogram dengan
// `require`, dan penerjemahan exception menjadi pesan yang ramah pengguna.
sealed class GalatTugas {
object JudulKosong : GalatTugas()
data class JudulTerlaluPendek(val minimal: Int) : GalatTugas()
data class IdTidakValid(val masukan: String) : GalatTugas()
}
data class Tugas(val id: Int, val judul: String, val selesai: Boolean = false)
object Validator {
const val MIN_JUDUL = 3
fun validasiJudul(judul: String): Result<String> {
if (judul.isBlank()) return Result.failure(GalatTugas.JudulKosong.asException())
if (judul.length < MIN_JUDUL) {
return Result.failure(GalatTugas.JudulTerlaluPendek(MIN_JUDUL).asException())
}
return Result.success(judul.trim())
}
fun parseId(id: String): Result<Int> {
val angka = id.toIntOrNull()
?: return Result.failure(GalatTugas.IdTidakValid(id).asException())
if (angka <= 0) return Result.failure(GalatTugas.IdTidakValid(id).asException())
return Result.success(angka)
}
private fun GalatTugas.asException(): IllegalStateException =
IllegalStateException(pesan())
}
private fun GalatTugas.pesan(): String = when (this) {
is GalatTugas.JudulKosong -> "Judul tugas tidak boleh kosong."
is GalatTugas.JudulTerlaluPendek -> "Judul minimal $minimal karakter."
is GalatTugas.IdTidakValid -> "ID \"$masukan\" bukan angka positif."
}
fun main() {
val daftarJudul = listOf("Belajar Kotlin", " ", "OK", "Membuat aplikasi CLI")
daftarJudul.forEach { judul ->
val hasil = Validator.validasiJudul(judul)
println("Judul \"$judul\": ${
hasil.fold(
onSuccess = { "valid -> \"$it\"" },
onFailure = { it.message }
)
}")
}
println("---")
val daftarId = listOf("3", "0", "abc", "12")
daftarId.forEach { id ->
val hasil = Validator.parseId(id)
println("ID \"$id\": ${
hasil.fold(
onSuccess = { "valid -> $it" },
onFailure = { it.message }
)
}")
}
}Output yang Dihasilkan:
Judul "Belajar Kotlin": valid -> "Belajar Kotlin"
Judul " ": Judul tugas tidak boleh kosong.
Judul "OK": Judul minimal 3 karakter.
Judul "Membuat aplikasi CLI": valid -> "Membuat aplikasi CLI"
---
ID "3": valid -> 3
ID "0": ID "0" bukan angka positif.
ID "abc": ID "abc" bukan angka positif.
ID "12": valid -> 12Mari kita bedah kode di atas:
sealed class GalatTugas: menampung tiga kemungkinan galat. Karenasealed, semua subkelas harus berada di file yang sama, sehinggawhen (this)dipesan()menjadi exhaustif.object JudulKosong: galat tanpa data tambahan cukup dideklarasikan sebagaiobject(singleton), bukandata class.data class JudulTerlaluPendek(val minimal: Int): galat yang membawa konteks (panjang minimal) menggunakandata classagar konteks tersebut dapat dipakai di pesan galat.Result<String>: tipe kembalianResult<T>membungkus sukses dan kegagalan dalam satu tipe, sehingga pemanggil tidak perlu try-catch.?.let { ... } ?: ...: polatoIntOrNull() ?: returnmengganti pengecualian parsing dengan galat domain yang lebih informatif.hasil.fold(onSuccess, onFailure): membongkarResultke satu nilai dengan dua cabang ekspresi, lebih ringkas daripadaif (hasil.isSuccess) ... else ....
Pola Result vs Exception #
Kotlin mendukung dua gaya penanganan galat: exception-based (throw/catch) dan value-based (Result<T> atau sealed class). Keduanya sah; pilihan tergantung pada jenis galat:
| Jenis galat | Gaya yang dianjurkan | Alasan |
|---|---|---|
| Kesalahan pemrograman (precondition) | require() / check() yang melempar IllegalArgumentException | Tidak seharusnya ditangkap; menandakan bug |
| Input pengguna tidak valid | Result<T> atau sealed class | Ditangkap dan ditampilkan ke pengguna |
| Kegagalan I/O (file tidak ada) | try-catch IOException | Di luar kontrol program, perlu pemulihan |
Pada proyek ini, validasi judul dan ID termasuk kategori input pengguna, sehingga memakai Result. Bagian 3 akan memakai try-catch untuk operasi file.
Penerjemahan ke Pesan Pengguna #
Setiap galat domain diterjemahkan menjadi pesan pengguna melalui function pesan(). Pemisahan ini penting karena pesan internal sering terlalu teknis (misalnya “NumberFormatException: For input string: "abc"”) untuk ditampilkan langsung. Dengan when (this) exhaustif, penambahan jenis galat baru akan memunculkan peringatan kompilasi jika pesan() belum diperbarui.
fun cetakPesan(hasil: Result<Int>) {
hasil.fold(
onSuccess = { id -> println("Berhasil, ID = $id") },
onFailure = { e -> System.err.println("Galat: ${e.message}") }
)
}Pola di atas membuat jalur sukses dan galat berdekatan, sehingga pembaca kode dapat melihat keduanya dalam satu pandangan.
Referensi Lanjutan #
- Kotlin basic syntax
— ringkasan sintaks Kotlin untuk
whendantry-catchsaat memvalidasi argumen CLI - Kotlin control flow
— rujukan
tryexpression untuk menangani kegagalan validasi input pengguna
Ringkasan Bagian 2 #
Validasi kini terstruktur: sealed class GalatTugas mengkategorikan jenis galat, Result<T> membawa sukses dan kegagalan, dan pesan() menerjemahkan setiap kasus menjadi teks yang ramah pengguna. Pada Bagian 3 kita akan membangun penyimpanan data ke file JSON lokal sehingga tugas tetap ada setelah aplikasi ditutup.