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 |
|---|---|---|
| Creational | Cara membuat objek yang fleksibel | Singleton, Factory, Builder |
| Structural | Menyusun objek dan class yang lebih besar | Adapter, Decorator, Facade |
| Behavioral | Interaksi dan komunikasi antar objek | Observer, Strategy, Command |
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā 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
- 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: