Fondasi model data dari Bagian 1 kini kita bungkus dalam sebuah class yang bertindak sebagai repository — istilah untuk komponen yang menyimpan dan mengambil data. Class Inventaris memiliki satu vector produk sebagai penyimpanan dan menawarkan empat operasi inti: tambah, ubah, hapus, dan cari. Semua aturan validasi yang tadi kita rumuskan kini dituang sebagai gerbang di depan data, dan semua operasi mengembalikan std::expected sehingga tidak ada satu pun jalur gagal yang bisa tersembunyi. Field penyimpannya dideklarasikan private, jadi satu-satunya cara masuk, keluar, atau berubah adalah lewat method yang memvalidasi — inilah encapsulation yang bekerja, bukan sekadar kata kunci di kelas.

Dengan pemisahan ini, main tinggal menerjemahkan perintah pengguna menjadi pemanggilan method — dan pada Bagian 5, test harness memanggil method yang sama tanpa menyentuh cin sama sekali. Peta perintah yang kita tuju sepanjang proyek terlihat pada daftar berikut; bagian ini menuntaskan empat yang pertama, sisanya menyusul:

Menambah Produk Lewat Tiga Gerbang #

Method tambah adalah penjaga utama kualitas data. Ia memeriksa tiga hal secara berurutan sebelum satu produk pun masuk vector:

cpp src/inventaris.cpp (potongan: class Inventaris, method tambah)
class Inventaris {
public:
    std::expected<void, Galat> tambah(const Produk& p) {
        if (p.kode.empty()) {
            return std::unexpected(Galat{JenisGalat::KodeKosong, ""});
        }
        if (!p.sah()) {
            return std::unexpected(Galat{JenisGalat::DataTidakSah, p.kode});
        }
        if (temukan(p.kode).has_value()) {
            return std::unexpected(Galat{JenisGalat::KodeDuplikat, p.kode});
        }
        produk_.push_back(p);
        return {};
    }

Urutan pemeriksaannya penting dan sengaja dibuat dari yang termurah. Gerbang pertama menolak kode kosong dengan jenis galat khusus, karena “produk tanpa kode” adalah kegagalan yang berbeda makna dari “produk dengan stok negatif”. Gerbang kedua mendelegasikan sisanya ke sah() — method invariant dari Bagian 1 — sehingga aturan tidak ditulis ulang di sini. Gerbang ketiga memanggil temukan() untuk memastikan kode belum dipakai; hanya setelah ketiganya lolos, push_back dijalankan. Pola ini disebut fail-closed: data hanya masuk lewat pintu yang memeriksa, tidak ada jalur samping.

Mencari Sebelum Mengubah atau Menghapus #

Tiga operasi lainnya berbagi satu kebutuhan: menemukan posisi produk berdasar kode. Sebelum melihat operasinya, mari lihat helper privat yang menjadi jantungnya:

cpp src/inventaris.cpp (potongan: helper privat temukan)
private:
    // posisi produk berdasar kode; std::nullopt bila tidak ada
    std::optional<std::size_t> temukan(std::string_view kode) const {
        for (std::size_t i = 0; i < produk_.size(); ++i) {
            if (std::string_view(produk_[i].kode) == kode) {
                return i;
            }
        }
        return std::nullopt;
    }

    std::vector<Produk> produk_;
};

temukan mengembalikan std::optional<std::size_t> — indeks bila produk ada, std::nullopt bila tidak. Menyimpan indeks alih-alih pointer atau iterator adalah pilihan yang aman untuk class ini: indeks tidak menjadi batal ketika vector tumbuh atau menyusun ulang memorinya. Dari helper inilah tiga method publik dibangun:

cpp src/inventaris.cpp (potongan: ubah, hapus, dan cari)
    std::expected<void, Galat> ubah(std::string_view kode, long long hargaBaru,
                                    int stokBaru) {
        auto indeks = temukan(kode);
        if (!indeks) {
            return std::unexpected(
                Galat{JenisGalat::TidakDitemukan, std::string(kode)});
        }
        if (hargaBaru < 0) {
            return std::unexpected(Galat{JenisGalat::DataTidakSah,
                                         std::string(kode) + " (harga negatif)"});
        }
        if (stokBaru < 0) {
            return std::unexpected(Galat{JenisGalat::DataTidakSah,
                                         std::string(kode) + " (stok negatif)"});
        }
        produk_[*indeks].harga = hargaBaru;
        produk_[*indeks].stok = stokBaru;
        return {};
    }

    std::expected<void, Galat> hapus(std::string_view kode) {
        auto indeks = temukan(kode);
        if (!indeks) {
            return std::unexpected(
                Galat{JenisGalat::TidakDitemukan, std::string(kode)});
        }
        produk_.erase(produk_.begin() + static_cast<std::ptrdiff_t>(*indeks));
        return {};
    }

    const Produk* cari(std::string_view kode) const {
        auto indeks = temukan(kode);
        return indeks ? &produk_[*indeks] : nullptr;
    }

