Python

Python Design Patterns: Panduan Lengkap

Tutorial lengkap Design Patterns dalam Python — Singleton, Factory, Observer, Strategy dengan contoh kode praktis dan studi kasus nyata

1. Pengenalan Design Patterns

Design Patterns adalah solusi yang telah teruji untuk masalah desain perangkat lunak yang umum. Diperkenalkan oleh Gang of Four (GoF) — Erich Gamma, Richard Helm, Ralph Johnson, dan John Vlissides — dalam buku klasik "Design Patterns: Elements of Reusable Object-Oriented Software" pada tahun 1994.

Dalam Python, Design Patterns memiliki implementasi yang lebih elegan dibanding bahasa statis seperti Java atau C++ karena sifat dynamic typing, first-class functions, dan metaclass yang dimilikinya.

Kategori Design Patterns

Kategori Fungsi Contoh Pattern
CreationalCara membuat objek yang fleksibelSingleton, Factory, Builder
StructuralMenyusun objek dan class yang lebih besarAdapter, Decorator, Facade
BehavioralInteraksi dan komunikasi antar objekObserver, Strategy, Command
Diagram: Kategori Design Patterns
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                    DESIGN PATTERNS (GoF)                     │
│                                                              │
│  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”       │
│  │  CREATIONAL  │  │  STRUCTURAL  │  │  BEHAVIORAL  │       │
│  │              │  │              │  │              │       │
│  │ ā— Singleton  │  │ ā— Adapter    │  │ ā— Observer   │       │
│  │ ā— Factory    │  │ ā— Decorator  │  │ ā— Strategy   │       │
│  │ ā— Builder    │  │ ā— Facade     │  │ ā— Command    │       │
│  │ ā— Prototype  │  │ ā— Composite  │  │ ā— State      │       │
│  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜       │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

2. Singleton Pattern

Singleton memastikan bahwa sebuah class hanya memiliki satu instance dan menyediakan satu titik akses global untuk instance tersebut. Pattern ini berguna untuk mengelola resource yang seharusnya hanya ada satu, seperti database connection pool, logger, atau konfigurasi aplikasi.

Implementasi Klasik dengan Metaclass

"""Singleton Pattern menggunakan Metaclass."""


class SingletonMeta(type):
    """Metaclass yang membuat class menjadi Singleton."""
    _instances: dict = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]


class DatabaseConnection(metaclass=SingletonMeta):
    """Database connection — hanya akan ada satu instance."""

    def __init__(self):
        print("šŸ”Œ Membuat koneksi database baru...")
        self._connection = None
        self._connected = False

    def connect(self, host: str = "localhost", port: int = 5432):
        if not self._connected:
            print(f"šŸ“” Connecting ke {host}:{port}...")
            self._connection = {"host": host, "port": port, "status": "connected"}
            self._connected = True
        return self._connection

    def execute(self, query: str) -> str:
        if not self._connected:
            raise RuntimeError("Belum terhubung ke database!")
        print(f"šŸ“Š Executing: {query}")
        return f"Result of: {query}"


# Menggunakan Singleton
db1 = DatabaseConnection()
db2 = DatabaseConnection()

# Kedua variabel menunjuk ke instance yang SAMA
print(db1 is db2)  # True
print(id(db1) == id(db2))  # True

# Menghubungkan di satu tempat
db1.connect("prod-db.example.com", 5432)

# Bisa diakses dari tempat lain dengan konfigurasi yang sama
db2.execute("SELECT * FROM users")
# Output: Result of: SELECT * FROM users

Singleton dengan Decorator

"""Singleton menggunakan decorator."""


def singleton(cls):
    """Decorator untuk membuat class menjadi Singleton."""
    instances = {}

    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance


@singleton
class Logger:
    """Application Logger — Singleton."""

    def __init__(self, level: str = "INFO"):
        self.level = level
        self.logs: list[str] = []
        print(f"šŸ“ Logger diinisialisasi dengan level: {level}")

    def log(self, message: str, level: str = "INFO"):
        entry = f"[{level}] {message}"
        self.logs.append(entry)
        print(entry)

    def get_logs(self) -> list[str]:
        return self.logs.copy()


# Menggunakan singleton logger
logger1 = Logger("DEBUG")
logger2 = Logger("ERROR")  # Instance lama dikembalikan, init baru TIDAK dipanggil

logger1.log("Server started")  # Akan bekerja karena menggunakan instance pertama
logger1.log("Processing request", "DEBUG")

# logger1 dan logger2 adalah instance yang sama
print(logger1 is logger2)  # True
print(len(logger2.get_logs()))  # 1 (log yang ditulis oleh logger1)

Singleton dengan Module-level (Pythonic)

"""Pendekatan paling Pythonic untuk Singleton: gunakan module."""

# config.py — file ini sendiri adalah singleton!
"""Konfigurasi aplikasi."""

import os
from dataclasses import dataclass, field


@dataclass
class AppConfig:
    """Konfigurasi aplikasi."""
    database_url: str = "sqlite:///app.db"
    secret_key: str = ""
    debug: bool = False
    allowed_origins: list[str] = field(default_factory=list)
    max_connections: int = 10

    def __post_init__(self):
        self.secret_key = os.getenv("SECRET_KEY", "dev-secret-key")
        self.debug = os.getenv("DEBUG", "false").lower() == "true"
        self.allowed_origins = os.getenv(
            "ALLOWED_ORIGINS", "http://localhost:3000"
        ).split(",")


# Module-level instance — secara otomatis singleton
# Karena Python hanya mengimport module sekali
config = AppConfig()


# Di file lain tinggal import:
# from config import config
# print(config.database_url)

Thread-Safe Singleton

"""Thread-safe Singleton dengan Lock."""

import threading


class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            with cls._lock:
                # Double-checked locking
                if not cls._instance:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        # Hindari inisialisasi berulang
        if not hasattr(self, "_initialized"):
            self._initialized = True
            self.data = {}
            self._counter = 0


# Dalam environment multithreading
def worker(worker_id: int):
    singleton = ThreadSafeSingleton()
    singleton._counter += 1
    print(f"Worker {worker_id}: counter = {singleton._counter}")


threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

3. Factory Pattern

Factory Pattern menyediakan antarmuka untuk membuat objek tanpa menentukan class konkret secara langsung. Pattern ini sangat berguna ketika logika pembuatan objek kompleks atau ketika kita ingin mengganti jenis objek berdasarkan parameter tertentu.

Simple Factory

