Interface #

Python tidak memiliki keyword interface seperti Java atau Go. Tapi Python memiliki sesuatu yang lebih fleksibel: tiga cara berbeda untuk mendefinisikan kontrak antara komponen — duck typing, Abstract Base Class (ABC), dan Protocol. Ketiganya menjawab pertanyaan yang sama: “Bagaimana saya memastikan sebuah objek memiliki metode yang saya butuhkan?” — tapi dengan tingkat keketatan dan ekspresi yang berbeda. Memahami kapan menggunakan masing-masing adalah kunci desain kode Python yang bersih dan dapat diperluas.

Duck Typing — Interface Implisit #

Sebelum membahas mekanisme formal, penting untuk memahami filosofi dasar Python: “Jika ia berjalan seperti bebek dan bersuara seperti bebek, maka ia adalah bebek.” Python tidak peduli tipe objek secara eksplisit — yang penting objek memiliki metode yang dibutuhkan.

# Tidak ada deklarasi interface — Python cukup panggil metodenya
class Anjing:
    def bersuara(self):
        return "Guk!"

class Kucing:
    def bersuara(self):
        return "Meow!"

class Bebek:
    def bersuara(self):
        return "Kwek!"

# Fungsi ini bekerja untuk semua objek yang punya metode bersuara()
# Tidak perlu inheritance atau interface deklarasi apapun
def buat_keributan(hewan_list):
    for hewan in hewan_list:
        print(hewan.bersuara())   # Python tidak cek tipe — langsung panggil

hewan = [Anjing(), Kucing(), Bebek()]
buat_keributan(hewan)
# → Guk!
# → Meow!
# → Kwek!

Duck typing sangat kuat dan fleksibel, tapi memiliki kelemahan: tidak ada mekanisme bawaan yang memastikan objek memang memiliki metode yang dibutuhkan sebelum runtime. Error baru muncul saat metode dipanggil — bukan saat objek dibuat.


Abstract Base Class (ABC) #

ABC adalah cara formal mendefinisikan interface di Python. Kelas yang mewarisi ABC dengan metode abstrak wajib mengimplementasikan semua metode abstrak tersebut — jika tidak, Python akan melempar TypeError saat mencoba membuat instance.

ABC Dasar #

from abc import ABC, abstractmethod

class Kendaraan(ABC):
    """Interface untuk semua jenis kendaraan."""

    @abstractmethod
    def bergerak(self) -> str:
        """Mengembalikan deskripsi gerakan kendaraan."""
        ...

    @abstractmethod
    def berhenti(self) -> None:
        """Menghentikan kendaraan."""
        ...

    @abstractmethod
    def kecepatan_maks(self) -> float:
        """Mengembalikan kecepatan maksimum dalam km/h."""
        ...

    # Metode konkret di ABC — tersedia untuk semua subkelas
    def info(self) -> str:
        return f"{self.__class__.__name__}: kecepatan maks {self.kecepatan_maks()} km/h"


# ANTI-PATTERN: subkelas yang tidak mengimplementasikan semua metode abstrak
class Sepeda(Kendaraan):
    def bergerak(self) -> str:
        return "mengayuh pedal"
    # berhenti() dan kecepatan_maks() belum diimplementasikan!

# sepeda = Sepeda()
# → TypeError: Can't instantiate abstract class Sepeda
#   with abstract methods berhenti, kecepatan_maks


# BENAR: implementasikan semua metode abstrak
class Mobil(Kendaraan):
    def __init__(self, merek: str, top_speed: float):
        self.merek = merek
        self._top_speed = top_speed
        self._berjalan = False

    def bergerak(self) -> str:
        self._berjalan = True
        return f"{self.merek} mulai bergerak"

    def berhenti(self) -> None:
        self._berjalan = False

    def kecepatan_maks(self) -> float:
        return self._top_speed


class Sepeda(Kendaraan):
    def bergerak(self) -> str:
        return "Mengayuh pedal"

    def berhenti(self) -> None:
        print("Menarik rem")

    def kecepatan_maks(self) -> float:
        return 30.0


class Kereta(Kendaraan):
    def bergerak(self) -> str:
        return "Melaju di rel"

    def berhenti(self) -> None:
        print("Rem pneumatik aktif")

    def kecepatan_maks(self) -> float:
        return 300.0


# Tidak bisa instantiasi ABC secara langsung
# kendaraan = Kendaraan()  # → TypeError

mobil = Mobil("Toyota", 180.0)
sepeda = Sepeda()
kereta = Kereta()

