Dua chapter sebelumnya memakai interface sebagai alat: kontrak PengirimNotifikasi membuat mailer bisa ditukar, dan kontrak SumberDataPenjualan membalik arah dependensi. Namun interface yang dirancang asal-asalan justru menambah beban — terlalu lebar, bocor detail teknis, atau dibuat untuk hal yang tidak pernah berganti implementasi. Chapter ini membahas keterampilan merancang abstraksi itu sendiri: siapa pemilik kontrak, seberapa sempit seharusnya isinya, dan kapan interface justru menjadi pemborosan. Studi kasusnya adalah pembayaran: satu kontrak, dua implementasi saling bertukar tempat, dan satu service yang tidak pernah mengetahui perbedaannya.

Interface sebagai Kontrak Tertulis #

Secara sintaks, interface adalah daftar method tanpa tubuh; class yang menulis implements wajib menyediakan seluruh method tersebut, dan type hint ke interface diperiksa interpreter saat runtime. Dasar-dasar ini telah kita pelajari pada chapter Interfaces di section Object-Oriented Programming. Dalam arsitektur, fungsi interface lebih besar dari sekadar sintaks: interface menandai batas antara dua sisi kode. Sisi pemanggil hanya melihat kontrak; sisi implementasi bebas memakai mekanisme apa pun selama kontrak terpenuhi. Kualitas batas itulah yang menentukan apakah arah dependensi dari chapter Dependency Inversion benar-benar longgar, atau hanya dipindah persendinya.

Kontrak Dimiliki oleh Konsumen #

Aturan praktis pertama: interface ditulis dari sudut pandang kode yang memakai, bukan kode yang memenuhi. Tanyakan pada use case — “apa yang saya butuhkan?” — lalu tuangkan jawabannya sebagai method. Kontrak pembayaran berikut ditulis dari sudut pandang proses checkout:

php
<?php

// Kontrak berbicara dalam bahasa konsumen (checkout), bukan bahasa penyedia.
interface GatewayPembayaran
{
    /** @return array{sukses: bool, ref: string} */
    public function bayar(string $nomorKartu, int $totalTagihan): array;
}

// Detail REST, header, dan autentikasi tinggal di dalam implementasi.
final class GatewayPembayaranRest implements GatewayPembayaran
{
    public function __construct(
        private readonly string $baseUrl,
        private readonly string $apiKey, // YOUR_API_KEY di dunia nyata
    ) {
    }

    public function bayar(string $nomorKartu, int $totalTagihan): array
    {
        // POST {baseUrl}/v1/charges dengan header Authorization: Bearer ...
        return ["sukses" => true, "ref" => "REST-1"];
    }
}

Method bayar() memakai kosakata checkout — nomor kartu dan total tagihan — bukan kosakata penyedia seperti endpoint, header, atau token. Constructor GatewayPembayaranRest tetap menyimpan baseUrl dan apiKey sebagai detail internal; tidak satu pun bocor ke kontrak. Bayangkan kebalikannya: method seperti setBaseUrl() di dalam interface memaksa setiap implementasi — termasuk yang bukan REST — ikut mengurus URL. Aturannya, hanya hal yang bisa dipenuhi setiap implementasi dengan caranya sendiri yang boleh masuk kontrak; bentuk kembalian array{sukses: bool, ref: string} juga bagian dari janji yang tidak boleh diubah sembarangan karena semua konsumen bergantung padanya.

Bundle chapter ini berisi pembayaran.php dengan tiga penghuni: kontrak GatewayPembayaran, implementasi sandbox GatewayPembayaranSandbox yang mensimulasikan panggilan penyedia sungguhan, dan LayananCheckout yang hanya mengenal kontrak. Mari jalankan checkout dengan implementasi sandbox:

php
<?php
require __DIR__ . "/pembayaran.php";

$checkout = new LayananCheckout(new GatewayPembayaranSandbox());

echo $checkout->selesaikan("[email protected]", "4111111111111111", 150000), "\n";
echo $checkout->selesaikan("[email protected]", "4000000000000002", 275000), "\n";

Output yang Dihasilkan:

text
pesanan [email protected] lunas (ref SBX-150000)
pembayaran [email protected] gagal