"""Simple Factory Pattern untuk notifikasi."""

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Notification(ABC):
    """Interface untuk semua jenis notifikasi."""

    @abstractmethod
    def send(self, message: str, recipient: str) -> bool:
        pass


@dataclass
class EmailNotification(Notification):
    """Notifikasi melalui email."""
    smtp_server: str = "smtp.gmail.com"
    port: int = 587

    def send(self, message: str, recipient: str) -> bool:
        print(f"šŸ“§ Email ke {recipient}: {message}")
        print(f"   Via SMTP: {self.smtp_server}:{self.port}")
        return True


@dataclass
class SMSNotification(Notification):
    """Notifikasi melalui SMS."""
    api_key: str = "sms-api-key"

    def send(self, message: str, recipient: str) -> bool:
        print(f"šŸ“± SMS ke {recipient}: {message}")
        print(f"   Via API Key: {self.api_key[:8]}...")
        return True


@dataclass
class PushNotification(Notification):
    """Notifikasi melalui push notification."""
    firebase_key: str = "firebase-key"

    def send(self, message: str, recipient: str) -> bool:
        print(f"šŸ”” Push ke {recipient}: {message}")
        print(f"   Via Firebase: {self.firebase_key[:8]}...")
        return True


class NotificationFactory:
    """Factory untuk membuat notifikasi berdasarkan tipe."""

    _registry: dict[str, type[Notification]] = {
        "email": EmailNotification,
        "sms": SMSNotification,
        "push": PushNotification,
    }

    @classmethod
    def create(cls, notification_type: str, **kwargs) -> Notification:
        """Membuat notifikasi berdasarkan tipe."""
        if notification_type not in cls._registry:
            available = ", ".join(cls._registry.keys())
            raise ValueError(
                f"Unknown notification type: '{notification_type}'. "
                f"Tersedia: {available}"
            )
        return cls._registry[notification_type](**kwargs)

    @classmethod
    def register(cls, type_name: str, notification_class: type[Notification]):
        """Mendaftarkan jenis notifikasi baru."""
        cls._registry[type_name] = notification_class


# Menggunakan factory
email = NotificationFactory.create("email", smtp_server="smtp.office365.com")
sms = NotificationFactory.create("sms")
push = NotificationFactory.create("push")

email.send("Halo, pesanan Anda sedang diproses!", "user@email.com")
sms.send("Kode OTP: 123456", "+6281234567890")
push.send("Ada promo baru hari ini!", "device-token-xxx")

Factory Method (dengan Abstraksi)

"""Factory Method Pattern untuk pembuatan document."""

from abc import ABC, abstractmethod
from datetime import datetime
from pathlib import Path


class Document(ABC):
    """Abstract document."""

    def __init__(self, title: str, content: str):
        self.title = title
        self.content = content
        self.created_at = datetime.now()

    @abstractmethod
    def render(self) -> str:
        pass

    @abstractmethod
    def save(self, directory: str) -> Path:
        pass


class PDFDocument(Document):
    """Document dalam format PDF."""

    def render(self) -> str:
        header = f"=== PDF Document ===\n"
        header += f"Title: {self.title}\n"
        header += f"Created: {self.created_at.strftime('%Y-%m-%d %H:%M')}\n"
        header += "=" * 20 + "\n"
        return header + self.content

    def save(self, directory: str) -> Path:
        path = Path(directory) / f"{self.title}.pdf"
        path.write_text(self.render())
        print(f"šŸ“„ PDF disimpan: {path}")
        return path


class HTMLDocument(Document):
    """Document dalam format HTML."""

    def render(self) -> str:
        return f"""<!DOCTYPE html>
<html>
<head><title>{self.title}</title></head>
<body>
  <h1>{self.title}</h1>
  <p>Dibuat: {self.created_at}</p>
  <div>{self.content}</div>
</body>
</html>"""

    def save(self, directory: str) -> Path:
        path = Path(directory) / f"{self.title}.html"
        path.write_text(self.render())
        print(f"🌐 HTML disimpan: {path}")
        return path


class MarkdownDocument(Document):
    """Document dalam format Markdown."""

    def render(self) -> str:
        return f"# {self.title}\n\n*Created: {self.created_at}*\n\n{self.content}"

    def save(self, directory: str) -> Path:
        path = Path(directory) / f"{self.title}.md"
        path.write_text(self.render())
        print(f"šŸ“ Markdown disimpan: {path}")
        return path


# Abstract Factory / Creator
class DocumentProcessor(ABC):
    """Abstract factory untuk document processing."""

    @abstractmethod
    def create_document(self, title: str, content: str) -> Document:
        pass

    def generate_report(self, title: str, data: dict) -> Document:
        """Template method yang menggunakan create_document."""
        content = self._format_data(data)
        doc = self.create_document(title, content)
        return doc

    def _format_data(self, data: dict) -> str:
        lines = [f"{key}: {value}" for key, value in data.items()]
        return "\n".join(lines)


class PDFProcessor(DocumentProcessor):
    def create_document(self, title: str, content: str) -> Document:
        return PDFDocument(title, content)


class HTMLProcessor(DocumentProcessor):
    def create_document(self, title: str, content: str) -> Document:
        return HTMLDocument(title, content)


class MarkdownProcessor(DocumentProcessor):
    def create_document(self, title: str, content: str) -> Document:
        return MarkdownDocument(title, content)


# Menggunakan Factory Method
def generate_invoice(processor: DocumentProcessor):
    """Fungsi yang tidak peduli format output apa yang digunakan."""
    data = {
        "Invoice": "INV-2026-001",
        "Customer": "PT Teknologi Indonesia",
        "Total": "Rp 15.500.000",
        "Status": "Lunas",
    }
    return processor.generate_report("Invoice Juni 2026", data)


# Bisa menghasilkan format berbeda tanpa mengubah kode di atas
pdf = generate_invoice(PDFProcessor())
html = generate_invoice(HTMLProcessor())
md = generate_invoice(MarkdownProcessor())

4. Observer Pattern

Observer Pattern mendefinisikan mekanisme satu-ke-banyak di mana perubahan pada satu objek (Subject) akan secara otomatis memberitahu semua objek yang bergantung (Observers). Pattern ini adalah dasar dari event-driven architecture.

Implementasi Klasik

