Bayangkan Anda sedang membangun sebuah rumah perangkat lunak: ada rancangan umum, ada juga kerangka utama yang menahan seluruh bangunan. Dalam pemrograman berorientasi objek, “rancangan umum” itu sering kali diwujudkan dengan interface, sementara “kerangka utama” kerap diwakili oleh abstract class. Keduanya sama-sama tidak bisa dijalankan langsung, tetapi berperan penting sebagai fondasi yang menentukan bentuk dan perilaku kode di atasnya.
Namun, di sinilah kebingungan sering muncul. Kapan sebaiknya menggunakan interface, dan kapan lebih tepat memilih abstract class? Mengapa bahasa pemrograman modern menyediakan keduanya, seolah menggandakan fitur dengan tujuan yang mirip? Di satu sisi, interface tampak ideal untuk mendefinisikan kontrak perilaku. Di sisi lain, abstract class menawarkan titik tengah antara konsep dan implementasi.
Artikel ini akan mengajak Anda mengenal perbedaan mendasar antara interface dan abstract class, memahami cara kerjanya, serta melihat contoh penerapannya dalam kode. Dengan begitu, Anda tidak hanya tahu definisinya, tetapi juga dapat menentukan pilihan yang tepat saat merancang arsitektur program Anda sendiri.
Perbedaan Fundamental Interface dan Abstract Class dalam Desain OOP Modern
Dalam OOP modern, interface dan abstract class sama-sama dipakai untuk mendefinisikan kontrak, tapi cara dan fleksibilitasnya berbeda cukup jauh. Interface biasanya dipakai untuk mengatakan “objek ini bisa melakukan apa”, tanpa peduli bagaimana caranya, sementara abstract class lebih cocok untuk “keluarga” objek yang punya dasar perilaku yang mirip. Di banyak framework modern, interface sering jadi tulang punggung arsitektur karena mendukung komposisi dan dependency injection yang lebih rapi. Sementara itu, abstract class berguna ketika kamu ingin menyediakan implementasi default, menyimpan state bersama, atau menaruh protected helper method yang bisa dipakai ulang oleh turunan.
| Interface | Abstract Class |
| Hanya kontrak, tanpa state | Bisa punya field dan state |
| Multi-implementasi diperbolehkan | Biasanya hanya single inheritance |
| Cocok untuk kemampuan (ability/behavior) | Cocok untuk hierarki “keluarga” kelas |
| Lebih fleksibel untuk komposisi | Lebih kuat untuk reuse dengan template dasar |
- Gunakan interface saat fokus pada perilaku dan kontrak antar modul.
- Gunakan abstract class saat butuh fondasi bersama dengan logika yang bisa langsung dipakai.
- Kombinasi keduanya seringkali yang paling praktis: interface sebagai kontrak, abstract class sebagai implementasi dasar.
Kapan Memilih Interface dan Kapan Menggunakan Abstract Class dalam Proyek Nyata
Dalam proyek nyata, interface biasanya terasa pas ketika kamu ingin mendefinisikan “kontrak” perilaku tanpa peduli bagaimana cara implementasinya. Misalnya pada sistem pembayaran, kamu bisa punya PaymentGateway yang harus punya metode pay(), refund(), dan checkStatus() untuk berbagai provider. Di titik ini, yang kamu butuhkan adalah kesepakatan perilaku, bukan pewarisan logika. Interface juga sangat cocok ketika satu class perlu “berperan” sebagai beberapa tipe sekaligus (multiple inheritance of type). Dalam praktik, ini sering muncul di proyek besar yang modular, misalnya microservice atau plugin WordPress yang ingin tetap longgar keterikatannya.
- Pakai interface saat ingin kontrak perilaku yang jelas, ringan, dan mudah di-mock untuk testing.
- Pakai abstract class saat butuh blueprint plus sebagian implementasi bersama.
- Interface enak untuk integrasi eksternal dan plugin.
- Abstract class enak untuk hierarki fitur internal yang punya logika dasar yang sama.
| Situasi | Pilihan Ideal |
|---|---|
| Banyak vendor / adapter | Interface |
| Butuh template + kode bersama | Abstract Class |
| Class perlu beberapa “peran” sekaligus | Interface |
| Ingin satu base kuat untuk diturunkan | Abstract Class |
Di sisi lain, abstract class lebih cocok dipakai ketika kamu ingin menyiapkan kerangka besar plus sedikit “bawaan” logika yang bisa langsung dipakai oleh turunan. Contoh di aplikasi web: kamu bisa bikin BaseController yang mengurus hal umum seperti validasi request dan response JSON, lalu controller lain cukup extends tanpa harus nulis ulang. Pendekatan ini terasa natural kalau kamu punya pola yang jelas dan stabil di dalam domainmu. Tapi hati-hati: kalau sejak awal kamu terlalu agresif pakai abstract class, struktur warisan bisa jadi kaku dan susah diubah begitu kebutuhan bisnis mulai geser, jadi jangan ragu mengombinasikan interface + abstract class jika memang lebih fleksibel.
Contoh Implementasi Interface dan Abstract Class di Java dan C Sebagai Panduan Praktis
Supaya lebih kebayang, kita bisa bandingkan cara sebuah kontrak perilaku diwakili di Java dan di C. Di Java, interface dan abstract class sudah jadi fitur bawaan, jadi kita cukup mendeklarasikan kata kunci yang tepat dan compiler mengurus sisanya. Sementara di C, tidak ada interface/abstract class secara langsung, jadi kita biasanya mengakalinya dengan struct + function pointer untuk meniru polimorfisme. Contoh sederhana: anggap kita sedang membangun sistem pembayaran; kita ingin punya kontrak umum untuk semua metode pembayaran (misalnya transfer bank dan e-wallet), tapi tiap metode punya cara eksekusi sendiri.
| Bahasa | Cara Meniru Kontrak | Kegunaan Singkat |
|---|---|---|
| Java | interface & abstract class | Kontrak jelas, sintaks rapi |
| C | struct + function pointer | Lebih fleksibel tapi manual |
- Contoh interface dan abstract class di Java (tema: Pembayaran):
// Interface sebagai kontrak perilaku
public interface PaymentMethod {
void pay(double amount);
String getName();
}
// Abstract class sebagai template dasar
public abstract class AbstractPayment implements PaymentMethod {
private String name;
public AbstractPayment(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
// Method non-abstract boleh punya logika default
protected void logPayment(double amount) {
System.out.println("Membayar " + amount + " dengan " + name);
}
}
// Implementasi konkret: Transfer Bank
public class BankTransfer extends AbstractPayment {
public BankTransfer() {
super("Bank Transfer");
}
@Override
public void pay(double amount) {
logPayment(amount);
System.out.println("Memproses pembayaran via rekening bank...");
}
}
// Implementasi konkret: E-Wallet
public class EWalletPayment extends AbstractPayment {
public EWalletPayment() {
super("E-Wallet");
}
@Override
public void pay(double amount) {
logPayment(amount);
System.out.println("Memproses pembayaran via saldo e-wallet...");
}
}
// Contoh penggunaan
public class PaymentDemo {
public static void main(String[] args) {
PaymentMethod bank = new BankTransfer();
PaymentMethod wallet = new EWalletPayment();
bank.pay(150000);
wallet.pay(50000);
}
}
- Contoh pendekatan mirip “interface” di C dengan struct dan function pointer:
#include
// "Interface" PaymentMethod diwakili oleh struct berisi function pointer
typedef struct PaymentMethod {
void (*pay)(double amount);
const char* (*getName)(void);
} PaymentMethod;
// Implementasi: BankTransfer
typedef struct {
PaymentMethod vtable; // "vtable" sederhana
const char* name;
} BankTransfer;
void bank_pay(double amount) {
printf("Membayar %.2f dengan Bank Transfern", amount);
printf("Memproses pembayaran via rekening bank...n");
}
const char* bank_get_name(void) {
return "Bank Transfer";
}
BankTransfer create_bank_transfer() {
BankTransfer bt;
bt.vtable.pay = bank_pay;
bt.vtable.getName = bank_get_name;
bt.name = "Bank Transfer";
return bt;
}
// Implementasi: EWalletPayment
typedef struct {
PaymentMethod vtable;
const char* name;
} EWalletPayment;
void wallet_pay(double amount) {
printf("Membayar %.2f dengan E-Walletn", amount);
printf("Memproses pembayaran via saldo e-wallet...n");
}
const char* wallet_get_name(void) {
return "E-Wallet";
}
EWalletPayment create_ewallet_payment() {
EWalletPayment ew;
ew.vtable.pay = wallet_pay;
ew.vtable.getName = wallet_get_name;
ew.name = "E-Wallet";
return ew;
}
// "Client code" yang hanya kenal PaymentMethod
void processPayment(PaymentMethod* method, double amount) {
printf("Metode: %sn", method->getName());
method->pay(amount);
}
int main(void) {
BankTransfer bank = create_bank_transfer();
EWalletPayment wallet = create_ewallet_payment();
processPayment((PaymentMethod*)&bank, 200000);
processPayment((PaymentMethod*)&wallet, 75000);
return 0;
}
Rekomendasi Pola Desain dan Best Practice Mengombinasikan Interface dan Abstract Class
Sering kali, solusi paling rapi justru muncul ketika interface dan abstract class saling melengkapi, bukan saling menggantikan. Salah satu pola yang cukup nyaman dipakai adalah menjadikan interface sebagai kontrak perilaku, lalu abstract class sebagai tempat logika dasar dan utilitas. Misalnya, di aplikasi pembayaran, kamu bisa punya interface PaymentGateway untuk mendefinisikan metode inti, lalu abstract class BasePaymentGateway yang mengurus hal-hal umum seperti logging, validasi sederhana, atau retry mechanism. Dengan cara ini, setiap implementasi konkrit (misalnya Midtrans, Xendit, atau payment dummy untuk testing) cukup fokus pada perbedaan bisnisnya saja.
- Gunakan interface saat ingin fleksibilitas tinggi dan memungkinkan satu class mengimplementasikan banyak “peran”.
- Gunakan abstract class saat ada perilaku dasar yang berulang dan perlu diwariskan.
- Kombinasikan ketika satu kontrak perilaku dibutuhkan di banyak tempat, tapi kamu juga ingin “starter pack” implementasi yang siap pakai.
- Hindari warisan berlapis-lapis; kalau hierarchy mulai bikin pusing, pertimbangkan komposisi atau memecah interface jadi lebih kecil.
| Pola | Interface | Abstract Class |
|---|---|---|
| Kontrak murni | Ya | Tidak wajib |
| Berbagi logika | Tidak | Ya |
| Fleksibel di banyak modul | Sangat | Terbatas |
interface PaymentGateway {
fun charge(amount: Double): Boolean
fun refund(transactionId: String): Boolean
}
abstract class BasePaymentGateway : PaymentGateway {
protected fun log(message: String) {
println("[PaymentLog] $message")
}
override fun refund(transactionId: String): Boolean {
log("Refund transaksi: $transactionId")
// Default behavior, bisa di-override
return true
}
}
class MidtransGateway : BasePaymentGateway() {
override fun charge(amount: Double): Boolean {
log("Charge via Midtrans: $amount")
// Panggil API Midtrans...
return true
}
}
In Conclusion
Pada akhirnya, memahami perbedaan antara interface dan abstract class bukan sekadar soal menghafal definisi, tetapi tentang bagaimana keduanya membentuk cara kita merancang solusi. Interface memberi kita kontrak yang jelas dan tegas, sementara abstract class menawarkan kerangka yang bisa diwarisi dan disempurnakan.
Di dunia nyata pengembangan perangkat lunak, keduanya jarang berdiri sendiri. Mereka saling melengkapi, dipilih sesuai kebutuhan arsitektur, skala proyek, dan fleksibilitas yang diinginkan. Kadang kita butuh kebebasan penuh yang ditawarkan interface, kadang kita mengandalkan fondasi kokoh dari abstract class.
Saat Anda menulis baris kode berikutnya, coba ajukan pertanyaan sederhana: “Apakah saya butuh kontrak, kerangka, atau keduanya?” Dari jawaban itulah desain yang bersih dan terstruktur akan lahir. Dan di situlah interface dan abstract class menemukan perannya masing-masing.