print(mobil.bergerak())    # → Toyota mulai bergerak
print(mobil.info())        # → Mobil: kecepatan maks 180.0 km/h
print(sepeda.info())       # → Sepeda: kecepatan maks 30.0 km/h
print(kereta.info())       # → Kereta: kecepatan maks 300.0 km/h

# Polimorfisme — fungsi bekerja pada semua Kendaraan
def balapkan(kendaraan_list: list[Kendaraan]) -> None:
    for k in sorted(kendaraan_list, key=lambda x: x.kecepatan_maks(), reverse=True):
        print(f"{k.__class__.__name__}: {k.bergerak()} ({k.kecepatan_maks()} km/h)")

balapkan([mobil, sepeda, kereta])
# → Kereta: Melaju di rel (300.0 km/h)
# → Mobil: Toyota mulai bergerak (180.0 km/h)
# → Sepeda: Mengayuh pedal (30.0 km/h)

Abstract Property #

ABC juga mendukung abstract property — atribut yang wajib diimplementasikan oleh subkelas:

from abc import ABC, abstractmethod

class Bentuk(ABC):
    """Interface untuk bentuk geometri 2D."""

    @property
    @abstractmethod
    def luas(self) -> float:
        """Luas bentuk dalam satuan persegi."""
        ...

    @property
    @abstractmethod
    def keliling(self) -> float:
        """Keliling bentuk."""
        ...

    def deskripsi(self) -> str:
        return (
            f"{self.__class__.__name__}: "
            f"luas={self.luas:.2f}, keliling={self.keliling:.2f}"
        )


class Lingkaran(Bentuk):
    def __init__(self, jari_jari: float):
        self.jari_jari = jari_jari

    @property
    def luas(self) -> float:
        import math
        return math.pi * self.jari_jari ** 2

    @property
    def keliling(self) -> float:
        import math
        return 2 * math.pi * self.jari_jari


class Persegi(Bentuk):
    def __init__(self, sisi: float):
        self.sisi = sisi

    @property
    def luas(self) -> float:
        return self.sisi ** 2

    @property
    def keliling(self) -> float:
        return 4 * self.sisi


class SegiTiga(Bentuk):
    def __init__(self, alas: float, tinggi: float, sisi_a: float, sisi_b: float, sisi_c: float):
        self.alas = alas
        self.tinggi = tinggi
        self._sisi = (sisi_a, sisi_b, sisi_c)

    @property
    def luas(self) -> float:
        return 0.5 * self.alas * self.tinggi

    @property
    def keliling(self) -> float:
        return sum(self._sisi)


bentuk_list: list[Bentuk] = [
    Lingkaran(7),
    Persegi(5),
    SegiTiga(6, 4, 6, 5, 5),
]

for bentuk in bentuk_list:
    print(bentuk.deskripsi())
# → Lingkaran: luas=153.94, keliling=43.98
# → Persegi: luas=25.00, keliling=20.00
# → SegiTiga: luas=12.00, keliling=16.00

ABC dengan Implementasi Default (Template Method Pattern) #

ABC tidak harus hanya berisi metode abstrak — bisa menyediakan implementasi default yang bisa di-override oleh subkelas:

from abc import ABC, abstractmethod

class Laporan(ABC):
    """Template untuk pembuatan laporan — pola Template Method."""

    def buat(self) -> str:
        """Urutan langkah sudah ditentukan, detail diimplementasikan subkelas."""
        bagian = [
            self._header(),
            self._isi(),
            self._footer(),
        ]
        return "\n".join(bagian)

    @abstractmethod
    def _header(self) -> str:
        ...

    @abstractmethod
    def _isi(self) -> str:
        ...

    def _footer(self) -> str:
        # Implementasi default — subkelas boleh override atau tidak
        return "--- Akhir Laporan ---"


class LaporanPenjualan(Laporan):
    def __init__(self, bulan: str, total: float):
        self.bulan = bulan
        self.total = total

    def _header(self) -> str:
        return f"=== LAPORAN PENJUALAN {self.bulan.upper()} ==="

    def _isi(self) -> str:
        return f"Total penjualan: Rp{self.total:,.0f}"


class LaporanInventaris(Laporan):
    def __init__(self, produk_list: list):
        self.produk_list = produk_list

    def _header(self) -> str:
        return "=== LAPORAN INVENTARIS ==="

    def _isi(self) -> str:
        baris = [f"- {p['nama']}: {p['stok']} unit" for p in self.produk_list]
        return "\n".join(baris)

    def _footer(self) -> str:
        # Override footer untuk laporan inventaris
        total_stok = sum(p['stok'] for p in self.produk_list)
        return f"Total stok keseluruhan: {total_stok} unit"


