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

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

שתי הודעות קומיט

23/02/2020

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

Fixed some stuff

והשניה:

Disabled server-side rendering because it was too slow.

In the future before turning this back on make sure to add caching support so we'll be able to reuse server-side rendered fragments between requests.

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

נ.ב. למקרה שבטעות לא הכרתם - אם במהלך טיפול בבאג נתקלתם בשורה חשודה, הרצת git blame על הקובץ עם השורה החשודה תראה לכם ליד כל שורה את מספר הקומיט שהכניס אותה, ואז git log -1 ומספר קומיט תראה לכם את ההודעה.

טיפ JavaScript: שמירת אוביקטים ב Map ו Set

22/02/2020

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

const text = "one two three one two one";
const wordCount = {};

for (let word of text.split(/\W+/)) {
  if (wordCount[word] == null) {
    wordCount[word] = 0;
  }
  wordCount[word] += 1;
}

console.log(wordCount);

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

const text = "hello toString world";
const wordCount = {};

for (let word of text.split(/\W+/)) {
  if (wordCount[word] == null) {
    wordCount[word] = 0;
  }
  wordCount[word] += 1;
}

console.log(wordCount);

פלט התוכנית:

{ hello: 1,
  toString: 'function toString() { [native code] }1',
  world: 1 }

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

מתכנתים טובים ידעו לעקוף את הבעיה באמצעות שימוש ב Object.create באופן הבא:

const text = "hello toString world";
const wordCount = Object.create(null);

for (let word of text.split(/\W+/)) {
  if (wordCount[word] == null) {
    wordCount[word] = 0;
  }
  wordCount[word] += 1;
}

console.log(wordCount);

אבל ממילא אף פעם לא היה קל למצוא מתכנתים טובים.

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

אני יכול לתקן את תוכנית הספירה שלי באמצעות Map די בקלות:

const text = "hello toString world";
const wordCount = new Map();

for (let word of text.split(/\W+/)) {
  const value = wordCount.get(word) || 0;
  wordCount.set(word, value + 1);
}

console.log(wordCount);

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

בחזרה לתוכנית הספירה - הקוד הבא לא יצליח לזהות שהמערך [2, 5] מופיע 3 פעמים בקלט:

const data = [[1, 2], [2, 3], [5, 2], [5, 2], [5, 2], [2, 3]];
const count = new Map();

for (let item of data) {
  const value = count.get(item) || 0;
  count.set(item, value + 1);
}

console.log(count);

וידפיס את זה:

Map {
  [ 1, 2 ] => 1,
  [ 2, 3 ] => 1,
  [ 5, 2 ] => 1,
  [ 5, 2 ] => 1,
  [ 5, 2 ] => 1,
  [ 2, 3 ] => 1 }

אבל אם נחליף את ה Map באוביקט רגיל הקוד יעבוד בלי בעיה:

const data = [[1, 2], [2, 3], [5, 2], [5, 2], [5, 2], [2, 3]];
const count = {};

for (let item of data) {
  if (!count[item]) {
    count[item] = 0;
  }
  count[item] += 1;
}

console.log(count);

וידפיס:

{ '1,2': 1, '2,3': 2, '5,2': 3 }

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

const data = [[1, 2], [2, 3], [5, 2], [5, 2], [5, 2], [2, 3]];
const count = new Map();

for (let iitem of data) {
  const item = String(iitem);
  const value = count.get(item) || 0;
  count.set(item, value + 1);
}

console.log(count);

אבל לא בטוח שזה שווה את המאמץ.

מובאקס (MobX) בעשר דקות

21/02/2020

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

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

המשך קריאה

אוף סיזן

20/02/2020

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

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

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

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

ממעורב למחויב

19/02/2020

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

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

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

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

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

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

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

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

מעגלים

18/02/2020

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

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

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

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

חידת שיפור ביצועים ב React

17/02/2020

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

ועם הרמז הזה אני שולח אתכם לקוד בקישור: https://codesandbox.io/s/sparkling-framework-zc8c1.

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

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

בהצלחה. ינון