"""Observer Pattern untuk sistem event."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum


class EventType(Enum):
    """Tipe event yang dapat terjadi."""
    USER_CREATED = "user_created"
    USER_UPDATED = "user_updated"
    ORDER_PLACED = "order_placed"
    PAYMENT_RECEIVED = "payment_received"
    STOCK_CHANGED = "stock_changed"


@dataclass
class Event:
    """Event data yang dikirimkan ke observers."""
    type: EventType
    data: dict
    timestamp: datetime = field(default_factory=datetime.now)

    def __str__(self):
        return f"[{self.type.value}] {self.data} @ {self.timestamp}"


class EventObserver(ABC):
    """Interface untuk semua observer."""

    @abstractmethod
    def update(self, event: Event) -> None:
        pass


class EventBus:
    """Event Bus — Subject utama yang mengelola subscriptions."""

    def __init__(self):
        self._observers: dict[EventType, list[EventObserver]] = {}
        self._event_history: list[Event] = []

    def subscribe(self, event_type: EventType, observer: EventObserver):
        """Subscribe observer ke event type tertentu."""
        if event_type not in self._observers:
            self._observers[event_type] = []
        if observer not in self._observers[event_type]:
            self._observers[event_type].append(observer)
            print(f"āœ… {observer.__class__.__name__} subscribe ke {event_type.value}")

    def unsubscribe(self, event_type: EventType, observer: EventObserver):
        """Unsubscribe observer dari event type tertentu."""
        if event_type in self._observers:
            self._observers[event_type].remove(observer)

    def publish(self, event: Event):
        """Publish event ke semua subscriber."""
        self._event_history.append(event)
        print(f"\nšŸ“¢ Event Published: {event}")

        if event.type in self._observers:
            for observer in self._observers[event.type]:
                try:
                    observer.update(event)
                except Exception as e:
                    print(f"āš ļø Error pada {observer.__class__.__name__}: {e}")

    def get_history(self) -> list[Event]:
        return self._event_history.copy()


# === Concrete Observers ===

class EmailService(EventObserver):
    """Mengirim email saat event tertentu terjadi."""

    def __init__(self, from_email: str = "noreply@company.com"):
        self.from_email = from_email

    def update(self, event: Event) -> None:
        if event.type == EventType.USER_CREATED:
            user = event.data
            print(f"   šŸ“§ Mengirim welcome email ke {user['email']}")
        elif event.type == EventType.PAYMENT_RECEIVED:
            payment = event.data
            print(f"   šŸ“§ Mengirim receipt email ke {payment['user_email']}")
            print(f"   šŸ“§ Jumlah: Rp {payment['amount']:,.0f}")


class AnalyticsService(EventObserver):
    """Mencatat event untuk analytics."""

    def __init__(self):
        self.metrics: dict[str, int] = {}

    def update(self, event: Event) -> None:
        event_name = event.type.value
        self.metrics[event_name] = self.metrics.get(event_name, 0) + 1
        print(f"   šŸ“Š Analytics: {event_name} count = {self.metrics[event_name]}")

    def get_report(self) -> dict:
        return self.metrics.copy()


class InventoryService(EventObserver):
    """Mengelola inventory saat order ditempatkan."""

    def __init__(self):
        self.stock: dict[str, int] = {}

    def update(self, event: Event) -> None:
        if event.type == EventType.ORDER_PLACED:
            items = event.data.get("items", [])
            for item in items:
                sku = item["sku"]
                qty = item["quantity"]
                current = self.stock.get(sku, 100)
                self.stock[sku] = current - qty
                print(f"   šŸ“¦ Stock {sku}: {current} → {self.stock[sku]}")

        elif event.type == EventType.STOCK_CHANGED:
            sku = event.data["sku"]
            new_qty = event.data["quantity"]
            self.stock[sku] = new_qty
            print(f"   šŸ“¦ Stock {sku} diperbarui: {new_qty}")


class AuditLogger(EventObserver):
    """Mencatat semua event untuk audit trail."""

    def __init__(self, log_file: str = "audit.log"):
        self.log_file = log_file
        self.entries: list[str] = []

    def update(self, event: Event) -> None:
        entry = f"{event.timestamp.isoformat()} | {event.type.value} | {event.data}"
        self.entries.append(entry)
        print(f"   šŸ“‹ Audit: {entry[:60]}...")


# === Menggunakan Observer Pattern ===

# Buat event bus
event_bus = EventBus()

# Buat observers
email_service = EmailService()
analytics = AnalyticsService()
inventory = InventoryService()
audit = AuditLogger()

# Subscribe observers ke event yang relevan
event_bus.subscribe(EventType.USER_CREATED, email_service)
event_bus.subscribe(EventType.USER_CREATED, analytics)
event_bus.subscribe(EventType.USER_CREATED, audit)

event_bus.subscribe(EventType.ORDER_PLACED, inventory)
event_bus.subscribe(EventType.ORDER_PLACED, analytics)
event_bus.subscribe(EventType.ORDER_PLACED, audit)

event_bus.subscribe(EventType.PAYMENT_RECEIVED, email_service)
event_bus.subscribe(EventType.PAYMENT_RECEIVED, analytics)
event_bus.subscribe(EventType.PAYMENT_RECEIVED, audit)

# Publish events
event_bus.publish(Event(
    EventType.USER_CREATED,
    {"name": "Budi Santoso", "email": "budi@email.com"}
))

event_bus.publish(Event(
    EventType.ORDER_PLACED,
    {
        "order_id": "ORD-001",
        "items": [
            {"sku": "LAPTOP-001", "quantity": 1},
            {"sku": "MOUSE-002", "quantity": 2},
        ],
    }
))

event_bus.publish(Event(
    EventType.PAYMENT_RECEIVED,
    {"order_id": "ORD-001", "amount": 12500000, "user_email": "budi@email.com"}
))

# Lihat laporan analytics
print(f"\nšŸ“Š Analytics Report: {analytics.get_report()}")

5. Strategy Pattern

Strategy Pattern mendefinisikan keluarga algoritma, masing-masing dalam class terpisah, dan membuatnya dapat dipertukarkan. Pattern ini memungkinkan algoritma bervariasi secara independen dari klien yang menggunakannya.

Contoh: Payment Processing

"""Strategy Pattern untuk payment processing."""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
import uuid


@dataclass
class PaymentResult:
    """Hasil dari proses pembayaran."""
    success: bool
    transaction_id: str
    amount: float
    method: str
    message: str
    timestamp: datetime

    def __str__(self):
        status = "āœ…" if self.success else "āŒ"
        return (
            f"{status} {self.method} | ID: {self.transaction_id} | "
            f"Rp {self.amount:,.0f} | {self.message}"
        )


class PaymentStrategy(ABC):
    """Interface untuk semua strategi pembayaran."""

    @abstractmethod
    def validate(self, amount: float, details: dict) -> bool:
        """Validasi detail pembayaran."""
        pass

    @abstractmethod
    def process(self, amount: float, details: dict) -> PaymentResult:
        """Proses pembayaran."""
        pass

    @abstractmethod
    def get_name(self) -> str:
        """Nama metode pembayaran."""
        pass


class CreditCardPayment(PaymentStrategy):
    """Strategi pembayaran kartu kredit."""

    def validate(self, amount: float, details: dict) -> bool:
        required = ["card_number", "cvv", "expiry_month", "expiry_year"]
        for field in required:
            if field not in details:
                return False
        if len(str(details["card_number"])) < 16:
            return False
        return True

    def process(self, amount: float, details: dict) -> PaymentResult:
        if not self.validate(amount, details):
            return PaymentResult(
                success=False,
                transaction_id="",
                amount=amount,
                method=self.get_name(),
                message="Validasi kartu kredit gagal",
                timestamp=datetime.now(),
            )

        # Simulasi proses payment gateway
        txn_id = f"CC-{uuid.uuid4().hex[:8].upper()}"
        fee = amount * 0.029  # Biaya admin 2.9%
        total = amount + fee

        return PaymentResult(
            success=True,
            transaction_id=txn_id,
            amount=total,
            method=self.get_name(),
            message=f"Berhasil! Fee: Rp {fee:,.0f}",
            timestamp=datetime.now(),
        )

    def get_name(self) -> str:
        return "Credit Card"


class EWalletPayment(PaymentStrategy):
    """Strategi pembayaran e-wallet (GoPay, OVO, Dana)."""

    VALID_WALLETS = ["gopay", "ovo", "dana", "shopeepay"]

    def __init__(self, wallet_name: str = "gopay"):
        self.wallet_name = wallet_name.lower()

    def validate(self, amount: float, details: dict) -> bool:
        if self.wallet_name not in self.VALID_WALLETS:
            return False
        if "phone_number" not in details:
            return False
        return True

    def process(self, amount: float, details: dict) -> PaymentResult:
        if not self.validate(amount, details):
            return PaymentResult(
                success=False,
                transaction_id="",
                amount=amount,
                method=self.get_name(),
                message=f"Validasi {self.wallet_name} gagal",
                timestamp=datetime.now(),
            )

        txn_id = f"EW-{self.wallet_name.upper()}-{uuid.uuid4().hex[:8].upper()}"
        return PaymentResult(
            success=True,
            transaction_id=txn_id,
            amount=amount,
            method=self.get_name(),
            message=f"Pembayaran via {self.wallet_name.upper()} berhasil",
            timestamp=datetime.now(),
        )

    def get_name(self) -> str:
        return f"e-Wallet ({self.wallet_name.upper()})"


class BankTransferPayment(PaymentStrategy):
    """Strategi pembayaran transfer bank."""

    SUPPORTED_BANKS = ["bca", "mandiri", "bni", "bri", "bsi"]

    def validate(self, amount: float, details: dict) -> bool:
        bank = details.get("bank", "").lower()
        return bank in self.SUPPORTED_BANKS and "account_name" in details

    def process(self, amount: float, details: dict) -> PaymentResult:
        if not self.validate(amount, details):
            return PaymentResult(
                success=False,
                transaction_id="",
                amount=amount,
                method=self.get_name(),
                message="Validasi transfer bank gagal",
                timestamp=datetime.now(),
            )

        bank = details["bank"].upper()
        txn_id = f"BT-{bank}-{uuid.uuid4().hex[:8].upper()}"
        va_number = f"88{uuid.uuid4().hex[:10]}"

        return PaymentResult(
            success=True,
            transaction_id=txn_id,
            amount=amount,
            method=self.get_name(),
            message=f"Transfer ke VA {va_number} ({bank})",
            timestamp=datetime.now(),
        )

    def get_name(self) -> str:
        return "Bank Transfer"


class PaymentProcessor:
    """Context class yang menggunakan strategy."""

    def __init__(self, strategy: PaymentStrategy):
        self._strategy = strategy

    @property
    def strategy(self) -> PaymentStrategy:
        return self._strategy

    @strategy.setter
    def strategy(self, strategy: PaymentStrategy):
        print(f"šŸ”„ Mengubah metode pembayaran: {strategy.get_name()}")
        self._strategy = strategy

    def pay(self, amount: float, details: dict) -> PaymentResult:
        """Memproses pembayaran dengan strategi saat ini."""
        print(f"\nšŸ’³ Memproses pembayaran: Rp {amount:,.0f}")
        print(f"   Metode: {self._strategy.get_name()}")
        return self._strategy.process(amount, details)


# === Menggunakan Strategy Pattern ===

# Proses dengan Credit Card
processor = PaymentProcessor(CreditCardPayment())
result = processor.pay(150000, {
    "card_number": "4111111111111111",
    "cvv": "123",
    "expiry_month": "12",
    "expiry_year": "2027",
})
print(result)

# Ubah strategi ke e-Wallet
processor.strategy = EWalletPayment("gopay")
result = processor.pay(50000, {
    "phone_number": "+6281234567890",
})
print(result)

# Ubah strategi ke Bank Transfer
processor.strategy = BankTransferPayment()
result = processor.pay(500000, {
    "bank": "bca",
    "account_name": "Budi Santoso",
})
print(result)

Strategy dengan Fungsi (Pythonic)

"""Strategy Pattern menggunakan fungsi — lebih Pythonic."""

from typing import Callable


# Strategi sorting
def bubble_sort(data: list) -> list:
    arr = data.copy()
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr


def quick_sort(data: list) -> list:
    if len(data) <= 1:
        return data
    pivot = data[len(data) // 2]
    left = [x for x in data if x < pivot]
    middle = [x for x in data if x == pivot]
    right = [x for x in data if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)


def python_builtin_sort(data: list) -> list:
    return sorted(data)


# Context class
class Sorter:
    def __init__(self, strategy: Callable[[list], list] = python_builtin_sort):
        self._strategy = strategy

    def sort(self, data: list) -> list:
        return self._strategy(data)

    def set_strategy(self, strategy: Callable[[list], list]):
        self._strategy = strategy


# Menggunakan
data = [64, 34, 25, 12, 22, 11, 90]

sorter = Sorter(bubble_sort)
print(f"Bubble Sort: {sorter.sort(data)}")

sorter.set_strategy(quick_sort)
print(f"Quick Sort:  {sorter.sort(data)}")

sorter.set_strategy(python_builtin_sort)
print(f"Builtin:     {sorter.sort(data)}")

6. Builder Pattern

Builder Pattern memisahkan konstruksi objek kompleks dari representasinya, memungkinkan proses konstruksi yang sama menghasilkan representasi yang berbeda. Pattern ini sangat berguna ketika objek memiliki banyak parameter konstruksi.

"""Builder Pattern untuk membuat HTTP Request."""

from dataclasses import dataclass, field
from typing import Self
from urllib.parse import urlencode


@dataclass
class HTTPRequest:
    """HTTP Request yang akan dibangun."""
    method: str = "GET"
    url: str = ""
    headers: dict[str, str] = field(default_factory=dict)
    query_params: dict[str, str] = field(default_factory=dict)
    body: str | None = None
    timeout: int = 30
    retry_count: int = 0
    auth_token: str | None = None

    def to_curl(self) -> str:
        """Konversi ke perintah curl."""
        parts = [f"curl -X {self.method}"]
        for key, value in self.headers.items():
            parts.append(f"-H '{key}: {value}'")
        if self.auth_token:
            parts.append(f"-H 'Authorization: Bearer {self.auth_token}'")
        if self.body:
            parts.append(f"-d '{self.body}'")
        url = self.url
        if self.query_params:
            url += "?" + urlencode(self.query_params)
        parts.append(f"'{url}'")
        return " \\\n  ".join(parts)


class HTTPRequestBuilder:
    """Builder untuk HTTPRequest."""

    def __init__(self, url: str):
        self._request = HTTPRequest(url=url)

    def method(self, method: str) -> Self:
        self._request.method = method.upper()
        return self

    def header(self, key: str, value: str) -> Self:
        self._request.headers[key] = value
        return self

    def headers(self, headers: dict[str, str]) -> Self:
        self._request.headers.update(headers)
        return self

    def query(self, key: str, value: str) -> Self:
        self._request.query_params[key] = value
        return self

    def body(self, body: str) -> Self:
        self._request.body = body
        return self

    def json(self, data: dict) -> Self:
        import json
        self._request.body = json.dumps(data)
        self._request.headers["Content-Type"] = "application/json"
        return self

    def timeout(self, seconds: int) -> Self:
        self._request.timeout = seconds
        return self

    def retry(self, count: int) -> Self:
        self._request.retry_count = count
        return self

    def bearer_token(self, token: str) -> Self:
        self._request.auth_token = token
        return self

    def build(self) -> HTTPRequest:
        """Membangun request."""
        if not self._request.url:
            raise ValueError("URL harus diisi!")
        return self._request


# Menggunakan Builder dengan fluent API
api_url = "https://api.example.com/v1/users"

# GET request
request = (
    HTTPRequestBuilder(api_url)
    .method("GET")
    .header("Accept", "application/json")
    .bearer_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
    .query("page", "1")
    .query("limit", "20")
    .timeout(10)
    .retry(3)
    .build()
)

print(request.to_curl())
print()

# POST request
post_request = (
    HTTPRequestBuilder(api_url)
    .method("POST")
    .bearer_token("my-token")
    .json({
        "name": "Budi Santoso",
        "email": "budi@example.com",
        "role": "admin",
    })
    .timeout(15)
    .build()
)

print(post_request.to_curl())

7. Decorator Pattern

Decorator Pattern memungkinkan menambahkan behavior baru ke objek secara dinamis dengan membungkus objek tersebut dalam "decorator" tanpa mengubah class aslinya.

"""Decorator Pattern untuk data processing pipeline."""

from abc import ABC, abstractmethod
from typing import Any


class DataSource(ABC):
    """Interface untuk data source."""

    @abstractmethod
    def read_data(self) -> Any:
        pass

    @abstractmethod
    def write_data(self, data: Any) -> None:
        pass


class FileDataSource(DataSource):
    """Membaca dan menulis data dari file."""

    def __init__(self, filename: str):
        self.filename = filename

    def read_data(self) -> str:
        try:
            with open(self.filename, "r") as f:
                data = f.read()
            print(f"šŸ“‚ Membaca dari {self.filename}: {len(data)} chars")
            return data
        except FileNotFoundError:
            print(f"āš ļø File {self.filename} tidak ditemukan, mengembalikan data kosong")
            return ""

    def write_data(self, data: Any) -> None:
        with open(self.filename, "w") as f:
            f.write(str(data))
        print(f"šŸ“‚ Menulis ke {self.filename}")


class DataSourceDecorator(DataSource):
    """Base decorator yang membungkus DataSource."""

    def __init__(self, source: DataSource):
        self._wrapped = source

    def read_data(self) -> Any:
        return self._wrapped.read_data()

    def write_data(self, data: Any) -> None:
        self._wrapped.write_data(data)


class EncryptionDecorator(DataSourceDecorator):
    """Decorator yang menambahkan enkripsi."""

    def __init__(self, source: DataSource, key: int = 3):
        super().__init__(source)
        self.key = key

    def _encrypt(self, data: str) -> str:
        """Caesar cipher sederhana untuk demo."""
        result = []
        for char in data:
            if char.isalpha():
                base = ord("A") if char.isupper() else ord("a")
                result.append(chr((ord(char) - base + self.key) % 26 + base))
            else:
                result.append(char)
        return "".join(result)

    def _decrypt(self, data: str) -> str:
        result = []
        for char in data:
            if char.isalpha():
                base = ord("A") if char.isupper() else ord("a")
                result.append(chr((ord(char) - base - self.key) % 26 + base))
            else:
                result.append(char)
        return "".join(result)

    def read_data(self) -> str:
        encrypted = self._wrapped.read_data()
        decrypted = self._decrypt(encrypted)
        print(f"šŸ”’ Data didekripsi")
        return decrypted

    def write_data(self, data: Any) -> None:
        encrypted = self._encrypt(str(data))
        print(f"šŸ”’ Data dienkripsi")
        self._wrapped.write_data(encrypted)


class CompressionDecorator(DataSourceDecorator):
    """Decorator yang menambahkan kompresi."""

    def read_data(self) -> str:
        data = self._wrapped.read_data()
        decompressed = self._decompress(data)
        print(f"šŸ—œļø Data dikompresi saat read: {len(data)} → {len(decompressed)}")
        return decompressed

    def write_data(self, data: Any) -> None:
        compressed = self._compress(str(data))
        print(f"šŸ—œļø Data dikompresi saat write: {len(str(data))} → {len(compressed)}")
        self._wrapped.write_data(compressed)

    def _compress(self, data: str) -> str:
        """Kompresi sederhana — run-length encoding."""
        if not data:
            return data
        result = []
        count = 1
        for i in range(1, len(data)):
            if data[i] == data[i - 1]:
                count += 1
            else:
                result.append(f"{data[i-1]}{count}" if count > 1 else data[i - 1])
                count = 1
        result.append(f"{data[-1]}{count}" if count > 1 else data[-1])
        return "".join(result)

    def _decompress(self, data: str) -> str:
        """Dekompress run-length encoding."""
        result = []
        i = 0
        while i < len(data):
            if i + 1 < len(data) and data[i + 1].isdigit():
                count = int(data[i + 1])
                result.append(data[i] * count)
                i += 2
            else:
                result.append(data[i])
                i += 1
        return "".join(result)


class LoggingDecorator(DataSourceDecorator):
    """Decorator yang menambahkan logging."""

    def read_data(self) -> Any:
        print(f"šŸ“ [LOG] Membaca data...")
        data = self._wrapped.read_data()
        print(f"šŸ“ [LOG] Berhasil membaca data: {type(data).__name__}")
        return data

    def write_data(self, data: Any) -> None:
        print(f"šŸ“ [LOG] Menulis data: {type(data).__name__} ({len(str(data))} chars)")
        self._wrapped.write_data(data)
        print(f"šŸ“ [LOG] Penulisan selesai")


# === Menggunakan Decorator Pattern ===

# Buat source dengan berbagai layer dekorasi
source = LoggingDecorator(
    EncryptionDecorator(
        CompressionDecorator(
            FileDataSource("data.txt")
        )
    )
)

# Data yang akan disimpan
data = "Hello World! Ini adalah contoh data yang akan dikompresi dan dienkripsi."

# Tulis data (akan dikompresi → dienkripsi → di-log → ditulis)
source.write_data(data)

# Baca data (akan dibaca → di-log → didekripsi → dikompresi/dekompresi)
result = source.read_data()
print(f"\nHasil: {result}")

8. Adapter Pattern

Adapter Pattern mengonversi antarmuka satu class ke antarmuka yang diharapkan oleh klien. Berguna saat mengintegrasikan library pihak ketiga yang memiliki interface berbeda.

"""Adapter Pattern untuk integrasi API payment yang berbeda."""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any


@dataclass
class UnifiedPayment:
    """Standar data pembayaran yang diseragamkan."""
    amount: float
    currency: str
    order_id: str
    status: str
    transaction_id: str
    provider: str


# === Library pihak ketiga dengan interface berbeda ===

class StripeSDK:
    """Simulasi library Stripe."""

    def create_charge(self, amount_in_cents: int, currency: str,
                      metadata: dict) -> dict:
        return {
            "id": f"ch_{hash(str(metadata)) % 10000:04d}",
            "amount": amount_in_cents,
            "currency": currency,
            "status": "succeeded",
            "metadata": metadata,
        }

    def get_charge(self, charge_id: str) -> dict:
        return {"id": charge_id, "status": "succeeded"}


class MidtransClient:
    """Simulasi library Midtrans."""

    def charge(self, payload: dict) -> dict:
        return {
            "transaction_id": payload.get("transaction_details", {}).get(
                "order_id", "N/A"
            ),
            "gross_amount": payload.get("transaction_details", {}).get(
                "gross_amount", "0"
            ),
            "transaction_status": "capture",
            "payment_type": payload.get("payment_type", "credit_card"),
        }


class XenditAPI:
    """Simulasi library Xendit."""

    def create_invoice(self, external_id: str, amount: float,
                       description: str) -> dict:
        return {
            "id": f"inv_{external_id}",
            "external_id": external_id,
            "amount": amount,
            "status": "PAID",
            "description": description,
        }


# === Adapter Classes ===

class PaymentAdapter(ABC):
    """Interface unified untuk semua adapter."""

    @abstractmethod
    def charge(self, amount: float, currency: str, order_id: str,
               **kwargs) -> UnifiedPayment:
        pass


class StripeAdapter(PaymentAdapter):
    """Adapter untuk Stripe SDK."""

    def __init__(self):
        self._sdk = StripeSDK()

    def charge(self, amount: float, currency: str, order_id: str,
               **kwargs) -> UnifiedPayment:
        # Konversi amount ke cents (Stripe format)
        amount_cents = int(amount * 100)
        result = self._sdk.create_charge(amount_cents, currency, {
            "order_id": order_id,
        })

        return UnifiedPayment(
            amount=amount,
            currency=currency,
            order_id=order_id,
            status="success" if result["status"] == "succeeded" else "failed",
            transaction_id=result["id"],
            provider="Stripe",
        )


class MidtransAdapter(PaymentAdapter):
    """Adapter untuk Midtrans Client."""

    def __init__(self):
        self._client = MidtransClient()

    def charge(self, amount: float, currency: str, order_id: str,
               **kwargs) -> UnifiedPayment:
        result = self._client.charge({
            "transaction_details": {
                "order_id": order_id,
                "gross_amount": str(int(amount)),
            },
            "payment_type": kwargs.get("payment_type", "credit_card"),
        })

        status_map = {"capture": "success", "pending": "pending", "deny": "failed"}
        return UnifiedPayment(
            amount=float(result["gross_amount"]),
            currency=currency,
            order_id=order_id,
            status=status_map.get(result["transaction_status"], "unknown"),
            transaction_id=result["transaction_id"],
            provider="Midtrans",
        )


class XenditAdapter(PaymentAdapter):
    """Adapter untuk Xendit API."""

    def __init__(self):
        self._api = XenditAPI()

    def charge(self, amount: float, currency: str, order_id: str,
               **kwargs) -> UnifiedPayment:
        result = self._api.create_invoice(
            external_id=order_id,
            amount=amount,
            description=kwargs.get("description", f"Payment for {order_id}"),
        )

        return UnifiedPayment(
            amount=result["amount"],
            currency=currency,
            order_id=order_id,
            status="success" if result["status"] == "PAID" else "pending",
            transaction_id=result["id"],
            provider="Xendit",
        )


# === Menggunakan Adapter Pattern ===

def process_payment(adapter: PaymentAdapter, amount: float, order_id: str):
    """Fungsi yang tidak peduli payment provider apa yang digunakan."""
    result = adapter.charge(amount, "IDR", order_id)
    print(f"āœ… {result.provider}: {result.transaction_id} | "
          f"Rp {result.amount:,.0f} | Status: {result.status}")
    return result


# Semua bekerja dengan interface yang sama
providers = [StripeAdapter(), MidtransAdapter(), XenditAdapter()]

for adapter in providers:
    process_payment(adapter, 150000, "ORD-2026-001")

9. Command Pattern

Command Pattern mengenkapsulasi permintaan sebagai objek, memungkinkan parameterisasi klien, antrian permintaan, logging, dan operasi yang dapat di-undo.

"""Command Pattern untuk undo/redo di text editor."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime


class Command(ABC):
    """Interface untuk semua perintah."""

    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass

    @abstractmethod
    def __str__(self) -> str:
        pass


class TextDocument:
    """Receiver — objek yang akan dimanipulasi."""

    def __init__(self, name: str = "Untitled"):
        self.name = name
        self.content: list[str] = []
        self.cursor_position = 0

    def insert_text(self, position: int, text: str):
        if position > len(self.content):
            position = len(self.content)
        for char in text:
            self.content.insert(position, char)
            position += 1
        self.cursor_position = position

    def delete_text(self, start: int, length: int) -> str:
        deleted = self.content[start:start + length]
        del self.content[start:start + length]
        self.cursor_position = start
        return "".join(deleted)

    def replace_text(self, start: int, length: int, new_text: str) -> str:
        old_text = self.delete_text(start, length)
        self.insert_text(start, new_text)
        return old_text

    def get_content(self) -> str:
        return "".join(self.content)


class InsertCommand(Command):
    """Perintah untuk menyisipkan teks."""

    def __init__(self, document: TextDocument, position: int, text: str):
        self.document = document
        self.position = position
        self.text = text

    def execute(self) -> None:
        self.document.insert_text(self.position, self.text)

    def undo(self) -> None:
        self.document.delete_text(self.position, len(self.text))

    def __str__(self) -> str:
        return f"Insert '{self.text}' at {self.position}"


