# Clean Code א': שמות טובים, פונקציות קטנות ואחריות אחת<no value>

## מה נלמד?

עד עכשיו המטרה הייתה "שהקוד יעבוד". מהיום המטרה היא גם שהוא יהיה **ברור**. נלמד:

- **Naming** — איך בוחרים שמות למשתנים, פונקציות וקבצים.
- **Small Functions** — למה פונקציות צריכות להיות קצרות.
- **Single Responsibility** — כל פונקציה עושה **דבר אחד**.

## למה צריך את זה?

**קוד נקרא הרבה יותר פעמים ממה שהוא נכתב.** את הפונקציה כותבים פעם אחת, אבל קוראים אותה בכל תיקון באג, בכל הוספת פיצ'ר, ובכל Code Review של חבר צוות. ובעוד חודש, גם אתם תקראו אותה כמו זרים.

בחברות, קוד שעובד אבל לא קריא **נדחה ב-Code Review**. בראיונות עבודה בודקים לא רק "האם פתרת", אלא גם "האם אפשר להבין את הפתרון".

השוו:

```python
def f(l):
    t = 0
    for x in l:
        t = t + x[1] * x[2]
    return t * 1.17
```

```python
VAT_RATE = 1.17

def calculate_order_total(items):
    subtotal = 0
    for item in items:
        subtotal = subtotal + item["price"] * item["quantity"]
    return subtotal * VAT_RATE
```

שתי הפונקציות עושות **בדיוק** אותו דבר. את השנייה מבינים בלי מאמץ.

## הסבר מהבסיס

### מה זה Clean Code?

**Clean Code** (קוד נקי) הוא קוד ש**אדם אחר** יכול לקרוא, להבין ולשנות בקלות, ובביטחון. זה לא סט של חוקים קשיחים, אלא אוסף הרגלים שהופכים קוד לברור.

### Naming — שמות טובים

שם טוב עונה על השאלה **"מה זה?"** או **"מה זה עושה?"** בלי שצריך לקרוא את הקוד מסביב.

**מוסכמות השמות של Python (לפי PEP 8, מדריך הסגנון הרשמי):**

| מה | סגנון | דוגמה |
|---|---|---|
| משתנים ופונקציות | `snake_case` | `total_price`, `send_email()` |
| קבועים (ערכים שלא משתנים) | `UPPER_SNAKE_CASE` | `VAT_RATE`, `MAX_RETRIES` |
| מחלקות (נושא OOP) | `PascalCase` | `Student`, `BankAccount` |
| קבצים ומודולים | `snake_case.py` | `order_utils.py` |

**כללים לשם טוב:**

1. **מתאר כוונה** — `days_until_exam` עדיף על `d`.
2. **פונקציה = פועל** — `calculate_total()`, `send_invoice()`, `is_valid_email()`. **משתנה = שם עצם** — `total`, `invoice`, `email`.
3. **בוליאני נשמע כמו שאלה** — `is_active`, `has_permission`, `can_edit`.
4. **רשימה ברבים** — `students` (רשימה), `student` (פריט אחד).
5. **בלי קיצורים לא ברורים** — `customer` ולא `cstmr`. (קיצורים מוכרים כמו `id`, `url`, `max` בסדר.)
6. **בלי מספרי קסם** (Magic Numbers) — במקום `* 1.17` שאיש לא יודע מה הוא, קבוע עם שם: `VAT_RATE = 1.17`.

### Small Functions — פונקציות קטנות

פונקציה ארוכה (50+ שורות) קשה להבין, קשה לבדוק וקשה לשנות. **כלל אצבע:** אם פונקציה לא נכנסת במסך אחד, או שקשה לתת לה שם בלי המילה "and", היא כנראה עושה יותר מדי.

### Single Responsibility — אחריות אחת

**Single Responsibility Principle (SRP)**: לכל פונקציה צריכה להיות **סיבה אחת להשתנות**, כלומר תפקיד אחד.

{{< box "analogy" "אנלוגיה: מסעדה" >}}
במסעדה יש מלצר, טבח וקופאי. אם הטבח גם מקבל הזמנות, גם מבשל וגם גובה כסף, כל שינוי (תפריט חדש, מכשיר אשראי חדש) מחייב "לשנות" אותו, וכל טעות שלו פוגעת בהכול. כשלכל אחד תפקיד אחד, קל להחליף, לשפר ולמצוא איפה הבעיה.
{{< /box >}}

