Setiap aplikasi berantarmuka sesungguhnya terdiri atas dua lapisan yang sifatnya berbeda: lapisan gambar — jendela, tombol, dan daftar yang dilihat pengguna — serta lapisan logika data yang menambah, mengubah, dan menghapus catatan. Bila kedua lapisan dipadatkan menjadi satu, setiap perbaikan logika harus diuji lewat antarmuka, dan bug data terselubung di balik klik tombol. Proyek ini memakai pola yang lebih mudah dikelola: bangun terlebih dahulu engine, yaitu class yang mengelola seluruh data catatan tanpa menampilkan apa pun; uji engine sampai tuntas dengan g++; lalu gambar antarmuka sebagai lapisan tipis yang hanya mendorong kejadian ke dalam engine. Lapisan GUI dibahas sebagai sketsa ilustratif pada bagian 4 — tidak ada satu pun logika catatan yang tinggal di sana.
Bagian pertama menaruh fondasinya: struct Catatan dan class MesinCatatan dengan empat operasi inti — tambah, ubah, hapus, dan daftar. Kita hanya memakai std::string dan std::vector, sehingga file ini dikompilasi tanpa library apa pun, dijalankan sebagai skenario demo yang deterministik, dan hasilnya bisa dibandingkan byte-per-byte dari run ke run.
Struktur Data dan Kelas Mesin #
Struktur datanya sengaja dibuat sekecil mungkin. Setiap catatan berdiri atas tiga kolom: id yang tetap sepanjang usia catatan, judul yang selalu satu baris, dan isi teks bebas yang boleh beberapa baris. Class MesinCatatan membungkus daftar catatan beserta aturan mainnya — pemanggil tidak pernah menyentuh std::vector secara langsung:
// mesin-catatan.cpp — inti aplikasi catatan: CRUD tanpa GUI.
// File ini mandiri: class MesinCatatan didefinisikan dan langsung diuji
// lewat skenario demo deterministik di main().
#include <iostream>
#include <string>
#include <vector>
namespace {
// Satu catatan: id tetap sepanjang usia catatan, judul satu baris,
// isi berupa teks bebas yang boleh beberapa baris.
struct Catatan {
int id = 0;
std::string judul;
std::string isi;
};
// MesinCatatan mengelola daftar catatan: tambah, ubah, hapus, daftar.
// Tidak ada GUI dan tidak ada file di lapisan ini — logika data diuji
// lebih dulu, antarmuka gambar menyusul di bagian 4.
class MesinCatatan {
public:
// Menambah catatan baru; id diisi otomatis mulai dari 1 dan naik terus.
int tambah(const std::string& judul, const std::string& isi) {
Catatan c;
c.id = idBerikutnya;
++idBerikutnya;
c.judul = judul;
c.isi = isi;
daftar_.push_back(c);
return c.id;
}
// Mengubah judul dan isi catatan milik id tertentu.
// Mengembalikan false bila id tidak ditemukan.
bool ubah(int id, const std::string& judulBaru, const std::string& isiBaru) {
Catatan* c = cariId(id);
if (c == nullptr) {
return false;
}
c->judul = judulBaru;
c->isi = isiBaru;
return true;
}
// Menghapus catatan milik id tertentu dari daftar.
// Mengembalikan false bila id tidak ditemukan.
bool hapus(int id) {
for (std::size_t i = 0; i < daftar_.size(); ++i) {
if (daftar_[i].id == id) {
daftar_.erase(daftar_.begin() + i);
return true;
}
}
return false;
}
// Jumlah catatan yang sedang tersimpan di mesin.
int jumlah() const {
return static_cast<int>(daftar_.size());
}
// Akses baca catatan pada posisi ke-i (0-based), mengikuti urutan daftar.
const Catatan& pada(int i) const {
return daftar_[static_cast<std::size_t>(i)];
}
private:
// Alamat catatan milik id tertentu, atau nullptr bila tidak ada.
Catatan* cariId(int id) {
for (Catatan& c : daftar_) {
if (c.id == id) {
return &c;
}
}
return nullptr;
}
std::vector<Catatan> daftar_;
int idBerikutnya = 1;
};
} // namespace
int main() {
MesinCatatan mesin;
// 1) Menambah tiga catatan; setiap tambahan mendapat id baru.
const int idA = mesin.tambah("Belajar C++",
"Hari ini kita membangun class MesinCatatan.");
const int idB = mesin.tambah("Daftar belanja", "Telur, beras, kopi.");
const int idC = mesin.tambah("Ide proyek",
"Aplikasi catatan dengan GUI sederhana.");
std::cout << "tambah: id " << idA << ", id " << idB << ", id "
<< idC << "\n";
// 2) Mengubah satu catatan dan menghapus catatan lain.
mesin.ubah(idB, "Daftar belanja pekan ini",
"Telur, beras, kopi, teh hijau.");
mesin.hapus(idA);
std::cout << "setelah ubah + hapus: " << mesin.jumlah()
<< " catatan tersisa\n";
// 3) Menampilkan seluruh catatan yang tersisa.
for (int i = 0; i < mesin.jumlah(); ++i) {
const Catatan& c = mesin.pada(i);
std::cout << "[" << c.id << "] " << c.judul << "\n"
<< " " << c.isi << "\n";
}
// 4) id yang sudah dihapus tidak bisa diubah lagi.
std::cout << "ubah id " << idA << " (sudah dihapus): "
<< (mesin.ubah(idA, "x", "y") ? "berhasil" : "ditolak")
<< "\n";
return 0;
}Output yang Dihasilkan:
tambah: id 1, id 2, id 3
setelah ubah + hapus: 2 catatan tersisa
[2] Daftar belanja pekan ini
Telur, beras, kopi, teh hijau.
[3] Ide proyek
Aplikasi catatan dengan GUI sederhana.
ubah id 1 (sudah dihapus): ditolakMari kita telaah baris-baris pentingnya:
struct Catatan: tiga kolom data dengan nilai default (id = 0) sehingga objek baru tidak pernah membawa anggota yang belum terinisiasi.tambah(): id diambil dariidBerikutnyayang naik terus, catatan ditaruh di ujungdaftar_lewatpush_back, dan id yang baru dikembalikan ke pemanggil agar pemanggil bisa menyebut catatan itu lagi pada operasi berikutnya.ubah(): meminta alamat elemen lewatcariId(); bila alamatnyanullptr, id tidak ada dan fungsi mengembalikanfalsetanpa menyentuh data.hapus(): mencari posisi id, laluerasemenggeser sisa elemen sehingga daftar tidak bolong; urutan catatan yang tersisa tetap sama.jumlah()danpada(i): antarmuka baca.pada(i)mengembalikanconst Catatan&, sehingga pembaca tidak bisa mengubah isi catatan tanpa lewatubah().cariId()private: pencarian alamat elemen adalah detail internal; pemanggil luar cukup bekerja dengan id.- anonymous namespace: seluruh tipe di file ini hanya terlihat di dalam file yang sama (internal linkage) — satu file, satu tanggung jawab.
main(): skenario tetap — tiga tambahan, satu perubahan, satu penghapusan, lalu satu penolakan terhadap id lama; urutan pencetakan pasti, sehingga output di atas selalu sama persis.
Keputusan yang Menjaga Kebenaran Data #
Dua keputusan kecil di mesin ini berdampak besar ke bagian-bagian berikutnya. Pertama, id tidak pernah dipakai ulang: setelah id 1 dihapus, catatan berikutnya tetap mendapat id 3, bukan kembali ke 1. Alasannya bukan selera, melainkan keselamatan identitas — kejadian Hapus id 1 yang masih tertunda di antrean (bagian 3) harus menyasar catatan yang benar, dan id yang dipakai ulang bisa membuat kejadian lama menyasar catatan baru yang berbeda. Kedua, ubah() dan hapus() mengembalikan bool, bukan melempar exception. Id tidak ditemukan adalah alur biasa dalam aplikasi catatan — pengguna bisa menekan tombol dua kali — sehingga penolakan itu diperlakukan sebagai hasil normal yang dilaporkan, bukan kondisi luar biasa.
Mesin sudah jalan, tetapi catatannya masih bertahan selama proses berjalan saja: begitu program berhenti, seluruh daftar kembali kosong. Bagian 2 menambahkan persistensi ke file teks dengan format satu catatan per blok.
Rujukan resmi: cppreference — std::vector , cppreference — std::basic_string .