class DeleteCommand(Command):
    """Perintah untuk menghapus teks."""

    def __init__(self, document: TextDocument, start: int, length: int):
        self.document = document
        self.start = start
        self.length = length
        self.deleted_text: str = ""

    def execute(self) -> None:
        self.deleted_text = self.document.delete_text(self.start, self.length)

    def undo(self) -> None:
        self.document.insert_text(self.start, self.deleted_text)

    def __str__(self) -> str:
        return f"Delete {self.length} chars at {self.start}"


class ReplaceCommand(Command):
    """Perintah untuk mengganti teks."""

    def __init__(self, document: TextDocument, start: int, length: int,
                 new_text: str):
        self.document = document
        self.start = start
        self.length = length
        self.new_text = new_text
        self.old_text: str = ""

    def execute(self) -> None:
        self.old_text = self.document.replace_text(
            self.start, self.length, self.new_text
        )

    def undo(self) -> None:
        self.document.replace_text(
            self.start, len(self.new_text), self.old_text
        )

    def __str__(self) -> str:
        return f"Replace at {self.start}: '{self.old_text}' → '{self.new_text}'"


class EditorInvoker:
    """Invoker — mengelola eksekusi dan history perintah."""

    def __init__(self, document: TextDocument):
        self.document = document
        self._history: list[Command] = []
        self._redo_stack: list[Command] = []

    def execute(self, command: Command):
        command.execute()
        self._history.append(command)
        self._redo_stack.clear()  # Clear redo setelah aksi baru
        print(f"  āœ… {command}")

    def undo(self):
        if not self._history:
            print("  āš ļø Tidak ada yang bisa di-undo")
            return
        command = self._history.pop()
        command.undo()
        self._redo_stack.append(command)
        print(f"  ā†©ļø Undo: {command}")

    def redo(self):
        if not self._redo_stack:
            print("  āš ļø Tidak ada yang bisa di-redo")
            return
        command = self._redo_stack.pop()
        command.execute()
        self._history.append(command)
        print(f"  ā†Ŗļø Redo: {command}")

    def get_history(self) -> list[str]:
        return [str(cmd) for cmd in self._history]