## תחביר

אין "תחביר חדש" לקוד נקי. זה **אותו** Python, רק מאורגן אחרת. התבנית:

```python
CONSTANT_NAME = value          # constants at the top, UPPER_SNAKE_CASE


def verb_noun(clear_parameter):   # a function does ONE thing, named by what it does
    ...
    return result


def main():                     # one function that connects the small pieces
    data = get_something()
    result = process_something(data)
    show_something(result)


main()
```

## דוגמת קוד ראשונה

**לפני** — קוד שעובד אבל לא נקי. `week-2/grades_before.py`:

```python
def do(g):
    s = 0
    for x in g:
        s = s + x
    a = s / len(g)
    if a >= 55:
        print("avg", a, "pass")
    else:
        print("avg", a, "fail")

do([80, 45, 92, 60])
```

**אחרי** — אותה התנהגות, קוד נקי. `week-2/grades_after.py`:

```python
PASSING_GRADE = 55


def calculate_average(grades):
    total = 0
    for grade in grades:
        total = total + grade
    return total / len(grades)


def is_passing(average):
    return average >= PASSING_GRADE


def print_report(average):
    if is_passing(average):
        status = "pass"
    else:
        status = "fail"
    print("Average:", average, "-", status)


student_grades = [80, 45, 92, 60]
average = calculate_average(student_grades)
print_report(average)
```

**פלט (גרסת "אחרי". הגרסה הישנה מדפיסה `avg 69.25 pass`, אותו חישוב בניסוח פחות ברור):**

```text
Average: 69.25 - pass
```

## פירוק הקוד שורה אחר שורה

| בקוד הישן | בקוד הנקי | למה זה טוב יותר |
|---|---|---|
| `do` | שלוש פונקציות עם שמות | השם אומר מה כל אחת עושה. |
| `g`, `s`, `x`, `a` | `grades`, `total`, `grade`, `average` | אין צורך "לפענח". |
| `55` בתוך הקוד | `PASSING_GRADE = 55` | ערך עם משמעות ושם. אם הציון משתנה, משנים במקום אחד. |
| חישוב + החלטה + הדפסה בפונקציה אחת | `calculate_average`, `is_passing`, `print_report` | כל פונקציה עושה **דבר אחד**. |

**היתרון המעשי:** רוצים להציג את הממוצע באתר במקום במסך? רק `print_report` משתנה. רוצים לשנות את ציון המעבר? רק `PASSING_GRADE`. ו-`calculate_average` שימושית גם במקומות אחרים, כי היא **מחזירה** ערך ולא מדפיסה.

## דוגמה נוספת

פונקציה "שעושה הכול" במערכת הרשמה, ואיך מפרקים אותה. **לפני:**

```python
def register(n, e, a):
    if "@" not in e:
        print("bad email")
        return
    if a < 16:
        print("too young")
        return
    u = n.strip().lower().replace(" ", ".")
    print("Welcome " + n + "! Your username is " + u)
```

**אחרי:**

```python
MIN_AGE = 16


def is_valid_email(email):
    return "@" in email


def is_old_enough(age):
    return age >= MIN_AGE


def create_username(full_name):
    return full_name.strip().lower().replace(" ", ".")


def register_user(full_name, email, age):
    if not is_valid_email(email):
        print("Invalid email address")
        return
    if not is_old_enough(age):
        print("You must be at least", MIN_AGE, "to register")
        return
    username = create_username(full_name)
    print("Welcome " + full_name + "! Your username is " + username)


register_user("Dana Cohen", "dana@mail.com", 24)
register_user("Avi Levi", "avi-mail.com", 30)
```

**פלט:**

```text
Welcome Dana Cohen! Your username is dana.cohen
Invalid email address
```

**פירוק:**

