Pendahuluan — capstone Seri 3

Selamat! Kamu sudah menempuh sembilan artikel Seri 3 — dari Mengenal OOP (#40) sampai Special Methods & Dataclass (#48). Artikel ini adalah capstone penutup: satu proyek kecil yang menggabungkan class, encapsulation, inheritance, polymorphism, ABC, composition, dan dataclass dalam domain yang familiar — sistem perpustakaan mini.

Bukan soal membuat aplikasi produksi. Tujuannya: membuktikan bahwa potongan konsep Seri 3 bisa saling mengunci dalam satu file yang bisa kamu jalankan.

Prasyarat: Sudah baca Special Methods & Dataclass (#48). Fondasi: Composition (#47) · ABC (#46) · Polymorphism (#45) · Inheritance (#44) · Encapsulation (#43) · Attribute (#42) · Class & Object (#41) · Mengenal OOP (#40).

Spesifikasi fitur

Perpustakaan mini kita mendukung lima operasi inti:

  • Tambah — masukkan buku/ebook ke koleksi
  • Pinjam — kurangi stok jika tersedia
  • Kembalikan — kembalikan stok setelah dipinjam
  • Cari — temukan item berdasarkan judul (substring)
  • Laporan — cetak ringkasan koleksi + status stok

Semua dijalankan lewat fungsi demo() yang mensimulasikan perintah CLI — tanpa menunggu keyboard, supaya skrip bisa diuji otomatis.

Arsitektur folder mini

Di proyek nyata kamu bisa memisah file. Untuk capstone ini kita tetap satu file supaya mudah disalin — tapi pikirkan batas tanggung jawab seperti folder berikut:

models/ Buku · Ebook · ABC services/ Perpustakaan main.py demo() / CLI models -> services -> main (satu arah) Capstone: ketiga lapisan digabung di perpustakaan_mini.py
Pola mental: data di models/, aturan bisnis di services/, pintu masuk di main.py.

Model data — dataclass, inheritance, ABC

Kita pakai @dataclass untuk Buku (lihat #48), Ebook(Buku) untuk inheritance (#44), dan ABC Pinjaman dengan kontrak pinjam/kembalikan (#46):

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool:
        ...

    @abstractmethod
    def kembalikan(self) -> bool:
        ...


@dataclass
class Buku(Pinjaman):
    judul: str
    penulis: str
    stok: int = 1

    def pinjam(self) -> bool:
        if self.stok < 1:
            return False
        self.stok -= 1
        return True

    def kembalikan(self) -> bool:
        self.stok += 1
        return True

    def info(self) -> str:
        return f"Buku: {self.judul} · {self.penulis} (stok={self.stok})"


@dataclass
class Ebook(Buku):
    format_file: str = "PDF"

    def info(self) -> str:
        return f"Ebook: {self.judul} · {self.format_file} (stok={self.stok})"


b = Buku("ESP32 Praktis", "Budi", stok=2)
e = Ebook("Belajar Python", "Sari", stok=1, format_file="EPUB")
print(b.info())
print(e.info())
print("pinjam buku:", b.pinjam(), "stok=", b.stok)

Output:

Buku: ESP32 Praktis · Budi (stok=2)
Ebook: Belajar Python · EPUB (stok=1)
pinjam buku: True stok= 1

Ebook mewarisi pinjam/kembalikan dari Buku, lalu menimpa info() — inheritance + polymorphism dalam satu langkah.

Batas encapsulation (#43) di sini sederhana: stok hanya berubah lewat method pinjam/kembalikan, bukan diubah bebas dari luar. (Naik ke @property boleh belakangan — lihat FAQ.)

Jembatan ke Special Methods (#48): @dataclass sudah menyediakan __init__, __repr__, dan __eq__. Untuk teks domain kita tetap pakai method info(); di kode lengkap nanti kita tambahkan __str__ yang memanggil info() supaya print(buku) ikut ramah manusia.

ABC menolak instance langsung — sama seperti di ABC (#46):

from abc import ABC, abstractmethod


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool:
        ...

    @abstractmethod
    def kembalikan(self) -> bool:
        ...


try:
    Pinjaman()
except TypeError as err:
    print("ABC menolak:", type(err).__name__)

Output:

ABC menolak: TypeError

Composition — class Perpustakaan punya koleksi

Sesuai Composition (#47): Perpustakaan punya daftar item, bukan “adalah” daftar:

from dataclasses import dataclass
from abc import ABC, abstractmethod


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool: ...
    @abstractmethod
    def kembalikan(self) -> bool: ...


@dataclass
class Buku(Pinjaman):
    judul: str
    penulis: str
    stok: int = 1

    def pinjam(self) -> bool:
        if self.stok < 1:
            return False
        self.stok -= 1
        return True

    def kembalikan(self) -> bool:
        self.stok += 1
        return True

    def info(self) -> str:
        return f"Buku: {self.judul} (stok={self.stok})"


class Perpustakaan:
    def __init__(self, nama: str):
        self.nama = nama
        self.koleksi = []  # composition: punya items

    def tambah(self, item: Buku) -> None:
        self.koleksi.append(item)

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

    def laporan(self) -> str:
        baris = [f"=== {self.nama} ({len(self.koleksi)} item) ==="]
        for i in self.koleksi:
            baris.append(i.info())
        return "\n".join(baris)


p = Perpustakaan("Kota A")
p.tambah(Buku("ESP32 Praktis", "Budi", stok=2))
p.tambah(Buku("MQTT Dasar", "Ani", stok=1))
print(p.laporan())
print("cari 'mqtt':", [x.judul for x in p.cari("mqtt")])

Output:

=== Kota A (2 item) ===
Buku: ESP32 Praktis (stok=2)
Buku: MQTT Dasar (stok=1)
cari 'mqtt': ['MQTT Dasar']

Polymorphism — loop info() pada campuran Buku/Ebook

Satu loop, dua tipe — pola dari Polymorphism (#45):

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool: ...
    @abstractmethod
    def kembalikan(self) -> bool: ...


@dataclass
class Buku(Pinjaman):
    judul: str
    penulis: str
    stok: int = 1

    def pinjam(self) -> bool:
        if self.stok < 1:
            return False
        self.stok -= 1
        return True

    def kembalikan(self) -> bool:
        self.stok += 1
        return True

    def info(self) -> str:
        return f"Buku: {self.judul}"


@dataclass
class Ebook(Buku):
    format_file: str = "PDF"

    def info(self) -> str:
        return f"Ebook: {self.judul} [{self.format_file}]"


items = [
    Buku("ESP32 Praktis", "Budi"),
    Ebook("Belajar Python", "Sari", format_file="EPUB"),
    Buku("MQTT Dasar", "Ani"),
]
for item in items:
    print(item.info())  # method yang tepat dipanggil otomatis

Output:

Buku: ESP32 Praktis
Ebook: Belajar Python [EPUB]
Buku: MQTT Dasar

Pinjam sampai stok habis — method mengembalikan False, bukan error:

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool: ...
    @abstractmethod
    def kembalikan(self) -> bool: ...


@dataclass
class Buku(Pinjaman):
    judul: str
    penulis: str
    stok: int = 1

    def pinjam(self) -> bool:
        if self.stok < 1:
            return False
        self.stok -= 1
        return True

    def kembalikan(self) -> bool:
        self.stok += 1
        return True


b = Buku("MQTT Dasar", "Ani", stok=1)
print("1:", b.pinjam(), "stok=", b.stok)
print("2:", b.pinjam(), "stok=", b.stok)  # False — stok habis
print("kembali:", b.kembalikan(), "stok=", b.stok)

Output:

1: True stok= 0
2: False stok= 0
kembali: True stok= 1

CLI demo — menu lewat skrip, bukan interaktif

Di CLI interaktif biasanya ada menu: 1) tambah 2) pinjam 3) kembalikan 4) cari 5) laporan. Untuk artikel & CI, kita tidak memakai input(). Fungsi demo() memanggil method yang sama secara berurutan — hasilnya deterministic dan bisa di-py_compile.

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool: ...
    @abstractmethod
    def kembalikan(self) -> bool: ...


@dataclass
class Buku(Pinjaman):
    judul: str
    penulis: str
    stok: int = 1

    def pinjam(self) -> bool:
        if self.stok < 1:
            return False
        self.stok -= 1
        return True

    def kembalikan(self) -> bool:
        self.stok += 1
        return True

    def info(self) -> str:
        return f"{self.judul} (stok={self.stok})"


@dataclass
class Ebook(Buku):
    format_file: str = "PDF"

    def info(self) -> str:
        return f"{self.judul} [{self.format_file}] (stok={self.stok})"


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

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

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

    def laporan(self):
        return "\n".join(i.info() for i in self.koleksi)


def demo():
    p = Perpustakaan("Mini")
    p.tambah(Buku("ESP32 Praktis", "Budi", stok=2))
    p.tambah(Ebook("Belajar Python", "Sari", format_file="EPUB"))
    # perintah terscript — setara menu: pinjam, kembalikan, cari, laporan
    print("pinjam:", p.koleksi[0].pinjam())
    print("kembalikan:", p.koleksi[0].kembalikan())
    print("cari:", [x.judul for x in p.cari("python")])
    print(p.laporan())


demo()

Output:

pinjam: True
kembalikan: True
cari: ['Belajar Python']
ESP32 Praktis (stok=2)
Belajar Python [EPUB] (stok=1)

Perhatikan: demo memanggil p.koleksi[0].pinjam() langsung — sengaja “bocor” ke item agar pola polymorphism terlihat. Latihan 1 meminta kamu naikkan ke Perpustakaan.pinjam_judul(...) supaya layanan yang mengorkestrasi.

Kalau nanti kamu ingin CLI interaktif di mesin sendiri, bungkus menu dengan loop + pembacaan keyboard — tapi jangan masukkan itu ke skrip yang harus jalan di CI/artikel.

Pola Dasar — lima langkah capstone

  1. 1
    Tulis kontrak ABC dulu Pinjaman mendefinisikan pinjam/kembalikan sebelum detail stok.
  2. 2
    Model data dengan dataclass Buku + Ebook(Buku) — field jelas, inheritance ringan.
  3. 3
    Composition di layanan Perpustakaan punya self.koleksi — tambah, cari, laporan.
  4. 4
    Polymorphism lewat info() Loop campuran Buku/Ebook tanpa if isinstance berlebihan.
  5. 5
    Demo terscript, deterministic Satu fungsi demo() memanggil alur pinjam/kembalikan/cari/laporan agar bisa diuji.
Ringkas: ABC -> model -> composition -> polymorphism -> demo deterministic (plus __str__ di kode lengkap).

Kode lengkap — salin & jalankan

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

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Pinjaman(ABC):
    @abstractmethod
    def pinjam(self) -> bool: ...
    @abstractmethod
    def kembalikan(self) -> bool: ...


@dataclass
class Buku(Pinjaman):
    judul: str
    penulis: str
    stok: int = 1

    def pinjam(self) -> bool:
        if self.stok < 1:
            return False
        self.stok -= 1
        return True

    def kembalikan(self) -> bool:
        self.stok += 1
        return True

    def info(self) -> str:
        return f"Buku: {self.judul} · {self.penulis} (stok={self.stok})"

    def __str__(self) -> str:
        return self.info()  # special method (#48): print(buku) pakai ini


@dataclass
class Ebook(Buku):
    format_file: str = "PDF"

    def info(self) -> str:
        return f"Ebook: {self.judul} · {self.format_file} (stok={self.stok})"


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

    def tambah(self, item: Buku) -> None:
        self.koleksi.append(item)

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

    def laporan(self) -> str:
        baris = [f"=== {self.nama} ({len(self.koleksi)} item) ==="]
        for i in self.koleksi:
            baris.append("  - " + str(i))  # __str__ -> info()
        return "\n".join(baris)


def demo():
    p = Perpustakaan("Kota A")
    p.tambah(Buku("ESP32 Praktis", "Budi", stok=2))
    p.tambah(Ebook("Belajar Python", "Sari", format_file="EPUB"))
    p.tambah(Buku("MQTT Dasar", "Ani", stok=1))
    print("pinjam ESP32:", p.koleksi[0].pinjam())
    print("pinjam lagi:", p.koleksi[0].pinjam())
    print("kembalikan:", p.koleksi[0].kembalikan())
    print("cari 'python':", [x.judul for x in p.cari("python")])
    print("print satu item:", p.koleksi[1])  # __str__ / polymorphism
    print(p.laporan())


if __name__ == "__main__":
    demo()

Output yang diharapkan (kurang lebih):

pinjam ESP32: True
pinjam lagi: True
kembalikan: True
cari 'python': ['Belajar Python']
print satu item: Ebook: Belajar Python · EPUB (stok=1)
=== Kota A (3 item) ===
  - Buku: ESP32 Praktis · Budi (stok=1)
  - Ebook: Belajar Python · EPUB (stok=1)
  - Buku: MQTT Dasar · Ani (stok=1)

Kesalahan umum

Gejala Penyebab tipikal Perbaikan
TypeError: Can't instantiate abstract class Subclass belum implementasi semua @abstractmethod Lengkapkan pinjam dan kembalikan (lihat ABC #46)
Stok jadi negatif pinjam tidak cek stok < 1 Return False lebih awal; jangan kurangi stok kosong
Perpustakaan diwariskan dari list Mencampur “adalah list” dengan “punya koleksi” Composition: self.koleksi = [] (#47)
Demo macet menunggu keyboard Skrip menunggu ketikan pengguna Ganti dengan demo() terscript
info() selalu teks Buku untuk Ebook Lupa override di subclass Override info() di Ebook — polymorphism
Field dataclass hilang di subclass Urutan field default di inheritance dataclass Field tanpa default dulu; field ber-default di subclass (Seri 3: mulai sederhana)

Latihan singkat

  1. Tambah method Perpustakaan.pinjam_judul(judul) yang mencari item lalu memanggil pinjam().
  2. Tolak duplikat judul saat tambah (bandingkan case-insensitive).
  3. Buat subclass Audiobook(Buku) (pola di Inheritance (#44)) dengan field durasi_menit dan override info().

FAQ singkat

Kenapa satu file, bukan folder models/services?
Supaya mudah disalin & diuji. Pola folder tetap berlaku saat proyek membesar.

Apa bedanya dengan encapsulation di #43?
Di sini stok diubah lewat method pinjam/kembalikan. Kamu bisa naikkan ke @property bila ingin validasi lebih ketat.

Apakah ABC wajib untuk proyek kecil?
Tidak wajib, tapi membantu menegaskan kontrak sebelum implementasi — terutama bila ada banyak tipe item.

Bolehkah CLI interaktif di rumah?
Boleh untuk latihan. Untuk artikel/CI, tetap pakai demo() terscript.

Ke mana setelah Seri 3?
Lihat kesimpulan di bawah — ide Tier 2 tanpa menunggu slug yang belum live.

Indeks Seri 3 lengkap

  1. Mengenal OOP — Cara Berpikir dengan Objek (#40)
  2. Class dan Object Pertama di Python (#41)
  3. Attribute, Method & Constructor __init__ (#42)
  4. Encapsulation & Property (#43)
  5. Inheritance — Pewarisan Class (#44)
  6. Polymorphism di Python OOP (#45)
  7. Abstraction & ABC (#46)
  8. Composition vs Inheritance (#47)
  9. Special Methods & Dataclass (#48)
  10. Capstone: Sistem Perpustakaan Mini (#49 (ini)) — artikel ini (capstone)

Kesimpulan — Seri 3 selesai

Capstone ini merangkai seluruh Seri 3: berpikir objek (#40), class & object (#41), attribute/__init__ (#42), encapsulation (#43), inheritance (#44), polymorphism (#45), ABC (#46), composition (#47), special methods/dataclass (#48) — lalu satu CLI mini yang runnable.

Seri 3 resmi selesai (10/10 artikel live). Ide lanjut (Tier 2, belum jadi artikel live): API Flask + model OOP, persistensi JSON untuk koleksi, atau pola OOP ringan di MicroPython untuk node IoT. Kalau kamu dari jalur perangkat, pola “service punya sensor/aktuator” sudah muncul di capstone smart greenhouse (#39) — OOP membantu merapikan firmware yang membesar.

Artikel ini adalah #49 (ini) — penutup Seri 3 setelah Special Methods (#48).

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