2.2 תבניות מבנה הרצאה
תבניות מבנה - Structural Patterns¶
תבניות מבנה עוסקות בהרכבת מחלקות ואובייקטים למבנים גדולים יותר, תוך שמירת גמישות.
נכיר שלוש תבניות מבנה נפוצות: Adapter, Decorator ו-Facade.
Adapter - מתאם¶
הבעיה: יש לנו ממשק קיים שאנחנו לא יכולים לשנות, אבל הוא לא תואם לממשק שאנחנו צריכים.
הפתרון: יוצרים מחלקת wrapper שממירה ממשק לממשק.
זה בדיוק כמו מתאם חשמלי - אתם בחו"ל, השקע בקיר שונה מהמטען שלכם. אתם לא משנים את השקע ולא את המטען - אתם משתמשים במתאם.
דוגמה¶
נניח שיש לנו שירות SMS ישן שרצינו להשתמש בו, אבל הממשק שלו שונה ממה שהמערכת שלנו מצפה.
# שירות SMS ישן שאנחנו לא יכולים לשנות (ספרייה חיצונית)
class LegacySMSService:
def send_text_message(self, phone_number: str, text_content: str, sender_id: str):
print(f"SMS מ-{sender_id} ל-{phone_number}: {text_content}")
# הממשק שהמערכת שלנו מצפה לו
class NotificationChannel:
def send(self, recipient: str, message: str):
pass
# Adapter - ממיר בין שני הממשקים
class SMSAdapter(NotificationChannel):
def __init__(self, sms_service: LegacySMSService, sender_id: str):
self.sms_service = sms_service
self.sender_id = sender_id
def send(self, recipient: str, message: str):
# ממיר מהממשק החדש לממשק הישן
self.sms_service.send_text_message(
phone_number=recipient,
text_content=message,
sender_id=self.sender_id
)
# שימוש - המערכת לא יודעת שמאחורה יש ספרייה ישנה
legacy_sms = LegacySMSService()
adapter = SMSAdapter(legacy_sms, sender_id="TaskFlow")
adapter.send("0501234567", "המשימה שלך עודכנה")
דוגמה נוספת: שילוב מסד נתונים חיצוני¶
# מסד נתונים חיצוני עם ממשק שונה
class ThirdPartyDB:
def query(self, sql: str) -> list[dict]:
return []
def insert(self, table: str, data: dict) -> int:
return 0
# הממשק שה-use cases שלנו מצפים לו
class TaskRepository:
def find_by_id(self, task_id: int) -> dict | None: ...
def save(self, task: dict) -> dict: ...
# Adapter
class ThirdPartyDBAdapter(TaskRepository):
def __init__(self, db: ThirdPartyDB):
self.db = db
def find_by_id(self, task_id: int) -> dict | None:
results = self.db.query(f"SELECT * FROM tasks WHERE id = {task_id}")
return results[0] if results else None
def save(self, task: dict) -> dict:
task_id = self.db.insert("tasks", task)
task["id"] = task_id
return task
Decorator - מעטר¶
הבעיה: רוצים להוסיף התנהגות לאובייקט קיים בלי לשנות את המחלקה המקורית, ובלי ירושה.
הפתרון: יוצרים מחלקת wrapper שמוסיפה התנהגות לפני/אחרי קריאה לאובייקט המקורי.
דוגמה: הוספת logging ו-caching לrepository¶
from abc import ABC, abstractmethod
class TaskRepository(ABC):
@abstractmethod
def find_by_id(self, task_id: int) -> dict | None: pass
@abstractmethod
def save(self, task: dict) -> dict: pass
class SQLiteTaskRepository(TaskRepository):
def find_by_id(self, task_id: int) -> dict | None:
print(f"שליפה מ-DB: משימה {task_id}")
return {"id": task_id, "title": "משימה לדוגמה"}
def save(self, task: dict) -> dict:
print(f"שמירה ל-DB: {task}")
return task
# Decorator 1: הוספת logging
class LoggingTaskRepository(TaskRepository):
def __init__(self, wrapped: TaskRepository):
self._wrapped = wrapped
def find_by_id(self, task_id: int) -> dict | None:
print(f"[LOG] find_by_id({task_id})")
result = self._wrapped.find_by_id(task_id)
print(f"[LOG] תוצאה: {result}")
return result
def save(self, task: dict) -> dict:
print(f"[LOG] save({task})")
result = self._wrapped.save(task)
print(f"[LOG] נשמר: {result}")
return result
# Decorator 2: הוספת cache
class CachingTaskRepository(TaskRepository):
def __init__(self, wrapped: TaskRepository):
self._wrapped = wrapped
self._cache: dict[int, dict] = {}
def find_by_id(self, task_id: int) -> dict | None:
if task_id in self._cache:
print(f"[CACHE] hit עבור משימה {task_id}")
return self._cache[task_id]
result = self._wrapped.find_by_id(task_id)
if result:
self._cache[task_id] = result
return result
def save(self, task: dict) -> dict:
result = self._wrapped.save(task)
# נקה מהcache כי הנתון השתנה
self._cache.pop(task.get("id"), None)
return result
# שימוש - אפשר לשרשר decorators
base_repo = SQLiteTaskRepository()
logged_repo = LoggingTaskRepository(base_repo)
cached_and_logged_repo = CachingTaskRepository(logged_repo)
task = cached_and_logged_repo.find_by_id(1)
task = cached_and_logged_repo.find_by_id(1) # הפעם מה-cache
היתרון: SQLiteTaskRepository לא השתנה. אפשר להוסיף או להסיר decorators בלי לגעת בקוד הבסיסי.
Facade - חזית¶
הבעיה: מערכת מורכבת עם הרבה תתי-מערכות. הקוד שמשתמש במערכת צריך לדעת יותר מדי.
הפתרון: יוצרים ממשק פשוט שמסתיר את המורכבות.
זה כמו לוח הבקרה של מכונית - אתם לוחצים "התנע" בלי לדעת על מנוע הבנזין, מצת החשמלי, מערכת ההזרקה, ועוד.
דוגמה¶
class AuthService:
def authenticate(self, username: str, password: str) -> dict | None:
print(f"מאמת משתמש: {username}")
return {"id": 1, "username": username, "email": f"{username}@example.com"}
class TaskRepository:
def find_by_owner(self, user_id: int) -> list[dict]:
return [{"id": 1, "title": "משימה לדוגמה", "status": "todo"}]
class NotificationService:
def get_unread_count(self, user_id: int) -> int:
return 3
class ProjectRepository:
def find_by_owner(self, user_id: int) -> list[dict]:
return [{"id": 1, "name": "פרויקט לדוגמה"}]
# ללא Facade - הקוד שקורא צריך לדעת הכל
def load_dashboard_without_facade(username: str, password: str):
auth = AuthService()
task_repo = TaskRepository()
notif_service = NotificationService()
project_repo = ProjectRepository()
user = auth.authenticate(username, password)
if not user:
return None
tasks = task_repo.find_by_owner(user["id"])
projects = project_repo.find_by_owner(user["id"])
unread = notif_service.get_unread_count(user["id"])
return {"user": user, "tasks": tasks, "projects": projects, "unread": unread}
# עם Facade - הקוד שקורא רואה רק פונקציה אחת פשוטה
class DashboardFacade:
def __init__(self):
self._auth = AuthService()
self._tasks = TaskRepository()
self._notifications = NotificationService()
self._projects = ProjectRepository()
def load_dashboard(self, username: str, password: str) -> dict | None:
user = self._auth.authenticate(username, password)
if not user:
return None
return {
"user": user,
"tasks": self._tasks.find_by_owner(user["id"]),
"projects": self._projects.find_by_owner(user["id"]),
"unread_notifications": self._notifications.get_unread_count(user["id"])
}
# שימוש פשוט
facade = DashboardFacade()
dashboard = facade.load_dashboard("amit", "secret123")
סיכום¶
| תבנית | מה היא עושה |
|---|---|
| Adapter | ממיר ממשק אחד לאחר - מאפשר שימוש בקוד לא-תואם |
| Decorator | מוסיף התנהגות לאובייקט בלי לשנות אותו |
| Facade | מספק ממשק פשוט למערכת מורכבת |