Interface

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. Gunakan abstract class atau interface 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 interface eksplisit tidak diperlukan.
  • implements mewajibkan implementasi ulang semua member — tidak ada warisan implementasi. Cocok untuk mendefinisikan kontrak tipe yang bisa dipenuhi berbagai implementasi berbeda.
  • abstract class adalah cara paling ekspresif mendefinisikan interface di Dart — bisa menggabungkan method abstrak (kontrak) dengan method konkret (implementasi default) dalam satu deklarasi.
  • Dart 3 memperkenalkan interface class untuk mencegah extends secara 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.

← Sebelumnya: Kelas   Berikutnya: Eksepsi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact