לדלג לתוכן

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 מונע בניית דברים שלא צריך.