Sampai bagian ini, seluruh perpustakaan hidup dalam satu objek di memori — menutup terminal berarti kehilangan semua buku, anggota, dan catatan peminjaman. Bagian ini memberi sistem ingatan jangka panjang: perintah SIMPAN menuliskan seluruh state ke berkas teks, dan MUAT membacanya kembali, bahkan ke dalam proses yang sama sekali baru. Persoalan menariknya bukan menulis, melainkan membaca: berkas bisa hilang, rusak, atau isinya melanggar invariant kita, dan muatDari harus menolak semuanya tanpa merusak data yang sudah ada di memori. Strateginya satu kalimat — bangun state baru secara terpisah, dan hanya bila seluruh berkas lolos validasi, gantikan state lama.
Format Berkas: Satu Baris, Satu Entitas #
Kita memilih teks polos dengan | sebagai pemisah kolom, satu baris untuk satu entitas. Tiga jenis baris yang ditulis dan dibaca program:
| Awalan baris | Kolom | Contoh |
|---|---|---|
BUKU | ISBN, judul, penulis, stok | BUKU|978-6021|Pemrograman C++ Modern|Budi Santoso|2 |
ANGGOTA | id, nama | ANGGOTA|1|Budi |
PEMINJAMAN | ISBN, id anggota, status | PEMINJAMAN|978-6021|1|Dipinjam |
Teks polos mungkin kurang padat dibanding format biner, tetapi dua keuntungannya sulit ditolak untuk proyek sebesar ini. Pertama, hasilnya bisa diperiksa dengan cat atau teks editor tanpa alat bantu — saat data terlihat aneh, Anda langsung tahu baris mana yang salah. Kedua, penguraian dan penulisan simetris: setiap baris adalah kumpulan kolom yang dipisah pisah, helper yang sama yang sudah kita pakai untuk perintah stdin sejak Bagian 1.
Menyimpan dengan ofstream #
Method simpanKe menuliskan ketiga collection secara berurutan:
bool simpanKe(const std::string& path) const {
std::ofstream keluar{path};
if (!keluar) {
return false;
}
for (const Buku& b : daftarBuku_) {
keluar << "BUKU|" << b.isbn << '|' << b.judul << '|' << b.penulis
<< '|' << b.stok << '\n';
}
for (const Anggota& a : daftarAnggota_) {
keluar << "ANGGOTA|" << a.id << '|' << a.nama << '\n';
}
for (const Peminjaman& p : daftarPeminjaman_) {
keluar << "PEMINJAMAN|" << p.isbn << '|' << p.idAnggota << '|'
<< statusKeTeks(p.status) << '\n';
}
return static_cast<bool>(keluar);
}Tiga hal penting di dalamnya. Konstruktor std::ofstream keluar{path} langsung membuka berkas untuk ditulis, dan if (!keluar) memeriksa keberhasilannya — berkas bisa gagal terbuka karena direktori tidak ada atau izin tertulis tidak cukup, dan method harus melapor false alih-alih menulis ke stream yang mati. Enum StatusPeminjaman dikembalikan menjadi teks lewat statusKeTeks, function kecil dari Bagian 1 yang menjaga representasi status konsisten di satu tempat. Terakhir, nilai kembalian static_cast<bool>(keluar) memeriksa stream setelah seluruh penulisan selesai — penulisan bisa gagal di tengah jalan karena disk penuh, dan method melaporkan keadaan akhir, bukan keadaan awal.
Memuat dengan Strategi Fail-Closed #
muatDari adalah method paling defensif di seluruh proyek:
bool muatDari(const std::string& path) {
std::ifstream masuk{path};
if (!masuk) {
return false;
}
// Muat ke objek sementara; data lama tetap utuh bila berkas rusak.
Perpustakaan hasil;
std::string baris;
while (std::getline(masuk, baris)) {
if (baris.empty()) {
continue;
}
const std::vector<std::string> kolom = pisah(baris, '|');
if (kolom[0] == "BUKU" && kolom.size() == 5) {
const auto stok = keInt(kolom[4]);
if (!stok || *stok < 0
|| !hasil.tambahBuku(Buku{kolom[1], kolom[2], kolom[3], *stok})) {
return false;
}
} else if (kolom[0] == "ANGGOTA" && kolom.size() == 3) {
const auto id = keInt(kolom[1]);
if (!id || *id <= 0 || !hasil.daftarAnggota(Anggota{*id, kolom[2]})) {
return false;
}
} else if (kolom[0] == "PEMINJAMAN" && kolom.size() == 4) {
const auto id = keInt(kolom[2]);
if (!id) {
return false;
}
const StatusPeminjaman status = kolom[3] == "Dipinjam"
? StatusPeminjaman::Dipinjam
: StatusPeminjaman::Kembali;
hasil.daftarPeminjaman_.push_back(Peminjaman{kolom[1], *id, status});
} else {
return false; // format tidak dikenal — gagal dan jangan ubah state
}
}
*this = std::move(hasil);
return true;
}Bedah mekanismenya:
- Objek sementara
hasil— seluruh isi berkas dibangun ke dalamPerpustakaankosong yang terpisah. Selama penguraian, state asli*thistidak disentuh sama sekali; kegagalan di baris ke-500 hanya membuang objek sementara. - Validasi berlapis per baris — jumlah kolom diperiksa dulu (
kolom.size() == 5untukBUKU, misalnya), lalu angka diurai dengankeIntyang ketat, lalu nilai dites terhadap invariant (*stok < 0,*id <= 0), dan akhirnya method penambah yang sama dipakai untuk mewarisi pemeriksaan keunikan. Berkas tidak bisa menyelundupkan state yang tidak akan lolos lewat pintu perintah. - Baris asing langsung menolak —
elseterakhir memperlakukan awalan yang tidak dikenal sebagai kegagalan total. Melanjutkan sisa berkas sambil mengabaikan baris asing berarti memuat data yang tidak pernah utuh. *this = std::move(hasil)— hanya setelah loop selesai tanpa kegagalan, state lama digantikan oleh state baru dalam satu assignment. Move semantics membuat penggantian itu murah: vector-vector dipindahkan alih-alih disalin elemen demi elemen.
Pola ini disebut fail-closed: seluruh berkas atau tidak sama sekali. Setengah koleksi yang berhasil dimuat justru lebih berbahaya daripada kegagalan total, karena terlihat normal sampai seseorang menyadari separuh datanya tidak ada.
Round-Trip: Data yang Bertahan antar-Proses #
Ujian sesungguhnya untuk persistensi adalah dua proses yang berbeda. Kita simpan state, biarkan proses pertama mati, lalu proses kedua yang sama sekali kosong memuat datanya:
g++ -std=c++23 -Wall -Wextra perpustakaan.cpp -o perpustakaan
printf 'BUKU|978-6021|Pemrograman C++ Modern|Budi Santoso|2\nBUKU|978-6022|Algoritma dan Struktur Data|Siti Rahmawati|1\nANGGOTA|1|Budi\nANGGOTA|2|Siti\nPINJAM|978-6021|1\nSIMPAN|data.txt\nLAPOR\n' | ./perpustakaan
printf 'MUAT|data.txt\nLAPOR\n' | ./perpustakaan
echo '--- isi data.txt ---'
cat data.txtOutput yang Dihasilkan:
ok: buku "Pemrograman C++ Modern" masuk koleksi
ok: buku "Algoritma dan Struktur Data" masuk koleksi
ok: anggota Budi (id 1) terdaftar
ok: anggota Siti (id 2) terdaftar
ok: Budi meminjam "Pemrograman C++ Modern"
ok: data disimpan ke data.txt
ISBN Judul Penulis Stok
----------------------------------------------------------------------
978-6021 Pemrograman C++ Modern Budi Santoso 1
978-6022 Algoritma dan Struktur Data Siti Rahmawati 1
Peminjaman aktif: 1
"Pemrograman C++ Modern" dipinjam oleh Budi
ok: data dimuat dari data.txt
ISBN Judul Penulis Stok
----------------------------------------------------------------------
978-6021 Pemrograman C++ Modern Budi Santoso 1
978-6022 Algoritma dan Struktur Data Siti Rahmawati 1
Peminjaman aktif: 1
"Pemrograman C++ Modern" dipinjam oleh Budi
--- isi data.txt ---
BUKU|978-6021|Pemrograman C++ Modern|Budi Santoso|1
BUKU|978-6022|Algoritma dan Struktur Data|Siti Rahmawati|1
ANGGOTA|1|Budi
ANGGOTA|2|Siti
PEMINJAMAN|978-6021|1|DipinjamBandingkan blok LAPOR milik proses pertama dan proses kedua: identik baris demi baris, termasuk stok 978-6021 yang bernilai 1 dan satu peminjaman aktif atas nama Budi. Padahal proses kedua lahir dengan ketiga collection kosong — seluruh keadaan itu berasal dari berkas. Isi data.txt di tiga baris terakhir memperlihatkan formatnya secara langsung: dua baris BUKU, dua baris ANGGOTA, dan satu baris PEMINJAMAN yang merekam pinjaman aktif beserta statusnya.
Berkas Rusak Ditolak, State Tetap Utuh #
Sekarang sisi gelapnya: berkas yang melanggar format. Kita buat satu berkas dengan stok berisi abc, lalu proses yang memuat data bagus terlebih dulu mencoba berkas rusak itu:
printf 'BUKU|978-99|Buku Rusak|Penulis|abc\n' > rusak.txt
printf 'MUAT|data.txt\nMUAT|rusak.txt\nLAPOR\n' | ./perpustakaanOutput yang Dihasilkan:
ok: data dimuat dari data.txt
gagal: tidak bisa membaca rusak.txt
ISBN Judul Penulis Stok
----------------------------------------------------------------------
978-6021 Pemrograman C++ Modern Budi Santoso 1
978-6022 Algoritma dan Struktur Data Siti Rahmawati 1
Peminjaman aktif: 1
"Pemrograman C++ Modern" dipinjam oleh BudiBaris kedua keluaran melaporkan gagal: tidak bisa membaca rusak.txt — keInt("abc") menolak dan muatDari berhenti. Yang penting ada di LAPOR berikutnya: seluruh data dari data.txt masih lengkap, termasuk peminjaman aktif Budi. Objek sementara yang gagal dibuang, state utama tidak pernah tersentuh, dan program berlanjut ke baris perintah berikutnya seolah tidak terjadi apa-apa. Perilaku inilah yang membuat persistensi layak dipercaya.
Seluruh fungsi aplikasi kini ada: koleksi, peminjaman, dan persistensi. Tinggal satu tugas yang memisahkan “program yang sepertinya benar” dari “program yang terbukti benar” — menguncinya dengan pengujian, itulah tugas Bagian 5.
Rujukan resmi: std::ofstream (cppreference) , std::ifstream (cppreference) .