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:

kotlin
// 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:

text
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 -> 12

Mari kita bedah kode di atas:

  1. sealed class GalatTugas: menampung tiga kemungkinan galat. Karena sealed, semua subkelas harus berada di file yang sama, sehingga when (this) di pesan() menjadi exhaustif.
  2. object JudulKosong: galat tanpa data tambahan cukup dideklarasikan sebagai object (singleton), bukan data class.
  3. data class JudulTerlaluPendek(val minimal: Int): galat yang membawa konteks (panjang minimal) menggunakan data class agar konteks tersebut dapat dipakai di pesan galat.
  4. Result<String>: tipe kembalian Result<T> membungkus sukses dan kegagalan dalam satu tipe, sehingga pemanggil tidak perlu try-catch.
  5. ?.let { ... } ?: ...: pola toIntOrNull() ?: return mengganti pengecualian parsing dengan galat domain yang lebih informatif.
  6. hasil.fold(onSuccess, onFailure): membongkar Result ke satu nilai dengan dua cabang ekspresi, lebih ringkas daripada if (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 galatGaya yang dianjurkanAlasan
Kesalahan pemrograman (precondition)require() / check() yang melempar IllegalArgumentExceptionTidak seharusnya ditangkap; menandakan bug
Input pengguna tidak validResult<T> atau sealed classDitangkap dan ditampilkan ke pengguna
Kegagalan I/O (file tidak ada)try-catch IOExceptionDi 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.

kotlin Contoh: penerjemahan di titik masuk
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 when dan try-catch saat memvalidasi argumen CLI
  • Kotlin control flow — rujukan try expression 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.