Interface & Implements
Kontrak murni dengan implements: banyak interface, tanpa warisan kode.
Kontrak tanpa warisan
extends mewarisi kode sekaligus kontrak. Tapi kadang kamu hanya butuh kontraknya: "class ini BISA diserialisasi ke JSON" tanpa mewarisi implementasi apa pun. implements menjadikan class sebagai interface murni.
Analogi: sertifikasi profesi. Sertifikat "bisa mengemudi" tidak memberimu mobil (implementasi), ia hanya menjamin kamu memenuhi standar. Banyak orang bisa punya sertifikat yang sama tanpa berbagi mobil.
Kenapa implements ada?
Dua alasan: multiple interface (satu class bisa memenuhi banyak kontrak, padahal extends hanya satu induk) dan decoupling (kode bergantung pada kontrak, bukan implementasi konkret, sehingga mudah diganti/mock saat testing).
Bentuk dasar
// Interface: class biasa yang dipakai sebagai kontrak
abstract class BisaDisimpan {
Map<String, dynamic> keJson();
void dariJson(Map<String, dynamic> json);
}
abstract class BisaDicetak {
String cetakRapi();
}
class Pengguna implements BisaDisimpan, BisaDicetak {
String nama;
int umur;
Pengguna(this.nama, this.umur);
@override
Map<String, dynamic> keJson() => {'nama': nama, 'umur': umur};
@override
void dariJson(Map<String, dynamic> json) {
nama = json['nama'];
umur = json['umur'];
}
@override
String cetakRapi() => '$nama ($umur thn)';
}
void main() {
var p = Pengguna('Budi', 17);
print(p.keJson());
print(p.cetakRapi());
// Bisa diperlakukan sebagai interface-nya
BisaDisimpan s = p;
print(s.keJson());
}Pengguna memenuhi DUA kontrak sekaligus. Tidak ada kode yang diwarisi; semua ditulis ulang. Sebagai gantinya, kamu bebas: Pengguna bisa tetap extends Manusia sambil implements banyak interface.
Contoh kedua, decoupling untuk testing:
abstract class Penyimpanan {
void simpan(String kunci, String nilai);
String? baca(String kunci);
}
// Implementasi asli: ke file
class PenyimpananFile implements Penyimpanan {
final _data = <String, String>{};
@override
void simpan(String kunci, String nilai) => _data[kunci] = nilai;
@override
String? baca(String kunci) => _data[kunci];
}
// Implementasi palsu: untuk testing
class PenyimpananPalsu implements Penyimpanan {
@override
void simpan(String kunci, String nilai) {} // tidak ngapa-ngapain
@override
String? baca(String kunci) => 'palsu';
}
void uji(Penyimpanan s) {
s.simpan('nama', 'Budi');
print(s.baca('nama'));
}
void main() {
uji(PenyimpananFile()); // Budi
uji(PenyimpananPalsu()); // palsu
}Fungsi uji hanya tahu kontrak Penyimpanan, tidak peduli implementasinya. Inilah fondasi dependency injection dan mock testing.
extends vs implements vs with
abstract class Hewan {
void makan() => print('nyam');
void bersuara();
}
class Kucing extends Hewan {
@override
void bersuara() => print('Meong');
// makan() diwarisi, tidak perlu tulis ulang
}
class Robot implements Hewan {
@override
void makan() => print('Robot isi baterai'); // wajib tulis SEMUA
@override
void bersuara() => print('Bip');
}| extends | implements | with | |
|---|---|---|---|
| Warisi kode | Ya | Tidak | Ya |
| Jumlah | 1 induk | Banyak | Banyak |
| Wajib override abstrak | Ya | Semua method | Tidak |
Kesalahan umum
1. implements lalu kaget harus tulis semua
abstract class A {
void satu() => print('satu');
void dua();
}
// SALAH: implements wajib implementasikan 'satu' juga!
// class B implements A {
// @override
// void dua() => print('dua');
// }// BENAR
class B implements A {
@override
void satu() => print('satu (versi B)');
@override
void dua() => print('dua');
}2. Memakai extends untuk "bisa" padahal bukan "adalah"
class Burung extends BisaTerbang salah secara konsep jika BisaTerbang bukan entitas. Hubungan kemampuan → implements atau with (mixin).
3. Interface berubah merusak semua implementasi
Menambah method ke interface memaksa SEMUA class implements mengimplementasikannya (compile error). Ini fitur (konsistensi), tapi desain interface harus stabil sejak awal.
Kesimpulan
implements = kontrak murni tanpa warisan kode, bisa banyak sekaligus. Pakai untuk kemampuan lintas-hierarki dan decoupling (testing/mock). Ingat tabel: extends untuk "adalah", implements untuk "bisa (kontrak)", with untuk "bisa (pakai kode)".
Catatan teknis: Di Dart 3,
interface classmemaksa class hanya dipakai sebagai interface (tidak bisa di-extends). Ini berguna untuk API publik yang ingin kamu kunci dari pewarisan.
Tantangan
Kontrak pembayaran
Buat abstract BisaDibayar dengan method bayar(int jumlah) dan String struk(). Buat 2 class yang implements: Tunai dan QRIS dengan struk berbeda. Uji keduanya lewat variabel bertipe BisaDibayar.
abstract class BisaDibayar {
void bayar(int jumlah);
String struk();
}
// TODO: class Tunai implements BisaDibayar
// TODO: class QRIS implements BisaDibayar
void main() {
// TODO: uji lewat List<BisaDibayar>
}