Interface #
Interface adalah mekanisme untuk mendefinisikan kontrak — kesepakatan tentang apa yang bisa dilakukan suatu objek, tanpa peduli bagaimana ia melakukannya. Dart tidak memiliki kata kunci interface tersendiri seperti Java atau C#, tapi justru pendekatannya lebih fleksibel: setiap kelas secara otomatis mendefinisikan implicit interface yang terdiri dari semua member publiknya, dan abstract class bisa berfungsi sebagai interface dengan kemampuan tambahan berupa method konkret dan dokumentasi yang lebih kaya. Memahami interface bukan hanya soal sintaks implements — ia adalah tentang bagaimana mendesain batas antara komponen agar kode bisa diuji, diperluas, dan diganti implementasinya tanpa perubahan besar.
Mengapa Interface? #
Bayangkan sebuah fitur yang mengambil data pengguna dari API dan menampilkannya. Jika kode presentasi langsung memanggil HTTP client, ia terikat erat dengan detail implementasi jaringan. Saat ingin menguji logika presentasi, kamu harus menyiapkan server sungguhan. Saat perusahaan pindah dari REST ke GraphQL, semua kode presentasi harus diubah.
Interface memutus ketergantungan ini:
// ANTI-PATTERN: ketergantungan langsung ke implementasi
class HalamanProfil {
final HttpClient _client = HttpClient(); // ✗ terikat ke HttpClient
Future<void> muat(String id) async {
final response = await _client.get('/api/users/$id');
// ... proses response
}
}
// BENAR: bergantung pada abstraksi, bukan implementasi
abstract class PenggunaSumber {
Future<Pengguna> ambil(String id);
Future<List<Pengguna>> ambilSemua();
}
class HalamanProfil {
final PenggunaSumber _sumber; // bergantung pada kontrak, bukan detail
HalamanProfil(this._sumber); // implementasi disuntikkan dari luar
Future<void> muat(String id) async {
final pengguna = await _sumber.ambil(id);
// ... tampilkan pengguna
}
}
Dengan pola ini, HalamanProfil bisa diuji dengan implementasi palsu (mock), dan implementasi nyata bisa diganti tanpa menyentuh kode presentasi sama sekali.
Implicit Interface — Setiap Kelas adalah Interface #
Di Dart, setiap kelas secara otomatis mendefinisikan implicit interface — kumpulan semua member publik yang bisa di-implements oleh kelas lain. Tidak perlu ada deklarasi khusus.
// Kelas biasa — sekaligus mendefinisikan implicit interface
class Printer {
void cetak(String teks) => print(teks);
void cetakBold(String teks) => print('**$teks**');
}
// Kelas ini mengimplementasikan implicit interface dari Printer
// WAJIB mengimplementasikan SEMUA member publik Printer
class PrinterPDF implements Printer {
@override
void cetak(String teks) {
// tulis ke file PDF
print('[PDF] $teks');
}
@override
void cetakBold(String teks) {
print('[PDF BOLD] $teks');
}
}
Perbedaan krusial antara extends dan implements:
class A {
void hello() => print('Hello dari A');
void salam() => print('Salam dari A');
}
// extends — mewarisi implementasi
class B extends A {
@override
void hello() => print('Hello dari B');
// salam() tidak perlu — sudah diwarisi dari A
}
// implements — WAJIB implementasikan semuanya dari nol
class C implements A {
@override
void hello() => print('Hello dari C'); // wajib
@override
void salam() => print('Salam dari C'); // wajib — tidak ada warisan
}
Menggunakan kelas konkret sebagai interface (implements KelasKonkret) memiliki risiko: setiap kali member publik baru ditambahkan ke kelas tersebut, semua implementor harus menambahkan override baru. Gunakanabstract classatauinterface class(Dart 3) sebagai kontrak eksplisit untuk menghindari ini.
abstract class sebagai Interface
#
abstract class adalah cara paling umum mendefinisikan interface di Dart. Ia bisa berisi method abstrak (kontrak wajib), method konkret (implementasi default), dan getter abstrak — memberikan fleksibilitas yang tidak ada di kata kunci interface bahasa lain.
// Interface untuk sumber data pengguna
abstract class PenggunaSumber {
// Kontrak wajib — harus diimplementasikan
Future<Pengguna?> ambilById(String id);
Future<List<Pengguna>> ambilSemua({int halaman = 1, int perHalaman = 20});
Future<Pengguna> simpan(Pengguna pengguna);
Future<void> hapus(String id);
// Method konkret — implementasi default yang bisa di-override
Future<bool> ada(String id) async {
return await ambilById(id) != null;
}
Future<int> hitungTotal() async {
final semua = await ambilSemua(perHalaman: 1);
return semua.length; // implementasi naif — bisa di-override
}
}
// Implementasi nyata — dari database
class PenggunaDatabaseSumber extends PenggunaSumber {
final Database _db;
PenggunaDatabaseSumber(this._db);
@override
Future<Pengguna?> ambilById(String id) async {
final rows = await _db.query('pengguna', where: 'id = ?', whereArgs: [id]);
return rows.isEmpty ? null : Pengguna.dariMap(rows.first);
}
@override
Future<List<Pengguna>> ambilSemua({int halaman = 1, int perHalaman = 20}) async {
final offset = (halaman - 1) * perHalaman;
final rows = await _db.query('pengguna', limit: perHalaman, offset: offset);
return rows.map(Pengguna.dariMap).toList();
}
@override
Future<Pengguna> simpan(Pengguna pengguna) async {
await _db.insert('pengguna', pengguna.keMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
return pengguna;
}
@override
Future<void> hapus(String id) =>
_db.delete('pengguna', where: 'id = ?', whereArgs: [id]);
// Override implementasi default dengan versi yang lebih efisien
@override
Future<int> hitungTotal() async {
final result = await _db.rawQuery('SELECT COUNT(*) FROM pengguna');
return Sqflite.firstIntValue(result) ?? 0;
}
}
// Implementasi untuk pengujian — tidak menyentuh database sama sekali
class PenggunaSumberFake extends PenggunaSumber {
final Map<String, Pengguna> _store = {};
@override
Future<Pengguna?> ambilById(String id) async => _store[id];
@override
Future<List<Pengguna>> ambilSemua({int halaman = 1, int perHalaman = 20}) async {
return _store.values.toList();
}
@override
Future<Pengguna> simpan(Pengguna pengguna) async {
_store[pengguna.id] = pengguna;
return pengguna;
}
@override
Future<void> hapus(String id) async => _store.remove(id);
}
interface class — Dart 3
#
Dart 3 memperkenalkan modifier kelas baru yang memberikan kontrol lebih eksplisit atas cara kelas digunakan:
// interface class — hanya bisa di-implements, tidak bisa di-extends
interface class Serializable {
Map<String, dynamic> keJson();
String keJsonString() => jsonEncode(keJson());
}
// abstract interface class — kombinasi: tidak bisa diinstansiasi,
// dan hanya bisa di-implements (tidak bisa di-extends)
abstract interface class Repository<T, ID> {
Future<T?> cariById(ID id);
Future<List<T>> cariSemua();
Future<T> simpan(T entitas);
Future<void> hapus(ID id);
}
| Modifier | Bisa diinstansiasi | Bisa extends |
Bisa implements |
Bisa with |
|---|---|---|---|---|
| (tanpa modifier) | ✓ | ✓ | ✓ | ✗ |
abstract |
✗ | ✓ | ✓ | ✗ |
interface |
✓ | ✗ | ✓ | ✗ |
abstract interface |
✗ | ✗ | ✓ | ✗ |
base |
✓ | ✓ | ✗ | ✗ |
final |
✓ | ✗ | ✗ | ✗ |
sealed |
✗ | ✗ subtype di file sama | ✗ | ✗ |
Mengimplementasikan Beberapa Interface #
Salah satu keunggulan implements dibanding extends adalah kemampuan mengimplementasikan beberapa interface sekaligus — mengatasi keterbatasan single inheritance Dart:
abstract class Dapat Disimpan {
Future<void> simpanKeDisk(String path);
Future<void> muatDariDisk(String path);
}
abstract class DapatDiekspor {
List<int> keBytes();
String keCsv();
}
abstract class DapatDivalidasi {
bool validasi();
List<String> pesanValidasi();
}
// Dokumen mengimplementasikan tiga interface sekaligus
class DokumenLaporan
implements DapatDisimpan, DapatDiekspor, DapatDivalidasi {
final String judul;
final List<Map<String, dynamic>> baris;
DokumenLaporan({required this.judul, required this.baris});
@override
Future<void> simpanKeDisk(String path) async {
final file = File(path);
await file.writeAsString(jsonEncode({'judul': judul, 'baris': baris}));
}
@override
Future<void> muatDariDisk(String path) async { /* ... */ }
@override
List<int> keBytes() => utf8.encode(keCsv());
@override
String keCsv() {
// konversi baris ke format CSV
return baris.map((b) => b.values.join(',')).join('\n');
}
@override
bool validasi() => judul.isNotEmpty && baris.isNotEmpty;
@override
List<String> pesanValidasi() {
final pesan = <String>[];
if (judul.isEmpty) pesan.add('Judul tidak boleh kosong');
if (baris.isEmpty) pesan.add('Laporan tidak boleh kosong');
return pesan;
}
}
Interface Segregation Principle #
Salah satu prinsip SOLID yang paling relevan dengan interface: Interface Segregation Principle (ISP) — klien tidak boleh dipaksa bergantung pada method yang tidak mereka gunakan. Interface yang terlalu besar harus dipecah menjadi interface yang lebih kecil dan terfokus.
// ANTI-PATTERN: interface monolitik yang memaksa implementasi method yang tidak relevan
abstract class PenggunaLayanan {
Future<Pengguna?> ambilById(String id);
Future<void> simpan(Pengguna p);
Future<void> hapus(String id);
Future<void> kirimEmail(String id, String subjek, String isi);
Future<void> kirimSms(String id, String pesan);
Future<void> pushNotifikasi(String id, String judul);
Future<void> ekspor(String format, String path);
Future<void> impor(String path);
Future<Map<String, int>> statistik();
}
// ✗ Komponen yang hanya butuh baca data terpaksa bergantung pada
// method kirimEmail, hapus, ekspor yang tidak pernah ia gunakan
// BENAR: interface kecil yang terfokus
abstract class PenggunaPembaca {
Future<Pengguna?> ambilById(String id);
Future<List<Pengguna>> cari({String? kata, int? halaman});
}
abstract class PenggunaPenulis {
Future<Pengguna> simpan(Pengguna p);
Future<void> hapus(String id);
}
abstract class PenggunaNotifikasi {
Future<void> kirimEmail(String id, String subjek, String isi);
Future<void> kirimSms(String id, String pesan);
Future<void> pushNotifikasi(String id, String judul);
}
abstract class PenggunaEkspor {
Future<void> ekspor(String format, String path);
Future<void> impor(String path);
}
// Implementasi bisa menggabungkan sesuai kebutuhan
class PenggunaSqlLayanan
implements PenggunaPembaca, PenggunaPenulis, PenggunaEkspor {
// implementasi yang relevan saja
}
// Komponen yang butuh baca saja bergantung hanya pada PenggunaPembaca
class HalamanPencarian {
final PenggunaPembaca _pembaca; // bersih — hanya butuh ini
HalamanPencarian(this._pembaca);
}
flowchart LR
subgraph Sebelum ISP
M["PenggunaLayanan\n(monolitik)"] --> A
M --> B
M --> C
end
subgraph Sesudah ISP
P["PenggunaPembaca"] --> A["HalamanPencarian"]
W["PenggunaPenulis"] --> B["FormEdit"]
N["PenggunaNotifikasi"] --> C["LayananEmail"]
end
Dependency Inversion Principle dengan Interface #
Dependency Inversion Principle (DIP) menyatakan: modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah — keduanya harus bergantung pada abstraksi. Interface adalah mekanisme utama untuk menerapkan prinsip ini di Dart.
// Tanpa DIP — modul tingkat tinggi bergantung langsung pada detail
class LayananOrder {
final MysqlDatabase _db = MysqlDatabase(); // ✗ terikat ke MySQL
final SendgridEmail _email = SendgridEmail(); // ✗ terikat ke Sendgrid
Future<void> buatOrder(Order order) async {
await _db.simpan(order);
await _email.kirim(order.emailPelanggan, 'Order dibuat');
}
}
// Ganti database ke PostgreSQL? Ganti email ke SES? Harus ubah LayananOrder.
// Dengan DIP — bergantung pada abstraksi
abstract class OrderRepository {
Future<void> simpan(Order order);
Future<Order?> ambil(String id);
}
abstract class EmailService {
Future<void> kirim(String kepada, String subjek, String isi);
}
class LayananOrder {
final OrderRepository _repo; // abstraksi
final EmailService _email; // abstraksi
LayananOrder({required OrderRepository repo, required EmailService email})
: _repo = repo, _email = email;
Future<void> buatOrder(Order order) async {
await _repo.simpan(order);
await _email.kirim(
order.emailPelanggan,
'Order #${order.id} berhasil dibuat',
'Terima kasih telah berbelanja!',
);
}
}
// Implementasi konkret — bisa diganti tanpa menyentuh LayananOrder
class MysqlOrderRepository implements OrderRepository { /* ... */ }
class PostgresOrderRepository implements OrderRepository { /* ... */ }
class SendgridEmailService implements EmailService { /* ... */ }
class AwsSesEmailService implements EmailService { /* ... */ }
// Perakitan di tingkat aplikasi (composition root)
final layanan = LayananOrder(
repo: PostgresOrderRepository(connectionString),
email: AwsSesEmailService(apiKey),
);
Interface untuk Pengujian dengan Mock #
Salah satu manfaat terbesar interface adalah kemudahan pengujian — kamu bisa membuat implementasi palsu (mock/fake/stub) yang berperilaku sesuai skenario uji tanpa menyentuh infrastruktur nyata.
// Interface yang akan diuji
abstract class PembayaranGateway {
Future<HasilPembayaran> proses(Pembayaran pembayaran);
Future<bool> verifikasi(String idTransaksi);
Future<void> refund(String idTransaksi, double jumlah);
}
// Implementasi fake untuk pengujian
class PembayaranGatewayFake implements PembayaranGateway {
final List<Pembayaran> pembayaranDisimpan = [];
bool _simulasiGagal = false;
void simulasiKegagalan() => _simulasiGagal = true;
@override
Future<HasilPembayaran> proses(Pembayaran pembayaran) async {
if (_simulasiGagal) {
return HasilPembayaran.gagal(pesan: 'Simulasi kegagalan');
}
pembayaranDisimpan.add(pembayaran);
return HasilPembayaran.sukses(idTransaksi: 'FAKE-${pembayaran.id}');
}
@override
Future<bool> verifikasi(String idTransaksi) async => true;
@override
Future<void> refund(String idTransaksi, double jumlah) async {
pembayaranDisimpan.removeWhere((p) => 'FAKE-${p.id}' == idTransaksi);
}
}
// Test menggunakan fake
void main() {
group('LayananPembayaran', () {
late PembayaranGatewayFake fakeGateway;
late LayananPembayaran layanan;
setUp(() {
fakeGateway = PembayaranGatewayFake();
layanan = LayananPembayaran(gateway: fakeGateway);
});
test('sukses memproses pembayaran valid', () async {
final pembayaran = Pembayaran(id: 'P001', jumlah: 100000);
final hasil = await layanan.prosesPembayaran(pembayaran);
expect(hasil.sukses, isTrue);
expect(fakeGateway.pembayaranDisimpan, contains(pembayaran));
});
test('menangani kegagalan gateway dengan benar', () async {
fakeGateway.simulasiKegagalan();
final hasil = await layanan.prosesPembayaran(Pembayaran(jumlah: 100000));
expect(hasil.sukses, isFalse);
expect(hasil.pesan, isNotEmpty);
});
});
}
Polimorfisme melalui Interface #
Interface memungkinkan kode bekerja dengan berbagai implementasi yang berbeda melalui tipe yang sama — inilah polimorfisme. Kode yang bergantung pada interface tidak perlu tahu implementasi konkret mana yang sedang digunakan.
abstract class PengeksplorData {
Stream<Map<String, dynamic>> baca();
Future<void> tutup();
}
class CsvEksplorer implements PengeksplorData {
final String path;
CsvEksplorer(this.path);
@override
Stream<Map<String, dynamic>> baca() async* {
final baris = await File(path).readAsLines();
final header = baris.first.split(',');
for (final b in baris.skip(1)) {
final nilai = b.split(',');
yield Map.fromIterables(header, nilai);
}
}
@override
Future<void> tutup() async {} // file tidak perlu ditutup eksplisit
}
class DatabaseEksplorer implements PengeksplorData {
final Database db;
final String tabel;
DatabaseEksplorer(this.db, this.tabel);
@override
Stream<Map<String, dynamic>> baca() async* {
final rows = await db.query(tabel);
for (final row in rows) yield row;
}
@override
Future<void> tutup() => db.close();
}
class ApiEksplorer implements PengeksplorData {
final String url;
ApiEksplorer(this.url);
@override
Stream<Map<String, dynamic>> baca() async* {
int halaman = 1;
while (true) {
final response = await http.get(Uri.parse('$url?page=$halaman'));
final data = jsonDecode(response.body) as List;
if (data.isEmpty) break;
for (final item in data) yield item as Map<String, dynamic>;
halaman++;
}
}
@override
Future<void> tutup() async {}
}
// Prosesor yang bekerja dengan semua implementasi — tidak peduli sumbernya
class ProsesorData {
Future<void> proses(PengeksplorData sumber) async {
int hitungBaris = 0;
await for (final baris in sumber.baca()) {
// proses setiap baris
hitungBaris++;
}
await sumber.tutup();
print('Diproses: $hitungBaris baris');
}
}
// Penggunaan — sumber bisa diganti tanpa mengubah ProsesorData
final prosesor = ProsesorData();
await prosesor.proses(CsvEksplorer('data.csv'));
await prosesor.proses(DatabaseEksplorer(db, 'pengguna'));
await prosesor.proses(ApiEksplorer('https://api.example.com/data'));
Kapan Menggunakan abstract class vs Kelas Biasa sebagai Interface
#
Memilih bentuk interface yang tepat bergantung pada beberapa faktor:
GUNAKAN abstract class ketika:
✓ Interface perlu menyediakan implementasi default untuk beberapa method
✓ Ada state atau properti yang bisa dibagi antar implementasi
✓ Ingin mendokumentasikan kontrak dengan DartDoc yang kaya
✓ Implementor kemungkinan besar menggunakan extends bukan implements
✓ Interface adalah bagian dari hierarki kelas yang lebih dalam
GUNAKAN interface class (Dart 3) ketika:
✓ Ingin secara eksplisit mencegah extends (hanya boleh implements)
✓ Interface adalah kontrak murni tanpa implementasi default
✓ Mendefinisikan batas antara library publik dan implementasi internal
GUNAKAN kelas biasa sebagai interface ketika:
✓ Kelas sudah ada dan ingin digunakan sebagai kontrak secara opportunistik
✓ Tidak peduli apakah implementor menggunakan extends atau implements
HINDARI menggunakan kelas konkret sebagai interface jika:
✗ Kelas tersebut sering berubah member publiknya
✗ Kelas tersebut memiliki konstruktor dengan banyak dependensi
✗ Ada lebih dari dua atau tiga implementasi — tandanya perlu abstract class
Ringkasan #
- Implicit interface — setiap kelas Dart secara otomatis mendefinisikan interface yang terdiri dari semua member publiknya. Kata kunci
interfaceeksplisit tidak diperlukan.implementsmewajibkan implementasi ulang semua member — tidak ada warisan implementasi. Cocok untuk mendefinisikan kontrak tipe yang bisa dipenuhi berbagai implementasi berbeda.abstract classadalah cara paling ekspresif mendefinisikan interface di Dart — bisa menggabungkan method abstrak (kontrak) dengan method konkret (implementasi default) dalam satu deklarasi.- Dart 3 memperkenalkan
interface classuntuk mencegahextendssecara eksplisit, serta modifier lain (base,final,sealed) untuk kontrol lebih ketat atas pewarisan.- Interface memungkinkan Dependency Inversion — bergantung pada abstraksi, bukan implementasi konkret. Ini membuat komponen bisa diuji secara terpisah dan diganti implementasinya tanpa perubahan besar.
- Interface Segregation — pecah interface besar menjadi interface kecil yang terfokus. Komponen harus bergantung hanya pada method yang benar-benar mereka gunakan.
- Fake/mock dari interface adalah cara terbaik untuk unit testing — implementasi palsu yang berperilaku sesuai skenario uji tanpa menyentuh database, jaringan, atau sistem eksternal.
- Polimorfisme melalui interface memungkinkan kode bekerja dengan berbagai implementasi berbeda melalui tipe yang sama — tanpa perlu tahu detail implementasi mana yang sedang aktif.
- Hindari interface monolitik — satu interface dengan 20 method memaksa setiap implementor mengimplementasikan semuanya, termasuk yang tidak relevan. Pisahkan berdasarkan tanggung jawab.