לדלג לתוכן

1.2 - DRY, KISS, YAGNI - הרצאה

שלושת העקרונות: DRY, KISS, YAGNI

מעבר ל-SOLID, ישנם שלושה עקרונות נוספים שמדריכים כל מפתח בכתיבת קוד פשוט ונקי.


DRY - אל תחזור על עצמך - Don't Repeat Yourself

ההגדרה: "כל פיסת ידע צריכה להיות בעלת ייצוג אחד, יחיד, לא-עמום, סמכותי בתוך מערכת."

בפשטות - אם כתבתם אותו קוד פעמיים, משהו לא תקין.

למה כפילות מסוכנת?

# Route for creating a task
@app.post("/tasks")
def create_task(title: str, user_id: int):
    if len(title) < 3:
        return {"error": "Title too short"}
    if len(title) > 200:
        return {"error": "Title too long"}
    db.execute("INSERT INTO tasks ...")

# Route for updating a task
@app.put("/tasks/{task_id}")
def update_task(task_id: int, title: str):
    if len(title) < 3:
        return {"error": "Title too short"}
    if len(title) > 200:
        return {"error": "Title too long"}
    db.execute("UPDATE tasks ...")

נניח שהחלטנו לשנות את הגבול מ-200 תווים ל-500. צריך לשנות בשני מקומות. ואם שכחנו מקום אחד? באג.

הפתרון

def validate_task_title(title: str):
    if len(title) < 3:
        raise ValueError("Title too short")
    if len(title) > 500:
        raise ValueError("Title too long")


@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 לא אומר "כל קוד שנראה דומה חייב להיות מאוחד". לפעמים כפילות היא בסדר.

# Two routes with "similar" validation but different logic
def validate_task_title(title: str):
    if len(title) < 3 or len(title) > 500:
        raise ValueError("Invalid title")


def validate_project_name(name: str):
    if len(name) < 3 or len(name) > 100:  # Different limit!
        raise ValueError("Invalid project name")

אם תאחדו את שניהם לפונקציה אחת validate_string(text, min, max), הקוד עלול להיות פחות קריא, ואם עוד עסקית הלוגיקה תתפצל יותר - תסבכו את עצמכם. ההפשטה הלא נכונה גרועה יותר מכפילות.


KISS - שמור על פשטות - Keep It Simple, Stupid

ההגדרה: הפתרון הפשוט ביותר שעובד הוא כמעט תמיד הפתרון הטוב ביותר.

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

דוגמה: קוד "חכם" לעומת קוד פשוט

# "clever" - hard to understand
def get_status_label(status_code: int) -> str:
    return ["todo", "in progress", "done"][status_code - 1] if 1 <= status_code <= 3 else "unknown"


# simple - clear immediately
def get_status_label(status_code: int) -> str:
    labels = {1: "todo", 2: "in progress", 3: "done"}
    return labels.get(status_code, "unknown")

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

דוגמה נוספת: 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"Invalid transition: {from_status} -> {to_status}")
        task.status = to_status
        for obs in self._observers:
            obs.on_status_change(task, from_status, to_status)


# simple - works great for most needs
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"Invalid status: {new_status}")
    task["status"] = new_status

הפתרון המורכב מתאים למערכת שבה יש עשרות סטטוסים ומעברים מורכבים. לרוב המקרים, הפשוט מספיק.

בדיקת KISS

לפני שכותבים קוד, שאלו: "האם יש דרך פשוטה יותר לפתור את זה?"


YAGNI - לא תצטרך את זה - You Aren't Gonna Need It

ההגדרה: אל תממשו פונקציונליות שאתם לא צריכים כרגע.

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

דוגמה

# You're building a notification system. The client asked for email only.

# YAGNI - write only what's needed now
class EmailNotification:
    def send(self, recipient: str, message: str):
        print(f"Sending email to {recipient}: {message}")


# Not YAGNI - preparing for all cases "we might need"
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 לא אומר "אל תתכנן קדימה בכלל". ארכיטקטורה טובה מאפשרת להרחיב בעתיד בקלות - אבל לא כותבת את ההרחבה מראש.

ההבדל:

# Writing infrastructure that allows extension (good)
class NotificationChannel(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str): pass

class EmailChannel(NotificationChannel):
    def send(self, recipient: str, message: str):
        print(f"Email to {recipient}: {message}")

# vs writing all channels now (YAGNI)
class SMSChannel(NotificationChannel):
    def send(self, recipient: str, message: str):
        pass  # TODO: implement in the future

class SlackChannel(NotificationChannel):
    def send(self, recipient: str, message: str):
        pass  # TODO: implement in the future

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

הקשר בין שלושת העקרונות

עיקרון שאלת הבדיקה
DRY האם אני כותב את אותו הדבר פעמיים?
KISS האם יש דרך פשוטה יותר?
YAGNI האם אני צריך את זה עכשיו?

שלושת העקרונות משלימים אחד את השני. DRY מונע כפילות, KISS מונע מורכבות מיותרת, ו-YAGNI מונע בניית דברים שלא צריך.