Chapter Dependency Injection menuntaskan persoalan perakitan: object yang saling bergantung kini disambung dari luar, di satu tempat yang jelas. Pertanyaan berikutnya menyasar permukaan yang dipajang itu sendiri — setiap function dan class yang kita buka untuk pemakai lain (rekan satu tim, modul lain, program test) adalah sebuah API (application programming interface), dan kualitasnya menentukan seberapa cepat orang lain bisa bekerja dengan kode kita. API yang baik punya tiga sifat yang saling menguatkan: terbaca di tempat pemanggilan, sulit dipakai dengan cara salah, dan jujur soal kegagalan. Ketiganya bukan bakat — hasil dari keputusan kecil yang bisa dilatih: memilih nama, mengurangi parameter, dan membiarkan type system menyampaikan kontrak. Chapter ini memperagakannya dengan satu perhitungan harga yang didesain dua kali.

Dua Desain untuk Perhitungan yang Sama #

Kode dibaca jauh lebih sering daripada ditulis, dan yang paling sering dibaca pemakai API bukan implementasinya melainkan tempat pemanggilannya. Program berikut menghitung harga total pesanan dengan dua API: versi lama yang benar secara logika tetapi samar, dan versi baru yang menyatakan seluruh kontraknya lewat tipe:

cpp
// api-harga.cpp — desain API yang sama, dua wajah: lama yang samar vs baru yang eksplisit
// Kompilasi: g++ -std=c++23 -Wall -Wextra api-harga.cpp -o api-harga

#include <expected>
#include <print>
#include <string>

// ===== API lama: benar secara logika, samar bagi pemakainya =====
namespace api_lama {
    // kualitas: 1 = standar, 2 = premium, 3 = eksklusif (hanya penulisnya yang tahu)
    // return: harga total, atau -1.0 bila ada yang tidak beres
    double hitung(std::string benda, int kualitas, bool grosir, bool pajak) {
        double hargaSatuan;
        switch (kualitas) {
            case 1:  hargaSatuan = 100000.0; break;
            case 2:  hargaSatuan = 250000.0; break;
            case 3:  hargaSatuan = 600000.0; break;
            default: return -1.0;
        }
        if (benda.empty()) {
            return -1.0;
        }
        double harga = hargaSatuan * (grosir ? 0.9 : 1.0);
        if (pajak) {
            harga *= 1.11;
        }
        return harga;
    }
}

// ===== API baru: nama eksplisit, tipe kuat, galat di dalam return type =====
namespace api_baru {
    enum class Kualitas { Standar, Premium, Eksklusif };

    /// Seluruh masukan hargaTotal, lengkap dengan namanya di tempat pemakaian.
    struct PesananHarga {
        std::string namaBarang;
        Kualitas kualitas;
        int jumlah;
        bool grosir;          // harga grosir: diskon 10%
        bool termasukPajak;   // tambahkan PPN 11% bila true
    };

    /// Deskripsi kegagalan — pemakai bisa membacanya, bukan menebak kode.
    struct GalatHarga {
        std::string pesan;
    };

    class KatalogHarga {
    public:
        /// Harga satuan per kelas kualitas. Tidak pernah gagal, tidak melempar.
        static double hargaSatuan(Kualitas k) noexcept {
            switch (k) {
                case Kualitas::Standar:   return 100000.0;
                case Kualitas::Premium:   return 250000.0;
                case Kualitas::Eksklusif: return 600000.0;
            }
            return 100000.0;   // nilai aman untuk kualitas di luar daftar
        }
    };

    /// Menghitung harga total pesanan dalam rupiah.
    /// Mengembalikan GalatHarga bila jumlah tidak positif.
    std::expected<double, GalatHarga> hargaTotal(const PesananHarga& pesanan) {
        if (pesanan.jumlah <= 0) {
            return std::unexpected(GalatHarga{"jumlah harus positif"});
        }
        if (pesanan.namaBarang.empty()) {
            return std::unexpected(GalatHarga{"nama barang kosong"});
        }
        double harga = KatalogHarga::hargaSatuan(pesanan.kualitas) * pesanan.jumlah;
        if (pesanan.grosir) {
            harga *= 0.9;
        }
        if (pesanan.termasukPajak) {
            harga *= 1.11;
        }
        return harga;
    }
}

