Proyek ketiga kita adalah sistem manajemen perpustakaan berbasis terminal: mendaftarkan buku dan anggota, memproses peminjaman dan pengembalian, lalu menyimpan seluruh data ke berkas. Perpustakaan adalah latihan pemodelan yang kaya karena relasinya: buku dan anggota berdiri sendiri-sendiri, sementara peminjaman menghubungkan keduanya dengan aturan yang tidak boleh dilanggar — stok tidak boleh minus, satu anggota maksimal meminjam tiga buku. Sebelum menulis logika peminjaman, kita tetapkan dulu bentuk data, relasi antarobjek, dan invariant-nya, karena keputusan kecil di bagian ini menentukan betapa mudahnya bagian-bagian berikutnya. Program finalnya tumbuh dalam satu berkas src/perpustakaan.cpp, dan antarmukanya berupa baris perintah berkolom | dari stdin sehingga seluruh skenario bisa direkam sebagai data dan dijalankan ulang secara persis sama.
Peta Proyek dan Struktur Berkas #
Selama lima bagian kita membangun satu berkas perpustakaan.cpp secara bertahap:
sistem-manajemen-perpustakaan/
├── _index.md (halaman overview proyek)
├── part-1-merancang-class-dan-relasi/
├── part-2-mengelola-koleksi-buku/
├── part-3-menerapkan-peminjaman/
├── part-4-menyimpan-dan-memuat-data/
├── part-5-menguji-alur-peminjaman/
├── src/
│ └── perpustakaan.cpp (program final, lengkap, teruji)
└── tests/
└── uji-perpustakaan.cpp (mini test harness, 42 pemeriksaan)Bagian ini menetapkan model data dan relasinya, Bagian 2 membangun pengelolaan koleksi dan pencarian, Bagian 3 menerapkan alur peminjaman dan pengembalian, Bagian 4 memindahkan data ke berkas, dan Bagian 5 mengunci semuanya dengan pengujian. Program utuh selalu tersedia di src/, sementara setiap bagian membedah potongan yang sedang dikerjakan.
Tiga Struct dan Satu Aturan Batas #
Domain perpustakaan kita bertumpu pada tiga struct — jenis class sederhana tanpa enkapsulasi ketat — plus satu konstanta aturan main:
// Satu buku diidentifikasi oleh ISBN yang unik. `stok` adalah jumlah
// eksemplar yang tersedia saat ini (invarian: tidak pernah negatif,
// berkurang satu saat dipinjam, bertambah satu saat dikembalikan).
struct Buku {
std::string isbn;
std::string judul;
std::string penulis;
int stok = 0;
};
// Anggota perpustakaan. Buku yang sedang dipinjam TIDAK disimpan di
// sini; hubungan itu dicatat oleh class Peminjaman agar ada satu
// sumber kebenaran (single source of truth).
struct Anggota {
int id = 0;
std::string nama;
};
enum class StatusPeminjaman { Dipinjam, Kembali };
// Satu catatan peminjaman menghubungkan satu Buku dan satu Anggota.
struct Peminjaman {
std::string isbn;
int idAnggota = 0;
StatusPeminjaman status = StatusPeminjaman::Dipinjam;
};
// Aturan main: satu anggota maksimal meminjam tiga buku sekaligus.
constexpr int BATAS_PINJAM = 3;Empat keputusan di sini layak dibedah. Pertama, Buku diidentifikasi oleh isbn yang unik, dan stok berarti jumlah eksemplar yang tersedia saat ini — bukan total koleksi; itu sebabnya invariant-nya dinyatakan eksplisit di komentar: tidak pernah negatif, berkurang satu saat dipinjam, bertambah satu saat dikembalikan. Kedua, Anggota sengaja tidak menyimpan daftar buku yang sedang dipinjamnya. Jika daftar itu ada di Anggota dan catatan peminjaman juga ada di tempat lain, dua sumber kebenaran pasti berbeda isi pada suatu saat. Dengan menyimpan hubungan hanya di Peminjaman, kita punya satu tempat untuk menjawab “siapa meminjam apa”. Ketiga, StatusPeminjaman bertipe enum class — nilai Dipinjam dan Kembali bukan bilangan biasa, sehingga tidak bisa tercampur dengan int lain secara tak sengaja. Keempat, BATAS_PINJAM dideklarasikan constexpr: batas tiga buku adalah konstanta aturan main, bukan angka yang dihitung saat program berjalan, dan namanya yang bermakna menggantikan angka gaib di dalam logika.
Relasi Antarobjek dalam Class Diagram #
Model di atas terlihat lebih utuh sebagai diagram class. Perpustakaan adalah class pengelola yang memiliki tiga collection, sedangkan Peminjaman berperan sebagai catatan penghubung antara Buku dan Anggota:
Bacaan diagramnya: Perpustakaan memegang banyak Buku, banyak Anggota, dan banyak Peminjaman (agregasi, ditandai o--), sementara setiap Peminjaman merujuk tepat satu Buku lewat isbn dan satu Anggota lewat idAnggota. Multiplisitas penting untuk alur peminjaman ada di aturan yang akan kita tegakkan di Bagian 3: satu anggota boleh memiliki nol sampai tiga peminjaman berstatus Dipinjam sekaligus, dan satu pasangan (ISBN, anggota) hanya boleh punya satu peminjaman aktif. Perhatikan bahwa Peminjaman tidak menghapus catatan saat buku dikembalikan — statusnya berubah menjadi Kembali, sehingga riwayat peminjaman tetap tersimpan tanpa struktur data tambahan.
Invariant yang Menjaga Konsistensi #
Invariant adalah kondisi yang harus benar setiap saat di antara pemanggilan method. Untuk sistem ini kita tetapkan enam, beserta penjaganya:
| Invariant | Ditegakkan oleh |
|---|---|
stok tidak pernah negatif | pinjam dan kembalikan — Bagian 3 |
| ISBN unik di koleksi | tambahBuku — Bagian 2 |
| id anggota positif dan unik | daftarAnggota — Bagian 2 |
maksimal BATAS_PINJAM peminjaman aktif per anggota | pinjam — Bagian 3 |
| satu pasangan (ISBN, anggota) hanya satu peminjaman aktif | pinjam — Bagian 3 |
| buku yang sedang dipinjam tidak bisa dihapus | hapusBuku — Bagian 2 |
Menuliskan invariant lebih dulu mengubah cara kita memprogram: setiap method yang akan datang bukan lagi “kode yang memanipulasi vector”, melainkan penjaga pintu yang menolak setiap operasi membuat keadaan program melanggar daftar ini. Pola ini sama dengan yang kita pakai pada proyek inventaris — validasi terpusat di class, bukan tersebar di main — hanya saja relasinya kini melibatkan tiga entitas, bukan satu.
Tiga Utilitas Kecil yang Dipakai Di Mana-mana #
Sebelum menutup bagian ini, kita siapkan tiga function utilitas yang melayani seluruh bagian berikutnya — pencarian perlu pengubah huruf, penguraian perintah perlu pengubah teks ke angka, dan persistensi perlu pemecah kolom:
// Mengubah huruf menjadi huruf kecil untuk pencarian yang tidak peka
// huruf besar-kecil. Cast ke unsigned char mencegah undefined behavior
// untuk karakter bernilai negatif.
std::string kecilkan(std::string teks) {
for (char& c : teks) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return teks;
}
// Konversi teks ke int yang ketat: seluruh teks harus berupa angka.
// Mengembalikan std::nullopt bila gagal — tanpa exception.
std::optional<int> keInt(const std::string& teks) {
int nilai = 0;
const char* awal = teks.data();
const char* akhir = awal + teks.size();
auto [ptr, ec] = std::from_chars(awal, akhir, nilai);
if (ec != std::errc{} || ptr != akhir) {
return std::nullopt;
}
return nilai;
}
// Memecah satu baris teks berdasarkan satu karakter pemisah. Kolom
// kosong tetap dihasilkan, sehingga jumlah kolom selalu bisa diverifikasi.
std::vector<std::string> pisah(const std::string& teks, char pemisah) {
std::vector<std::string> hasil;
std::string::size_type awal = 0;
while (true) {
const std::string::size_type posisi = teks.find(pemisah, awal);
if (posisi == std::string::npos) {
hasil.push_back(teks.substr(awal));
return hasil;
}
hasil.push_back(teks.substr(awal, posisi - awal));
awal = posisi + 1;
}
}Tiga detail teknisnya penting. kecilkan melakukan cast ke unsigned char sebelum memanggil std::tolower karena melewatkan char bernilai negatif ke function itu adalah undefined behavior — jebakan klasik yang jarang terlihat di data ASCII. keInt memakai std::from_chars dengan pemeriksaan ganda: ec != std::errc{} menangkap teks yang sama sekali bukan angka, dan ptr != akhir menangkap sisa karakter seperti "12abc" — jadi penguraian harus habis, bukan sebagian. pisah sengaja tidak membuang kolom kosong; baris "A||B" menghasilkan tiga kolom, sehingga pemeriksa jumlah kolom di bagian berikutnya selalu bekerja dengan data yang jujur.
Kerangka main dan Kontrak Keluaran #
main kita sengaja dibuat kecil: satu loop yang membaca stdin baris demi baris dan menyerahkannya ke dispatcher prosesBaris:
int main() {
Perpustakaan p;
std::string baris;
while (std::getline(std::cin, baris)) {
if (!baris.empty()) {
prosesBaris(p, baris);
}
}
}Perpustakaan p hidup di stack selama program berjalan dan membawa seluruh state; main sendiri tidak tahu apa-apa tentang ISBN, stok, atau batas pinjaman. Pemisahan ini yang membuat pengujian di Bagian 5 menjadi mudah: logika bisnis bisa dipanggil tanpa menyentuh cin sama sekali. Baris kosong dilewati tanpa dihitung sebagai perintah, dan setiap baris lain dipetakan prosesBaris menjadi panggilan method plus keluaran teks yang deterministik.
Fondasinya sudah siap: tiga struct, satu aturan batas, enam invariant, dan tiga utilitas. Bagian 2 membangun class Perpustakaan di atas fondasi itu — menambah dan menghapus koleksi, mendaftarkan anggota, serta mencari buku tanpa mempedulikan huruf besar-kecil.
Rujukan resmi: std::from_chars (cppreference) , std::tolower (cppreference) .