לדלג לתוכן

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 מספק ממשק פשוט למערכת מורכבת