Sebuah aplikasi catatan cepat berisi banyak entri, dan pengguna tidak mengingat-ingat id catatannya — pengguna mencari dengan kata. Bagian ini menambahkan mesin pencarian substring pada judul dan isi, lengkap dengan sifat yang wajar bagi pengguna: pencarian tidak membedakan huruf besar dan kecil.

Separuh lain dari bagian ini menyiapkan jembatan menuju antarmuka. Pada aplikasi GUI, tombol Tambah tidak mengubah data secara langsung — tombol itu mengumumkan kejadian, lalu kejadian diterapkan ke data oleh pemroses tunggal. Kita membangun bentuk paling sederhana dari pola tersebut: struct Kejadian dan class AntreanKejadian yang menyimpan kejadian Tambah, Ubah, Hapus, dan Tampilkan, lalu memprosesnya satu per satu berurutan. Pola inilah yang nanti diikuti signals dan slots pada bagian 4.

Pencarian Substring pada Judul dan Isi #

Metode cari() menerima satu kata kunci dan mengembalikan daftar catatan yang judul atau isinya memuat kata itu. Agar pencarian seragam, kata kunci dan teks catatan diseragamkan ke huruf kecil lewat kecilHuruf() sebelum dibandingkan:

cpp Metode MesinCatatan::cari
std::vector<Catatan> cari(const std::string& kunci) const {
    const std::string kunciKecil = kecilHuruf(kunci);
    std::vector<Catatan> hasil;
    for (const Catatan& c : daftar_) {
        if (kecilHuruf(c.judul).find(kunciKecil) != std::string::npos ||
            kecilHuruf(c.isi).find(kunciKecil) != std::string::npos) {
            hasil.push_back(c);
        }
    }
    return hasil;
}

Hasil berupa salinan (std::vector<Catatan>), bukan referensi ke dalam daftar internal, sehingga penerima bebas menyimpan hasilnya tanpa harus mengikuti perubahan mesin. Hasil yang kosong pun wajar: kata kunci yang tidak ditemukan bukan keadaan salah, melainkan jawaban normal dari mesin.

Antrean Kejadian Sederhana #

Empat jenis kejadian cukup untuk aplikasi catatan: Tambah, Ubah, Hapus, dan Tampilkan. Setiap kejadian dibawa struct Kejadian yang menyimpan jenisnya beserta data pendamping — judul dan isi untuk Tambah atau Ubah, id sasaran untuk Ubah dan Hapus. Antreannya sendiri adalah std::deque<Kejadian>: antarmuka mendorong ke belakang, dan prosesSatu() mengambil dari depan.

File antrean-kejadian.cpp berikut berdiri sendiri: engine dari bagian 1, metode cari, dan antrean kejadian diuji dalam satu skenario deterministik — enam kejadian didorong sekaligus, diproses berurutan, lalu hasilnya dicari:

cpp
// antrean-kejadian.cpp — mesin catatan + pencarian + antrean kejadian UI.
// Kejadian dari antarmuka (Tambah/Ubah/Hapus/Tampilkan) didorong ke antrean
// lalu diproses berurutan — pola jembatan menuju GUI di bagian 4.
#include <cctype>
#include <deque>
#include <iostream>
#include <string>
#include <vector>

namespace {

// Menyeragamkan teks ke huruf kecil agar pencarian tidak membedakan
// bentuk huruf besar maupun kecil.
std::string kecilHuruf(std::string teks) {
    for (char& c : teks) {
        c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
    }
    return teks;
}

// 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;
};

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)];
    }

    // Mengumpulkan catatan yang judul atau isinya memuat kunci pencarian;
    // hasil mengikuti urutan daftar, dan bisa saja kosong.
    std::vector<Catatan> cari(const std::string& kunci) const {
        const std::string kunciKecil = kecilHuruf(kunci);
        std::vector<Catatan> hasil;
        for (const Catatan& c : daftar_) {
            if (kecilHuruf(c.judul).find(kunciKecil) != std::string::npos ||
                kecilHuruf(c.isi).find(kunciKecil) != std::string::npos) {
                hasil.push_back(c);
            }
        }
        return hasil;
    }

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;
};

// Empat jenis kejadian yang mungkin dipicu antarmuka pengguna.
enum class JenisKejadian { Tambah, Ubah, Hapus, Tampilkan };

// Satu kejadian UI: jenisnya, data yang menyertainya, dan id sasaran.
// Kolom yang tidak relevan untuk jenis tertentu dibiarkan kosong.
struct Kejadian {
    JenisKejadian jenis;
    std::string judul;   // dipakai Tambah dan Ubah
    std::string isi;     // dipakai Tambah dan Ubah
    int id = 0;          // dipakai Ubah dan Hapus
};

// Antrean kejadian: antarmuka mendorong kejadian, mesin memprosesnya
// satu per satu dari depan ke belakang — tanpa satu pun akses langsung.
class AntreanKejadian {
public:
    // Menyimpan satu kejadian di belakang antrean (UI memanggil ini).
    void dorong(const Kejadian& k) {
        antrean_.push_back(k);
    }

    // Jumlah kejadian yang belum diproses.
    int jumlah() const {
        return static_cast<int>(antrean_.size());
    }

    // Menandakan antrean sudah habis diproses.
    bool kosong() const {
        return antrean_.empty();
    }

