# Clean Code ב': DRY, KISS, הפרדת אחריות, הערות ו-Refactoring<no value>

## מה נלמד?

- **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** = שינוי **המבנה** של הקוד **בלי לשנות את ההתנהגות** שלו. משנים שמות, מפרקים פונקציות, מוציאים קוד כפול. בסוף, הקוד עושה בדיוק את אותו דבר, רק ברור יותר.

**הכלל החשוב:** **צעדים קטנים, ובדיקה אחרי כל צעד.** משנים דבר אחד, מריצים, משווים לפלט המקורי, וממשיכים.

## תחביר

```python
# 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` מנושא מחשב, מערכת הפעלה ורשת? **לפני:**

```python
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")
```

**אחרי:**

```python
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)
```

**פלט (אותה התנהגות):**

```text
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 + הערות.** תוכנית שמחשבת עלות משלוח. **לפני** — "חכמה", מעורבבת ועם הערות מיותרות:

```python
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
```

**אחרי:**

```python
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)
```

**הרצה:**

```text
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: הערה במקום שם טוב.**

```python
d = 30  # number of days until the subscription expires
```

עדיף: `days_until_expiry = 30`, בלי הערה בכלל. אם צריך הערה כדי להסביר משתנה, השם לא מספיק טוב.

**הטעות 2: הערה שמשקרת.**

```python
# Discount is 10%
price = price * 0.85
```

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

**הטעות 3: DRY מוקדם מדי.** שני קטעי קוד שנראים דומים **במקרה**, אבל מייצגים כללים עסקיים שונים, לא חייבים להתאחד. למשל, "מע"מ 17%" ו"עמלה 17%" הם מקרה, לא אותו ידע. אם יאוחדו לקבוע אחד, שינוי של אחד ישבור את השני.

**הטעות 4: Refactoring "גדול" בבת אחת.** משנים 10 דברים, מריצים, משהו נשבר, ואין מושג מה. **צעד אחד, הרצה, השוואה, צעד הבא.**

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: DRY — קבלה בלי העתק-הדבק

```python
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)
```

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

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

{{< solution >}}
```python
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])
```

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

כל חישוב מופיע עכשיו **פעם אחת**. מוצר רביעי = עוד ערך ברשימות, בלי עוד שורת קוד.
{{< /solution >}}

### תרגיל 2: KISS — קוד "חכם" מדי

```python
def parity(n):
    return (n % 2 == 0) * "even" + (n % 2 == 1) * "odd"

print(parity(4))
print(parity(7))
```

הקוד עובד, אבל קשה להבין אותו. שכתבו את `parity` בצורה הכי פשוטה וקריאה.

**פלט צפוי:**

```text
even
odd
```

{{< solution >}}
```python
def parity(n):
    if n % 2 == 0:
        return "even"
    return "odd"


print(parity(4))
print(parity(7))
```

```text
even
odd
```

**KISS** — Keep It Simple. `if` רגיל נקרא מיד, בלי לחשוב איך כפל של `True` במחרוזת עובד.
{{< /solution >}}

### תרגיל 3: הערות — למה, לא מה

לכל הערה — האם היא טובה, מיותרת או מזיקה? הסבירו.

```text
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
```

{{< solution >}}
1. **מיותרת** — חוזרת על מה שהקוד כבר אומר.
2. **טובה** — מסבירה **למה** 30, מידע שלא מופיע בקוד.
3. **מזיקה** — משקרת (הקוד מכפיל ב-2). הערה שלא עודכנה גרועה יותר מאין הערה.
4. **טובה** — מסבירה **למה** הקוד עושה משהו שנראה מוזר.

הכלל: הקוד אומר **מה**, ההערה אומרת **למה**.
{{< /solution >}}

### תרגיל 4: הפרדת אחריות — חישוב מול קלט ופלט

```python
def bmi():
    weight = float(input("Weight: "))
    height = float(input("Height: "))
    print("BMI:", round(weight / (height * height), 1))
```

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

1. `read_number(message)` — רק קוראת מספר מהמשתמש ומחזירה אותו.
2. `calculate_bmi(weight, height)` — רק מחשבת ומחזירה, בלי `input` ובלי `print`.
3. `print_bmi(bmi)` — רק מדפיסה תוצאה שכבר חושבה.

בדקו את החישוב ואת ההדפסה בלי מקלדת:

```py
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)
```

**פלט צפוי:**

```text
BMI: 22.9
```

{{< solution >}}
```python
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)
```

```text
BMI: 22.9
```

בתוכנית האמיתית מחברים את שלוש הפונקציות, וכל שורה מפעילה אחריות אחת:

```py
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`.
{{< /solution >}}

### תרגיל 5: Refactoring בצעדים

```python
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]`.

**פלט צפוי:**

```text
12 cold
24 pleasant
33 hot
```

{{< solution >}}
```python
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))
```

```text
12 cold
24 pleasant
33 hot
```

הצעדים: (1) לחלץ פונקציה, (2) להוציא את 15 ו-28 לקבועים, (3) להחליף את הקוד הכפול בלולאה — ולהריץ אחרי **כל** צעד ולוודא שהפלט לא השתנה.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **DRY** — כל ידע במקום אחד. קוד שחוזר ‏←‏ פונקציה, לולאה או קבוע.
- **KISS** — פשוט וקריא עדיף על "חכם".
- **Separation of Concerns** — קלט, לוגיקה ופלט בחלקים נפרדים. זה המבנה של כל שרת.
- **הערות** מסבירות **למה**. הקוד עצמו (עם שמות טובים) מסביר **מה**. הערה שמשקרת גרועה מאין הערה.
- **Refactoring** — משפרים מבנה בלי לשנות התנהגות, בצעדים קטנים עם הרצה אחרי כל צעד.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/) — Simple is better than complex
- [PEP 8 — Comments](https://peps.python.org/pep-0008/#comments) — מתי ואיך כותבים הערות
- [Python Tutorial — Defining Functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions) — פונקציה אחת במקום קוד כפול

<!-- docs:end -->
