לדלג לתוכן

2.3 תבניות התנהגות הרצאה

תבניות התנהגות - Behavioral Patterns

תבניות התנהגות עוסקות בתקשורת ואחריות בין אובייקטים - מי מחליט מה, ואיך אובייקטים מגיבים לאירועים.

נכיר שלוש תבניות: Observer, Strategy ו-Command.


Observer - משקיף

הבעיה: כשמשהו קורה באובייקט אחד, אובייקטים אחרים צריכים לדעת ולהגיב - אבל אנחנו לא רוצים שהאובייקט הראשון יידע מי מקשיב לו.

הפתרון: האובייקט "מפרסם" אירועים, והמעוניינים "נרשמים" לאירועים שמעניינים אותם.

בלי Observer - בעיתי

class TaskService:
    def complete_task(self, task_id: int):
        task = self._repo.find_by_id(task_id)
        task["status"] = "done"
        self._repo.save(task)

        # TaskService מכיר את כל מי שצריך להודיע לו - בעיה!
        EmailService().send_completion_email(task)
        SlackService().notify_channel(task)
        AuditLogger().log_completion(task)
        StatisticsService().increment_completed(task)

אם רוצים להוסיף שירות חדש, צריך לשנות את TaskService. הוא יודע יותר מדי.

עם Observer

from abc import ABC, abstractmethod


class TaskEvent:
    def __init__(self, event_type: str, task: dict):
        self.event_type = event_type
        self.task = task


class TaskEventListener(ABC):
    @abstractmethod
    def on_task_event(self, event: TaskEvent):
        pass


class EventBus:
    def __init__(self):
        self._listeners: dict[str, list[TaskEventListener]] = {}

    def subscribe(self, event_type: str, listener: TaskEventListener):
        if event_type not in self._listeners:
            self._listeners[event_type] = []
        self._listeners[event_type].append(listener)

    def publish(self, event: TaskEvent):
        for listener in self._listeners.get(event.event_type, []):
            listener.on_task_event(event)


class EmailNotificationListener(TaskEventListener):
    def on_task_event(self, event: TaskEvent):
        if event.event_type == "task_completed":
            print(f"שולח מייל: משימה '{event.task['title']}' הושלמה")


class SlackNotificationListener(TaskEventListener):
    def on_task_event(self, event: TaskEvent):
        print(f"Slack: משימה '{event.task['title']}' שונתה - {event.event_type}")


class AuditLogListener(TaskEventListener):
    def on_task_event(self, event: TaskEvent):
        print(f"[AUDIT] {event.event_type}: {event.task}")


class TaskService:
    def __init__(self, event_bus: EventBus):
        self._event_bus = event_bus
        self._tasks = {}

    def complete_task(self, task_id: int):
        task = self._tasks.get(task_id, {"id": task_id, "title": f"משימה {task_id}"})
        task["status"] = "done"
        # TaskService לא יודע מי מקשיב - רק מפרסם
        self._event_bus.publish(TaskEvent("task_completed", task))


# חיבור הכל יחד
bus = EventBus()
bus.subscribe("task_completed", EmailNotificationListener())
bus.subscribe("task_completed", SlackNotificationListener())
bus.subscribe("task_completed", AuditLogListener())

service = TaskService(bus)
service.complete_task(42)

עכשיו אפשר להוסיף StatisticsListener בלי לשנות שורה אחת ב-TaskService.


Strategy - אסטרטגיה

הבעיה: יש לנו אלגוריתם שמשתנה בהתאם להקשר. אנחנו לא רוצים ערמות של if/elif.

הפתרון: הגדרת "משפחה" של אלגוריתמים שניתן להחלפה בזמן ריצה.

דוגמה: מיון משימות

from abc import ABC, abstractmethod


class TaskSortStrategy(ABC):
    @abstractmethod
    def sort(self, tasks: list[dict]) -> list[dict]:
        pass


class SortByDueDate(TaskSortStrategy):
    def sort(self, tasks: list[dict]) -> list[dict]:
        return sorted(tasks, key=lambda t: t.get("due_date") or "9999-12-31")


class SortByPriority(TaskSortStrategy):
    PRIORITY_ORDER = {"critical": 0, "high": 1, "normal": 2, "low": 3}

    def sort(self, tasks: list[dict]) -> list[dict]:
        return sorted(tasks, key=lambda t: self.PRIORITY_ORDER.get(t.get("priority", "normal"), 2))