# === Menggunakan Command Pattern ===
doc = TextDocument("Tutorial Python")
editor = EditorInvoker(doc)

print("=== Text Editor dengan Undo/Redo ===\n")

# Teks awal
editor.execute(InsertCommand(doc, 0, "Halo Dunia"))
print(f"Content: {doc.get_content()}")

editor.execute(InsertCommand(doc, 10, "! Selamat datang"))
print(f"Content: {doc.get_content()}")

editor.execute(ReplaceCommand(doc, 0, 10, "Hello World"))
print(f"Content: {doc.get_content()}")

# Undo beberapa operasi
print("\n--- Undo ---")
editor.undo()
print(f"Content: {doc.get_content()}")

editor.undo()
print(f"Content: {doc.get_content()}")

# Redo
print("\n--- Redo ---")
editor.redo()
print(f"Content: {doc.get_content()}")

# History
print(f"\nšŸ“œ History: {editor.get_history()}")

10. Studi Kasus: E-Commerce System

Mari gabungkan beberapa pattern dalam satu studi kasus sistem e-commerce sederhana.

"""Studi Kasus: Sistem E-Commerce sederhana menggunakan beberapa design patterns."""

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Self
import uuid


# --- Factory Pattern: Product Creation ---

@dataclass
class Product:
    id: str
    name: str
    price: float
    category: str
    stock: int

    def __str__(self):
        return f"{self.name} (Rp {self.price:,.0f}) - Stok: {self.stock}"


