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

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

קוד נכון

21/12/2022

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

שתי דוגמאות מהימים האחרונים ואז שורה תחתונה-

  1. פה באתר כפתור "אחורה" בדפדפן לא עבד בחלק מהדפים. הבאג נגרם בגלל השורה:
window.history.replaceState(null, null, url.toString());

בשביל לתקן בסך הכל היה צריך להחליף את השורה ל:

window.history.replaceState(window.history.state, null, url.toString());

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

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

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

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

ואיך זה ישפיע על הפיתוח בעתיד?

20/12/2022

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

  1. איך הפיצ'ר הזה משפיע על פיתוחים עתידיים? איפה הבחירה הזאת הולכת לעזור? ואיפה היא הולכת להזיק?

  2. איזה בדיקות חדשות אצטרך להריץ בכל סבב?

  3. מהם מקרי הקצה החדשים שאני צריך עכשיו לקחת בחשבון?

  4. איך זה יישבר?

  5. איזה פיצ'רים אחרים במערכת יושפעו?

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

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

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

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

חדש באתר: קורס TypeScript

19/12/2022

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

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

טייפסקריפט נכתבה כדי לתת למפתחים כלי עבודה טוב יותר לכתוב אפליקציות JavaScript. היא לא הולכת להפוך את JavaScript ל Java או ל C#, והיא לא הולכת להכניס שיטות עבודה "של אנשי בקאנד" לעולם הפרונט-אנד. היא בסך הכל נותנת לכם כלים טובים יותר לכתוב את ה JavaScript שאתם כבר מכירים, כדי שתוכלו לקודד מהר יותר ועם פחות הסחות דעת. אלה כמה מהפיצ'רים שלא חשבתי שאני בכלל צריך, אבל היום לא יכול בלעדיהם:

  1. השלמה אוטומטית של import-ים, כך שאני לא צריך לעצור ולהיזכר מאיזה קובץ מגיעה כל פונקציה.

  2. ריפקטורינג אוטומטי שאפשר לסמוך עליו, כך שאני לא צריך להיתקע עם שמות לא רלוונטיים לפונקציות או משתנים.

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

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

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

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

https://www.tocode.co.il/bundles/typescript

למה כל כך מסובך?

18/12/2022

ארבע סיבות מרכזיות בגללן אנחנו כותבים קוד מסובך:

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

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

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

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

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

[טייפסקריפט] דוגמה טובה לשימוש ב Function Overloading

17/12/2022

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

המשך קריאה

היום למדתי: הפקודה readarray ב bash

16/12/2022

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

ubuntu@primary:~$ read name < <(echo ynon)
ubuntu@primary:~$ echo "$name"
ynon

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

ubuntu@primary:~$ read name < <(echo "   one")
ubuntu@primary:~$ echo "$name"
one

ואם בפלט יש כמה שורות, read פשוט תזרוק את כל מה שבא אחרי השורה הראשונה:

ubuntu@primary:~$ read name < <(cowsay hello)
ubuntu@primary:~$ echo "$name"
_______

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

ubuntu@primary:~$ readarray lines < <(echo "   one")
ubuntu@primary:~$ echo "$lines"
   one

וזאת הדוגמה השלישית, שמדפיסה הפעם את כל השורות:

ubuntu@primary:~$ readarray lines < <(cowsay hello)
ubuntu@primary:~$ echo "${lines[@]}"
 _______
 < hello >
  -------
         \   ^__^
          \  (oo)\_______
             (__)\       )\/\
                 ||----w |
                 ||     ||

יותר מזה, בגלל שהפלט נשמר בתור מערך אפשר לגשת לכל שורה ממנו למשל:

echo "${lines[1]}"

ל readarray יש די הרבה אפשרויות להתאמה כולל המתג -t שמוחק את תו ירידת השורה מכל שורת קלט, המתג -n שמאפשר להגביל את מספר השורות שנקרא, המתג -s שמאפשר לדלג על השורות הראשונות.

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

ubuntu@primary:~$ cowsay hello | readarray lines

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

[ריאקט] אפקט לחישוב אסינכרוני הוא Anti Pattern

15/12/2022

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

function Twice({ x }) {
    return <p>{x} * 2 = {x * 2}</p>
}

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

// Don't Do This:
function DataFromApi({ id }) {
    const [name, setName] = useState(null);

    useEffect(() => {
        fetch(`/api/product/${id}`)
            .then(p => p.json())
            .then(p => setName(p.name));
    }, [id]);

    if (!name) {
        return <p>Loading...</p>
    }

    return <p>Product Name = {name}</p>
}

אבל זאת Anti Pattern. אלה הבעיות המרכזיות עם הגישה:

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

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

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

[טייפסקריפט] ואז Parameters הציל את היום

14/12/2022

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

const fetcher = url => fetch(url).then(r => r.json())

אבל איך מתרגמים את זה לטייפסקריפט?

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

const { data, error } = useSWR({ url: '...', method: 'POST' });

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

מה שאנחנו באמת רוצים כאן זה להגדיר את fetcher שיקבל בדיוק את הפרמטרים ש fetch מקבלת. ואיך כותבים את זה? תשמחו לשמוע שבטייפסקריפט יש Utility Type שנקרא Parameters. זה עובד ככה:

const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then(res => res.json())

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

משחקים עם טייפסקריפט - יצירת טיפוס לאינדקס חוקי במערך

13/12/2022

ניקח מערך של פריטים קבועים, למשל מערך של צבעים:

const colors = ['red', 'blue', 'green', 'white'];

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

function getColorName(index: number) {
    return colors[index];
}

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

// compiles OK
getColorName(1);

// also compiles OK - but 129 is not a valid index
getColorName(129);

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

function getColorName(index: 0|1|2|3) {
    return colors[index];
}

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

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

type StringToNumber<T extends string, A extends any[] = []> =
  T extends keyof [0, ...A] ? A['length'] : StringToNumber<T, [0, ...A]>

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

ומשתמשים בה כדי לייצר טיפוסים באופן הבא:

// Two == 2
type Two = StringToNumber<'2'>;

// Three == 3
type Three = StringToNumber<'3'>;

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

type Error = StringToNumber<'hello'>;

עם השגיאה המופלאה:

Type instantiation is excessively deep and possibly infinite.(2589)

וזה מהמם כי אנחנו יכולים "לתפוס" את השגיאה ולהשתמש בה כדי לזרוק מפתחות מיותרים בעזרת Mapped Types. הרעיון הוא לקחת את כל המפתחות ב items, שזה כל האינדקסים (בתור מחרוזות) אבל גם המאפיין length וכל שאר הפונקציות של המערך, ולסנן מהם רק את אלה שאפשר להמיר למספר, ועל הדרך גם להמיר אותם למספרים. בסוף נפעיל keyof על התוצאה ונקבל את איחוד המספרים שחיפשנו. זה הקוד:

type ValidIndex = keyof {
    [i in keyof typeof items as (i extends string ? StringToNumber<i> : never)]: 2;
}

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

// Compiles OK - as 2 is a valid index in the array
const valid: ValidIndex = 2;

// Compilation error - 10 is not a valid index
const invalid: ValidIndex = 10;

רגע, למה זה עובד?

12/12/2022

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

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

וזה מביא אותנו לכמה בעיות-

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

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

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

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