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 הם כלי עזר לחשיבה, לא חוקים נוקשים. הם עוזרים לכתוב קוד שניתן לתחזוקה ולבדיקה. לא כל קוד צריך לממש את כל העקרונות בצורה מושלמת.