Panggilan pertama memakai nomor kartu yang disimulasikan berhasil, sehingga checkout melunaskan pesanan Budi dengan referensi dari gateway. Panggilan kedua memakai kartu berawalan 4000 yang oleh sandbox selalu ditolak, dan LayananCheckout menerjemahkannya menjadi kegagalan pembayaran untuk Siti. Sekarang bagian pentingnya — ganti implementasinya dengan test double yang menyimpan status panggilan terakhirnya:

php
<?php
require __DIR__ . "/pembayaran.php";

final class GatewayPembayaranTiruan implements GatewayPembayaran
{
    public bool $panggilanTerakhirSukses = false;

    public function bayar(string $nomorKartu, int $totalTagihan): array
    {
        $this->panggilanTerakhirSukses = $totalTagihan <= 1_000_000;

        return ["sukses" => $this->panggilanTerakhirSukses, "ref" => "TES-42"];
    }
}

$fake = new GatewayPembayaranTiruan();
$checkout = new LayananCheckout($fake);

echo $checkout->selesaikan("[email protected]", "4111111111111111", 150000), "\n";
echo $checkout->selesaikan("[email protected]", "4111111111111111", 2_000_000), "\n";
printf("gateway terakhir dipanggil: sukses=%s\n", $fake->panggilanTerakhirSukses ? "ya" : "tidak");

Output yang Dihasilkan:

text
pesanan [email protected] lunas (ref TES-42)
pembayaran [email protected] gagal
gateway terakhir dipanggil: sukses=tidak

GatewayPembayaranTiruan menolak tagihan di atas satu juta rupiah dan mengabarkan statusnya lewat property panggilanTerakhirSukses — perilaku yang sengaja dibuat mudah diperiksa oleh pengujian. LayananCheckout tidak berubah satu baris pun, karena yang diganti hanyalah object yang disuntikkan lewat constructor. Inilah janji interface: implementasi boleh bertambah — sandbox, REST sungguhan, test double, gateway QR — selama kontrak terpenuhi, dan konsumen tidak perlu tahu. Teknik pengujian seperti ini akan dibedah bersama PHPUnit pada section Testing dan Quality Assurance .

Interface Sempit Mengalahkan yang Lebar #

Kontrak lebar biasanya lahir dari cara berpikir “interface adalah daftar kemampuan sebuah class”. Bayangkan satu interface yang menampung seluruh operasi penyimpanan toko:

php
<?php

// Terlalu lebar: satu kontrak untuk semua kebutuhan penyimpanan.
interface RepositoriToko
{
    public function cariPengguna(int $id): ?array;
    public function simpanPengguna(array $data): int;
    public function hapusPengguna(int $id): void;
    public function cariProduk(int $id): ?array;
    public function simpanProduk(array $data): int;
    public function tulisLog(string $pesan): void;
}

Padahal use case pendaftaran — seperti yang kita bangun di chapter Controller-Service-Repository — hanya membutuhkan dua kemampuan: menyimpan pengguna baru dan mencari pengguna berdasarkan email. Kebutuhan itulah yang seharusnya menjadi kontrak:

php
<?php

// Sempit: kontrak diturunkan dari kebutuhan use case, bukan dari tabel.
interface PenyimpanPengguna
{
    /** @return int id pengguna yang baru tersimpan */
    public function simpan(string $nama, string $email): int;
}

interface PencariPengguna
{
    /** @return array{id: int, nama: string, email: string}|null */
    public function cariByEmail(string $email): ?array;
}
PertanyaanKontrak lebarKontrak sempit
Dari mana isinya berasal?daftar kemampuan satu classkebutuhan satu use case
Beban implementasi baruwajib memenuhi semua methodhanya method yang relevan
Pembaca kontrakharus menyaring method tak relevanlangsung melihat kemampuannya

Setiap method pada kontrak adalah beban bersama: semua implementasi masa depan wajib memenuhinya, betapapun tidak relevannya. Pemecahan kontrak lebar menjadi beberapa kontrak sempit adalah inti Interface Segregation Principle yang dibahas formal pada chapter Interface Segregation . Sebuah service boleh saja menerima dua interface sempit sekaligus bila memang butuh keduanya — itulah yang dilakukan LayananPendaftaran kita selama ini terhadap PenyimpanPengguna dan PencariPengguna.

