Pada Bagian 1 kita merancang struktur entitas. Sekarang kita mengelola entitas tersebut melalui operasi CRUD: Create, Read, Update, Delete. Operasi ini dibungkus dalam class RepoBuku dan RepoAnggota yang menyimpan data di memori dan memvalidasi aturan bisnis.

Pola Repository #

Repository adalah class yang menyimpan koleksi entitas dan menyediakan operasi CRUD. Pemanggil tidak tahu detail penyimpanan internal (Map, List, atau database); mereka hanya berinteraksi melalui method publik seperti tambah, cari, semua, perbarui, dan hapus.

OperasiMethodHasil
Createtambah(b)Entitas disimpan, exception jika ID duplikat
Readcari(id)Entitas atau null
Read allsemua()List entitas terurut
Updateperbarui(b)Entitas diperbarui, null jika ID tidak ada
Deletehapus(id)Boolean sukses

Pemisahan operasi ke dalam class repo membuat kode lebih mudah diuji dan diganti. Saat Bagian 4 mengganti penyimpanan ke file JSON, kita cukup menambah lapisan persistensi tanpa mengubah pemanggil.

Implementasi RepoBuku dan RepoAnggota #

Kode berikut menunjukkan implementasi penuh repo in-memory dengan validasi:

kotlin
// Bagian 2: Pengelolaan buku dan anggota (CRUD).
// Mendemonstrasikan operasi CRUD in-memory untuk Buku dan Anggota dengan
// validasi sederhana dan pelaporan via println.

data class Buku(
    val id: String,
    val judul: String,
    val penulis: String,
    val stok: Int = 1
)

data class Anggota(
    val id: String,
    val nama: String,
    val email: String
)

class RepoBuku {
    private val daftar = mutableMapOf<String, Buku>()

    fun tambah(buku: Buku): Buku {
        require(!daftar.containsKey(buku.id)) { "ID ${buku.id} sudah dipakai" }
        require(buku.judul.length >= 3) { "Judul minimal 3 karakter" }
        daftar[buku.id] = buku
        return buku
    }

    fun cari(id: String): Buku? = daftar[id]

    fun semua(): List<Buku> = daftar.values.toList().sortedBy { it.id }

    fun perbarui(buku: Buku): Buku? {
        if (!daftar.containsKey(buku.id)) return null
        daftar[buku.id] = buku
        return buku
    }

    fun hapus(id: String): Boolean = daftar.remove(id) != null
}

class RepoAnggota {
    private val daftar = mutableMapOf<String, Anggota>()

    fun tambah(anggota: Anggota): Anggota {
        require(!daftar.containsKey(anggota.id)) { "ID ${anggota.id} sudah dipakai" }
        require(anggota.email.contains("@")) { "Email tidak valid" }
        daftar[anggota.id] = anggota
        return anggota
    }

    fun cari(id: String): Anggota? = daftar[id]

    fun semua(): List<Anggota> = daftar.values.toList().sortedBy { it.id }

    fun hapus(id: String): Boolean = daftar.remove(id) != null
}

fun main() {
    val repoBuku = RepoBuku()
    val repoAnggota = RepoAnggota()

    println("=== Tambah buku ===")
    repoBuku.tambah(Buku("B001", "Belajar Kotlin", "Budi Santoso", stok = 2))
    repoBuku.tambah(Buku("B002", "Pemrograman Modern", "Siti Aminah", stok = 1))
    repoBuku.semua().forEach { println("[${it.id}] ${it.judul} stok=${it.stok}") }

    println()
    println("=== Tambah anggota ===")
    repoAnggota.tambah(Anggota("A001", "Dewi Lestari", "[email protected]"))
    repoAnggota.tambah(Anggota("A002", "Rizki Ramadhan", "[email protected]"))
    repoAnggota.semua().forEach { println("[${it.id}] ${it.nama} <${it.email}>") }

    println()
    println("=== Perbarui stok buku B001 ===")
    val bukuLama = repoBuku.cari("B001")!!
    repoBuku.perbarui(bukuLama.copy(stok = 5))
    println("Stok baru: ${repoBuku.cari("B001")?.stok}")

    println()
    println("=== Hapus anggota A002 ===")
    val terhapus = repoAnggota.hapus("A002")
    println("Terhapus: $terhapus. Sisa: ${repoAnggota.semua().size} anggota")

    println()
    println("=== Validasi gagal ===")
    try {
        repoBuku.tambah(Buku("B001", "Duplikat", "Penulis X"))
    } catch (e: IllegalArgumentException) {
        println("Galat: ${e.message}")
    }
    try {
        repoAnggota.tambah(Anggota("A003", "Email Salah", "email-tidak-valid"))
    } catch (e: IllegalArgumentException) {
        println("Galat: ${e.message}")
    }
}

