Ketika method menerima parameter null atau saldo negatif, siapa yang bersalah — pemanggil atau method? Tanpa aturan tertulis, jawabannya berubah-ubah dan debugging menjadi saling menuduh. Design by Contract (DbC), dicetuskan Bertrand Meyer untuk bahasa Eiffel, menjawabnya dengan analogi kontrak bisnis: kedua pihak menyepakati kewajiban sebelum bekerja. Method menyatakan prasyarat yang harus dipenuhi pemanggil, jaminan yang dijanjikan method setelah selesai, dan invarian yang harus selalu benar. Chapter ini menerapkan ketiganya di Java melalui Objects.requireNonNull, Optional, assert, dan Javadoc.

Tiga Klausul Kontrak #

KlausulPertanyaanYang menanggung saat dilanggar
PreconditionApa yang harus benar sebelum method dipanggil?Pemanggil wajib memperbaiki panggilannya
PostconditionApa yang dijamin benar setelah method selesai?Method wajib diperbaiki
InvariantApa yang harus selalu benar pada setiap keadaan teramati?Class wajib diperbaiki

Pembagian tanggung jawab inilah kekuatan utama DbC. Prasyarat gagal berarti pemanggil salah — cukup lempar IllegalArgumentException dan biarkan pengecualian menunjuk baris pemanggil. Jaminan gagal berarti method curang — inilah bug pada method, bukan pada pemanggil.

Kontrak Transfer Rekening #

Program berikut memindahkan saldo antarrekening dengan kontrak lengkap: prasyarat divalidasi di awal, jaminan diperiksa assert di akhir, dan seluruh kontrak didokumentasikan lewat Javadoc:

import java.util.Objects;

public class RekeningKontrak {
    public static void main(String[] args) {
        Rekening budi = new Rekening("Budi", 500_000);
        Rekening siti = new Rekening("Siti", 100_000);

        budi.transfer(siti, 150_000);
        System.out.println("Saldo Budi: Rp" + budi.saldo());
        System.out.println("Saldo Siti: Rp" + siti.saldo());

        try {
            budi.transfer(null, 50_000); // melanggar prasyarat
        } catch (NullPointerException e) {
            System.out.println("Ditolak: " + e.getMessage());
        }

        try {
            budi.transfer(siti, 999_999); // melanggar prasyarat saldo
        } catch (IllegalArgumentException e) {
            System.out.println("Ditolak: " + e.getMessage());
        }
    }
}

class Rekening {
    private final String pemilik;
    private long saldo;

    Rekening(String pemilik, long saldoAwal) {
        this.pemilik = Objects.requireNonNull(pemilik, "pemilik tidak boleh null");
        if (saldoAwal < 0) {
            throw new IllegalArgumentException("saldo awal tidak boleh negatif");
        }
        this.saldo = saldoAwal;
    }

    /**
     * Memindahkan saldo ke rekening tujuan.
     *
     * Prasyarat : tujuan tidak null; jumlah positif; jumlah <= saldo rekening ini.
     * Jaminan   : saldo rekening ini berkurang tepat jumlah;
     *             saldo tujuan bertambah tepat jumlah;
     *             total dana kedua rekening tetap.
     */
    void transfer(Rekening tujuan, long jumlah) {
        Objects.requireNonNull(tujuan, "tujuan tidak boleh null");
        if (jumlah <= 0) {
            throw new IllegalArgumentException("jumlah harus positif: " + jumlah);
        }
        if (jumlah > saldo) {
            throw new IllegalArgumentException(
                    "saldo tidak cukup: Rp" + saldo + " < Rp" + jumlah);
        }

        long totalSebelum = this.saldo + tujuan.saldo;

        this.saldo -= jumlah;
        tujuan.saldo += jumlah;

        assert this.saldo + tujuan.saldo == totalSebelum : "total dana berubah";
        assert this.saldo >= 0 && tujuan.saldo >= 0 : "saldo negatif";
    }

