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

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

הפונקציות map ו filter לאוביקטים ב JavaScript

15/09/2021

הפונקציות 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 תמיד יחזיר זוגות של מפתח וערך.

זה היה שם לפניי

14/09/2021

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

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

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

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

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

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

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

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

לאן הולכים הקומיטים אחרי reset?

13/09/2021

טריק מעניין של גיט שהצליח לבלבל אותי כשרק התחלתי לעבוד עם הכלי הוא האופן בו גיט מנהל את הקומיטים שלו. בקצרה בגיט כל המידע נשמר בתור גרף של קומיטים, כאשר כל קומיט "מצביע" על הקומיט שבא לפניו. חוץ מהקומיטים יש גם דבר שנקרא 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 כדי שבאמת אי אפשר יהיה להגיע אליו או לכל קומיט אחר שמצביע עליו. אבל מהלך כזה עלול לצאת מתחום ההומור הבריא ולגרום לחברים שלכם להיבהל באמת.

הערכות זמנים (טייק 712)

12/09/2021

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

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

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

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

אינדאסטרי סטנדרט

10/09/2021

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

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

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

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

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

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

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

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

טיפ JavaScript: טיפול באוביקט או Promise מאותה פונקציה

09/09/2021

הטריק הבא לא מסובך למימוש אבל יכול לחסוך לכם קצת 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);
  };
}

מתי לכתוב קוד גרוע

08/09/2021

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

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

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

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

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

העלאת סרביס מ docker-compose ל Kubernetes

07/09/2021

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

המשך קריאה

פרופורציה

06/09/2021

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

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

כל חודש יש אינסוף משימות דחופות שלא השאירו לי זמן ללמוד מיומנות חדשה.

וכל שנה יש אינסוף ישיבות מיותרות שלא השאירו לי זמן ליצור קשרים אמיתיים עם אנשים מחוץ לחברה.

ועדיין-

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

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

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

ואם נצליח במהלך השנה ליצור קשרים משמעותיים עם אנשים מחוץ לחברה, לאורך זמן יהיו לנו הרבה יותר אנשים להחליף איתם רעיונות יצירתיים ויהיה לנו הרבה יותר קל לבנות מסלול קריירה שמתאים לנו.

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