class ProductFactory:
    """Membuat produk berdasarkan kategori."""

    _discount_rates = {
        "electronics": 0.05,
        "fashion": 0.10,
        "food": 0.02,
    }

    @classmethod
    def create(cls, name: str, price: float, category: str,
               stock: int = 100) -> Product:
        discount = cls._discount_rates.get(category, 0)
        discounted_price = price * (1 - discount)
        return Product(
            id=f"PRD-{uuid.uuid4().hex[:8].upper()}",
            name=name,
            price=round(discounted_price, 2),
            category=category,
            stock=stock,
        )


# --- Strategy Pattern: Discount Calculation ---

class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, subtotal: float) -> float:
        pass


class NoDiscount(DiscountStrategy):
    def calculate(self, subtotal: float) -> float:
        return 0.0


class PercentageDiscount(DiscountStrategy):
    def __init__(self, percent: float):
        self.percent = percent

    def calculate(self, subtotal: float) -> float:
        return subtotal * (self.percent / 100)


class FlatDiscount(DiscountStrategy):
    def __init__(self, amount: float):
        self.amount = amount

    def calculate(self, subtotal: float) -> float:
        return min(self.amount, subtotal)


# --- Observer Pattern: Order Notifications ---

class OrderObserver(ABC):
    @abstractmethod
    def on_order_created(self, order: Order) -> None:
        pass

    @abstractmethod
    def on_order_status_changed(self, order: Order, old: str, new: str) -> None:
        pass


