לדלג לתוכן

2.1 - תבניות יצירה - הרצאה

תבניות יצירה - Creational Patterns

תבניות יצירה עוסקות ביצירת אובייקטים. הן פותרות את הבעיה: "איך יוצרים אובייקט בצורה גמישה שניתן לשנות בעתיד?"

נכיר שלוש תבניות יצירה נפוצות: Factory, Singleton ו-Builder.


Factory - מפעל

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

הפתרון: במקום לקרוא ישירות ל-constructor, קוראים לפונקציית "Factory" שמחליטה מה ליצור.

דוגמה בלי Factory

@app.post("/tasks/{task_id}/notify")
def notify(task_id: int, channel: str):
    if channel == "email":
        notifier = EmailNotifier()
        notifier.send(...)
    elif channel == "sms":
        notifier = SMSNotifier()
        notifier.send(...)
    elif channel == "push":
        notifier = PushNotifier()
        notifier.send(...)
    # every time we add a channel, we need to change this here

Factory Method

from abc import ABC, abstractmethod


class Notifier(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str):
        pass


class EmailNotifier(Notifier):
    def send(self, recipient: str, message: str):
        print(f"Email to {recipient}: {message}")


class SMSNotifier(Notifier):
    def send(self, recipient: str, message: str):
        print(f"SMS to {recipient}: {message}")


class PushNotifier(Notifier):
    def send(self, recipient: str, message: str):
        print(f"Push to {recipient}: {message}")


def create_notifier(channel: str) -> Notifier:
    notifiers = {
        "email": EmailNotifier,
        "sms": SMSNotifier,
        "push": PushNotifier
    }
    notifier_class = notifiers.get(channel)
    if notifier_class is None:
        raise ValueError(f"Unknown channel: {channel}")
    return notifier_class()


# Usage
@app.post("/tasks/{task_id}/notify")
def notify(task_id: int, channel: str):
    notifier = create_notifier(channel)
    notifier.send(...)

עכשיו להוסיף ערוץ חדש - פשוט מוסיפים מחלקה ומוסיפים ל-dict. הroute לא משתנה.


Singleton - יחיד

הבעיה: חלק מהאובייקטים צריכים להיות ממופים בדיוק פעם אחת בתוכנה - חיבור למסד נתונים, logger, cache connection.

הפתרון: Singleton מבטיח שמחלקה מסוימת תיווצר רק פעם אחת לאורך כל חיי התוכנה.

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            print("Creating a new database connection")
            cls._instance = super().__new__(cls)
            import sqlite3
            cls._instance.connection = sqlite3.connect("taskflow.db")
        return cls._instance

    def execute(self, query: str, params: tuple = ()):
        return self.connection.execute(query, params)


# Anywhere in the code
db1 = DatabaseConnection()
db2 = DatabaseConnection()

print(db1 is db2)  # True - same object!

Singleton בפייתון - גישה פשוטה יותר

בפייתון, module-level variables הם בעצמם singletons - נוצרים פעם אחת כשה-module נטען.

# database.py
import sqlite3

_connection = None

def get_connection():
    global _connection
    if _connection is None:
        _connection = sqlite3.connect("taskflow.db")
    return _connection

אזהרה על Singleton

Singleton נחשב לעיתים כ-"anti-pattern" כי הוא יוצר תלות גלובלית שקשה לבדוק. בדיקה שמשתמשת ב-Singleton אמיתי לא יכולה להחליף אותו ב-mock. מומלץ להשתמש בזה במשורה.


Builder - בונה

הבעיה: יצירת אובייקטים מורכבים עם הרבה פרמטרים אופציונליים. Constructor עם 10 פרמטרים קשה לשימוש.

# Hard to read - what does each parameter mean?
task = Task("Test", "Description", 1, "high", True, None, "red", 3, datetime.now(), None)

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

from datetime import datetime


class TaskBuilder:
    def __init__(self, title: str, owner_id: int):
        self._title = title
        self._owner_id = owner_id
        self._description = ""
        self._priority = "normal"
        self._due_date = None
        self._tags = []
        self._color = None

    def with_description(self, description: str) -> "TaskBuilder":
        self._description = description
        return self

    def with_priority(self, priority: str) -> "TaskBuilder":
        if priority not in {"low", "normal", "high", "critical"}:
            raise ValueError(f"Invalid priority: {priority}")
        self._priority = priority
        return self

    def with_due_date(self, due_date: datetime) -> "TaskBuilder":
        self._due_date = due_date
        return self

    def with_tags(self, *tags: str) -> "TaskBuilder":
        self._tags = list(tags)
        return self

    def with_color(self, color: str) -> "TaskBuilder":
        self._color = color
        return self

    def build(self) -> dict:
        return {
            "title": self._title,
            "owner_id": self._owner_id,
            "description": self._description,
            "priority": self._priority,
            "due_date": self._due_date,
            "tags": self._tags,
            "color": self._color
        }


# Usage - very readable
task = (
    TaskBuilder("Fix bug in login", owner_id=5)
    .with_description("Users report a problem logging in via Safari")
    .with_priority("high")
    .with_due_date(datetime(2026, 4, 10))
    .with_tags("bug", "auth", "safari")
    .with_color("red")
    .build()
)

מתי להשתמש ב-Builder?

  • כשיש יותר מ-4-5 פרמטרים, חלקם אופציונליים
  • כשרוצים לאפשר קריאות גבוהה ("fluent interface")
  • כשיצירת האובייקט כוללת ולידציה או לוגיקה

סיכום

תבנית מתי להשתמש
Factory כשרוצים גמישות באיזה סוג אובייקט ייווצר
Singleton כשצריך דיוק מופע אחד (connection, logger)
Builder כשיצירת אובייקט מורכבת עם הרבה פרמטרים