class SortByTitle(TaskSortStrategy):
    def sort(self, tasks: list[dict]) -> list[dict]:
        return sorted(tasks, key=lambda t: t.get("title", ""))


class TaskList:
    def __init__(self, tasks: list[dict]):
        self._tasks = tasks
        self._sort_strategy: TaskSortStrategy = SortByDueDate()

    def set_sort_strategy(self, strategy: TaskSortStrategy):
        self._sort_strategy = strategy

    def get_sorted_tasks(self) -> list[dict]:
        return self._sort_strategy.sort(self._tasks)


# שימוש
tasks = [
    {"title": "תקן באג", "priority": "high", "due_date": "2026-04-10"},
    {"title": "כתוב תיעוד", "priority": "low", "due_date": "2026-04-05"},
    {"title": "review קוד", "priority": "normal", "due_date": "2026-04-08"},
]

task_list = TaskList(tasks)

task_list.set_sort_strategy(SortByDueDate())
print("לפי תאריך:", [t["title"] for t in task_list.get_sorted_tasks()])

task_list.set_sort_strategy(SortByPriority())
print("לפי עדיפות:", [t["title"] for t in task_list.get_sorted_tasks()])

task_list.set_sort_strategy(SortByTitle())
print("לפי כותרת:", [t["title"] for t in task_list.get_sorted_tasks()])

Command - פקודה

הבעיה: רוצים לעטוף פעולות כאובייקטים - כדי לדחות ביצוע, לתור אותן, לבטל אותן (undo), או לתעד אותן.

הפתרון: כל פעולה הופכת לאובייקט עם execute() ו-undo().

דוגמה: undo/redo במערכת משימות

from abc import ABC, abstractmethod


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

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

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


class TaskDB:
    """מדמה מסד נתונים פשוט"""
    def __init__(self):
        self._tasks: dict[int, dict] = {}
        self._next_id = 1

    def insert(self, task: dict) -> int:
        task_id = self._next_id
        self._tasks[task_id] = {**task, "id": task_id}
        self._next_id += 1
        return task_id

    def delete(self, task_id: int) -> dict:
        return self._tasks.pop(task_id, None)

    def update(self, task_id: int, **fields):
        if task_id in self._tasks:
            self._tasks[task_id].update(fields)

    def get(self, task_id: int) -> dict | None:
        return self._tasks.get(task_id)


class CreateTaskCommand(Command):
    def __init__(self, db: TaskDB, title: str, owner_id: int):
        self.db = db
        self.title = title
        self.owner_id = owner_id
        self._created_id: int | None = None

    def execute(self) -> None:
        self._created_id = self.db.insert({"title": self.title, "owner_id": self.owner_id})

    def undo(self) -> None:
        if self._created_id:
            self.db.delete(self._created_id)

    def description(self) -> str:
        return f"יצירת משימה: {self.title}"


class UpdateTaskStatusCommand(Command):
    def __init__(self, db: TaskDB, task_id: int, new_status: str):
        self.db = db
        self.task_id = task_id
        self.new_status = new_status
        self._old_status: str | None = None

    def execute(self) -> None:
        task = self.db.get(self.task_id)
        if task:
            self._old_status = task.get("status")
            self.db.update(self.task_id, status=self.new_status)

    def undo(self) -> None:
        if self._old_status is not None:
            self.db.update(self.task_id, status=self._old_status)

    def description(self) -> str:
        return f"עדכון סטטוס משימה {self.task_id} ל-{self.new_status}"


class CommandHistory:
    def __init__(self):
        self._history: list[Command] = []

    def execute(self, command: Command) -> None:
        command.execute()
        self._history.append(command)
        print(f"בוצע: {command.description()}")

    def undo(self) -> None:
        if not self._history:
            print("אין פעולות לביטול")
            return
        command = self._history.pop()
        command.undo()
        print(f"בוטל: {command.description()}")

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


# שימוש
db = TaskDB()
history = CommandHistory()

history.execute(CreateTaskCommand(db, "תקן באג", owner_id=1))
history.execute(UpdateTaskStatusCommand(db, 1, "in_progress"))
history.execute(UpdateTaskStatusCommand(db, 1, "done"))

print("היסטוריה:", history.history())

history.undo()  # מבטל "done"
history.undo()  # מבטל "in_progress"

print("היסטוריה אחרי undo:", history.history())

סיכום

תבנית מה היא פותרת
Observer תקשורת בין אובייקטים בלי תלות ישירה
Strategy החלפת אלגוריתמים בזמן ריצה
Command עטיפת פעולות כאובייקטים לצורך undo/queue/log