1.2 DRY, KISS, YAGNI הרצאה
שלושת העקרונות: DRY, KISS, YAGNI¶
מעבר ל-SOLID, ישנם שלושה עקרונות נוספים שמדריכים כל מפתח בכתיבת קוד פשוט ונקי.
DRY - אל תחזור על עצמך - Don't Repeat Yourself¶
ההגדרה: "כל פיסת ידע צריכה להיות בעלת ייצוג אחד, יחיד, לא-עמום, סמכותי בתוך מערכת."
בפשטות - אם כתבתם אותו קוד פעמיים, משהו לא תקין.
למה כפילות מסוכנת?¶
# route של יצירת משימה
@app.post("/tasks")
def create_task(title: str, user_id: int):
if len(title) < 3:
return {"error": "כותרת קצרה מדי"}
if len(title) > 200:
return {"error": "כותרת ארוכה מדי"}
db.execute("INSERT INTO tasks ...")
# route של עדכון משימה
@app.put("/tasks/{task_id}")
def update_task(task_id: int, title: str):
if len(title) < 3:
return {"error": "כותרת קצרה מדי"}
if len(title) > 200:
return {"error": "כותרת ארוכה מדי"}
db.execute("UPDATE tasks ...")
נניח שהחלטנו לשנות את הגבול מ-200 תווים ל-500. צריך לשנות בשני מקומות. ואם שכחנו מקום אחד? באג.
הפתרון¶
def validate_task_title(title: str):
if len(title) < 3:
raise ValueError("כותרת קצרה מדי")
if len(title) > 500:
raise ValueError("כותרת ארוכה מדי")
@app.post("/tasks")
def create_task(title: str, user_id: int):
validate_task_title(title)
db.execute("INSERT INTO tasks ...")
@app.put("/tasks/{task_id}")
def update_task(task_id: int, title: str):
validate_task_title(title)
db.execute("UPDATE tasks ...")
עכשיו שינוי הגבול מצריך שינוי במקום אחד בלבד.
מתי לא להפעיל DRY?¶
DRY לא אומר "כל קוד שנראה דומה חייב להיות מאוחד". לפעמים כפילות היא בסדר.
# שני route-ים עם וולידציה "דומה" אבל לוגיקה שונה
def validate_task_title(title: str):
if len(title) < 3 or len(title) > 500:
raise ValueError("כותרת לא תקינה")
def validate_project_name(name: str):
if len(name) < 3 or len(name) > 100: # גבול שונה!
raise ValueError("שם פרויקט לא תקין")
אם תאחדו את שניהם לפונקציה אחת validate_string(text, min, max), הקוד עלול להיות פחות קריא, ואם עוד עסקית הלוגיקה תתפצל יותר - תסבכו את עצמכם. ההפשטה הלא נכונה גרועה יותר מכפילות.
KISS - שמור על פשטות - Keep It Simple, Stupid¶
ההגדרה: הפתרון הפשוט ביותר שעובד הוא כמעט תמיד הפתרון הטוב ביותר.
מפתחים לפעמים כותבים קוד מסובך מדי כי הם רוצים להיות "חכמים" או מוכנים לכל מקרה עתידי.
דוגמה: קוד "חכם" לעומת קוד פשוט¶
# "חכם" - קשה להבין
def get_status_label(status_code: int) -> str:
return ["לעשות", "בתהליך", "הושלם"][status_code - 1] if 1 <= status_code <= 3 else "לא ידוע"
# פשוט - ברור מיד
def get_status_label(status_code: int) -> str:
labels = {1: "לעשות", 2: "בתהליך", 3: "הושלם"}
return labels.get(status_code, "לא ידוע")
שני הפתרונות עובדים. אבל הפשוט יותר קריא יותר, ניתן לשינוי יותר, ופחות עלול לטעויות.
דוגמה נוספת: over-engineering¶
# over-engineered
class TaskStatusMachine:
def __init__(self):
self._transitions = {
"todo": ["in_progress"],
"in_progress": ["todo", "done"],
"done": []
}
self._observers = []
def register_observer(self, observer):
self._observers.append(observer)
def transition(self, task, from_status: str, to_status: str):
if to_status not in self._transitions.get(from_status, []):
raise ValueError(f"מעבר לא חוקי: {from_status} -> {to_status}")
task.status = to_status
for obs in self._observers:
obs.on_status_change(task, from_status, to_status)
# פשוט - עובד מצוין עבור רוב הצרכים
VALID_STATUSES = {"todo", "in_progress", "done"}
def update_task_status(task: dict, new_status: str):
if new_status not in VALID_STATUSES:
raise ValueError(f"סטטוס לא תקין: {new_status}")
task["status"] = new_status
הפתרון המורכב מתאים למערכת שבה יש עשרות סטטוסים ומעברים מורכבים. לרוב המקרים, הפשוט מספיק.
בדיקת KISS¶
לפני שכותבים קוד, שאלו: "האם יש דרך פשוטה יותר לפתור את זה?"
YAGNI - לא תצטרך את זה - You Aren't Gonna Need It¶
ההגדרה: אל תממשו פונקציונליות שאתם לא צריכים כרגע.
זהו אחד מהעקרונות הקשים ביותר להקפיד עליו, כי מפתחים אוהבים לחשוב קדימה ולהכין פתרונות "למקרה שנצטרך".
דוגמה¶
# אתם בונים מערכת להתראות. הלקוח ביקש ממם מייל בלבד.
# YAGNI - כתבו רק מה שצריך עכשיו
class EmailNotification:
def send(self, recipient: str, message: str):
print(f"שולח מייל ל-{recipient}: {message}")
# לא-YAGNI - מכינים לכל המקרים "שאולי נצטרך"
class NotificationSystem:
def send_email(self, recipient, message): pass
def send_sms(self, recipient, message): pass
def send_push(self, recipient, message): pass
def send_slack(self, recipient, message): pass
def send_whatsapp(self, recipient, message): pass
def schedule_notification(self, recipient, message, time): pass
def batch_send(self, recipients, message): pass
הקוד השני נראה "מוכן יותר" - אבל:
- לקח כפול או משולש לכתוב
- רוב הקוד לעולם לא ישמש
- צריך לתחזק כוד ש"ישן" ולא בשימוש
YAGNI ואדריכלות¶
YAGNI לא אומר "אל תתכנן קדימה בכלל". ארכיטקטורה טובה מאפשרת להרחיב בעתיד בקלות - אבל לא כותבת את ההרחבה מראש.
ההבדל:
# כתיבת תשתית שמאפשרת הרחבה (טוב)
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}")
# vs כתיבת כל הערוצים עכשיו (YAGNI)
class SMSChannel(NotificationChannel):
def send(self, recipient: str, message: str):
pass # TODO: לממש בעתיד
class SlackChannel(NotificationChannel):
def send(self, recipient: str, message: str):
pass # TODO: לממש בעתיד
הגישה הראשונה נכונה - יצרנו ממשק שמאפשר להוסיף ערוצים בקלות בעתיד. הגישה השנייה מוסיפה קוד מת.
הקשר בין שלושת העקרונות¶
| עיקרון | שאלת הבדיקה |
|---|---|
| DRY | האם אני כותב את אותו הדבר פעמיים? |
| KISS | האם יש דרך פשוטה יותר? |
| YAGNI | האם אני צריך את זה עכשיו? |
שלושת העקרונות משלימים אחד את השני. DRY מונע כפילות, KISS מונע מורכבות מיותרת, ו-YAGNI מונע בניית דברים שלא צריך.