לדלג לתוכן

1.1 SOLID הרצאה

עקרונות SOLID

עקרונות SOLID הם חמישה עקרונות עיצוב שנוסחו על ידי Robert C. Martin (המכונה "Uncle Bob"). הם מהווים את הבסיס לכתיבת קוד מונחה-עצמים שניתן לתחזק ולהרחיב.

S - Single Responsibility Principle
O - Open/Closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle

S - עקרון האחריות הבודדת - Single Responsibility Principle

ההגדרה: לכל מחלקה צריכה להיות סיבה אחת בלבד לשינוי.

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

דוגמה גרועה

class Task:
    def __init__(self, title: str, description: str):
        self.title = title
        self.description = description

    def save_to_db(self):
        # מחלקת Task לא צריכה לדעת על מסד הנתונים
        import sqlite3
        db = sqlite3.connect("taskflow.db")
        db.execute("INSERT INTO tasks VALUES (?, ?)", (self.title, self.description))
        db.commit()

    def send_notification(self, email: str):
        # מחלקת Task לא צריכה לדעת על שליחת מיילים
        import smtplib
        server = smtplib.SMTP("smtp.gmail.com", 587)
        server.sendmail("system@taskflow.com", email, f"משימה {self.title} נוצרה")

    def to_json(self):
        # זה בסדר - המרה של עצמה
        return {"title": self.title, "description": self.description}

הבעיה: למחלקה Task יש שלוש סיבות לשינוי - אם נשנה את מסד הנתונים, אם נשנה את שיטת שליחת המיילים, ואם נשנה את מבנה הנתונים.

דוגמה טובה

class Task:
    def __init__(self, title: str, description: str):
        self.title = title
        self.description = description

    def to_json(self):
        return {"title": self.title, "description": self.description}


class TaskRepository:
    def save(self, task: Task):
        import sqlite3
        db = sqlite3.connect("taskflow.db")
        db.execute("INSERT INTO tasks VALUES (?, ?)", (task.title, task.description))
        db.commit()


class NotificationService:
    def notify_task_created(self, task: Task, email: str):
        import smtplib
        server = smtplib.SMTP("smtp.gmail.com", 587)
        server.sendmail("system@taskflow.com", email, f"משימה {task.title} נוצרה")

עכשיו לכל מחלקה יש אחריות אחת ברורה.


O - עקרון הפתוח-סגור - Open/Closed Principle

ההגדרה: מחלקה צריכה להיות פתוחה להרחבה אבל סגורה לשינוי.

כלומר - אפשר להוסיף פונקציונליות חדשה בלי לשנות את הקוד הקיים. שינוי בקוד קיים עלול לשבור דברים שכבר עובדים.

דוגמה גרועה

class NotificationService:
    def send(self, notification_type: str, recipient: str, message: str):
        if notification_type == "email":
            # שליחת מייל
            print(f"שולח מייל ל-{recipient}: {message}")
        elif notification_type == "sms":
            # שליחת SMS
            print(f"שולח SMS ל-{recipient}: {message}")
        elif notification_type == "push":
            # שליחת push notification
            print(f"שולח push ל-{recipient}: {message}")
        # כל סוג התראה חדש מחייב שינוי של הפונקציה הזו

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

דוגמה טובה

from abc import ABC, abstractmethod

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


class EmailChannel(NotificationChannel):
    def send(self, recipient: str, message: str):
        print(f"שולח מייל ל-{recipient}: {message}")


class SMSChannel(NotificationChannel):
    def send(self, recipient: str, message: str):
        print(f"שולח SMS ל-{recipient}: {message}")


class PushChannel(NotificationChannel):
    def send(self, recipient: str, message: str):
        print(f"שולח push ל-{recipient}: {message}")


class NotificationService:
    def __init__(self, channel: NotificationChannel):
        self.channel = channel

    def send(self, recipient: str, message: str):
        self.channel.send(recipient, message)

עכשיו כדי להוסיף סוג התראה חדש, מוסיפים מחלקה חדשה שיורשת מ-NotificationChannel - בלי לגעת בקוד הקיים.


L - עקרון ההחלפה של ליסקוב - Liskov Substitution Principle

ההגדרה: אם B יורשת מ-A, אפשר להשתמש ב-B בכל מקום שמצפים ל-A, בלי לשבור את הפרוגרמה.

דוגמה גרועה

class Storage:
    def save(self, key: str, value: str):
        pass

    def get(self, key: str) -> str:
        pass

    def delete(self, key: str):
        pass


class ReadOnlyStorage(Storage):
    def save(self, key: str, value: str):
        # מפר את LSP - תת-מחלקה שלא מממשת מה שהיא מבטיחה
        raise NotImplementedError("אסור לשמור ב-ReadOnlyStorage")

    def delete(self, key: str):
        raise NotImplementedError("אסור למחוק ב-ReadOnlyStorage")

הבעיה: ReadOnlyStorage אומרת שהיא Storage, אבל לא עונה על כל ההתנהגויות שמצפים מ-Storage. קוד שמקבל Storage ומנסה לקרוא save יקרוס בהפתעה.

דוגמה טובה

from abc import ABC, abstractmethod

class ReadableStorage(ABC):
    @abstractmethod
    def get(self, key: str) -> str:
        pass


class WritableStorage(ReadableStorage):
    @abstractmethod
    def save(self, key: str, value: str):
        pass

    @abstractmethod
    def delete(self, key: str):
        pass


