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.
| Operasi | Method | Hasil |
|---|---|---|
| Create | tambah(b) | Entitas disimpan, exception jika ID duplikat |
| Read | cari(id) | Entitas atau null |
| Read all | semua() | List entitas terurut |
| Update | perbarui(b) | Entitas diperbarui, null jika ID tidak ada |
| Delete | hapus(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:
// 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:
=== 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 validMari kita bedah elemen kunci:
mutableMapOf<String, Buku>(): Map menyimpan pasangan ID dan entitas. Akses berdasarkan ID bersifat O(1) sehingga efisien untuk ratusan entitas.require(!daftar.containsKey(buku.id)) { ... }:requiremelemparIllegalArgumentExceptionjika kondisi salah. Cocok untuk preconditon yang menandakan bug pemanggil.daftar.values.toList().sortedBy { it.id }: mengembalikan list snapshot terurut. Pemanggil tidak dapat mengubah internal repo melalui list ini.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.bukuLama.copy(stok = 5): membuat instance baru dengan stok diubah. Instance aslibukuLamatetap utuh.daftar.remove(id) != null:removemengembalikan entitas yang dihapus ataunull. 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 #
- Kotlin Collections overview
— pengantar koleksi Kotlin untuk
Mapdanfilterpada pengelolaan buku dan anggota
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.