Proyek kedua kita adalah sistem manajemen inventaris berbasis terminal: aplikasi REPL yang menerima perintah seperti tambah P001 Kopi 15000 20, menyimpan produk di memori, lalu mencetak daftarnya dalam tabel rapi. Sebelum ada satu perintah pun yang berfungsi, kita tetapkan dulu fondasinya — bentuk data produk, aturan sahnya, dan tipe kegagalannya — karena seluruh bagian berikutnya bertumpu pada keputusan kecil ini. Pola kerjanya sama seperti proyek kalkulator sebelumnya: logika ditulis sebagai function murni yang melaporkan kegagalan lewat tipe kembalian, dan I/O hanya terjadi di main. Perbedaannya, kali ini datanya lebih kaya (empat field per produk), aturannya lebih ketat (harga dan stok tidak boleh negatif), dan programnya tumbuh sampai lima bagian.
Peta Proyek dan Struktur Berkas #
Selama lima bagian kita membangun satu berkas inventaris.cpp secara bertahap:
sistem-manajemen-inventaris/
├── _index.md (halaman overview proyek)
├── part-1-merancang-model-data/
├── part-2-mengelola-produk/
├── part-3-menerapkan-pencarian-dan-filter/
├── part-4-menyimpan-data-ke-file/
├── part-5-menguji-fungsi-aplikasi/
├── src/
│ └── inventaris.cpp (program final, lengkap, teruji)
└── tests/
└── uji-inventaris.cpp (mini test harness, 51 pemeriksaan)Bagian ini menetapkan model data, Bagian 2 membangun operasi CRUD di atas class Inventaris, Bagian 3 menambahkan pencarian dan pengurutan, Bagian 4 memindahkan data ke berkas, dan Bagian 5 mengunci semuanya dengan pengujian. Program utuh selalu bisa dilihat di src/, sementara setiap bagian membedah potongan yang sedang dikerjakan.
Struct Produk dan Invariant-nya #
Model data kita adalah struct dengan empat field dan satu function pemeriksa:
struct Produk {
std::string kode;
std::string nama;
long long harga = 0; // dalam rupiah
int stok = 0;
// invariant yang harus dipatuhi setiap produk
bool sah() const {
return !kode.empty() && !nama.empty() && harga >= 0 && stok >= 0;
}
};Tiga keputusan di sini layak dibedah. Pertama, harga bertipe long long — harga rupiah bisa mencapai puluhan juta tanpa pecahan, dan long long memberi rentang jauh di atas kebutuhan sehingga meluap bukan pertimbangan; stok cukup dengan int. Kedua, aturan kesahan ditulis sekali sebagai method sah(): kode dan nama tidak boleh kosong, harga dan stok tidak boleh negatif. Menyatukan aturan di satu tempat mencegah empat if tersebar yang lama-kelamaan berbeda satu sama lain. Ketiga, sah() bertanda const — pemeriksaan tidak boleh mengubah data, dan compiler kini menegakkannya.
Perhatikan juga inisialisasi default = 0 pada dua field angka. Tanpa itu, Produk p; akan membawa harga dan stok berisi nilai sampah; dengannya, setiap produk baru lahir dalam keadaan yang bisa dijelaskan, lalu diisi lewat jalur yang memeriksa kesahan. Model data yang baik membuat keadaan tidak sah sulit terjadi, bukan sekadar mudah dideteksi.
Tipe Galat sebagai Bagian dari Kontrak #
Aplikasi inventaris punya banyak cara gagal: kode kosong, data negatif, kode duplikat, produk tidak ditemukan, berkas tidak bisa dibuka. Semua itu kita daftarkan sebagai enum, sehingga pemanggil tahu persis jenis kegagalan apa saja yang mungkin terjadi:
enum class JenisGalat {
KodeKosong, // kode produk berupa string kosong
DataTidakSah, // nilai melanggar invariant (negatif, kosong, bukan angka)
KodeDuplikat, // kode sudah dipakai produk lain
TidakDitemukan, // kode tidak ada dalam inventaris
BerkasGagal, // berkas tidak dapat dibuka
BarisRusak, // baris berkas tidak mengikuti format
};
struct Galat {
JenisGalat jenis;
std::string rincian;
};
std::string pesanGalat(const Galat& g) {
switch (g.jenis) {
case JenisGalat::KodeKosong:
return "kode tidak boleh kosong";
case JenisGalat::DataTidakSah:
return "data tidak sah: " + g.rincian;
case JenisGalat::KodeDuplikat:
return "kode duplikat: " + g.rincian;
case JenisGalat::TidakDitemukan:
return "produk tidak ditemukan: " + g.rincian;
case JenisGalat::BerkasGagal:
return "berkas tidak dapat dibuka: " + g.rincian;
case JenisGalat::BarisRusak:
return "baris rusak: " + g.rincian;
}
return "galat tidak diketahui";
}Setiap jenis galat punya pesan Indonesia yang jelas lewat pesanGalat, dan field rincian membawa konteks — kode produk yang bermasalah, nomor baris berkas, atau nama berkas yang gagal dibuka. Pengembalian function yang bisa gagal bertipe std::expected<void, Galat> atau std::expected<T, Galat>: kegagalan masuk ke dalam tipe, jadi pemanggil yang mengabaikannya akan terlihat jelas saat review. Enum ini juga menjadi kosakata bersama antara kode aplikasi dan test harness di Bagian 5.
Bandingkan pendekatan ini dengan pola lama yang masih sering ditemui: function mengembalikan bool, mencetak pesannya sendiri, dan pemanggil hanya tahu “gagal” tanpa tahu mengapa. Dengan tipe galat yang terstruktur, satu function yang sama bisa dipakai dua pemanggil dengan kebijakan berbeda — REPL mencetak pesannya, test harness memeriksa jenisnya — tanpa mengubah logika sedikit pun. Itulah buah dari pemisahan logika dan I/O yang kita pegang sejak awal.
Utilitas: Tokenisasi dan Pengurai Angka #
Dua function utilitas melayani seluruh aplikasi. Parser perintah memotong baris masukan menjadi token, dan pengurai angka mengubah token menjadi bilangan:
std::vector<std::string_view> tokenisasi(std::string_view baris) {
std::vector<std::string_view> hasil;
std::size_t i = 0;
while (i < baris.size()) {
while (i < baris.size() && (baris[i] == ' ' || baris[i] == '\t')) {
++i;
}
std::size_t mulai = i;
while (i < baris.size() && baris[i] != ' ' && baris[i] != '\t') {
++i;
}
if (i > mulai) {
hasil.push_back(baris.substr(mulai, i - mulai));
}
}
return hasil;
}
// pengurai angka bulat: seluruh token harus terpakai (dari_chars, tanpa exception)
template <typename T>
std::expected<T, std::string> parseAngka(std::string_view teks) {
T nilai{};
const char* akhir = teks.data() + teks.size();
auto [ptr, ec] = std::from_chars(teks.data(), akhir, nilai);
if (ec != std::errc{} || ptr != akhir) {
return std::unexpected(std::string(teks));
}
return nilai;
}tokenisasi memakai teknik yang sama dengan kalkulator pada proyek sebelumnya: dua indeks berjalan di atas string_view, token berupa jendela tanpa salinan. Spasi ganda dan tab diperlakukan sama, sehingga pengguna yang mengetik dengan berbagai gaya tetap dilayani. Yang baru adalah parseAngka yang kini berupa template — parseAngka<long long> untuk harga dan parseAngka<int> untuk stok memakai satu implementasi yang sama. Sama seperti di kalkulator, std::from_chars dipilih karena tidak melempar exception, tidak mengalokasi, dan melaporkan sisa karakter yang tidak terurai; abc dan 12abc sama-sama ditolak, bukan diterima sebagian.
Mencoba Gerbang Validasi #
Kompilasi program utuh, lalu coba empat perintah tambah — satu sah, tiga melanggar aturan yang berbeda-beda:
g++ -std=c++23 -Wall -Wextra inventaris.cpp -o inventaris
printf 'tambah P001 Kopi 15000 20\ntambah P002 Gula 12000 -5\ntambah P001 Teh 5000 7\ntambah P003 Gula abc 7\ndaftar\n' | ./inventarisOutput yang Dihasilkan:
ditambahkan: P001 (Kopi)
galat: data tidak sah: harga dan stok tidak boleh negatif
galat: kode duplikat: P001
galat: data tidak sah: harga dan stok harus angka bulat
KODE NAMA HARGA STOK
P001 Kopi 15000 20
selesai.Empat jalur keputusan terlihat dalam satu sesi pendek. Stok -5 ditolak sebelum menyentuh inventaris, kode P001 kedua ditolak karena duplikat, dan harga abc tertahan pengurai angka — setiap kegagalan menghasilkan pesan yang menyebut penyebabnya, dan program tetap berjalan ke baris berikutnya. Hanya produk yang lolos semua gerbang yang muncul di daftar.
Fondasi ini sudah cukup untuk bekerja: modelnya jelas, aturannya terpusat, dan kegagalannya bertipe. Bagian 2 membangun class Inventaris yang menjaga vector produk dan menawarkan operasi tambah, ubah, hapus, dan cari.
Rujukan resmi: std::from_chars (cppreference) , std::expected (cppreference) .