int main() {
    std::println("== API lama ==");
    double a = api_lama::hitung("Meja", 2, true, true);
    std::println("hitung(\"Meja\", 2, true, true)     -> {:.0f}", a);
    double b = api_lama::hitung("Kursi", 7, false, false);
    std::println("hitung(\"Kursi\", 7, false, false)  -> {:.0f}   <- galat atau harga?", b);

    std::println("== API baru ==");
    auto c = api_baru::hargaTotal({
        .namaBarang = "Meja", .kualitas = api_baru::Kualitas::Premium,
        .jumlah = 2, .grosir = true, .termasukPajak = true});
    if (c) {
        std::println("Meja premium x2, grosir + pajak   -> Rp{:.0f}", *c);
    }

    auto d = api_baru::hargaTotal({
        .namaBarang = "Kursi", .kualitas = api_baru::Kualitas::Standar,
        .jumlah = 0, .grosir = false, .termasukPajak = false});
    if (d) {
        std::println("tidak terduga: Rp{:.0f}", *d);
    } else {
        std::println("Kursi x0                          -> galat: {}", d.error().pesan);
    }
    return 0;
}

Output yang Dihasilkan:

text
== API lama ==
hitung("Meja", 2, true, true)     -> 249750
hitung("Kursi", 7, false, false)  -> -1   <- galat atau harga?
== API baru ==
Meja premium x2, grosir + pajak   -> Rp499500
Kursi x0                          -> galat: jumlah harus positif

Mari kita bedah keputusan desainnya:

  1. Baris 12 (signature API lama): hitung("Meja", 2, true, true) adalah kalimat tanpa subjek — 2 itu apa, dan dua true masing-masing menyalakan apa? Pemakai terpaksa membuka implementasi atau mencari komentar; informasi kontrak tersimpan di luar kode.
  2. Baris 18 dan 21 (nilai sentinel -1.0): kegagalan disembunyikan di dalam double — compiler tidak punya cara menolak pemakai yang lupa memeriksa, dan panggilan kedua di main() memperlihatkan bahayanya: -1 itu galat atau harga diskon ekstrem? Tidak ada yang bisa menjawab dari tipenya.
  3. Baris 33 (enum class Kualitas): nilai yang tidak valid kini mustahil ditulis — tidak ada lagi kualitas = 7 yang lolos kompilasi lalu gagal saat dijalankan.
  4. Baris 36-42 dan 90-92 (PesananHarga + designated initializer): lima parameter bermakna tunggal dikemas menjadi satu object, dan tempat pemanggilan membaca seperti formulir — .jumlah = 2, .grosir = true menjelaskan dirinya sendiri.
  5. Baris 64 (std::expected<double, GalatHarga>): kegagalan menjadi bagian dari return type, persis kontrak yang dibangun di chapter std::expected — nilai hanya bisa diambil setelah keberhasilannya diperiksa, seperti *c yang hanya muncul di dalam if (c).
  6. Baris 52 (noexcept) dan baris 64 (const&): dua janji yang diperiksa compiler — kita bedah di bagian berikutnya.

Aturan Praktis: Nama, Parameter, dan Return Type #

Tiga keputusan di atas berulang di hampir setiap API. Untuk nama: pilih kata kerja plus objeknya (hargaTotal, tambahBaris), dan bila satuan atau batas penting, tulis di nama — tungguMilidetik(int ms) jauh lebih aman daripada tunggu(int) yang memaksa pemakai menebak. Nama panjang yang jelas selalu menang atas nama pendek yang menggantung. Untuk parameter: jaga tetap sedikit — dua atau tiga masih nyaman; bila terus bertambah, biasanya itu beberapa data yang berubah bersama dan layak dikemas menjadi struct. Bool adalah parameter paling licin: cetak(laporan, true) tidak bercerita — ganti dengan enum class yang menyatakan makna, persis langkah dari Kualitas di atas. Masukan yang hanya dibaca dikirim sebagai const& seperti const PesananHarga& pesanan — tanpa salinan, tanpa risiko diubah.