@dataclass
class Order:
    id: str
    items: list[dict]
    total: float
    discount: float
    status: str = "pending"
    created_at: datetime = field(default_factory=datetime.now)


class OrderManager:
    """Mengelola pesanan dengan Observer pattern."""

    def __init__(self):
        self._observers: list[OrderObserver] = []
        self.orders: dict[str, Order] = {}

    def add_observer(self, observer: OrderObserver):
        self._observers.append(observer)

    def create_order(self, items: list[dict], discount_strategy: DiscountStrategy) -> Order:
        subtotal = sum(i["product"].price * i["quantity"] for i in items)
        discount = discount_strategy.calculate(subtotal)
        total = subtotal - discount

        order = Order(
            id=f"ORD-{uuid.uuid4().hex[:8].upper()}",
            items=items,
            total=total,
            discount=discount,
        )
        self.orders[order.id] = order

        # Kurangi stok
        for item in items:
            item["product"].stock -= item["quantity"]

        # Notify observers
        for obs in self._observers:
            obs.on_order_created(order)

        return order

    def update_status(self, order_id: str, new_status: str):
        order = self.orders[order_id]
        old_status = order.status
        order.status = new_status
        for obs in self._observers:
            obs.on_order_status_changed(order, old_status, new_status)


class CustomerNotifier(OrderObserver):
    def on_order_created(self, order: Order):
        print(f"  šŸ“± Notifikasi: Pesanan {order.id} berhasil dibuat! "
              f"Total: Rp {order.total:,.0f}")

    def on_order_status_changed(self, order: Order, old: str, new: str):
        print(f"  šŸ“± Notifikasi: Pesanan {order.id} berubah status: {old} → {new}")


class SellerNotifier(OrderObserver):
    def on_order_created(self, order: Order):
        print(f"  šŸŖ Penjual: Ada pesanan baru {order.id} ({len(order.items)} item)")

    def on_order_status_changed(self, order: Order, old: str, new: str):
        print(f"  šŸŖ Penjual: Pesanan {order.id} sekarang: {new}")


# --- Builder Pattern: Order Builder ---

class OrderBuilder:
    """Builder untuk membuat pesanan dengan fluent API."""

    def __init__(self, manager: OrderManager):
        self._manager = manager
        self._items: list[dict] = []
        self._discount: DiscountStrategy = NoDiscount()

    def add_item(self, product: Product, quantity: int = 1) -> Self:
        if quantity > product.stock:
            print(f"  āš ļø Stok {product.name} tidak cukup ({product.stock})")
            return self
        self._items.append({"product": product, "quantity": quantity})
        return self

    def apply_discount(self, strategy: DiscountStrategy) -> Self:
        self._discount = strategy
        return self

    def build(self) -> Order:
        if not self._items:
            raise ValueError("Pesanan harus memiliki minimal 1 item!")
        return self._manager.create_order(self._items, self._discount)


# === Menggunakan semua pattern bersamaan ===
print("=" * 50)
print("šŸ›’ E-Commerce System dengan Design Patterns")
print("=" * 50)

# Buat produk dengan Factory
laptop = ProductFactory.create("Laptop ASUS", 12000000, "electronics", 10)
phone = ProductFactory.create("iPhone 15", 15000000, "electronics", 5)
tshirt = ProductFactory.create("Kaos Polos", 150000, "fashion", 50)

print(f"\nšŸ“¦ Produk:")
print(f"  {laptop}")
print(f"  {phone}")
print(f"  {tshirt}")

# Setup order manager dengan observers
manager = OrderManager()
manager.add_observer(CustomerNotifier())
manager.add_observer(SellerNotifier())

# Buat pesanan dengan Builder + Strategy
print("\n--- Pesanan 1 ---")
order1 = (
    OrderBuilder(manager)
    .add_item(laptop, 1)
    .add_item(tshirt, 3)
    .apply_discount(PercentageDiscount(10))
    .build()
)
print(f"  šŸ’° Subtotal sebelum diskon: Rp {(order1.total + order1.discount):,.0f}")
print(f"  šŸ’ø Diskon: Rp {order1.discount:,.0f}")
print(f"  šŸ’³ Total: Rp {order1.total:,.0f}")

# Update status
manager.update_status(order1.id, "paid")
manager.update_status(order1.id, "shipped")

11. Anti-Pattern yang Harus Dihindari

āš ļø Hindari Anti-Pattern Berikut
  • God Object — Class yang terlalu besar dan melakukan terlalu banyak hal
  • Spaghetti Code — Kode tanpa struktur yang jelas
  • Golden Hammer — Menggunakan pattern di mana-mana meski tidak perlu
  • Premature Optimization — Menerapkan pattern sebelum benar-benar dibutuhkan
  • Copy-Paste Programming — Duplikasi kode alih-alih menggunakan abstraksi
# āŒ JANGAN: Menggunakan Singleton di mana-mana
class BadSingleton(metaclass=SingletonMeta):
    def __init__(self):
        self.data = {}

    def process(self, something):
        # Semua proses ada di satu class!
        self.validate(something)
        self.transform(something)
        self.save_to_db(something)
        self.send_email(something)
        self.log(something)

# āœ… SEBAIKNYA: Pisahkan tanggung jawab
class Validator:
    def validate(self, something): ...

class Transformer:
    def transform(self, something): ...

class Repository:
    def save(self, something): ...

class EmailService:
    def send(self, something): ...

class Processor:
    def __init__(self, validator, transformer, repo, email):
        self.validator = validator
        self.transformer = transformer
        self.repo = repo
        self.email = email

    def process(self, something):
        self.validator.validate(something)
        result = self.transformer.transform(something)
        self.repo.save(result)
        self.email.send(result)

12. Quiz Pemahaman

Uji pemahaman Anda tentang Design Patterns dengan quiz berikut:

1. Pattern apa yang memastikan hanya ada satu instance dari sebuah class?

2. Pattern apa yang mendefinisikan keluarga algoritma yang dapat dipertukarkan?

3. Kapan Observer Pattern paling cocok digunakan?

4. Pattern apa yang memisahkan konstruksi objek kompleks dari representasinya?

5. Pendekatan paling Pythonic untuk implementasi Singleton adalah?

šŸ” Zoom
100%
šŸŽØ Tema