- `"@" in email` — `in` בודק אם טקסט מופיע בתוך טקסט אחר, ומחזיר `True`/`False`.
- `.strip()` — מסירה רווחים מההתחלה ומהסוף. `.lower()` — אותיות קטנות. `.replace(" ", ".")` — מחליפה רווחים בנקודות. אלה **מתודות** של `str` שאפשר לשרשר אחת אחרי השנייה.
- `return` בלי ערך — יוצא מהפונקציה מיד (מחזיר `None`). כך לא ממשיכים להרשמה אחרי שגיאה. הטכניקה נקראת **Early Return** (או Guard Clause).
- `register_user` עכשיו "קוראת כמו סיפור": בדוק מייל, בדוק גיל, צור שם משתמש, ברך. כל הפרטים בפונקציות קטנות.

## טעות נפוצה

**הטעות 1: שמות שמשקרים.**

```python
def get_user(user_id):
    user = find_in_list(user_id)
    user["last_login"] = "today"   # changes data!
    delete_old_sessions(user)      # deletes data!
    return user
```

השם אומר "get" (קבל), אבל הפונקציה **משנה ומוחקת**. מי שקורא לה כדי "רק לקבל משתמש" יופתע. שם צריך לתאר את **כל** מה שהפונקציה עושה. ואם קשה לתאר בלי "and", יש יותר מאחריות אחת.

**הטעות 2: פירוק מוגזם.** גם ההפך הוא בעיה:

```python
def add_one(number):
    return number + 1
```

פונקציה של שורה אחת שמסתירה פעולה ברורה מאליה רק מוסיפה קפיצות בקריאה. מפרקים כשיש **רעיון בעל שם** (`is_old_enough`), לא כל שורה.

**הטעות 3: שמות גנריים מדי.** `data`, `info`, `temp`, `result`, `manager`, `process()`. שמות כאלה כמעט תמיד אפשר לשפר: `data` → `student_records`, `process()` → `apply_discount()`.

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

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

### תרגיל 1: שמות שמסבירים את עצמם

הקוד הבא עובד, אבל אי אפשר להבין מה הוא עושה:

```python
def f(a, b):
    c = a * b
    if c > 500:
        c = c - 50
    return c

print(f(120, 5))
print(f(30, 2))
```

שכתבו אותו עם שמות טובים לפונקציה ולמשתנים, והחליפו את המספרים 500 ו-50 בקבועים עם שם. הפלט חייב להישאר זהה:

```text
550
60
```

{{< solution >}}
```python
DISCOUNT_THRESHOLD = 500
DISCOUNT_AMOUNT = 50


def calculate_order_price(unit_price, quantity):
    price = unit_price * quantity
    if price > DISCOUNT_THRESHOLD:
        price = price - DISCOUNT_AMOUNT
    return price


print(calculate_order_price(120, 5))
print(calculate_order_price(30, 2))
```

```text
550
60
```

- שם הפונקציה הוא **פועל** שמתאר מה היא עושה.
- `500` ו-`50` היו **Magic Numbers** — עכשיו ברור מה הם, ואם ההנחה תשתנה, משנים במקום אחד.
{{< /solution >}}

### תרגיל 2: מוסכמות השמות

לכל שם — האם הוא כתוב לפי מוסכמות Python? אם לא, כתבו את השם הנכון.

```text
TotalPrice      (a variable)
numberofstudents (a variable)
max_grade       (a variable)
vat_rate        (a constant that never changes)
GetAverage      (a function)
```

{{< solution >}}
| שם | תקין? | שם נכון |
|---|---|---|
| `TotalPrice` | לא — משתנה כותבים ב-snake_case | `total_price` |
| `numberofstudents` | לא — קשה לקרוא בלי קו תחתון | `number_of_students` |
| `max_grade` | כן | — |
| `vat_rate` | לא — קבוע כותבים באותיות גדולות | `VAT_RATE` |
| `GetAverage` | לא — פונקציה כותבים ב-snake_case, ועדיף פועל | `get_average` / `calculate_average` |
{{< /solution >}}

### תרגיל 3: פונקציה אחת — אחריות אחת

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

```python
def report(name, grades):
    total = 0
    for g in grades:
        total = total + g
    avg = total / len(grades)
    if avg >= 55:
        print(name + ": average", avg, "- passed")
    else:
        print(name + ": average", avg, "- failed")

report("Dana", [80, 85])
report("Avi", [50, 46])
```

