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

מטרות השיעור
  • להסביר מה זה Clean Code ולמה קוד נקרא הרבה יותר משהוא נכתב
  • לבחור שמות ברורים למשתנים, פונקציות וקבצים לפי המוסכמות של Python
  • לזהות פונקציה שעושה יותר מדבר אחד ולפרק אותה
  • ליישם את עקרון Single Responsibility ברמת הפונקציה

מה נלמד?

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

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

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

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

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

השוו:

def f(l):
    t = 0
    for x in l:
        t = t + x[1] * x[2]
    return t * 1.17
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_casetotal_price, send_email()
קבועים (ערכים שלא משתנים)UPPER_SNAKE_CASEVAT_RATE, MAX_RETRIES
מחלקות (נושא OOP)PascalCaseStudent, BankAccount
קבצים ומודוליםsnake_case.pyorder_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): לכל פונקציה צריכה להיות סיבה אחת להשתנות, כלומר תפקיד אחד.

אנלוגיה: מסעדה

במסעדה יש מלצר, טבח וקופאי. אם הטבח גם מקבל הזמנות, גם מבשל וגם גובה כסף, כל שינוי (תפריט חדש, מכשיר אשראי חדש) מחייב “לשנות” אותו, וכל טעות שלו פוגעת בהכול. כשלכל אחד תפקיד אחד, קל להחליף, לשפר ולמצוא איפה הבעיה.

תחביר

אין “תחביר חדש” לקוד נקי. זה אותו 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:

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:

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, אותו חישוב בניסוח פחות ברור):

Average: 69.25 - pass

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

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

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

דוגמה נוספת

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

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)

אחרי:

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)

פלט:

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: שמות שמשקרים.

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: פירוק מוגזם. גם ההפך הוא בעיה:

def add_one(number):
    return number + 1

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

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

תרגול

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

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

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

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 בקבועים עם שם. הפלט חייב להישאר זהה:

550
60
הצג פתרון
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))
550
60
  • שם הפונקציה הוא פועל שמתאר מה היא עושה.
  • 500 ו-50 היו Magic Numbers — עכשיו ברור מה הם, ואם ההנחה תשתנה, משנים במקום אחד.

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

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

TotalPrice      (a variable)
numberofstudents (a variable)
max_grade       (a variable)
vat_rate        (a constant that never changes)
GetAverage      (a function)
הצג פתרון
שםתקין?שם נכון
TotalPriceלא — משתנה כותבים ב-snake_casetotal_price
numberofstudentsלא — קשה לקרוא בלי קו תחתוןnumber_of_students
max_gradeכן—
vat_rateלא — קבוע כותבים באותיות גדולותVAT_RATE
GetAverageלא — פונקציה כותבים ב-snake_case, ועדיף פועלget_average / calculate_average

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

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

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 מקבלת ממוצע שכבר חושב ורק מדפיסה. הפלט חייב להישאר:

Dana: average 82.5 - passed
Avi: average 48.0 - failed
הצג פתרון
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)
Dana: average 82.5 - passed
Avi: average 48.0 - failed
  • calculate_average רק מחשבת, is_passing רק מחליטה, ו-print_report מקבלת ממוצע מוכן ורק מציגה אותו. החישוב קורה מחוץ לה.
  • אם print_report הייתה קוראת בעצמה ל-calculate_average, היא שוב הייתה גם מחשבת וגם מדפיסה. הקוד קצר יותר, אבל האחריות לא מופרדת.
  • עכשיו אפשר להשתמש ב-calculate_average גם במקום שלא צריך להדפיס.

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

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

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

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

פלט צפוי:

180
250
הצג פתרון
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))
180
250

200 הוא סכום מינימלי למשלוח חינם, ו-30 הוא דמי המשלוח. עם שמות, אף אחד לא צריך לנחש.

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

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

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

פלט צפוי:

True
False
False
הצג פתרון
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"))
True
False
False

השם is_valid_password מתחיל ב-is_ — ברור שהיא מחזירה True או False. הבדיקה של הספרה הופרדה לפונקציה קטנה משלה.

סיכום

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

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