Bau Abstraksi Berlebihan #

Sisi lain dari keterampilan ini adalah menahan diri. Interface untuk hal yang tidak akan pernah punya implementasi lain hanyalah upacara kosong:

php
<?php

// Pemborosan: kontrak untuk hal yang tidak akan pernah punya implementasi lain.
interface Penjumlah
{
    public function jumlah(int $a, int $b): int;
}

final class PenjumlahBiasa implements Penjumlah
{
    public function jumlah(int $a, int $b): int
    {
        return $a + $b;
    }
}

Blok di atas membuat pembaca harus melompat dua definisi untuk sampai pada return $a + $b; — biaya navigasi tanpa manfaat pembalikan arah. Bau serupa: kontrak dengan satu implementasi yang tidak pernah ditukar saat menguji, atau interface yang meniru persis bentuk internal satu class tertentu sehingga implementasi kedua mustahil mengejar.

Bergantung pada Hal yang Stabil #

Ukuran terakhir adalah kestabilan. Detail implementasi berubah sering: string SQL ikut berubah ketika skema tabel bergeser, endpoint API berubah versi, format berkas diperbarui. Kontrak use case berubah jauh lebih jarang, karena kebutuhan “proses pembayaran dan beri referensi” bertahan lama meski penyedianya berganti. Panah dependensi yang baik menunjuk ke sisi yang lebih stabil itulah — sama seperti modul PHP yang bergantung pada bahasa dan extension-nya yang berubah sangat jarang.

LapisanContohVolatilitas perubahan
Detail implementasistring SQL, endpoint API, format berkastinggi
Kontrak use caseGatewayPembayaran::bayar()rendah
Bahasa dan runtimesintaks PHP 8.4, extension PDOsangat rendah

Kestabilan ini juga menjadi alasan menaruh kontrak di sisi konsumen: kebutuhan konsumen adalah kepingan aturan bisnis, dan aturan bisnis justru bagian paling stabil dari aplikasi dibanding mekanisme yang mewujudkannya.

Ringkasan #

Abstraksi yang baik dimiliki konsumen, ditulis dalam kosakata use case, dan sesempit kebutuhannya. Kontrak GatewayPembayaran menunjukkan bahwa satu definisi kecil sanggup menjaga service tetap tak tersentuh ketika implementasi sandbox, REST, dan test double berganti tempat. Sisi disiplinnya sama pentingnya: abstraksi tanpa pemicu nyata hanyalah ceremony, dan kualitasnya diukur dari kestabilan di balik panah dependensi. Interface memagari perilaku dari luar; chapter berikutnya masuk ke isi kerajaannya — Domain Modeling , tempat entity, value object, dan aturan bisnis berhak hidup di dalam model.

Referensi #

Berkas Sumber

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

pembayaran.php

Unduh berkas
php
<?php

// Bundle chapter Interfaces dan Abstractions: checkout di atas kontrak pembayaran.

interface GatewayPembayaran
{
    /** @return array{sukses: bool, ref: string} */
    public function bayar(string $nomorKartu, int $totalTagihan): array;
}

final class GatewayPembayaranSandbox implements GatewayPembayaran
{
    public function bayar(string $nomorKartu, int $totalTagihan): array
    {
        // Dunia nyata: panggilan cURL ke API penyedia pembayaran sungguhan.
        // Kartu berawalan 4000 disimulasikan gagal, kartu lain berhasil.
        $sukses = !str_starts_with($nomorKartu, "4000");

        return [
            "sukses" => $sukses,
            "ref" => "SBX-" . str_pad((string) $totalTagihan, 6, "0", STR_PAD_LEFT),
        ];
    }
}

final class LayananCheckout
{
    public function __construct(private readonly GatewayPembayaran $gateway)
    {
    }

    public function selesaikan(
        string $email,
        string $nomorKartu,
        int $totalTagihan,
    ): string {
        $hasil = $this->gateway->bayar($nomorKartu, $totalTagihan);
        if (!$hasil["sukses"]) {
            return "pembayaran $email gagal";
        }

        return "pesanan $email lunas (ref " . $hasil["ref"] . ")";
    }
}