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

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

למה את קוראת "טעות"?

18/11/2022

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

  1. האם קוד גרוע הוא קוד שלא עובד? או שגם קוד עובד יכול להיות גרוע?

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

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

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

  5. האם קוד גרוע קשור ל Best Practices ולסגנון כתיבה? קוד גרוע הוא קוד שלא כתוב לפי סטנדרטים מסוימים? איזה סטנדרטים אלה? איזה סוג קוד קשה לכם לקרוא?

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

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

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

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

התיקון הלא נכון (או: למה הוובינר הופיע בשעה הלא נכונה)

16/11/2022

רבים מכם שמו לב שהוובינר באתר הופיע בשני מקומות שונים בשתי שעות שונות: בדף רשימת הוובינרים הוא הופיע בשעה הנכונה (עשר בבוקר), אבל בדף האירוע עצמו הוא הופיע בשעה שמונה בבוקר.

בואו נראה למה זה קרה ומה אפשר ללמוד מזה.

המשך קריאה

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

15/11/2022

ריאקט היא רק ספריית תצוגה.

לריאקט יש ביצועים מצוינים.

ריאקט הרבה פחות מסובכת מ X/Y/Z ולכן קוד ריאקט תמיד יוצא יותר יעיל.

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

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

  1. טעויות נפוצות ב Data Flow בין קומפוננטות.

  2. טעויות נפוצות בנושא Immutable Data.

  3. טעויות נפוצות בשימוש ב Hooks, במיוחד במערך התלויות ו useEffect.

  4. איך לבחור קומפוננטות בצורה יותר יעילה.

  5. זמן לשאלות שלכם.

הכנתי הרבה דוגמאות אז תבואו ערניים ונתראה בחמישי, אה כמעט שכחתי - קישור להרשמה: https://www.tocode.co.il/workshops/121.

קוד טוב, קוד עובד

14/11/2022

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

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

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

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

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

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

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

  3. שימוש ב Git Hooks כדי לוודא הודעות קומיט מפורטות בגיט.

  4. הכנסת שידרוגי תלויות ותשתית בתור משימה חוזרת ב Jira, פעם ב X ספרינטים.

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

עדכונים אופטימיים ב Redux Toolkit Query

13/11/2022

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

ל RTK Query יש דרך מובנית ומאוד נוחה לממש עדכונים אופטימיים בזכות שילוב של מספר מנגנונים של הספריה:

  1. ספריית RTK Query מחזיקה את כל התוצאות של כל השאילתות בזיכרון, ומאפשרת לנו לעדכן את השאילתות השמורות.

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

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

ככה זה נראה בקוד:

    createNote: builder.mutation({
      query: (noteText) => ({
        url: `/notes`,
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ text: noteText }),
      }),      
      async onQueryStarted(noteText, { dispatch, queryFulfilled }) {
        let newNoteId = nanoid();

        const patchResult = dispatch(
          notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
            return [...draft, { text: noteText, id: newNoteId }];
          }));
        try {
          const data = await queryFulfilled
          dispatch(
            notesApi.util.updateQueryData('getNotes', undefined, (draft) => {        
              return draft.map(d => d.id === newNoteId ? data.data : d )
            }));
       } catch (err) {
        console.log(`error`);
        patchResult.undo();
       }
      }
    })

הקוד מגדיר פעולה חדשה בשם createNote, עבור אפליקציה ששומרת רשימה של פתקים. בשביל ההגדרה אני מפעיל את builder.mutation ומעביר לו אוביקט עם שני מפתחות:

  1. המפתח query יוצר את קוד בקשת ה POST שאני שולח לשרת כדי ליצור פתק חדש. לפתק חדש יש בסך הכל את הטקסט בפתק.

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

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

const patchResult = dispatch(
  notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
    return [...draft, { text: noteText, id: newNoteId }];
}));

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

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

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

const data = await queryFulfilled

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

dispatch(
  notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
    return draft.map(d => d.id === newNoteId ? data.data : d )
  }));

ואם היתה שגיאה והשרת לא הצליח לשמור את הפתק? בשביל זה יש לי בלוק catch:

} catch (err) {
 patchResult.undo();
}

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

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

} catch (err) {
 dispatch(
   notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
     return draft.filter(note => note.id !== newNoteId);
   }));
  }

קוד כזה מאפשר ל undo להיות יותר ספציפי וממש למחוק את הפתק החדש שניסינו להכניס במקום להחזיר את המצב למה שהיה כשיצרנו את ה patch.

טיפ רידאקס - השתמשו ב preloadedState כדי לבדוק יותר בקלות

12/11/2022

בדוגמת ההתחלה מהירה של Redux Tolkit הם יוצרים וימייצאים את מחסן המידע באותה שורה באופן הבא:

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

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

export function createStore(initialState) {
  return configureStore(Object.assign({}, {
    reducer: {
      counter: counterReducer,
    },
  },
  initialState ? { preloadedState: initialState } : {}));
}

וככה אני יכול להשתמש בפונקציה כדי לבדוק את ה store בכל state ראשוני שארצה:

test('inc from 10 to 11', () => {
  const store = createStore({ counter: { value: 10 }});
  store.dispatch(increment());
  expect(store.getState().counter).toEqual({ value: 11 });
});

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

multipass היא הדרך הכי קלה לקבל מכונת אובונטו וירטואלית על המחשב שלכם

10/11/2022

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

המשך קריאה

היום למדתי: הפקודה fc תאפשר לכם לערוך היסטוריה ולהריץ אותה שוב

09/11/2022

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

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

mkdir foo
touch foo/bar
cp /etc/passwd foo

אז אני יכול אחרי זה לכתוב:

fc -l mkdir cp

ולקבל את רשימת כל הפקודות מ mkdir עד cp כולל:

544      mkdir foo
545      touch foo/bar
546      cp /etc/passwd foo

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

mkdir bar
touch bar/bar
cp /etc/passwd bar

שומר ויוצא וכך יצרתי את תיקיית bar בדיוק כמו שיצרתי קודם את תיקיית foo.

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