Pada Bagian 2 kita mengelola entitas secara terpisah. Sekarang kita menghubungkannya melalui transaksi peminjaman. Logika ini adalah inti bisnis perpustakaan: meminjamkan buku mengurangi stok, mengembalikan menambah stok, dan status transaksi melacak keadaan setiap peminjaman.

Aturan Bisnis Peminjaman #

Empat aturan yang harus dipenuhi:

  • Buku harus terdaftar di repo.
  • Anggota harus terdaftar di repo.
  • Stok buku harus lebih besar dari nol.
  • Satu transaksi hanya dapat dikembalikan satu kali.

Pelanggaran aturan tidak boleh melempar exception tak terduga; sebaliknya, fungsi mengembalikan Result<Transaksi> yang berisi pesan galat yang informatif.

Implementasi Class Perpustakaan #

Class Perpustakaan menyatukan buku, anggota, dan transaksi. Logika bisnis terkonsentrasi di sini sehingga mudah diuji:

kotlin
// Bagian 3: Logika peminjaman dan pengembalian.
// Mendemonstrasikan aturan bisnis peminjaman (stok berkurang, transaksi
// dibuat) dan pengembalian (stok kembali, transaksi ditandai selesai).

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

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

enum class StatusTransaksi { AKTIF, DIKEMBALIKAN }

data class Transaksi(
    val id: String,
    val bukuId: String,
    val anggotaId: String,
    val status: StatusTransaksi = StatusTransaksi.AKTIF
)

class Perpustakaan {
    private val buku = mutableMapOf<String, Buku>()
    private val anggota = mutableMapOf<String, Anggota>()
    private val transaksi = mutableListOf<Transaksi>()
    private var urutanId = 0

    fun daftarkanBuku(b: Buku) { buku[b.id] = b }
    fun daftarkanAnggota(a: Anggota) { anggota[a.id] = a }

    fun pinjam(bukuId: String, anggotaId: String): Result<Transaksi> {
        val b = buku[bukuId] ?: return Result.failure(IllegalStateException("Buku tidak ditemukan"))
        val a = anggota[anggotaId] ?: return Result.failure(IllegalStateException("Anggota tidak ditemukan"))
        if (b.stok <= 0) return Result.failure(IllegalStateException("Stok ${b.judul} habis"))
        buku[bukuId] = b.copy(stok = b.stok - 1)
        urutanId += 1
        val t = Transaksi("T%03d".format(urutanId), bukuId, anggotaId)
        transaksi.add(t)
        return Result.success(t)
    }

    fun kembalikan(transaksiId: String): Result<Transaksi> {
        val t = transaksi.firstOrNull { it.id == transaksiId }
            ?: return Result.failure(IllegalStateException("Transaksi tidak ditemukan"))
        if (t.status == StatusTransaksi.DIKEMBALIKAN) {
            return Result.failure(IllegalStateException("Transaksi sudah dikembalikan"))
        }
        val idx = transaksi.indexOf(t)
        transaksi[idx] = t.copy(status = StatusTransaksi.DIKEMBALIKAN)
        val b = buku[t.bukuId]!!
        buku[t.bukuId] = b.copy(stok = b.stok + 1)
        return Result.success(transaksi[idx])
    }

    fun laporanStok(): List<Pair<String, Int>> = buku.values.map { it.judul to it.stok }.sortedBy { it.first }
    fun transaksiAktif(): List<Transaksi> = transaksi.filter { it.status == StatusTransaksi.AKTIF }
}

fun main() {
    val p = Perpustakaan()
    p.daftarkanBuku(Buku("B001", "Belajar Kotlin", stok = 2))
    p.daftarkanBuku(Buku("B002", "Pemrograman Modern", stok = 1))
    p.daftarkanAnggota(Anggota("A001", "Dewi Lestari"))
    p.daftarkanAnggota(Anggota("A002", "Rizki Ramadhan"))

    println("=== Peminjaman ===")
    val t1 = p.pinjam("B001", "A001").getOrThrow()
    println("Transaksi ${t1.id}: buku=${t1.bukuId} anggota=${t1.anggotaId}")
    val t2 = p.pinjam("B001", "A002").getOrThrow()
    println("Transaksi ${t2.id}: buku=${t2.bukuId} anggota=${t2.anggotaId}")
    println("Stok setelah pinjam: ${p.laporanStok()}")

    println()
    println("=== Peminjaman ketiga (stok habis) ===")
    val gagal = p.pinjam("B001", "A001")
    println("Hasil: ${gagal.fold(onSuccess = { "berhasil" }, onFailure = { it.message })}")

    println()
    println("=== Pengembalian ===")
    val k1 = p.kembalikan(t1.id).getOrThrow()
    println("Transaksi ${k1.id}: status=${k1.status}")
    println("Stok setelah kembali: ${p.laporanStok()}")

    println()
    println("=== Pengembalian ganda ===")
    val kGanda = p.kembalikan(t1.id)
    println("Hasil: ${kGanda.fold(onSuccess = { "berhasil" }, onFailure = { it.message })}")

    println()
    println("=== Transaksi aktif ===")
    p.transaksiAktif().forEach { println("[${it.id}] buku=${it.bukuId} anggota=${it.anggotaId}") }
}