    // Mengambil satu kejadian dari depan antrean dan menerapkannya ke mesin.
    void prosesSatu(MesinCatatan& mesin) {
        const Kejadian k = antrean_.front();
        antrean_.pop_front();
        switch (k.jenis) {
        case JenisKejadian::Tambah: {
            const int id = mesin.tambah(k.judul, k.isi);
            std::cout << "antrean: Tambah -> id " << id << " \""
                      << k.judul << "\"\n";
            break;
        }
        case JenisKejadian::Ubah: {
            const bool ok = mesin.ubah(k.id, k.judul, k.isi);
            std::cout << "antrean: Ubah id " << k.id << " -> "
                      << (ok ? "berhasil" : "id tidak ditemukan") << "\n";
            break;
        }
        case JenisKejadian::Hapus: {
            const bool ok = mesin.hapus(k.id);
            std::cout << "antrean: Hapus id " << k.id << " -> "
                      << (ok ? "berhasil" : "id tidak ditemukan") << "\n";
            break;
        }
        case JenisKejadian::Tampilkan:
            std::cout << "antrean: Tampilkan -> " << mesin.jumlah()
                      << " catatan\n";
            for (int i = 0; i < mesin.jumlah(); ++i) {
                const Catatan& c = mesin.pada(i);
                std::cout << "  [" << c.id << "] " << c.judul << "\n";
            }
            break;
        }
    }

    // Memproses seluruh antrean, satu per satu dari depan ke belakang.
    void prosesSemua(MesinCatatan& mesin) {
        while (!antrean_.empty()) {
            prosesSatu(mesin);
        }
    }

private:
    std::deque<Kejadian> antrean_;
};

}  // namespace

int main() {
    MesinCatatan mesin;
    AntreanKejadian antrean;

    // 1) Antarmuka (di bagian 4: tombol Qt) hanya mendorong kejadian.
    antrean.dorong({JenisKejadian::Tambah, "Belajar C++",
                    "Mulai dari engine yang teruji.", 0});
    antrean.dorong({JenisKejadian::Tambah, "Daftar belanja",
                    "Telur, beras, kopi.", 0});
    antrean.dorong({JenisKejadian::Tambah, "Ide proyek",
                    "Pencarian catatan.", 0});
    antrean.dorong({JenisKejadian::Ubah, "Daftar belanja pekan ini",
                    "Telur, beras, kopi, teh hijau.", 2});
    antrean.dorong({JenisKejadian::Hapus, "", "", 1});
    antrean.dorong({JenisKejadian::Tampilkan, "", "", 0});
    std::cout << "kejadian menunggu: " << antrean.jumlah() << "\n";

    // 2) Seluruh antrean diproses berurutan dari depan ke belakang.
    antrean.prosesSemua(mesin);
    std::cout << "antrean kosong: "
              << (antrean.kosong() ? "ya" : "tidak") << "\n";

    // 3) Pencarian substring pada judul dan isi.
    std::cout << "cari \"belanja\":\n";
    for (const Catatan& c : mesin.cari("belanja")) {
        std::cout << "  [" << c.id << "] " << c.judul << "\n";
    }
    std::cout << "cari \"proyek\":\n";
    for (const Catatan& c : mesin.cari("proyek")) {
        std::cout << "  [" << c.id << "] " << c.judul << "\n";
    }
    std::cout << "cari \"tidak ada\": " << mesin.cari("tidak ada").size()
              << " hasil\n";
    return 0;
}

Output yang Dihasilkan:

text
kejadian menunggu: 6
antrean: Tambah -> id 1 "Belajar C++"
antrean: Tambah -> id 2 "Daftar belanja"
antrean: Tambah -> id 3 "Ide proyek"
antrean: Ubah id 2 -> berhasil
antrean: Hapus id 1 -> berhasil
antrean: Tampilkan -> 2 catatan
  [2] Daftar belanja pekan ini
  [3] Ide proyek
antrean kosong: ya
cari "belanja":
  [2] Daftar belanja pekan ini
cari "proyek":
  [3] Ide proyek
cari "tidak ada": 0 hasil

Bedah Pencarian dan Antrean #

Mari kita telaah bagian-bagian pentingnya:

  1. kecilHuruf(): memanggil std::tolower per karakter dengan cast ke unsigned char terlebih dahulu; tolower terhadap char negatif adalah undefined behavior, dan cast satu baris ini menutup jebakannya.
  2. cari(): kata kunci dikecilkan sekali di luar perulangan; find() != std::string::npos berarti substring ditemukan; urutan hasil mengikuti urutan daftar.
  3. enum class JenisKejadian: nama nilainya ber-scoped — harus ditulis JenisKejadian::Tambah — sehingga tidak bentrok dengan nama lain di file.
  4. struct Kejadian: satu struct untuk empat jenis kejadian; kolom yang tidak relevan dibiarkan kosong. Struktur sederhana ini mudah dicatat log dan mudah diuji.
  5. dorong(), jumlah(), kosong(): satu-satunya pintu bagi antarmuka; UI tidak pernah memanggil tambah/ubah/hapus secara langsung.
  6. prosesSatu(): mengambil front() lalu pop_front(), dan switch menerapkan kejadian sambil mencatat hasilnya — termasuk penolakan Hapus id 99 yang tidak ditemukan.
  7. Tampilkan: pada file ini hasil dicetak ke terminal; pada bagian 4 pekerjaan yang sama berpindah ke QListWidget, dan mesin tidak berubah sedikit pun.
  8. prosesSemua(): perulangan menghabiskan antrean; urutan eksekusi persis urutan dorongan — Ubah id 2 diterapkan sebelum Hapus id 1, sehingga hasil akhir selalu sama.

Sekarang mesin lengkap: CRUD di bagian 1, file di bagian 2, pencarian dan antrean di bagian ini. Bagian 4 menyambungkan semuanya ke widget Qt sebagai sketsa ilustratif, lalu menutup proyek dengan harness pengujian yang membuktikan seluruh perilaku engine.

Rujukan resmi: cppreference — std::deque , cppreference — std::string::find .