Untuk return type: biarkan tipe yang bercerita. Tabel keputusannya singkat:

Kondisi hasilReturn typeContoh di file
Selalu ada nilainyaT biasahargaSatuan()
Mungkin tidak adastd::optional<T>pencarian yang boleh kosong
Berhasil atau galat yang wajarstd::expected<T, E>hargaTotal()
Nilai sentinel (-1, nullptr)hindarihitung() versi lama

noexcept dan const: Janji yang Diperiksa Compiler #

Sebagian kontrak tidak cukup dinyatakan dengan nama — ia perlu dipaksa. noexcept (recap chapter noexcept) adalah janji “function ini tidak melempar exception”; compiler menegakkan dengan konsekuensi tegas: bila exception lolos dari function noexcept, program dihentikan lewat std::terminate. Karena itu tempat yang tepat adalah function yang memang tidak punya jalur gagal: lookup kecil seperti hargaSatuan(), operasi swap, dan move constructor/assignment — yang terakhir penting karena container seperti std::vector hanya memakai jalur pindah yang cepat saat realokasi bila move-nya noexcept. Jangan menempelkan noexcept pada function yang kegagalannya wajar; janji yang tidak bisa dipenuhi lebih buruk daripada tidak ada janji.

Const correctness melengkapi pasangan ini. Parameter const PesananHarga& mengikat dua janji sekaligus: tidak ada salinan yang mahal, dan function tidak boleh mengubah masukan — pelanggarannya tertangkap saat kompilasi. Pada member function, const di akhir (seperti totalBaris() const) menjanjikan object tidak diubah, sehingga object const dan reference const tetap bisa memanggilnya. Kontrak semacam ini dibaca compiler, bukan dibaca manusia — itulah bedanya dengan komentar yang bisa basi.

Mengubah API Tanpa Merusak Pemakai #

API yang sudah dipakai orang lain hidup dengan aturan tambahan: setiap perubahan signature adalah breaking change — seluruh tempat pemanggilan harus ikut diubah, dan proyek dengan puluhan pemakai langsung merasakannya. Strategi yang lazim bersifat additive: tambahkan function atau overload baru, pertahankan yang lama untuk sementara, lalu migrasikan pemakai bertahap. File di atas memperagakan pola itu dalam bentuk paling sederhana: api_lama::hitung masih ada dan masih berfungsi, sementara api_baru::hargaTotal adalah generasi penerusnya — kedua versi hidup berdampingan tanpa saling merusak. Setelah semua pemakai pindah, versi lama dilepas dengan tenang.

Dokumentasi melengkapi semuanya. Kontrak minimum yang harus tertulis di atas setiap function yang dipajang: prasyarat pemanggilan (jumlah harus positif), perilaku saat gagal (mengembalikan GalatHarga, tidak melempar), dan satuan hasil (rupiah). Di file contoh, komentar /// di atas hargaTotal() dan hargaSatuan() memegang peran itu — pendek, dekat kodenya, dan ikut terversioning bersama kode. Komentar yang menjelaskan kenapa dan kontraknya bernilai; komentar yang mengulang apa yang sudah terbaca dari signature hanya menambah beban pemeliharaan.

Desain API yang kita bahas menyentuh permukaan yang terlihat: nama, parameter, return type. Padahal setiap class membawa permukaan lain yang jarang disadari sebagai API — copy-nya bisa dilakukan atau tidak, move-nya tersedia atau hilang, destructor-nya melakukan apa. Fungsi-fungsi khusus itu ikut didesain, dan aturan desainnya adalah topik chapter berikutnya: Rule of Zero.

Referensi: std::expected (cppreference) , noexcept specifier (cppreference) .