Clean Code ב': DRY, KISS, הפרדת אחריות, הערות ו-Refactoring
- לזהות קוד כפול ולהחליף אותו בפונקציה או בלולאה (DRY)
- להעדיף פתרון פשוט וקריא על פני פתרון “חכם” (KISS)
- להפריד בין קלט, לוגיקה ופלט (Separation of Concerns)
- לדעת מתי לכתוב הערה ומתי הקוד צריך להסביר את עצמו
- לבצע Refactoring בצעדים קטנים תוך שמירה על אותה התנהגות
מה נלמד?
- DRY — Don’t Repeat Yourself.
- KISS — Keep It Simple, Stupid.
- Separation of Concerns — הפרדת אחריות בין חלקי התוכנה.
- Comments — מתי הערה עוזרת ומתי היא מזיקה.
- Refactoring — שיפור מבנה הקוד בלי לשנות את ההתנהגות שלו.
למה צריך את זה?
בנושא המחשב ומערכת ההפעלה כתבנו project_check.py עם ארבעה בלוקים כמעט זהים. מה קורה כשצריך לבדוק עוד 20 קבצים? או לשנות את ההודעה מ-OK ל-✓? צריך לגעת ב-20 מקומות, ובאחד מהם בטוח נשכח.
העקרונות של היום עונים על השאלה: איך כותבים קוד שקל לשנות? במציאות, רוב הזמן של מפתח הולך לשינוי קוד קיים, לא לכתיבת קוד חדש.
הסבר מהבסיס
DRY — Don’t Repeat Yourself (אל תחזור על עצמך)
כל פיסת ידע צריכה להופיע במקום אחד בלבד. כשאותה לוגיקה מופיעה בכמה מקומות:
- תיקון באג צריך להיעשות בכל המקומות, ואחד תמיד נשכח.
- הקוד ארוך יותר, וקשה לראות מה ייחודי בכל חלק.
הכלים לפתרון: פונקציה (לוגיקה שחוזרת), לולאה (פעולה על הרבה פריטים), קבוע (ערך שחוזר).
סימן אזהרה: אם אתם עושים Copy-Paste לקוד ומשנים בו פרט קטן, כנראה צריך פונקציה עם פרמטר.
KISS — Keep It Simple (שמור על פשטות)
הפתרון הפשוט ביותר שעובד, הוא בדרך כלל הטוב ביותר. קוד “חכם” (שורה אחת מסובכת, טריקים) קשה לקרוא, קשה לדבג וקשה לשנות. מטרת הקוד היא שיבינו אותו, לא שיתרשמו ממנו.
Separation of Concerns — הפרדת אחריות
Concern = תחום אחריות. בכל תוכנה יש כמה תחומים שונים:
- קלט — מאיפה מגיע המידע (מקלדת, קובץ, בקשת HTTP).
- לוגיקה — מה עושים איתו (חישובים, החלטות, כללים).
- פלט — לאן המידע הולך (מסך, קובץ, תשובת HTTP).
כשהם מופרדים, אפשר להחליף אחד בלי לגעת באחרים. זה בדיוק המבנה של שרת בנושא Backend ומסדי נתונים: Route (קלט/פלט HTTP) מופרד מקוד הגישה למסד (Database).
SRP (מהשיעור הקודם) מדבר על פונקציה בודדת. Separation of Concerns מדבר על התוכנה כולה: אילו חלקים קיימים, ואיך הם מופרדים.
הערות (Comments)
הקוד מסביר “מה” ו"איך". הערה מסבירה “למה”.
| הערה טובה | הערה מיותרת / מזיקה |
|---|---|
| מסבירה למה בחרנו בדרך מסוימת | חוזרת על מה שהקוד כבר אומר |
| מזהירה מהתנהגות לא צפויה | מפצה על שם גרוע |
| מפנה למקור (חוק, תקן, מסמך) | “קוד מת” שהוכנס להערה |
| הערה ישנה שכבר לא נכונה, וזה הכי גרוע |
Refactoring
Refactoring = שינוי המבנה של הקוד בלי לשנות את ההתנהגות שלו. משנים שמות, מפרקים פונקציות, מוציאים קוד כפול. בסוף, הקוד עושה בדיוק את אותו דבר, רק ברור יותר.
הכלל החשוב: צעדים קטנים, ובדיקה אחרי כל צעד. משנים דבר אחד, מריצים, משווים לפלט המקורי, וממשיכים.
תחביר
# DRY: repeated code → one function + loop
def check(item):
...
for item in items:
check(item)
# Comment: explain WHY, not WHAT
# Tax authority requires rounding up to whole shekels (regulation 12b)
total = math.ceil(total)
# Docstring: a string right under def that describes the function
def calculate_shipping(weight_kg):
"""Return shipping cost in NIS for a package of the given weight."""
...Docstring היא מחרוזת בשלוש מירכאות ("""...""") שמופיעה בשורה הראשונה של פונקציה ומתארת מה היא עושה. VS Code מציג אותה כשמרחפים מעל קריאה לפונקציה.
דוגמת קוד ראשונה
DRY בפעולה. זוכרים את project_check.py מנושא מחשב, מערכת הפעלה ורשת? לפני:
import os
base = "../shop"
if os.path.exists(base + "/data"):
print("data -> OK")
else:
print("data -> MISSING")
if os.path.exists(base + "/logs"):
print("logs -> OK")
else:
print("logs -> MISSING")
if os.path.exists(base + "/app.py"):
print("app.py -> OK")
else:
print("app.py -> MISSING")אחרי:
import os
PROJECT_DIR = "../shop"
REQUIRED_PATHS = ["data", "logs", "app.py", "data/products.txt"]
def check_path(relative_path):
full_path = PROJECT_DIR + "/" + relative_path
if os.path.exists(full_path):
status = "OK"
else:
status = "MISSING"
print(relative_path, "->", status)
for path in REQUIRED_PATHS:
check_path(path)פלט (אותה התנהגות):
data -> OK
logs -> OK
app.py -> OK
data/products.txt -> OKפירוק הקוד שורה אחר שורה
| שורה | הסבר |
|---|---|
PROJECT_DIR = "../shop" | הנתיב מוגדר פעם אחת. אם הפרויקט עובר תיקייה, משנים שורה אחת. |
REQUIRED_PATHS = [...] | המידע (מה לבדוק) מופרד מהלוגיקה (איך לבדוק). להוסיף בדיקה = להוסיף שם לרשימה. |
def check_path(relative_path): | הלוגיקה שחזרה ארבע פעמים, כתובה פעם אחת. החלק שהשתנה בין הבלוקים (שם הנתיב) הפך לפרמטר. |
status = ... ואז print אחד | במקום שני print כמעט זהים, משתנה עם הסטטוס, והדפסה אחת. גם זה DRY בקטן. |
for path in REQUIRED_PATHS: | הלולאה מפעילה את הבדיקה על כל פריט. |
דוגמה נוספת
KISS + Separation of Concerns + הערות. תוכנית שמחשבת עלות משלוח. לפני — “חכמה”, מעורבבת ועם הערות מיותרות:
w = float(input("w: ")) # get w
# calculate the price
print("Price:", (w <= 1) * 20 + (1 < w <= 5) * 35 + (w > 5) * (35 + (w - 5) * 4)) # printאחרי:
BASE_PRICE_SMALL = 20
BASE_PRICE_MEDIUM = 35
PRICE_PER_EXTRA_KG = 4
SMALL_MAX_KG = 1
MEDIUM_MAX_KG = 5
def read_weight():
return float(input("Package weight (kg): "))
def calculate_shipping(weight_kg):
"""Return the shipping price in NIS for a package of the given weight."""
if weight_kg <= SMALL_MAX_KG:
return BASE_PRICE_SMALL
if weight_kg <= MEDIUM_MAX_KG:
return BASE_PRICE_MEDIUM
# Above the medium tier the courier charges per extra kg (price list 2026)
extra_kg = weight_kg - MEDIUM_MAX_KG
return BASE_PRICE_MEDIUM + extra_kg * PRICE_PER_EXTRA_KG
def show_price(price):
print("Shipping price:", price, "NIS")
weight = read_weight()
price = calculate_shipping(weight)
show_price(price)הרצה:
Package weight (kg): 7.5
Shipping price: 45.0 NISפירוק:
- KISS: הטריק
(w <= 1) * 20עובד (כיTrueנחשב 1 ו-Falseנחשב 0), אבל אף אחד לא יבין אותו במבט. שרשרתifפשוטה קריאה מיד. - Separation of Concerns:
read_weight(קלט),calculate_shipping(לוגיקה),show_price(פלט). מחר המשקל יגיע מבקשת HTTP? מחליפים רק אתread_weight. הלוגיקה לא משתנה. - הערות: נמחקו
# get wו-# print, כי הן חזרו על הקוד. נשארה הערה אחת שמסבירה למה (מחירון של חברת השליחויות), מידע שאי אפשר להבין מהקוד עצמו. - Docstring מתאר מה הפונקציה מחזירה וביחידות מה (ש"ח).
טעות נפוצה
הטעות 1: הערה במקום שם טוב.
d = 30 # number of days until the subscription expiresעדיף: days_until_expiry = 30, בלי הערה בכלל. אם צריך הערה כדי להסביר משתנה, השם לא מספיק טוב.
הטעות 2: הערה שמשקרת.
# Discount is 10%
price = price * 0.85מישהו עדכן את הקוד ושכח את ההערה. עכשיו קורא הקוד לא יודע במה להאמין. הערות לא נבדקות על ידי Python, ולכן הן “נרקבות”. זו סיבה נוספת לכתוב מעט הערות, וטובות.
הטעות 3: DRY מוקדם מדי. שני קטעי קוד שנראים דומים במקרה, אבל מייצגים כללים עסקיים שונים, לא חייבים להתאחד. למשל, “מע"מ 17%” ו"עמלה 17%" הם מקרה, לא אותו ידע. אם יאוחדו לקבוע אחד, שינוי של אחד ישבור את השני.
הטעות 4: Refactoring “גדול” בבת אחת. משנים 10 דברים, מריצים, משהו נשבר, ואין מושג מה. צעד אחד, הרצה, השוואה, צעד הבא.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: DRY — קבלה בלי העתק-הדבק
print("Pen", 5, "x", 4, "=", 5 * 4)
print("Notebook", 12, "x", 2, "=", 12 * 2)
print("Bag", 60, "x", 1, "=", 60 * 1)
print("Total:", 5 * 4 + 12 * 2 + 60 * 1)שכתבו את הקוד כך שהנתונים יישמרו ברשימות, ופונקציה אחת עם לולאה תדפיס את הקבלה. הפלט חייב להישאר:
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104הצג פתרון
def line_total(price, quantity):
return price * quantity
def print_receipt(names, prices, quantities):
total = 0
for i in range(len(names)):
amount = line_total(prices[i], quantities[i])
print(names[i], prices[i], "x", quantities[i], "=", amount)
total = total + amount
print("Total:", total)
print_receipt(["Pen", "Notebook", "Bag"], [5, 12, 60], [4, 2, 1])Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104כל חישוב מופיע עכשיו פעם אחת. מוצר רביעי = עוד ערך ברשימות, בלי עוד שורת קוד.
תרגיל 2: KISS — קוד “חכם” מדי
def parity(n):
return (n % 2 == 0) * "even" + (n % 2 == 1) * "odd"
print(parity(4))
print(parity(7))הקוד עובד, אבל קשה להבין אותו. שכתבו את parity בצורה הכי פשוטה וקריאה.
פלט צפוי:
even
oddהצג פתרון
def parity(n):
if n % 2 == 0:
return "even"
return "odd"
print(parity(4))
print(parity(7))even
oddKISS — Keep It Simple. if רגיל נקרא מיד, בלי לחשוב איך כפל של True במחרוזת עובד.
תרגיל 3: הערות — למה, לא מה
לכל הערה — האם היא טובה, מיותרת או מזיקה? הסבירו.
1. count = count + 1 # add 1 to count
2. # 30 days: the refund policy of the shop
REFUND_DAYS = 30
3. total = price * 2 # multiply price by 3
4. # retry 3 times — the payment API sometimes fails on the first callהצג פתרון
- מיותרת — חוזרת על מה שהקוד כבר אומר.
- טובה — מסבירה למה 30, מידע שלא מופיע בקוד.
- מזיקה — משקרת (הקוד מכפיל ב-2). הערה שלא עודכנה גרועה יותר מאין הערה.
- טובה — מסבירה למה הקוד עושה משהו שנראה מוזר.
הכלל: הקוד אומר מה, ההערה אומרת למה.
תרגיל 4: הפרדת אחריות — חישוב מול קלט ופלט
def bmi():
weight = float(input("Weight: "))
height = float(input("Height: "))
print("BMI:", round(weight / (height * height), 1))הפונקציה עושה שלושה דברים: קוראת קלט, מחשבת ומדפיסה. לכן אי אפשר להשתמש בחישוב בלי מקלדת. פרקו אותה לשלוש פונקציות, אחת לכל אחריות:
read_number(message)— רק קוראת מספר מהמשתמש ומחזירה אותו.calculate_bmi(weight, height)— רק מחשבת ומחזירה, בליinputובליprint.print_bmi(bmi)— רק מדפיסה תוצאה שכבר חושבה.
בדקו את החישוב ואת ההדפסה בלי מקלדת:
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)פלט צפוי:
BMI: 22.9הצג פתרון
def read_number(message):
return float(input(message))
def calculate_bmi(weight, height):
return round(weight / (height * height), 1)
def print_bmi(bmi):
print("BMI:", bmi)
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)BMI: 22.9בתוכנית האמיתית מחברים את שלוש הפונקציות, וכל שורה מפעילה אחריות אחת:
weight = read_number("Weight: ")
height = read_number("Height: ")
bmi = calculate_bmi(weight, height)
print_bmi(bmi)inputנמצא רק ב-read_number, ו-printרק ב-print_bmi. ל-calculate_bmiאין קלט ואין פלט, ולכן אפשר לקרוא לה עם נתונים מכל מקום: מקלט, מקובץ או בבדיקה.print("BMI:", calculate_bmi(70, 1.75))הוא קוד קצר יותר, אבל ההדפסה עדיין צמודה לחישוב באותה שורה ואין פונקציה שאחראית על התצוגה. רוצים להציג את התוצאה אחרת? עם ההפרדה מחליפים רק אתprint_bmi.
תרגיל 5: Refactoring בצעדים
t = 12
if t < 15:
print(12, "cold")
elif t < 28:
print(12, "pleasant")
else:
print(12, "hot")
t = 24
if t < 15:
print(24, "cold")
elif t < 28:
print(24, "pleasant")
else:
print(24, "hot")בצעו Refactoring: פונקציה classify(temperature) שמחזירה את הסיווג, קבועים לגבולות, ולולאה על רשימת הטמפרטורות [12, 24, 33].
פלט צפוי:
12 cold
24 pleasant
33 hotהצג פתרון
COLD_LIMIT = 15
HOT_LIMIT = 28
def classify(temperature):
if temperature < COLD_LIMIT:
return "cold"
if temperature < HOT_LIMIT:
return "pleasant"
return "hot"
for temperature in [12, 24, 33]:
print(temperature, classify(temperature))12 cold
24 pleasant
33 hotהצעדים: (1) לחלץ פונקציה, (2) להוציא את 15 ו-28 לקבועים, (3) להחליף את הקוד הכפול בלולאה — ולהריץ אחרי כל צעד ולוודא שהפלט לא השתנה.
סיכום
- DRY — כל ידע במקום אחד. קוד שחוזר ← פונקציה, לולאה או קבוע.
- KISS — פשוט וקריא עדיף על “חכם”.
- Separation of Concerns — קלט, לוגיקה ופלט בחלקים נפרדים. זה המבנה של כל שרת.
- הערות מסבירות למה. הקוד עצמו (עם שמות טובים) מסביר מה. הערה שמשקרת גרועה מאין הערה.
- Refactoring — משפרים מבנה בלי לשנות התנהגות, בצעדים קטנים עם הרצה אחרי כל צעד.
דוקומנטציה רשמית
- PEP 20 — The Zen of Python — Simple is better than complex
- PEP 8 — Comments — מתי ואיך כותבים הערות
- Python Tutorial — Defining Functions — פונקציה אחת במקום קוד כפול