Pendahuluan — pola yang merapikan Capstone

Di Capstone Perpustakaan Mini (#49) kamu sudah punya class, inheritance, polymorphism, ABC, composition, dan special methods dalam satu sistem. Tier 2 mulai di sini: bukan konsep OOP baru, melainkan design pattern ringan — resep nama yang sering dipakai agar kode tidak mengeras di hutan if.

Model di artikel ini sengaja lebih ramping daripada Capstone (fokus label(), tanpa alur stok/pinjam) supaya Factory & Strategy tidak tenggelam di detail CLI.

Dua pola yang paling berguna untuk domain perpustakaan kita:

  • Factory — satu pintu membuat object (Buku / Ebook) tanpa pemanggil harus tahu class mana.
  • Strategy — aturan yang bisa diganti (misalnya perhitungan denda) tanpa menulis ulang layanan yang memanggilnya.

Keduanya berdiri di atas Polymorphism (#45), kontrak ABC (#46), dan “siapa punya siapa” dari Composition (#47). Jangan over-engineer: dua class + satu factory sudah cukup untuk belajar.

Prasyarat: Selesai Capstone (#49). Nyaman dengan Polymorphism (#45) · ABC (#46) · Composition (#47). Fondasi: Encapsulation (#43) · Inheritance (#44) · Special Methods (#48) · Mengenal OOP (#40).

Masalah: hutan if di mana-mana

Tanpa pola, pembuatan item dan aturan denda sering tersebar:

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


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


def buat_item_lama(jenis, judul, penulis, format_file=None):
    if jenis == "buku":
        return Buku(judul, penulis)
    elif jenis == "ebook":
        return Ebook(judul, penulis, format_file or "pdf")
    else:
        raise ValueError(f"jenis tidak dikenal: {jenis}")


def hitung_denda_lama(hari_terlambat, mode="flat"):
    if mode == "flat":
        return 5000 if hari_terlambat > 0 else 0
    elif mode == "per_hari":
        return max(0, hari_terlambat) * 1000
    else:
        raise ValueError("mode denda tidak dikenal")


print(type(buat_item_lama("buku", "ESP32", "Budi")).__name__)
print(hitung_denda_lama(3, "per_hari"))

Output:

Buku
3000

Fungsi di atas masih “jalan”, tapi setiap tempat yang butuh item baru atau mode denda baru akan menyalin cabang yang sama. Factory & Strategy memindahkan cabang itu ke satu tempat yang punya nama jelas.

Factory & Strategy — domain perpustakaan buat_item(...) Buku Ebook Kasir.denda DendaFlat DendaPerHari hitung(hari) satu antarmuka
Factory memusatkan pembuatan object; Strategy memusatkan perilaku yang bisa diganti di runtime.

Factory — satu pintu membuat item

Factory di level Tier 2 (skala Capstone) cukup berupa fungsi (atau method class) yang mengembalikan subtype yang tepat. Pemanggil hanya tahu “saya butuh item”, bukan detail class:

from dataclasses import dataclass


@dataclass
class Buku:
    judul: str
    penulis: str

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


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

    def label(self) -> str:
        return f"Ebook ({self.format_file}): {self.judul} · {self.penulis}"


def buat_item(jenis: str, judul: str, penulis: str, format_file: str = "pdf"):
    jenis = jenis.lower().strip()
    if jenis == "buku":
        return Buku(judul, penulis)
    if jenis == "ebook":
        return Ebook(judul, penulis, format_file)
    raise ValueError(f"jenis tidak dikenal: {jenis}")


items = [
    buat_item("buku", "ESP32 Praktis", "Budi"),
    buat_item("ebook", "Python OOP", "Sari", "epub"),
]
for item in items:
    print(item.label())

try:
    buat_item("majalah", "X", "Y")
except ValueError as e:
    print("error:", e)

Output:

Buku: ESP32 Praktis · Budi
Ebook (epub): Python OOP · Sari
error: jenis tidak dikenal: majalah

Loop memakai label() yang sama — itu polymorphism dari Polymorphism (#45). Factory hanya menjawab: object mana yang dibuat. Cabang ValueError menjaga jenis yang belum didaftarkan gagal jelas di satu tempat.

Kenapa tidak membiarkan pemanggil menulis Buku(...) / Ebook(...) langsung? Boleh, untuk kode kecil. Factory berguna saat jenis datang dari string (form, JSON, CLI) atau saat nanti kamu menambah Audiobook tanpa menyentuh sepuluh pemanggil.

Strategy — ganti aturan tanpa tulis ulang kasir

Strategy = object yang mewakili satu cara melakukan sesuatu. Kasir (atau perpustakaan) punya strategi denda lewat composition, lalu memanggil antarmuka yang sama:

from abc import ABC, abstractmethod


class StrategiDenda(ABC):
    @abstractmethod
    def hitung(self, hari_terlambat: int) -> int:
        """Kembalikan denda dalam rupiah."""


class DendaFlat(StrategiDenda):
    def __init__(self, nominal: int = 5000):
        self.nominal = nominal

    def hitung(self, hari_terlambat: int) -> int:
        return self.nominal if hari_terlambat > 0 else 0


class DendaPerHari(StrategiDenda):
    def __init__(self, per_hari: int = 1000):
        self.per_hari = per_hari

    def hitung(self, hari_terlambat: int) -> int:
        return max(0, hari_terlambat) * self.per_hari


class Kasir:
    def __init__(self, strategi: StrategiDenda):
        self.strategi = strategi

    def denda(self, hari_terlambat: int) -> int:
        return self.strategi.hitung(hari_terlambat)


kasir_a = Kasir(DendaFlat(5000))
kasir_b = Kasir(DendaPerHari(1000))
print("flat 3 hari:", kasir_a.denda(3))
print("per hari 3 hari:", kasir_b.denda(3))
print("flat 0 hari:", kasir_a.denda(0))

Output:

flat 3 hari: 5000
per hari 3 hari: 3000
flat 0 hari: 0

Kasir tidak peduli flat atau per hari — ia hanya memanggil hitung. Itu Strategy + composition. Kontrak ABC memastikan subclass baru tidak “lupa” method (lihat ABC (#46)). Di runtime kamu bisa ganti: kasir_a.strategi = DendaPerHari(1500).

Gabungkan di domain Capstone

Factory membuat koleksi; Strategy mengatur denda. Keduanya hidup berdampingan tanpa saling mengunci. Di Capstone, titik masuk praktis: ganti pembuatan Buku/Ebook mentah dengan buat_item, dan pindahkan rumus denda ke object strategi yang dipegang layanan (bukan diwariskan ke class buku).

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Buku:
    judul: str
    penulis: str

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


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

    def label(self) -> str:
        return f"Ebook ({self.format_file}): {self.judul} · {self.penulis}"


def buat_item(jenis, judul, penulis, format_file="pdf"):
    key = jenis.lower().strip()
    if key == "buku":
        return Buku(judul, penulis)
    if key == "ebook":
        return Ebook(judul, penulis, format_file)
    raise ValueError(f"jenis tidak dikenal: {jenis}")


class StrategiDenda(ABC):
    @abstractmethod
    def hitung(self, hari_terlambat: int) -> int:
        raise NotImplementedError


class DendaPerHari(StrategiDenda):
    def __init__(self, per_hari=1000):
        self.per_hari = per_hari

    def hitung(self, hari_terlambat: int) -> int:
        return max(0, hari_terlambat) * self.per_hari


class Kasir:
    def __init__(self, strategi: StrategiDenda):
        self.strategi = strategi

    def denda(self, hari_terlambat: int) -> int:
        return self.strategi.hitung(hari_terlambat)


koleksi = [
    buat_item("buku", "MQTT Dasar", "Andi"),
    buat_item("ebook", "Flask Ringkas", "Dewi", "pdf"),
]
kasir = Kasir(DendaPerHari(1000))
for item in koleksi:
    print(item.label(), "-> denda 2 hari:", kasir.denda(2))

Output:

Buku: MQTT Dasar · Andi -> denda 2 hari: 2000
Ebook (pdf): Flask Ringkas · Dewi -> denda 2 hari: 2000

Pola Dasar — Factory & Strategy

  1. 1
    Kenali cabang yang tersebar String jenis item atau mode denda yang di-if di banyak file -> kandidat Factory/Strategy.
  2. 2
    Factory untuk “apa yang dibuat” buat_item(jenis, ...) mengembalikan Buku/Ebook; pemanggil tidak import semua class.
  3. 3
    Strategy untuk “bagaimana dihitung” ABC hitung + DendaFlat/DendaPerHari; layanan hanya menyimpan referensi strategi.
  4. 4
    Tempel lewat composition Kasir/Perpustakaan punya strategi — jangan warisi denda ke class buku (lihat Composition (#47)).
  5. 5
    Berhenti sebelum over-engineer Dua–tiga strategi + satu factory cukup. Jangan tambah AbstractFactory/Singleton “karena keren”.

Kode lengkap — factory_strategy_perpustakaan.py

Simpan sebagai factory_strategy_perpustakaan.py, lalu jalankan python factory_strategy_perpustakaan.py. Skenario memakai demo() tetap — tidak ada prompt interaktif yang menggantung.

"""Factory & Strategy ringan — domain perpustakaan (Tier 2 #50)."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Buku:
    judul: str
    penulis: str

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


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

    def label(self) -> str:
        return f"Ebook ({self.format_file}): {self.judul} · {self.penulis}"


def buat_item(
    jenis: str,
    judul: str,
    penulis: str,
    format_file: str = "pdf",
) -> Buku:
    key = jenis.lower().strip()
    if key == "buku":
        return Buku(judul, penulis)
    if key == "ebook":
        return Ebook(judul, penulis, format_file)
    raise ValueError(f"jenis tidak dikenal: {jenis}")


class StrategiDenda(ABC):
    @abstractmethod
    def hitung(self, hari_terlambat: int) -> int:
        raise NotImplementedError


class DendaFlat(StrategiDenda):
    def __init__(self, nominal: int = 5000) -> None:
        self.nominal = nominal

    def hitung(self, hari_terlambat: int) -> int:
        return self.nominal if hari_terlambat > 0 else 0


class DendaPerHari(StrategiDenda):
    def __init__(self, per_hari: int = 1000) -> None:
        self.per_hari = per_hari

    def hitung(self, hari_terlambat: int) -> int:
        return max(0, hari_terlambat) * self.per_hari


class Perpustakaan:
    """Composition: punya koleksi + strategi denda."""

    def __init__(self, nama: str, strategi: StrategiDenda) -> None:
        self.nama = nama
        self.strategi = strategi
        self._items: list[Buku] = []

    def tambah(self, jenis: str, judul: str, penulis: str, format_file: str = "pdf") -> Buku:
        item = buat_item(jenis, judul, penulis, format_file)
        self._items.append(item)
        return item

    def denda(self, hari_terlambat: int) -> int:
        return self.strategi.hitung(hari_terlambat)

    def ganti_strategi(self, strategi: StrategiDenda) -> None:
        self.strategi = strategi

    @property
    def items(self) -> list[Buku]:
        """Salinan baca — jangan mutasi stok dari luar (encapsulation)."""
        return list(self._items)

    def __str__(self) -> str:
        return f"{self.nama}: {len(self._items)} item"


def demo() -> None:
    lib = Perpustakaan("Kota A", DendaFlat(5000))
    lib.tambah("buku", "ESP32 Praktis", "Budi")
    lib.tambah("ebook", "Python OOP", "Sari", "epub")
    print(lib)
    for item in lib.items:
        print(" -", item.label())
    print("denda flat 4 hari:", lib.denda(4))
    lib.ganti_strategi(DendaPerHari(1000))
    print("denda per hari 4 hari:", lib.denda(4))
    print("denda 0 hari:", lib.denda(0))


if __name__ == "__main__":
    demo()

Output yang diharapkan:

Kota A: 2 item
 - Buku: ESP32 Praktis · Budi
 - Ebook (epub): Python OOP · Sari
denda flat 4 hari: 5000
denda per hari 4 hari: 4000
denda 0 hari: 0

Perpustakaan.tambah memakai Factory; denda / ganti_strategi memakai Strategy. __str__ mengikuti kebiasaan dari Special Methods (#48). Property items mengikuti semangat Encapsulation (#43) — koleksi tidak diutak-atik lewat _items dari luar.

Kesalahan umum

Gejala Penyebab tipikal Perbaikan
Pemanggil masih penuh if jenis Factory ada tapi tidak dipakai Semua pembuatan lewat buat_item / method factory
Denda di-hardcode di method pinjam Belum dipisah sebagai strategi Pindahkan rumus ke class strategi (method hitung); layanan hanya memanggil
Buku mewarisi DendaFlat Salah pakai inheritance Composition: layanan punya strategi, buku tetap data
AbstractFactory + 12 class untuk 2 jenis Over-engineer Fungsi factory + 2–3 strategy cukup untuk skala Capstone
Mutasi lib._items dari luar Melewati encapsulation koleksi Pakai property/method publik (items / tambah) — lihat Encapsulation (#43)
ValueError: jenis tidak dikenal Typo string / belum daftarkan jenis baru Normalisasi lower().strip(); tambah cabang di satu tempat

Latihan singkat

  1. Tambah jenis audiobook di buat_item (lihat pola inheritance di Inheritance (#44)) dengan field durasi_menit.
  2. Buat DendaBertingkat: 0 hari = 0; 1–3 hari = 2000; >3 hari = 2000 + 1500 per hari ekstra.
  3. Ubah demo() agar mencetak denda untuk setiap item (tetap tanpa prompt interaktif).

FAQ singkat

Apa bedanya Factory dengan constructor biasa?
Constructor membuat satu class. Factory memilih class mana berdasarkan input (sering string/enum) lalu memanggil constructor yang tepat.

Haruskah Strategy selalu pakai ABC?
Tidak wajib. Duck typing + method hitung sudah polymorphism (Polymorphism (#45)). ABC membantu IDE/kontrak saat tim membesar — pola yang sama di ABC (#46).

Apakah ini menggantikan Capstone?
Tidak. Capstone (#49) adalah sistem utuh. Artikel ini menambahkan dua resep agar Capstone lebih mudah dikembangkan tanpa hutan if.

Kapan tidak perlu pattern?
Satu jenis object dan satu rumus denda selamanya -> class biasa cukup. Pattern dibayar dengan indirection; pakai saat cabang mulai menyebar.

Lanjut ke mana?
Ide Tier 2 berikutnya: OOP di MicroPython / ESP32 (jembatan ke Seri IoT) — belum live, jadi belum ada slug hardlink. Setelah itu: class di Flask/FastAPI sebagai pintu Web Dev.

Kesimpulan & langkah berikutnya

Factory memusatkan pembuatan object; Strategy memusatkan perilaku yang bisa diganti. Keduanya memakai polymorphism + composition — bukan hierarchy baru yang rumit.

Artikel ini adalah #50 (ini) — pembuka Tier 2 setelah Seri 3 selesai di Capstone (#49).

Berikutnya (backlog): OOP di MicroPython untuk node IoT, lalu class di Flask/FastAPI. Seri 3 tetap fondasi yang sudah lengkap (sepuluh artikel inti sampai Capstone).

Tier 2 progress: langkah #50 (ini) · Seri 3 tetap 10/10 live. Prasyarat: Capstone (#49) · Polymorphism (#45) · ABC (#46) · Composition (#47) · Special Methods (#48) · Encapsulation (#43) · Inheritance (#44) · Mengenal OOP (#40).