class RedisCache(WritableStorage):
    def get(self, key: str) -> str:
        return "value from redis"

    def save(self, key: str, value: str):
        print(f"שומר ב-Redis: {key}={value}")

    def delete(self, key: str):
        print(f"מוחק מ-Redis: {key}")


class ReadOnlyCache(ReadableStorage):
    def get(self, key: str) -> str:
        return "cached value"

I - עקרון הפרדת הממשקים - Interface Segregation Principle

ההגדרה: עדיף הרבה ממשקים קטנים ומדויקים על פני ממשק אחד גדול.

מחלקה לא צריכה להיות מאולצת לממש מתודות שהיא לא צריכה.

דוגמה גרועה

from abc import ABC, abstractmethod

class TaskService(ABC):
    @abstractmethod
    def create_task(self, title: str): pass

    @abstractmethod
    def delete_task(self, task_id: int): pass

    @abstractmethod
    def export_to_pdf(self): pass

    @abstractmethod
    def send_report_email(self, email: str): pass

    @abstractmethod
    def import_from_csv(self, filepath: str): pass

הבעיה: מחלקה שרוצה רק ליצור ולמחוק משימות מאולצת לממש גם PDF, מייל ו-CSV.

דוגמה טובה

from abc import ABC, abstractmethod

class TaskCRUD(ABC):
    @abstractmethod
    def create_task(self, title: str): pass

    @abstractmethod
    def delete_task(self, task_id: int): pass


class TaskExport(ABC):
    @abstractmethod
    def export_to_pdf(self): pass


class TaskImport(ABC):
    @abstractmethod
    def import_from_csv(self, filepath: str): pass


class BasicTaskService(TaskCRUD):
    def create_task(self, title: str):
        print(f"יוצר משימה: {title}")

    def delete_task(self, task_id: int):
        print(f"מוחק משימה: {task_id}")


class FullTaskService(TaskCRUD, TaskExport, TaskImport):
    def create_task(self, title: str): pass
    def delete_task(self, task_id: int): pass
    def export_to_pdf(self): pass
    def import_from_csv(self, filepath: str): pass

D - עקרון היפוך התלות - Dependency Inversion Principle

ההגדרה:
- מודולים ברמה גבוהה לא צריכים להיות תלויים במודולים ברמה נמוכה
- שניהם צריכים להיות תלויים בהפשטות (abstractions)

במילים פשוטות: הקוד שלנו לא צריך לדעת בדיוק איזה database הוא משתמש בו - רק שיש לו database.

דוגמה גרועה

import sqlite3

class TaskService:
    def __init__(self):
        # תלות ישירה ב-SQLite - אי אפשר להחליף!
        self.db = sqlite3.connect("taskflow.db")

    def create_task(self, title: str):
        self.db.execute("INSERT INTO tasks (title) VALUES (?)", (title,))
        self.db.commit()

הבעיה: TaskService תלוי ישירות ב-SQLite. אם רוצים לעבור ל-PostgreSQL, צריך לשנות את TaskService. אם רוצים לבדוק את הקוד בלי מסד נתונים אמיתי, זה בלתי אפשרי.

דוגמה טובה

from abc import ABC, abstractmethod

class TaskRepository(ABC):
    @abstractmethod
    def save(self, title: str) -> int:
        pass

    @abstractmethod
    def find_by_id(self, task_id: int) -> dict:
        pass


class SQLiteTaskRepository(TaskRepository):
    def save(self, title: str) -> int:
        import sqlite3
        db = sqlite3.connect("taskflow.db")
        cursor = db.execute("INSERT INTO tasks (title) VALUES (?)", (title,))
        db.commit()
        return cursor.lastrowid

    def find_by_id(self, task_id: int) -> dict:
        import sqlite3
        db = sqlite3.connect("taskflow.db")
        row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
        return {"id": row[0], "title": row[1]}


class InMemoryTaskRepository(TaskRepository):
    """לשימוש בבדיקות בלבד"""
    def __init__(self):
        self._tasks = {}
        self._next_id = 1

    def save(self, title: str) -> int:
        task_id = self._next_id
        self._tasks[task_id] = {"id": task_id, "title": title}
        self._next_id += 1
        return task_id

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


class TaskService:
    def __init__(self, repository: TaskRepository):
        # תלוי בהפשטה, לא בימוש ספציפי
        self.repository = repository

    def create_task(self, title: str) -> int:
        return self.repository.save(title)


# שימוש בסביבת הפקה
production_service = TaskService(SQLiteTaskRepository())

# שימוש בבדיקות
test_service = TaskService(InMemoryTaskRepository())

סיכום

עקרון שאלת הבדיקה
Single Responsibility כמה סיבות יש למחלקה הזו להשתנות?
Open/Closed האם מוסיפים feature חדש בלי לשנות קוד קיים?
Liskov Substitution האם אפשר להחליף את מחלקת-האב במחלקת-הבן בלי הפתעות?
Interface Segregation האם המחלקה מממשת מתודות שהיא לא צריכה?
Dependency Inversion האם הקוד הגבוה תלוי בקוד נמוך ישירות, או בהפשטה?

עקרונות SOLID הם כלי עזר לחשיבה, לא חוקים נוקשים. הם עוזרים לכתוב קוד שניתן לתחזוקה ולבדיקה. לא כל קוד צריך לממש את כל העקרונות בצורה מושלמת.