Output yang Dihasilkan:

text
=== Peminjaman ===
Transaksi T001: buku=B001 anggota=A001
Transaksi T002: buku=B001 anggota=A002
Stok setelah pinjam: [(Belajar Kotlin, 0), (Pemrograman Modern, 1)]

=== Peminjaman ketiga (stok habis) ===
Hasil: Stok Belajar Kotlin habis

=== Pengembalian ===
Transaksi T001: status=DIKEMBALIKAN
Stok setelah kembali: [(Belajar Kotlin, 1), (Pemrograman Modern, 1)]

=== Pengembalian ganda ===
Hasil: Transaksi sudah dikembalikan

=== Transaksi aktif ===
[T002] buku=B001 anggota=A002

Mari kita bedah logika bisnis:

  1. val b = buku[bukuId] ?: return Result.failure(...): operator Elvis memilih antara buku yang ditemukan atau mengembalikan kegagalan. Tidak ada exception yang dilempar; pemanggil memutuskan cara menangani.
  2. if (b.stok <= 0) return Result.failure(...): validasi stok. Pesan galat menyertakan judul buku agar pengguna tahu buku mana yang habis.
  3. buku[bukuId] = b.copy(stok = b.stok - 1): mengganti entri Map dengan instance buku baru yang stoknya dikurangi. Instance lama tetap tidak berubah.
  4. Transaksi("T%03d".format(urutanId), ...): format ID dengan tiga digit, misalnya T001, T002. Konsistensi format penting untuk pelaporan.
  5. if (t.status == StatusTransaksi.DIKEMBALIKAN): mencegah pengembalian ganda. Tanpa pemeriksaan ini, stok bisa ditambah berulang kali.
  6. transaksi[idx] = t.copy(status = ...): mengganti transaksi di list dengan instance baru berstatus DIKEMBALIKAN. List mutable memungkinkan operasi ini tanpa membuat list baru.

Laporan dan Query #

Method laporanStok() dan transaksiAktif() menyediakan tampilan agregat:

  • laporanStok() mengembalikan list pasangan (judul, stok) terurut abjad. Berguna untuk audit cepat.
  • transaksiAktif() memfilter transaksi dengan status AKTIF. Berguna untuk melihat siapa yang masih memegang buku.

Kedua method ini tidak mengembalikan mutable collection, sehingga pemanggil tidak dapat mengubah internal Perpustakaan lewat hasil query.

Atomicity dan Konsistensi #

Operasi peminjaman mengubah dua hal: stok buku dan daftar transaksi. Jika salah satu gagal, yang lain harus tetap konsisten. Pada kode di atas, urutan operasi dipilih agar aman:

  1. Validasi (buku ada, anggota ada, stok cukup) — semua sebelum mutasi.
  2. Mutasi stok (buku berkurang).
  3. Penambahan transaksi.

Jika langkah 3 gagal (misalnya kehabisan memori), stok buku sudah berkurang tanpa transaksi yang sesuai. Untuk aplikasi sederhana ini dapat ditoleransi; pada produksi, gunakan transaksi database atau rollback manual.

Referensi Lanjutan #

Ringkasan Bagian 3 #

Logika peminjaman dan pengembalian kini berfungsi: pinjam() membuat transaksi AKTIF dan mengurangi stok, kembalikan() menandai transaksi DIKEMBALIKAN dan menambah stok. Aturan bisnis (stok tidak negatif, tidak ada pengembalian ganda) diterapkan via Result. Pada Bagian 4 kita menambahkan persistensi ke file JSON dan unit test untuk logika bisnis ini.