lap_jual = LaporanPenjualan("Maret", 125_500_000)
print(lap_jual.buat())
# → === LAPORAN PENJUALAN MARET ===
# → Total penjualan: Rp125,500,000
# → --- Akhir Laporan ---

Virtual Subclass — Daftarkan Tanpa Mewarisi #

ABC juga mendukung virtual subclass — kelas yang dianggap sebagai implementasi ABC tanpa mewarisinya secara langsung. Berguna untuk mengintegrasikan kelas pihak ketiga ke dalam sistem tipe yang sudah ada.

from abc import ABC, abstractmethod

class Serializable(ABC):
    @abstractmethod
    def serialize(self) -> str:
        ...

    @abstractmethod
    def deserialize(self, data: str) -> None:
        ...

# Kelas pihak ketiga yang tidak mewarisi Serializable
class KelasLuarBiasa:
    def serialize(self) -> str:
        return '{"data": "ok"}'

    def deserialize(self, data: str) -> None:
        pass

# Daftarkan sebagai virtual subclass
Serializable.register(KelasLuarBiasa)

obj = KelasLuarBiasa()
print(isinstance(obj, Serializable))   # → True  (meski tidak mewarisi)
print(issubclass(KelasLuarBiasa, Serializable))   # → True

Protocol — Structural Subtyping (Duck Typing Formal) #

Protocol (diperkenalkan di Python 3.8 via PEP 544) adalah cara mendefinisikan interface berdasarkan struktur — bukan berdasarkan pewarisan. Kelas dianggap mengimplementasikan Protocol selama ia memiliki metode dan atribut yang diperlukan, tanpa perlu mewarisi atau mendaftarkan diri secara eksplisit.

from typing import Protocol, runtime_checkable

class Drawable(Protocol):
    """Protocol untuk objek yang bisa digambar."""
    def draw(self) -> None:
        ...

    def resize(self, factor: float) -> None:
        ...


# Kelas-kelas berikut TIDAK mewarisi Drawable — tapi dianggap mengimplementasikannya
# karena memiliki metode draw() dan resize()
class Lingkaran:
    def __init__(self, r: float):
        self.r = r

    def draw(self) -> None:
        print(f"Menggambar lingkaran r={self.r}")

    def resize(self, factor: float) -> None:
        self.r *= factor


class Kotak:
    def __init__(self, w: float, h: float):
        self.w = w
        self.h = h

    def draw(self) -> None:
        print(f"Menggambar kotak {self.w}x{self.h}")

    def resize(self, factor: float) -> None:
        self.w *= factor
        self.h *= factor


# Type checker (mypy) akan menerima keduanya sebagai Drawable
def render_semua(shapes: list[Drawable]) -> None:
    for shape in shapes:
        shape.draw()

shapes = [Lingkaran(5), Kotak(10, 8)]
render_semua(shapes)
# → Menggambar lingkaran r=5
# → Menggambar kotak 10x8

@runtime_checkable — Protocol yang Bisa Dicek dengan isinstance #

from typing import Protocol, runtime_checkable

@runtime_checkable
class Dapat_Ditutup(Protocol):
    def close(self) -> None:
        ...


class KoneksiDatabase:
    def close(self) -> None:
        print("Menutup koneksi database")


class FileHandler:
    def close(self) -> None:
        print("Menutup file")


class Timer:
    def mulai(self) -> None:  # tidak punya close()
        pass


db = KoneksiDatabase()
fh = FileHandler()
tm = Timer()

print(isinstance(db, Dapat_Ditutup))   # → True
print(isinstance(fh, Dapat_Ditutup))   # → True
print(isinstance(tm, Dapat_Ditutup))   # → False

# Berguna untuk resource cleanup
def tutup_jika_bisa(obj: object) -> None:
    if isinstance(obj, Dapat_Ditutup):
        obj.close()

tutup_jika_bisa(db)   # → Menutup koneksi database
tutup_jika_bisa(tm)   # → (tidak ada output — Timer tidak punya close())

ABC vs Protocol — Kapan Menggunakan Yang Mana #

