שלום אורח התחבר

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

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

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

או הזינו את כתובת המייל וקבלו את הפוסט היומי בכל בוקר אליכם לתיבה:

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

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

symbols = (
        [10, 'X'],
        [5, 'V'],
        [1, 'I' ]
        )

def convert_to_roman(number):
    result = ''
    for i, r in symbols:
        result += r * (number // i)
        number = number % i
    return result

קוד הבדיקה הבא משתמש בפיתרון ומראה שהוא עובד לפחות על 5 מספרים:

result = {
        28: 'XXVIII',
        12: 'XII',
        6: 'VI',
        32: 'XXXII',
        21: 'XXI',
        }

for k, v in result.items():
    if convert_to_roman(k) != v:
        raise Exception(f"Conversion failed for {k}. Expected: '{v}'; Got: '{convert_to_roman(k)}'")

print("All OK")

אבל כשננסה להפעיל את ההמרה על מספר כמו 19 נקבל שגיאה. ההמרה הנכונה של 19 לספירה רומית היא XIX אבל אצלנו הוא יופיע בתור XVIIII. איך מתקנים?

כיוון אחד יכול להיות לנסות לשכנע את המחשב לבדוק כל מיני אפשרויות לבנות את המספרים, ולכתוב את הקוד שמזהה ש 19 הוא 20-1 בדיוק כמו שהוא 15+4 אבל ש 20-1 יותר קצר לכתיבה ולכן כדאי להמיר לזה.

כיוון יותר קל הוא לרמות:

symbols = (
        [10, 'X'],
        [9, 'IX'],
        [5, 'V'],
        [4, 'IV'],
        [1, 'I' ],
        )

אם טבלת הסמלים שלי כוללת גם את 9 ו-4, אז כשהפונקציה תתקל ב 19 היא קודם כל תיקח את ה 10 ותהפוך אותו ל X, ואז את ה 9 שנשאר תהפוך ל IX. הפונקציה לא מספיק חכמה בשביל להבין ש 9 זה עשר פחות אחד, אבל גם לא צריכה להיות. את החוכמה אפשר להשאיר לרומאים.

הפונקציות Object.entries ו Object.fromEntries הן סוג של אוצר לכל מי שאוהב תכנות פונקציונאלי ו JavaScript כי הן מאפשרות לנו לסגור את הפער בין אוביקטים למערכים.

במערכים הפונקציות map ו filter מגיעות מובנות עם המערך, כך שאם יש לי רשימה של מספרים אני יכול בקלות לקבל את ריבועי המספרים, או את ריבועי המספרים הגדולים מ 50:

const numbers = [5, 6, 7, 8, 9, 10];
const squares = numbers.map(x => x * x);
const largeSquares = numbers.map(x => x * x).filter(x => x > 50);

אבל מה לגבי אוביקטים? הדרך הקלה להפעיל map ו filter על אוביקטים היא להפוך אותם למערכים, להפעיל את הלוגיקה ואז להחזיר חזרה לאוביקטים.

לדוגמה קחו אוביקט קונפיגורציה שמגדיר מספר URL-ים חשובים ליישום:

const urls = {
  google: 'http://www.google.com',
  ddg: 'http://www.duckduckgo.com',
  bing: 'http://www.bing.com'
}

בעזרת Object.entries ו Object.fromEntries אני מריץ בקלות טרנספורמציות על האוביקט. למשל בשביל להפוך את כל ה URL-ים ל https ולוותר על כל מנוע חיפוש שהשם שלו ארוך מ-4 אותיות אני מריץ:

Object.fromEntries(
    Object.entries(urls).
        map(([key, value]) => 
            [key, value.replace('http://', 'https://')]).
        filter(([key, value]) => key.length < 5))

ומקבל חזרה:

{ ddg: 'https://www.duckduckgo.com', bing: 'https://www.bing.com' }

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

ואולי את יודעת יותר ממה שידע המתכנת שכתב את זה במקור?

ואולי את רואה יותר דברים ממה שהוא ראה?

ואולי המערכת נמצאת היום במצב אחר ממה שהיתה כשהקוד הזה נכתב במקור?

ואולי את יותר מנוסה ממה שהוא היה כשהוא כתב את זה במקור?

ואולי היום מקובל לעשות דברים אחרת?

ואולי כשהקוד יופיע במקום נוסף אפשר יהיה לכתוב אותו בצורה גנרית יותר?

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

כשאת מעתיקה את הקוד הישן למקום חדש את נותנת ציון 100 לקוד הישן. את בטוחה שזה מגיע לו?

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

חוץ מה Branch-ים וה Commit-ים יש עוד סימניה חשובה שנקראת HEAD. ה HEAD הוא מצביע ל Branch והוא מסמן לנו מה הקומיט "הנוכחי", כלומר מה הקומיט שעכשיו אנחנו עובדים עליו.

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

* 8131ef2 (HEAD -> main) third commit
* 3a40bc4 second commit
* e8006f8 first commit

בגרף יש שלושה קומיטים, בראנץ אחד בשם main וה HEAD שלנו הוא אותו main.

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

$ git reset --hard HEAD~

ואז נסתכל בלוג:

3a40bc4 (HEAD -> main) second commit
e8006f8 first commit

בום! לאן נעלם הקומיט השלישי?

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

אבל זה לא אומר שאותו קומיט נעלם או נמחק.

אם אתם זוכרים את מזהה הקומיט אתם יכולים בקלות לחזור אליו עם:

$ git reset --hard 8131ef2

אפילו אם אתם לא זוכרים את המזהה אתם יכולים בקלות לבדוק מה הוא היה עם:

$ git reflog

3a40bc4 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~
8131ef2 HEAD@{1}: reset: moving to 8131ef2
3a40bc4 (HEAD -> main) HEAD@{2}: reset: moving to main
3a40bc4 (HEAD -> main) HEAD@{3}: reset: moving to HEAD~
8131ef2 HEAD@{4}: commit: third commit
3a40bc4 (HEAD -> main) HEAD@{5}: commit: second commit
e8006f8 HEAD@{6}: commit (initial): first commit

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

$ git reflog expire --expire-unreachable=now --all

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

מחיקה מוחלטת של אותו קומיט אפשרית עם הפקודה:

$ git gc --prune=now

רק אחרי שמחקתם את הקומיט מה reflog כדי שבאמת אי אפשר יהיה להגיע אליו או לכל קומיט אחר שמצביע עליו. אבל מהלך כזה עלול לצאת מתחום ההומור הבריא ולגרום לחברים שלכם להיבהל באמת.

איפה רושמים את כל הפעמים שכתבתי ומחקתי את הקוד עד שהוא עבד? וכמה זמן עבודה זה לקח?

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

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

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

לפי האתר שלהם, בראמבל הם Production Ready GraphQL Federation. מאחר ואני תמיד שמח לבדוק חלופות פשוטות יותר ל Apollo הלכתי לבנות פרויקט דוגמה קטן עם בראמבל. קוד הפרויקט כאן: https://github.com/ynonp/playing-with-bramble

בפוסט אספר איך ברמבל עובד ומתי שווה לשקול אותו.

המשך קריאה...

הכלי שנחשב Industry Standard הוא לא בהכרח יותר טוב, לא בהכרח יותר פשוט ולא בהכרח יותר יעיל מהמתחרים. היתרונות הם אחרים:

  1. מישהו שמשתמש בו כנראה כבר נתקל ברוב הבעיות שאתם צפויים להיתקל בהן.

  2. יש מנגנון מובנה להתמודד עם הארכיטקטורה המוזרה של המערכת שלכם.

  3. יש תיעוד לא רע בכלל, ודי הרבה תשובות ב Stack Overflow וב Github Issues של האנשים שלא הצליחו למצוא את מה שמחפשים בתיעוד.

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

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

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

כשמישהו מספר לי על Bundler יותר טוב מ Webpack, על Web Framework יותר טוב מ Express, על GraphQL Federation יותר טוב מ Apollo או על Orchestrator יותר טוב מקוברנטס אני לא מתווכח. כל עוד אני שומר לעצמי את האפשרות להחליף בעתיד, אני אפילו שמח לתת לכלי החדש צ'אנס.

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

function capitalize(item) {
  return item.replace(/\b(\w)/g, c => c.toUpperCase());
}

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

function capitalize(itemOrPromise) {
  if (itemOrPromise.then) {
    return itemOrPromise.then(capitalize);
  }

  // Now we know it's an item
  return itemOrPromise.replace(/\b(\w)/g, c => c.toUpperCase());
}

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

ומי שבכלל רוצה להתחשב יכול להוסיף גם טיפול במערכים ועל הדרך במערכים של Promises:

function capitalize(itemOrPromise) {
  if (Array.isArray(itemOrPromise)) {
    return itemOrPromise.map(capitalize);
  }

  if (itemOrPromise.then) {
    return itemOrPromise.then(capitalize);
  }

  // Now we know it's an item
  return itemOrPromise.replace(/\b(\w)/g, c => c.toUpperCase());
}

אם קיבלנו מערך נריץ map כדי להפוך לאותיות גדולות את כל התאים שלו (ואם התאים הם Promises הכל עדיין יעבוד); אם קיבלנו Promise אז נחכה שהוא יסתיים, ואז הרקורסיה תטפל גם במקרה שקיבלנו Promise למערך וכשמגיעים בסוף למחרוזת טקסט אפשר לסיים ולהפוך את האות הראשונה בכל מילה לאות גדולה.

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

function handleAllThenThings(func) {
  return function handler(itemOrPromise) {
    if (Array.isArray(itemOrPromise)) {
      return itemOrPromise.map(handler);
    }

    if (itemOrPromise.then) {
      return itemOrPromise.then(handler);
    }

    // Now we know it's an item
    return func(itemOrPromise);
  };
}

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

זה לעתים נדירות המקרה.

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

זה לא סתם שכל Front End Framwork שהיתה בשכונה מספיק זמן עברה אינסוף סבבי Refactoring ובשלב מסוים גם כתיבה מחדש מאפס. באנגולר זה קרה במעבר מגירסה 1 ל-2, בריאקט במעבר מגירסה 15 ל 16 ובויו זה היה המעבר ל Vue3. אנחנו יכולים לקשקש עד מחר על Design Patterns וכתיבה נכונה, אבל בסוף כשמערכת פוגשת את העולם דברים משתנים.

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

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

המשך קריאה...