Output yang Dihasilkan:

text
=== Tambah buku ===
[B001] Belajar Kotlin stok=2
[B002] Pemrograman Modern stok=1

=== Tambah anggota ===
[A001] Dewi Lestari <[email protected]>
[A002] Rizki Ramadhan <[email protected]>

=== Perbarui stok buku B001 ===
Stok baru: 5

=== Hapus anggota A002 ===
Terhapus: true. Sisa: 1 anggota

=== Validasi gagal ===
Galat: ID B001 sudah dipakai
Galat: Email tidak valid

Mari kita bedah elemen kunci:

  1. mutableMapOf<String, Buku>(): Map menyimpan pasangan ID dan entitas. Akses berdasarkan ID bersifat O(1) sehingga efisien untuk ratusan entitas.
  2. require(!daftar.containsKey(buku.id)) { ... }: require melempar IllegalArgumentException jika kondisi salah. Cocok untuk preconditon yang menandakan bug pemanggil.
  3. daftar.values.toList().sortedBy { it.id }: mengembalikan list snapshot terurut. Pemanggil tidak dapat mengubah internal repo melalui list ini.
  4. repoBuku.cari("B001")!!: operator !! meyakinkan kompilator bahwa hasil tidak null. Hanya aman dipakai jika kita yakin ID ada; di kode produksi, gunakan ?.let { ... } untuk menangani null dengan aman.
  5. bukuLama.copy(stok = 5): membuat instance baru dengan stok diubah. Instance asli bukuLama tetap utuh.
  6. daftar.remove(id) != null: remove mengembalikan entitas yang dihapus atau null. Boolean hasilnya langsung dipakai sebagai indikator sukses.

Validasi yang Tepat di Lapisan Tepat #

Validasi dibagi dua:

  • Validasi format (panjang judul, format email) ada di repo. Setiap entri wajib lulus.
  • Validasi bisnis (stok tidak boleh negatif, status transaksi) akan diatur di Bagian 3.

Memisahkan validasi format di repo mencegah duplikasi: tidak peduli dari mana pemanggil berasal (CLI, REST API, test), aturan yang sama berlaku.

Immutability dan Copy #

Seluruh property pada Buku dan Anggota memakai val. Pembaruan tidak mengubah instance asli, melainkan membuat instance baru via copy(). Pendekatan ini disebut immutable domain model dan memberi tiga manfaat:

  • Aman concurrency: instance tidak berubah, tidak perlu sinkronisasi.
  • Mudah ditelusuri: setiap perubahan menghasilkan instance baru yang dapat dilacak.
  • Prediktabilitas: instance yang sama selalu menghasilkan output yang sama.

Kekurangannya adalah alokasi memori tambahan, tetapi untuk aplikasi perpustakaan kecil ini tidak signifikan.

Referensi Lanjutan #

Ringkasan Bagian 2 #

Operasi CRUD untuk buku dan anggota kini berfungsi. RepoBuku dan RepoAnggota memvalidasi ID unik, format email, dan panjang judul. Pola copy() menjaga immutability sambil memungkinkan pembaruan. Pada Bagian 3 kita menambahkan logika peminjaman dan pengembalian yang menghubungkan kedua entitas ini.