Gunakan ABC jika:
  ✓ Ingin memaksa implementasi metode saat kelas dibuat (bukan saat dipakai)
  ✓ ABC menyediakan implementasi default yang dibagikan ke subkelas
  ✓ Hubungan IS-A yang jelas dan eksplisit diperlukan
  ✓ Ingin menggunakan Template Method pattern
  ✓ Perlu virtual subclass untuk kelas pihak ketiga

Gunakan Protocol jika:
  ✓ Ingin duck typing yang terdokumentasi dan bisa dicek oleh type checker
  ✓ Kelas implementasi sudah ada dan tidak bisa diubah (tidak bisa tambahkan inheritance)
  ✓ Tidak perlu implementasi default — hanya mendefinisikan kontrak
  ✓ Ingin interface yang lebih ringan tanpa coupling ke hirarki kelas
  ✓ Bekerja dengan library pihak ketiga yang tidak bisa diubah

Untuk memperjelas perbedaan konseptual antara Nominal Subtyping (berdasarkan nama dan pewarisan eksplisit) dengan Structural Subtyping (berdasarkan kesamaan struktur tanpa pewarisan), perhatikan diagram kelas perbandingan di bawah ini:

classDiagram
    class ABC_Model {
        <<Abstract>>
        +simpan(kunci, nilai)
        +ambil(kunci)
    }
    class RedisStorage_ABC {
        +simpan(kunci, nilai)
        +ambil(kunci)
    }
    ABC_Model <|-- RedisStorage_ABC : Pewarisan Eksplisit (Nominal)

    class Protocol_Model {
        <<Protocol>>
        +simpan(kunci, nilai)
        +ambil(kunci)
    }
    class RedisStorage_Protocol {
        +simpan(kunci, nilai)
        +ambil(kunci)
    }
    Protocol_Model <.. RedisStorage_Protocol : Kesamaan Struktur (Structural)

Pada model ABC (Nominal), kelas RedisStorage harus secara eksplisit mewarisi PenyimpananABC. Sedangkan pada model Protocol (Structural), RedisStorage dianggap mengimplementasikan PenyimpananProtocol secara otomatis selama memiliki metode dengan struktur/signature yang sama, tanpa perlu mewarisi kelas apa pun.

Perbandingan langsung: ABC vs Protocol untuk interface yang sama #

# === Dengan ABC ===
from abc import ABC, abstractmethod

class PenyimpananABC(ABC):
    @abstractmethod
    def simpan(self, kunci: str, nilai: str) -> None: ...
    @abstractmethod
    def ambil(self, kunci: str) -> str | None: ...

class RedisStorage(PenyimpananABC):  # HARUS mewarisi PenyimpananABC
    def simpan(self, kunci, nilai): ...
    def ambil(self, kunci): ...

# === Dengan Protocol ===
from typing import Protocol

class PenyimpananProtocol(Protocol):
    def simpan(self, kunci: str, nilai: str) -> None: ...
    def ambil(self, kunci: str) -> str | None: ...

class RedisStorage:  # tidak perlu deklarasi apapun
    def simpan(self, kunci, nilai): ...
    def ambil(self, kunci): ...

# RedisStorage sudah kompatibel dengan PenyimpananProtocol
# tanpa mewarisi apapun — type checker akan memverifikasi strukturnya

Ringkasan #

  • Duck typing adalah default Python — jika objek memiliki metode yang dibutuhkan, ia bisa digunakan tanpa deklarasi formal apapun.
  • ABC memberikan kontrak eksplisit yang di-enforce saat kelas dibuat — subkelas yang tidak mengimplementasikan metode abstrak akan TypeError saat diinstansiasi, bukan saat metode dipanggil.
  • @abstractmethod menandai metode yang wajib diimplementasikan. @property + @abstractmethod untuk atribut wajib.
  • ABC boleh memiliki implementasi default — subkelas bisa mewarisi atau meng-override. Pola ini disebut Template Method.
  • Virtual subclass (ABC.register()) memungkinkan kelas pihak ketiga dianggap sebagai implementasi ABC tanpa mengubah kodenya.
  • Protocol mendefinisikan interface berdasarkan struktur, bukan pewarisan — kelas apapun yang memiliki metode yang diperlukan dianggap kompatibel.
  • @runtime_checkable memungkinkan isinstance() bekerja pada Protocol — berguna untuk resource cleanup dan dispatch dinamis.
  • Pilih ABC jika butuh enforcement saat kelas didefinisikan atau implementasi default dishare. Pilih Protocol jika butuh interface yang lebih ringan dan tidak ingin coupling ke hirarki kelas.

← Sebelumnya: Kelas   Berikutnya: Eksepsi →

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