פרקו אותה לשלוש פונקציות: `calculate_average`, `is_passing` ו-`print_report(name, average)`. `print_report` מקבלת ממוצע **שכבר חושב** ורק מדפיסה. הפלט חייב להישאר:

```text
Dana: average 82.5 - passed
Avi: average 48.0 - failed
```

{{< solution >}}
```python
PASSING_GRADE = 55


def calculate_average(grades):
    return sum(grades) / len(grades)


def is_passing(average):
    return average >= PASSING_GRADE


def print_report(name, average):
    if is_passing(average):
        status = "passed"
    else:
        status = "failed"
    print(name + ": average", average, "-", status)


dana_average = calculate_average([80, 85])
print_report("Dana", dana_average)
avi_average = calculate_average([50, 46])
print_report("Avi", avi_average)
```

```text
Dana: average 82.5 - passed
Avi: average 48.0 - failed
```

- `calculate_average` רק מחשבת, `is_passing` רק מחליטה, ו-`print_report` מקבלת ממוצע מוכן ורק מציגה אותו. החישוב קורה **מחוץ** לה.
- אם `print_report` הייתה קוראת בעצמה ל-`calculate_average`, היא שוב הייתה גם מחשבת וגם מדפיסה. הקוד קצר יותר, אבל האחריות לא מופרדת.
- עכשיו אפשר להשתמש ב-`calculate_average` גם במקום שלא צריך להדפיס.
{{< /solution >}}

### תרגיל 4: בלי מספרי קסם

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

```python
def final_price(total):
    if total < 200:
        total = total + 30
    return total

print(final_price(150))
print(final_price(250))
```

1. מה המשמעות של 200 ושל 30 בחנות? (רמז: מה קורה להזמנה קטנה?) תנו להם שמות כקבועים.
2. שכתבו את הפונקציה כך שהשם שלה יסביר בדיוק מה היא מוסיפה.

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

```text
180
250
```

{{< solution >}}
```python
FREE_SHIPPING_MINIMUM = 200
SHIPPING_FEE = 30


def add_shipping(order_total):
    if order_total < FREE_SHIPPING_MINIMUM:
        return order_total + SHIPPING_FEE
    return order_total


print(add_shipping(150))
print(add_shipping(250))
```

```text
180
250
```

200 הוא **סכום מינימלי למשלוח חינם**, ו-30 הוא **דמי המשלוח**. עם שמות, אף אחד לא צריך לנחש.
{{< /solution >}}

### תרגיל 5: פונקציה קטנה שעונה על שאלה אחת

כתבו פונקציה `is_valid_password(password)` שמחזירה `True` אם הסיסמה באורך 8 תווים לפחות **וגם** מכילה לפחות ספרה אחת, אחרת `False`. הפונקציה לא מדפיסה — רק מחזירה.

```python
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))
```

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

```text
True
False
False
```

{{< solution >}}
```python
MIN_PASSWORD_LENGTH = 8


def has_digit(text):
    for char in text:
        if char.isdigit():
            return True
    return False


def is_valid_password(password):
    return len(password) >= MIN_PASSWORD_LENGTH and has_digit(password)


print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))
```

```text
True
False
False
```

השם `is_valid_password` מתחיל ב-`is_` — ברור שהיא מחזירה `True` או `False`. הבדיקה של הספרה הופרדה לפונקציה קטנה משלה.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Clean Code** = קוד שאחרים (ואתם בעוד חודש) מבינים ומשנים בקלות. קוד נקרא הרבה יותר משנכתב.
- **שמות:** `snake_case` למשתנים ופונקציות, `UPPER_SNAKE_CASE` לקבועים. פונקציה = פועל, בוליאני = שאלה (`is_...`).
- **בלי מספרי קסם.** ערך עם משמעות מקבל קבוע עם שם.
- **פונקציות קטנות** עם **אחריות אחת** (SRP). אם צריך "and" בשם, כנראה צריך לפרק.
- פונקציה שמחשבת **מחזירה**. ההדפסה היא אחריות נפרדת.

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

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

- [PEP 8 — Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions) — הכללים הרשמיים לשמות ב-Python
- [PEP 8 — Function and Variable Names](https://peps.python.org/pep-0008/#function-and-variable-names) — snake_case לפונקציות ומשתנים
- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/) — Readability counts

<!-- docs:end -->