    long saldo() {
        return saldo;
    }
}

Output yang Dihasilkan:

text
Saldo Budi: Rp350000
Saldo Siti: Rp250000
Ditolak: tujuan tidak boleh null
Ditolak: saldo tidak cukup: Rp350000 < Rp999999

Mari kita bedah klausul kontrak pada program tersebut:

  1. Baris 47 (Objects.requireNonNull(tujuan, ...)): prasyarat “tujuan tidak null” dinyatakan satu baris. Method bawaan java.util.Objects ini melempar NullPointerException dengan pesan yang kita tulis — singkat, tidak dapat dilupakan, dan tetap berjalan di production.
  2. Baris 48-54 (dua IllegalArgumentException): prasyarat bisnis divalidasi sebelum state disentuh. Pemanggil menerima pesan yang menjelaskan angka aktual (Rp350000 < Rp999999), sehingga perbaikan panggilan tidak perlu menebak.
  3. Baris 56 (long totalSebelum = ...): nilai “sebelum” direkam untuk memeriksa jaminan “sesudah”. Tanpa rekaman ini, postcondition tidak dapat diuji.
  4. Baris 61-62 (dua assert): jaminan diperiksa setelah mutasi — total dana tetap dan tidak ada saldo negatif. Kata kunci assert berjalan hanya bila JVM diaktifkan dengan flag -ea (enable assertions); pada production flag ini biasanya mati, sehingga assert menjadi pemeriksaan gratis untuk pengembangan dan pengujian.

Javadoc pada baris 38-45 bukan hiasan. Dokumentasi kontrak inilah yang dibaca pemanggil agar pelanggaran tidak terjadi sejak awal — IDE menampilkannya pada setiap saran pelengkapan kode.

Optional sebagai Kontrak Hasil #

Kontrak tidak berhenti di prasyarat. Method pencarian sering mengembalikan null untuk kasus “tidak ada”, lalu pemanggil lupa memeriksa — dan NullPointerException muncul jauh dari sumber masalahnya. Optional<T> mengubah kontrak hasil menjadi eksplisit dalam tanda tangan method: “hasil mungkin kosong, dan compiler memaksa Anda menyadarinya”:

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class PencarianOpsional {
    public static void main(String[] args) {
        KatalogProduk katalog = new KatalogProduk();
        katalog.tambah("P-001", "Keyboard mekanis");
        katalog.tambah("P-002", "Mouse nirkabel");

        System.out.println(katalog.cari("P-001")
                .map(nama -> "Ditemukan: " + nama)
                .orElse("Tidak ditemukan"));
        System.out.println(katalog.cari("P-999")
                .map(nama -> "Ditemukan: " + nama)
                .orElse("Tidak ditemukan"));
    }
}

class KatalogProduk {
    private final Map<String, String> produk = new HashMap<>();

    void tambah(String kode, String nama) {
        produk.put(kode, nama);
    }

    /**
     * Mencari nama produk berdasarkan kode.
     *
     * Prasyarat : kode tidak null.
     * Jaminan   : hasil tidak pernah null — kode tak dikenal
     *             dikembalikan sebagai Optional.empty().
     */
    Optional<String> cari(String kode) {
        return Optional.ofNullable(produk.get(kode));
    }
}

Output yang Dihasilkan:

text
Ditemukan: Keyboard mekanis
Tidak ditemukan

Dua hal patut dicermati pada kontrak hasil tersebut:

  1. Baris 34-36 (Optional<String> cari(...)): tanda tangan method itu sendiri adalah kontrak yang dapat dibaca tanpa membuka isi. Jaminan “tidak pernah null” tercantum di Javadoc dan ditegakkan oleh tipe — pemanggil tidak bisa mengambil nilai tanpa melewati Optional.
  2. Baris 11-16 (.map(...).orElse(...)): kedua cabang hasil — ada dan kosong — ditangani berdampingan dalam satu ekspresi. Ketika kode P-999 tidak dikenal, Optional.empty() mengalir ke orElse dan tercetak Tidak ditemukan; tidak ada cabang tersembunyi yang melempar pengecualian.

