הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

בדיקות בתור First Class Citizen

01/12/2023

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

על פניו TDD היה אמור לפתור את הבעיה הזאת, אבל ב TDD הדגש הוא על כתיבת קוד הבדיקות ולא על התשתית או קוד המערכת. במילים אחרות אנשי TDD ישמחו לכתוב Mock Objects כדי שיוכלו לכתוב בדיקה לרכיב מסוים לפני שכותבים את קוד הרכיב. שלב הריפקטורינג ב TDD מגיע בסוף ומתמקד בריפקטורינג של הקוד אחרי שהבדיקה כתובה.

בראיה של בדיקות בתור First Class Citizen אנחנו מסתכלים על כלל הקוד ואוסף התלויות שלו בתור משהו שצריך להיבדק בצורה יעילה. זה לא משנה אם הבדיקה נכתבת לפני או אחרי הקוד, הדבר החשוב הוא מה היא בודקת, וכמה קל להריץ אותה. הדבר משפיע על כל שלבי הפיתוח ובחירת הכלים ולפעמים מחייב החלטות קשות, לדוגמה:

  1. נעדיף להשתמש בבסיס נתונים שיכול לרוץ מקומית מתוך קוד הבדיקה, במקום בבסיס נתונים שמחייב חיבור לרשת.

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

  3. נעטוף קוד שמגיע מספריות חיצוניות כדי שיהיה יותר קל להחליף אותו בקוד בדיקה צפוי.

אנחנו רוצים להגיע למצב שתוך פחות מדקה אנחנו יכולים לקבל הבנה לגבי רמת הבשלות של פיצ'ר חדש שכתבנו, וכמה הקוד החדש שובר מנגנונים קיימים. התיחסות לבדיקות בתור חלק בלתי נפרד מהתהליך והימנעות מתבניות ורכיבים שאי אפשר לבדוק אותם (כי אחרי זה כבר נכתוב לזה mock או קודם נבדוק שזה עובד) הם המפתח לפיתוח מוצרים שמחזיקים מעמד לאורך זמן.

קוד אידמפוטנטי

30/11/2023

רדיטור אקראי שואל - אני מנסה להשתמש בריאקט כדי לאמת כתובת אימייל. גולש נכנס לכתובת שכוללת קוד אימות בתור Query Parameter, ואז קומפוננטת ריאקט מושכת את הפרמטר ופונה לשרת כדי לדווח על כניסה. כל זה קורה ב useEffect של הקומפוננטה ולכן נקרא פעמיים במצב פיתוח. מה עושים?

אם נשים בצד את האתגר הריאקטי, מבחינת מערכת הפיתרון הכי קל כבר כתוב בשאלה והוא דווקא מחוץ לסקופ של ריאקט - אולי אפשר להפוך את ה API לאידמפוטונטי. קוד אידמפוטנטי זה קוד שאפשר להריץ אותו כמה פעמים בלי לשבור שום דבר, והמוסכמה באינטרנט היא שכך יתנהגו בקשות HTTP Get.

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

לא תמיד נוכל לארגן את קוד המערכת בצורה אידמפוטנטית, אבל כשאפשר זה שווה את המאמץ.

זה הממשק, טמבל

28/11/2023

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

max_found = 0
for i in range(100, 1_000):
    for j in range(100, 1_000):
        product = i * j
        if str(product) == str(product)[::-1] and product > max_found:
            max_found = product

print(max_found)

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

  1. איתור כל המספרים שהם מכפלה של שני מספרים תלת ספרתיים.

  2. איתור כל המספרים מתוכם שהם פלינדרומים.

  3. חישוב המספר המקסימלי.

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

ארגון מחדש של הקוד בחשיבה על המנגנונים והממשק ביניהם נראה כך:

import itertools

three_digit_numbers = range(10, 1_000)
pairs = itertools.product(three_digit_numbers, repeat=2)
products = (i * j for (i, j) in pairs)
palindroms = (p for p in products if str(p) == str(p)[::-1])
print(max(palindroms))

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

איך למנוע באגים (שאלה של גישה)

27/11/2023

דרך אחת לייצר פחות באגים היא לבנות מנגנונים נוגדי באגים - לדוגמה בדיקות אוטומטיות, שימוש ב Strong Types, שילוב בודקי תוכנה בתהליך הפיתוח ותוספת הכשרות לצוות.

דרך אחרת היא לזהות ולהעניש אנשים שמייצרים באגים. ו"עונש" כאן יכול להיות Shaming פומבי, אבל גם יכול להיות לאלץ אותם לעבוד שעות נוספות כדי לתקן את הבאגים (מה שבטעות נקרא Code Ownership).

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

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

5 טיפים למבחני בית טובים יותר

26/11/2023

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

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

המשך קריאה

היום למדתי: שלוש נקודות ברובי

25/11/2023

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

def bar(x, y, z, **extra)
  puts "x = #{x}, y = #{y}, z = #{z}, #{extra}"
end

def foo(...)
  bar(...)
end

foo(10, 20, 30, hello: "world")

האופרטור מחליף תבנית ישנה יותר וקצת יותר מסורבלת שגם קיימת בפייתון:

def bar(x, y, z, **extra)
  puts "x = #{x}, y = #{y}, z = #{z}, #{extra}"
end

def foo(*args, **kwargs)
  bar(*args, **kwargs)
end

foo(10, 20, 30, hello: "world")

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

שתי גישות ל Code Reviews ובעלות על קוד

24/11/2023

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

דרך שניה להסתכל על Code Reviews היא בתור הזדמנות לחזק את חברי הצוות. בגישה זו המבקר מקבל הצצה לתהליך המחשבה של המתכנתת שכתבה את הקוד ויכול לשאול - "למה התכוונת פה?", "מה אם הקובץ לא קיים?" או "שמתי לב לאופטימיזציה שאפשר להוסיף כאן". המבקר בגישה זו אינו צוואר בקבוק. אפשר לקבל את העצות, אפשר להשאיר אותן בצד, ואפשר לדחות אותן לגמרי.

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

משחקים עם JSON בסקאלה

23/11/2023

יש משהו חמוד כשספריית תוכנה מתאמצת כדי שדברים יעבדו בצורה אינטואיטיבית אבל יש בזה גם סכנה - והיא שהצעד האינטואיטיבי הבא כבר לא יעבוד. זה הסיפור עם ספריית circe של סקאלה, ספריה לעבודה עם JSON-ים.

המשך קריאה