יותר מסובך ממה שחשבתי שיהיה

15/02/2020

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

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

״אני צריכה מסגרת מוגדרת בשביל להתקדם וללמוד״

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

״אני פוחדת לעבוד שבועיים רק בשביל לגלות בסוף שהלכתי בדרך הלא נכונה״

״אני פוחד להיתקע ולגלות שהפרויקט הזה גדול עליי״

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

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

למה ואיך להשתמש ב Generics ב TypeScript

14/02/2020

אחד החיבורים שאני אוהב הוא בין TypeScript ל React Hooks - בצד של ריאקט המעבר ל Function Components הפך את הקוד להרבה יותר פשוט מאשר בתקופה שעבדנו עם מחלקות, ובצד של TypeScript הוא מצליח להבין כמעט את כל מה שאני זורק עליו והתמיכה בטיפוסים עוזרת לכתוב קוד יותר יציב.

עד שמגיעים לכתוב Custom Hooks.

ניקח לדוגמא את הקוד הבא שמגדיר Custom Hook שפונה לשרת להביא מידע בתור JSON:

function useRemoteData(endpoint: string, id: string) {
    const [data, setData] = useState<any|null>(null);

    useEffect(function() {
        setData(null);
        const CancelToken = axios.CancelToken;
        const source = CancelToken.source();

        const req = axios.get(`https://swapi.co/api/${endpoint}/${id}/`, {
            cancelToken: source.token,
        });
        req.then(function(response) {
            // when we get response
            setData(response.data);
        });

        return function cancel() {
            source.cancel();
        }
        // code continues here
    }, [id]);

    return data;
}

הקוד עובד אבל שימו לב לשורה שמגדירה את סוג המשתנה data:

    const [data, setData] = useState<any|null>(null);

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

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

function FilmInfo(props: { id: string }) {
    const { id } = props;
    // Get character data ???
    const data = useRemoteData('films', id);

    if (data === null) {
        return <p>Loading, please wait...</p>
    }

    return (
        <div>
            <p>title: {data.title}</p>
            <p>release_date: {data.release_date}</p>
            <hr />
        </div>
    )
}

בהנחה שאני יודע לגבי כל נתיב מה הוא אמור להחזיר, דרך אחת לגרום לקוד שמשתמש ב Hook לוודא שהוא משתמש רק בשדות שהוא צריך היא להשתמש בפקודה as מתוך הקוד שקורא ל hook, כלומר להחליף את השורה השניה בפונקציה בשורה הזו:

    const data = (useRemoteData('films', id) as IDataFilm);

ולהוסיף באיזשהו מקום בקובץ את הגדרת הממשק IDataFilm. אבל זה לא באמת פותר לנו את הבעיה: לא כולם יודעים או זוכרים שכדאי להשתמש ב as כל פעם לפני שמפעילים פונקציה ומהר מאוד נתחיל לראות מתכנתים שמוותרים על זה. דרך קצת יותר ברורה היא להשתמש ב Generics. המילה Generics בסך הכל אומרת שהקוד שקורא ל Hook חייב להעביר גם את סוג המידע שהוא מצפה לקבל, ואז ה Custom Hook שלנו יראה כך:

function useRemoteData<T>(endpoint: string, id: string) {
    const [data, setData] = useState<T|null>(null);

    useEffect(function() {
        setData(null);
        const CancelToken = axios.CancelToken;
        const source = CancelToken.source();

        const req = axios.get(`https://swapi.co/api/${endpoint}/${id}/`, {
            cancelToken: source.token,
        });
        req.then(function(response) {
            // when we get response
            setData(response.data);
        });

        return function cancel() {
            source.cancel();
        }
        // code continues here
    }, [id]);

    return data;
}

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

function FilmInfo(props: { id: string }) {
    const { id } = props;
    // Get character data ???
    const data = useRemoteData<IDataFilm>('films', id);

    if (data === null) {
        return <p>Loading, please wait...</p>
    }

    return (
        <div>
            <p>title: {data.title}</p>
            <p>release_date: {data.release_date}</p>
            <hr />
        </div>
    )
}

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