Versi pertama class Kasir menandatangani parameternya dengan tipe konkret: void Proses(KartuKredit metode, int total). Setiap saluran pembayaran baru memaksa kita menambah overload atau mencabang kode dengan if-else, padahal logika kasir sebenarnya sederhana: proses pembayaran, lalu catat transaksi. Prinsip programming to interface menyuruh kita membalik arah ketergantungan: kode pemanggil bergantung pada kontrak, bukan pada class konkret.

Parameter Bertipe Interface #

csharp
public class Kasir
{
    public void Proses(IPembayaran metode, int total)
    {
        metode.ProsesPembayaran(total);
        Console.WriteLine($"Kasir mencatat pembayaran Rp{total} melalui {metode.GetType().Name}.");
    }
}

Kasir hanya melihat janji IPembayaran; class tersebut tidak tahu implementasi konkret mana yang datang. Pemanggilan metode.GetType().Name hanya berperan sebagai pelaporan, bukan sebagai dasar pengambilan keputusan.

Pola ini berlaku bukan hanya pada parameter method. Field, return type, dan elemen koleksi juga sebaiknya bertipe interface selama kode pemanggil tidak membutuhkan member di luar kontrak. Semakin sempit kontrak yang dilihat pemanggil, semakin kecil dampak perubahan implementasi di kemudian hari.

Manfaat yang Langsung Terlihat #

csharp
Kasir kasir = new Kasir();
kasir.Proses(new KartuKredit(), 150000);
kasir.Proses(new TransferBank(), 150000);
kasir.Proses(new QrisMerchant(), 150000);

Output yang Dihasilkan:

text
Kartu kredit: pembayaran Rp150000 diproses (biaya admin Rp2000).
Kasir mencatat pembayaran Rp150000 melalui KartuKredit.
Transfer bank: pembayaran Rp150000 diproses (biaya admin Rp0).
Kasir mencatat pembayaran Rp150000 melalui TransferBank.
QRIS: pembayaran Rp150000 terverifikasi.
Kasir mencatat pembayaran Rp150000 melalui QrisMerchant.

Mari kita bedah baris demi baris:

  1. kasir.Proses(new KartuKredit(), 150000) — object konkret disuntikkan dari luar, dan Kasir tetap berbicara lewat kontrak.
  2. kasir.Proses(new QrisMerchant(), 150000) — QrisMerchant adalah saluran yang baru hadir pada bab Interface Inheritance, dan baris ini berjalan tanpa mengubah satu baris kode Kasir sekalipun.
  3. metode.ProsesPembayaran(total) — satu pemanggilan untuk semua saluran; runtime memilih versi milik object yang ditunjuk, dan Kasir tidak perlu diubah bila saluran lain hadir.

Prinsip ini memberikan tiga manfaat yang terukur:

  • Perluasan aman: saluran baru cukup menepati kontrak, dan kode pemanggil tidak berubah.
  • Pengujian mudah: unit test dapat menyuntikkan implementasi tiruan IPembayaran untuk memeriksa perilaku Kasir tanpa proses pembayaran sungguhan.
  • Kerja paralel: satu tim menulis kontrak beserta kode pemanggil, tim lain mengisi implementasinya secara mandiri.

Ketergantungan yang mengarah ke kontrak juga menjadi fondasi dependency injection yang dipakai luas oleh framework modern. Karena manfaatnya menyeluruh, programmer C# lazim memilih tipe interface sejak desain awal, bukan setelah kode sulit diubah.