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):
# The Task class shouldn't know about the database
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):
# The Task class shouldn't know about sending emails
import smtplib
server = smtplib.SMTP("smtp.gmail.com", 587)
server.sendmail("system@taskflow.com", email, f"Task {self.title} created")
def to_json(self):
# This is fine - converting itself
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 {task.title} created")
עכשיו לכל מחלקה יש אחריות אחת ברורה.
O - עקרון הפתוח-סגור - Open/Closed Principle¶
ההגדרה: מחלקה צריכה להיות פתוחה להרחבה אבל סגורה לשינוי.
כלומר - אפשר להוסיף פונקציונליות חדשה בלי לשנות את הקוד הקיים. שינוי בקוד קיים עלול לשבור דברים שכבר עובדים.
דוגמה גרועה¶
class NotificationService:
def send(self, notification_type: str, recipient: str, message: str):
if notification_type == "email":
# Send an email
print(f"Sending email to {recipient}: {message}")
elif notification_type == "sms":
# Send an SMS
print(f"Sending SMS to {recipient}: {message}")
elif notification_type == "push":
# Send a push notification
print(f"Sending push to {recipient}: {message}")
# Every new notification type requires changing this function
הבעיה: בכל פעם שמוסיפים סוג התראה חדש, צריך לשנות את הקוד הקיים. זה מסוכן - עלולים לשבור את הסוגים שכבר עובדים.
דוגמה טובה¶
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"Sending email to {recipient}: {message}")
class SMSChannel(NotificationChannel):
def send(self, recipient: str, message: str):
print(f"Sending SMS to {recipient}: {message}")
class PushChannel(NotificationChannel):
def send(self, recipient: str, message: str):
print(f"Sending push to {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):
# Violates LSP - a subclass that doesn't implement what it promises
raise NotImplementedError("Not allowed to save in ReadOnlyStorage")
def delete(self, key: str):
raise NotImplementedError("Not allowed to delete in 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"Saving to Redis: {key}={value}")
def delete(self, key: str):
print(f"Deleting from Redis: {key}")
class ReadOnlyCache(ReadableStorage):
def get(self, key: str) -> str:
return "cached value"
I - עקרון הפרדת הinterfaces - Interface Segregation Principle¶
ההגדרה: עדיף הרבה interfaces קטנים ומדויקים על פני interface אחד גדול.
מחלקה לא צריכה להיות מאולצת לממש מתודות שהיא לא צריכה.
דוגמה גרועה¶
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"Creating task: {title}")
def delete_task(self, task_id: int):
print(f"Deleting task: {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):
# Direct dependency on SQLite - can't be swapped!
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):
"""For use in tests only"""
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):
# Depends on the abstraction, not a specific implementation
self.repository = repository
def create_task(self, title: str) -> int:
return self.repository.save(title)
# Usage in production environment
production_service = TaskService(SQLiteTaskRepository())
# Usage in tests
test_service = TaskService(InMemoryTaskRepository())
סיכום¶
| עקרון | שאלת הבדיקה |
|---|---|
| Single Responsibility | כמה סיבות יש למחלקה הזו להשתנות? |
| Open/Closed | האם מוסיפים feature חדש בלי לשנות קוד קיים? |
| Liskov Substitution | האם אפשר להחליף את מחלקת-האב במחלקת-הבן בלי הפתעות? |
| Interface Segregation | האם המחלקה מממשת מתודות שהיא לא צריכה? |
| Dependency Inversion | האם הקוד הגבוה תלוי בקוד נמוך ישירות, או בהפשטה? |
עקרונות SOLID הם כלי עזר לחשיבה, לא חוקים נוקשים. הם עוזרים לכתוב קוד שניתן לתחזוקה ולבדיקה. לא כל קוד צריך לממש את כל העקרונות בצורה מושלמת.