Perhatikan tiga karakter yang berbeda dari tiga kembalian ini. ubah memeriksa kembali harga dan stok baru sebelum menulis — invariant berlaku juga saat pembaruan, bukan hanya saat pertama masuk. hapus menghitung iterator lewat produk_.begin() + *indeks, dengan static_cast<std::ptrdiff_t> agar penjumlahan dengan iterator memakai difference_type yang benar. Sementara cari sengaja mengembalikan const Produk*: nullptr berarti tidak ada, dan pointer const memastikan pemanggil hanya bisa membaca — untuk keperluan lain seperti mengubah data, pemanggil harus melewati ubah yang memvalidasi.

Antarmuka REPL untuk Perintah #

Di main, setiap perintah dipetakan ke method dengan pola yang seragam: pilah token, parse angka bila perlu, panggil method, lalu cetak hasilnya atau pesan galatnya. Karena galat sudah tipe, pencetakannya satu baris:

cpp src/inventaris.cpp (potongan: penanganan hasil tambah di main)
            Produk p{std::string(token[1]), std::string(token[2]), *harga, *stok};
            auto hasil = inv.tambah(p);
            if (hasil) {
                std::println("ditambahkan: {} ({})", p.kode, p.nama);
            } else {
                ++jumlahGalat;
                std::println("galat: {}", pesanGalat(hasil.error()));
            }

Penghitung jumlahGalat kembali berperan seperti pada kalkulator: di akhir program, exit code 1 menandakan ada perintah yang gagal. Perintah daftar memanggil cetakDaftar yang mencetak tabel dengan format {:<5} {:<16} {:>8} {:>5} — lebar kolom tetap sehingga output bisa dibandingkan baris demi baris, sifat yang kelak berguna saat pengujian.

Untuk memetakan seluruh antarmuka yang sedang kita bangun, berikut daftar perintah aplikasi beserta statusnya di titik ini:

PerintahFungsiStatus di Bagian 2
tambah <kode> <nama> <harga> <stok>menambahkan produk baruselesai
ubah <kode> <harga> <stok>mengubah harga dan stokselesai
hapus <kode>menghapus produkselesai
daftarmenampilkan seluruh produkselesai
cari <kata>mencari berdasar namaBagian 3
filter <stok-minimum>produk dengan stok >= nilaiBagian 3
urut harga|nama|stokdaftar terurutBagian 3
simpan <berkas>menyimpan data ke berkasBagian 4
muat <berkas>memuat data dari berkasBagian 4
bantuan / keluardaftar perintah / mengakhirisudah ada di main

Mencoba CRUD Lengkap #

Kompilasi lalu jalankan sesi yang menyentuh keempat operasi, termasuk dua percobaan yang harus ditolak:

bash
g++ -std=c++23 -Wall -Wextra inventaris.cpp -o inventaris
printf 'tambah P001 Kopi 15000 20\ntambah P002 Gula 12000 5\ntambah P003 Pensil 3000 100\ndaftar\nubah P002 13000 10\ndaftar\nubah P999 100 1\nhapus P003\ndaftar\nhapus P999\n' | ./inventaris

Output yang Dihasilkan:

text
ditambahkan: P001 (Kopi)
ditambahkan: P002 (Gula)
ditambahkan: P003 (Pensil)
KODE  NAMA                HARGA  STOK
P001  Kopi                15000    20
P002  Gula                12000     5
P003  Pensil               3000   100
diubah: P002
KODE  NAMA                HARGA  STOK
P001  Kopi                15000    20
P002  Gula                13000    10
P003  Pensil               3000   100
galat: produk tidak ditemukan: P999
dihapus: P003
KODE  NAMA                HARGA  STOK
P001  Kopi                15000    20
P002  Gula                13000    10
galat: produk tidak ditemukan: P999
selesai.

Sesi ini memperlihatkan siklus hidup data secara utuh: tiga produk masuk, P002 diperbarui dari 12000/5 menjadi 13000/10, P003 keluar, dan kedua percobaan terhadap P999 — mengubah maupun menghapus — ditolak dengan galat yang sama persis. Data yang tampil di daftar selalu konsisten dengan aturan Bagian 1 karena tidak ada satu pun jalur masuk yang melompati gerbang.

CRUD di memori sudah lengkap, tetapi sesuatu masih kurang: daftar yang berisi ratusan produk sulit digulir dengan mata. Bagian 3 menambahkan pencarian nama, filter stok, dan pengurutan agar informasi menemukan penggunanya.

Rujukan resmi: std::optional (cppreference) , std::expected (cppreference) .