Pendahuluan — “adalah” vs “punya”

Di Inheritance (#44) kita pakai is-a: ebook adalah buku. Di Abstraction & ABC (#46) kita tulis kontrak kemampuan. Hari ini pertanyaan berikutnya: kapan objek sebaiknya punya objek lain (composition, has-a), bukan mewarisi class lain?

Aturan praktis: warisi karena hubungan “adalah-sejenis” yang jujur. Kalau hanya butuh “menyimpan/mengelola” object lain — biasanya composition lebih bersih.

Prasyarat: Selesai Inheritance (#44) dan nyaman dengan ABC (#46). Fondasi: Polymorphism (#45) · Encapsulation (#43) · Attribute (#42) · Class & Object (#41) · Mengenal OOP (#40).

Inheritance yang masih tepat — is-a

Cuplikan ringkas dari jalur Seri 3: ebook memang sejenis buku digital.

class Buku:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def info(self):
        return f"{self.judul} · {self.penulis}"


class Ebook(Buku):
    def __init__(self, judul, penulis, format_file="pdf"):
        super().__init__(judul, penulis)
        self.format_file = format_file

    def info(self):
        return f"{super().info()} · {self.format_file.upper()}"


e = Ebook("Belajar Python", "Sari", "epub")
print(e.info())
print("Ebook adalah Buku?", isinstance(e, Buku))

Output: Belajar Python · Sari · EPUB lalu Ebook adalah Buku? True. Inheritance di sini jujur — bukan tipuan demi hemat mengetik.

Anti-pola — warisi hanya demi “punya daftar”

Kesalahan umum: membuat perpustakaan mewarisi Buku supaya “langsung dapat” atribut judul. Padahal perpustakaan bukan sebuah buku.

class Buku:
    def __init__(self, judul):
        self.judul = judul


# SALAH — Perpustakaan "adalah" Buku? Tidak masuk akal
class PerpustakaanSalah(Buku):
    def __init__(self, nama, judul_buku_pertama):
        super().__init__(judul_buku_pertama)  # memaksa "judul" ke perpustakaan
        self.nama = nama


p = PerpustakaanSalah("Kota A", "ESP32 Praktis")
print("nama:", p.nama)
print("judul (milik 'Buku'?):", p.judul)
print("isinstance perpustakaan->Buku:", isinstance(p, Buku))  # True — menyesatkan

Output kurang lebih:

nama: Kota A
judul (milik 'Buku'?): ESP32 Praktis
isinstance perpustakaan->Buku: True

isinstance bilang True, tapi model domainnya bohong. Itu sinyal inheritance dipakai sebagai “kantong atribut”, bukan hubungan is-a.

Composition — Perpustakaan punya koleksi Buku

Composition: object menyimpan object lain sebagai bagian (biasanya di list/dict/atribut). Perpustakaan punya banyak buku — bukan adalah buku.

class Buku:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def info(self):
        return f"{self.judul} · {self.penulis}"


class Perpustakaan:
    def __init__(self, nama):
        self.nama = nama
        self.koleksi = []  # punya daftar Buku

    def tambah(self, buku):
        self.koleksi.append(buku)

    def daftar(self):
        return [b.info() for b in self.koleksi]


lib = Perpustakaan("Kota A")
lib.tambah(Buku("ESP32 Praktis", "Budi"))
lib.tambah(Buku("Belajar Python", "Sari"))
print(lib.nama)
for baris in lib.daftar():
    print("-", baris)
print("isinstance lib->Buku:", isinstance(lib, Buku))

Output kurang lebih:

Kota A
- ESP32 Praktis · Budi
- Belajar Python · Sari
isinstance lib->Buku: False

Sekarang modelnya jujur: perpustakaan mengelola koleksi; tiap buku tetap object sendiri. Composition juga sering berpasangan dengan ABC (#46): class pemilik koleksi tidak perlu mewarisi kontrak — ia cukup menyimpan object yang sudah memenuhi antarmuka (mis. item yang bisa info() atau pinjam()).

Perpustakaan koleksi[] (has-a) Buku ESP32 Praktis Buku Belajar Python
Panah “punya”: Perpustakaan mengelola Buku — composition, bukan pewarisan.

Refactor — dari SALAH ke BENAR

Bandingkan sisi pemanggil. Composition membuat API perpustakaan berbicara tentang koleksi, bukan menyamar sebagai buku:

class Buku:
    def __init__(self, judul):
        self.judul = judul


class PerpustakaanSalah(Buku):
    def __init__(self, nama, judul):
        super().__init__(judul)
        self.nama = nama


class PerpustakaanBenar:
    def __init__(self, nama):
        self.nama = nama
        self.koleksi = []

    def tambah(self, buku):
        self.koleksi.append(buku)

    def jumlah(self):
        return len(self.koleksi)


salah = PerpustakaanSalah("Kota A", "ESP32 Praktis")
benar = PerpustakaanBenar("Kota A")
benar.tambah(Buku("ESP32 Praktis"))
benar.tambah(Buku("Cerita Sensor"))
print("SALAH judul di perpustakaan:", salah.judul)
print("BENAR jumlah buku:", benar.jumlah())

Output: SALAH judul di perpustakaan: ESP32 Praktis lalu BENAR jumlah buku: 2. Versi benar bisa tumbuh (cari, pinjam, statistik) tanpa memaksa perpustakaan “jadi” buku.

Kapan inheritance masih pilihan pertama?

  • Is-a jujurEbook(Buku), Audiobook(Buku)
  • Perilaku bersama + override — method induk dipakai ulang dengan variasi (lihat Polymorphism (#45))
  • Kontrak formal — ABC di Abstraction (#46) memang memakai pewarisan untuk menegakkan antarmuka

Kalau alasanmu hanya “biar tidak copy-paste atribut” atau “biar dapat method list gratis” — berhenti sejenak. Composition (atau fungsi helper) sering lebih aman. Contoh klasik: mewarisi list:

class Buku:
    def __init__(self, judul):
        self.judul = judul


# SALAH — katalog "adalah" list? API jadi bocor ke method list mentah
class KatalogSalah(list):
    def judul_semua(self):
        return [b.judul for b in self]


# BENAR — katalog punya items
class KatalogBenar:
    def __init__(self):
        self.items = []

    def tambah(self, buku):
        self.items.append(buku)

    def judul_semua(self):
        return [b.judul for b in self.items]


salah = KatalogSalah([Buku("ESP32 Praktis"), Buku("Belajar Python")])
benar = KatalogBenar()
benar.tambah(Buku("ESP32 Praktis"))
benar.tambah(Buku("Belajar Python"))
print("SALAH isinstance list:", isinstance(salah, list))
print("BENAR isinstance list:", isinstance(benar, list))
print("BENAR judul:", benar.judul_semua())

Output kurang lebih:

SALAH isinstance list: True
BENAR isinstance list: False
BENAR judul: ['ESP32 Praktis', 'Belajar Python']

KatalogSalah “kebetulan” bisa append/extend seperti list — tapi tipe domainnya menyesatkan. Prefer bungkus list di dalam atribut.

Composition juga memudahkan menambah perilaku di pemilik koleksi — tanpa mengubah tipe Buku:

class Buku:
    def __init__(self, judul, tahun):
        self.judul = judul
        self.tahun = tahun


class Perpustakaan:
    def __init__(self, nama):
        self.nama = nama
        self.koleksi = []

    def tambah(self, buku):
        self.koleksi.append(buku)

    def cari(self, kata):
        kata = kata.lower()
        return [b for b in self.koleksi if kata in b.judul.lower()]


lib = Perpustakaan("Kota A")
lib.tambah(Buku("ESP32 Praktis", 2023))
lib.tambah(Buku("Belajar Python", 2024))
hasil = lib.cari("python")
print([b.judul for b in hasil])

Output: ['Belajar Python']. Fitur pencarian hidup di perpustakaan — pemilik koleksi — bukan disisipkan lewat warisan aneh.

Pola Dasar — memilih composition atau inheritance

  1. 1
    Uji kalimat is-a “X adalah Y?” Kalau terdengar aneh di dunia nyata, curigai inheritance.
  2. 2
    Uji kalimat has-a “X punya Y / mengelola banyak Y?” → composition (atribut/koleksi).
  3. 3
    Jangan warisi demi reuse buta Butuh perilaku mirip ≠ wajib jadi subclass. Ekstrak fungsi/helper atau composition.
  4. 4
    Biarkan API pemilik koleksi tambah, cari, jumlah hidup di class yang punya daftar.
  5. 5
    Campur dengan sadar Inheritance untuk is-a/kontrak; composition untuk struktur “punya”. Keduanya sering dipakai bersama.
Ringkas: is-a? → inheritance. has-a? → composition. reuse buta? → jangan warisi.

Kode lengkap — salin & jalankan

Simpan sebagai perpustakaan_komposisi.py, lalu python perpustakaan_komposisi.py:

class Buku:
    def __init__(self, judul, penulis, tahun):
        self.judul = judul
        self.penulis = penulis
        self.tahun = tahun

    def info(self):
        return f"{self.judul} · {self.penulis} ({self.tahun})"


class Ebook(Buku):
    """Inheritance tetap dipakai di mana is-a jujur."""

    def __init__(self, judul, penulis, tahun, format_file="pdf"):
        super().__init__(judul, penulis, tahun)
        self.format_file = format_file

    def info(self):
        return f"{super().info()} · {self.format_file.upper()}"


class Perpustakaan:
    """Composition: punya koleksi item katalog."""

    def __init__(self, nama):
        self.nama = nama
        self.koleksi = []

    def tambah(self, item):
        self.koleksi.append(item)

    def daftar(self):
        return [item.info() for item in self.koleksi]

    def cari(self, kata):
        kata = kata.lower()
        return [item for item in self.koleksi if kata in item.info().lower()]


lib = Perpustakaan("Kota A")
lib.tambah(Buku("ESP32 Praktis", "Budi", 2023))
lib.tambah(Ebook("Belajar Python", "Sari", 2024, "epub"))
print("Perpustakaan:", lib.nama)
for baris in lib.daftar():
    print("-", baris)
print("cari python:", [x.judul for x in lib.cari("python")])
print("Ebook adalah Buku?", isinstance(lib.koleksi[1], Buku))
print("Perpustakaan adalah Buku?", isinstance(lib, Buku))

Output yang diharapkan (kurang lebih):

Perpustakaan: Kota A
- ESP32 Praktis · Budi (2023)
- Belajar Python · Sari (2024) · EPUB
cari python: ['Belajar Python']
Ebook adalah Buku? True
Perpustakaan adalah Buku? False

Kesalahan umum

Gejala Penyebab tipikal Perbaikan
isinstance(perpustakaan, Buku) True padahal aneh Warisi class domain yang salah Ganti ke composition: simpan list Buku
Class induk jadi “god class” Menjejalkan semua fitur lewat inheritance Pecah tanggung jawab; pemilik koleksi terpisah
Takut inheritance sama sekali Mengira composition selalu lebih baik Pakai inheritance untuk is-a / kontrak ABC (#46) yang jujur
Copy-paste atribut antar class Menghindari inheritance tanpa alternatif Composition, helper, atau dataclass (artikel berikutnya)
Bingung dengan Encapsulation Menyamakan “sembunyikan detail” dengan “punya object” Encapsulation (#43) jaga akses; composition jaga struktur kepemilikan

Latihan singkat

  1. Tambah method hapus(judul) di Perpustakaan yang mengeluarkan buku dari koleksi.
  2. Ubah KatalogSalah menjadi composition murni (tanpa mewarisi list), lalu bandingkan isinstance(..., list).
  3. Jelaskan dalam dua kalimat: kenapa Ebook(Buku) OK tapi Perpustakaan(Buku) tidak.

FAQ singkat

Apakah composition menggantikan inheritance?
Tidak. Keduanya alat. Inheritance untuk is-a/kontrak; composition untuk has-a/struktur.

Apa bedanya dengan ABC (#46)?
ABC memaksa daftar method. Composition menjawab “siapa punya object siapa” — sering dipakai bersama.

Bolehkah class mewarisi list atau dict?
Bisa secara teknis, sering menyulitkan. Prefer composition: self.items = [] lalu bungkus method yang kamu butuhkan.

Apakah “favor composition over inheritance” berarti inheritance jelek?
Tidak. Itu peringatan agar tidak warisi demi reuse buta — selaras pesan di Inheritance (#44).

Di mana atribut koleksi sebaiknya divalidasi?
Di method pemilik (tambah) atau lewat property — lihat lagi Encapsulation (#43).

Kesimpulan & langkah berikutnya

Tanya dulu: adalah atau punya? Is-a yang jujur → inheritance. Has-a / mengelola banyak object → composition. Hindari mewarisi hanya supaya atribut “ikut gratis”.

Artikel ini adalah #47 (ini) — langkah kedelapan Seri 3 setelah Abstraction & ABC (#46).

Lanjut ke artikel berikutnya — Special Methods & Dataclass: __str__, __repr__, __eq__, dan @dataclass agar object lebih nyaman dibaca dan dibandingkan. (Belum di-hyperlink sampai artikel itu live.)

Seri 3 progress: 8/10 artikel live. Kamu di langkah #47 (ini). Prasyarat: Abstraction (#46) · Inheritance (#44) · Polymorphism (#45) · Encapsulation (#43) · Attribute (#42) · Class & Object (#41) · Mengenal OOP (#40).