Kontrak menyatakan aturan; tetap ada satu pertanyaan praktis — bagaimana bila data dari luar memang tidak bisa dipercaya? Jawabannya adalah validasi di batas sistem, topik chapter berikutnya: Defensive Programming.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PencarianOpsional.java

Unduh berkas
java
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class PencarianOpsional {
    public static void main(String[] args) {
        KatalogProduk katalog = new KatalogProduk();
        katalog.tambah("P-001", "Keyboard mekanis");
        katalog.tambah("P-002", "Mouse nirkabel");

        System.out.println(katalog.cari("P-001")
                .map(nama -> "Ditemukan: " + nama)
                .orElse("Tidak ditemukan"));
        System.out.println(katalog.cari("P-999")
                .map(nama -> "Ditemukan: " + nama)
                .orElse("Tidak ditemukan"));
    }
}

class KatalogProduk {
    private final Map<String, String> produk = new HashMap<>();

    void tambah(String kode, String nama) {
        produk.put(kode, nama);
    }

    /**
     * Mencari nama produk berdasarkan kode.
     *
     * Prasyarat : kode tidak null.
     * Jaminan   : hasil tidak pernah null — kode tak dikenal
     *             dikembalikan sebagai Optional.empty().
     */
    Optional<String> cari(String kode) {
        return Optional.ofNullable(produk.get(kode));
    }
}

RekeningKontrak.java

Unduh berkas
java
import java.util.Objects;

public class RekeningKontrak {
    public static void main(String[] args) {
        Rekening budi = new Rekening("Budi", 500_000);
        Rekening siti = new Rekening("Siti", 100_000);

        budi.transfer(siti, 150_000);
        System.out.println("Saldo Budi: Rp" + budi.saldo());
        System.out.println("Saldo Siti: Rp" + siti.saldo());

        try {
            budi.transfer(null, 50_000); // melanggar prasyarat
        } catch (NullPointerException e) {
            System.out.println("Ditolak: " + e.getMessage());
        }

        try {
            budi.transfer(siti, 999_999); // melanggar prasyarat saldo
        } catch (IllegalArgumentException e) {
            System.out.println("Ditolak: " + e.getMessage());
        }
    }
}

class Rekening {
    private final String pemilik;
    private long saldo;

    Rekening(String pemilik, long saldoAwal) {
        this.pemilik = Objects.requireNonNull(pemilik, "pemilik tidak boleh null");
        if (saldoAwal < 0) {
            throw new IllegalArgumentException("saldo awal tidak boleh negatif");
        }
        this.saldo = saldoAwal;
    }

    /**
     * Memindahkan saldo ke rekening tujuan.
     *
     * Prasyarat : tujuan tidak null; jumlah positif; jumlah <= saldo rekening ini.
     * Jaminan   : saldo rekening ini berkurang tepat jumlah;
     *             saldo tujuan bertambah tepat jumlah;
     *             total dana kedua rekening tetap.
     */
    void transfer(Rekening tujuan, long jumlah) {
        Objects.requireNonNull(tujuan, "tujuan tidak boleh null");
        if (jumlah <= 0) {
            throw new IllegalArgumentException("jumlah harus positif: " + jumlah);
        }
        if (jumlah > saldo) {
            throw new IllegalArgumentException(
                    "saldo tidak cukup: Rp" + saldo + " < Rp" + jumlah);
        }

        long totalSebelum = this.saldo + tujuan.saldo;

        this.saldo -= jumlah;
        tujuan.saldo += jumlah;

        assert this.saldo + tujuan.saldo == totalSebelum : "total dana berubah";
        assert this.saldo >= 0 && tujuan.saldo >= 0 : "saldo negatif";
    }

    long saldo() {
        return saldo;
    }
}