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

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

פיתרון Advent Of Code יום 1 בעזרת יוניקס

02/12/2022

כמו כל שנה בדצמבר החידות של Advent Of Code התחילו להתפרסם היום. אומנם עדיין לא החלטתי באיזו שפה להשתמש או אפילו אם אני מתכנן לפתור את כולן, אבל החידה הראשונה שהתפרסמה הזכירה לי שלפעמים הכי כיף לקחת כלים פשוטים למשימות פשוטות. בקיצור בואו נראה איך לפתור את Advent Of Code 2022 Day 1 בלי לצאת משורת הפקודה.

המשך קריאה

איפה היית עד עכשיו?

01/12/2022

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

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

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

איתחול רידאקס ממידע שמגיע מהשרת

30/11/2022

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

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

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

המשך קריאה

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

29/11/2022

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

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

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

def subscribe(duration)
    self.subscription_end_date = (Time.zone.now + duration)
end

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

def subscribe(duration)
    self.subscription_end_date = ((subscription_end_date || Time.zone.now) + duration)
end

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

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

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

def subscribe(duration)
    existing_subscription_period = if subscription_end_date.present? && Time.zone.now < subscription_end_date
      subscription_end_date
    else
      Time.zone.now
    end

    self.subscription_end_date = (existing_subscription_period + duration)
end

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

היום למדתי: אירועי mousedown ו mouseup ב JavaScript לא תמיד מתואמים

28/11/2022

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

התוכנית אחרי ניקוי עושה דבר מאוד פשוט - כל פעם שמישהו לוחץ על כפתור של העכבר היא מציגה את הטקסט down, וכשעוזבים את הכפתור היא משנה את הטקסט ל up. אפשר לשחק עם הקוד בקודפן בקישור הזה: https://codepen.io/ynonp/pen/vYrrRPz?editors=1010

או לראות אותו מוטמע כאן:

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

קוד דומה ייכשל גם אם נעבור להשתמש ב Pointer Events.

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

הזמנה לוובינר: טייפסקריפט בקטנה

27/11/2022

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

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

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

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

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

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

  3. איך לשלב טייפסקריפט בפרויקטים ומהו קובץ ההגדרות tsconfig

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

פרטים נוספים והרשמה בדף הוובינר בקישור: https://www.tocode.co.il/workshops/122.

נתראה בחמישי בבוקר, ינון

ככה מצאתי את זה

26/11/2022

בשיטוט מקרי בקוד של axios נתקלתי בקוד הבא עבור אחת הבדיקות:

// from: https://github.com/axios/axios/blob/v1.x/test/specs/promise.spec.js#L10

it('should provide succinct object to then', function (done) {
  let response;

  axios('/foo').then(function (r) {
    response = r;
  });

  getAjaxRequest().then(function (request) {
    request.respondWith({
      status: 200,
      responseText: '{"hello":"world"}'
    });

    setTimeout(function () {
      expect(typeof response).toEqual('object');
      expect(response.data.hello).toEqual('world');
      expect(response.status).toEqual(200);
      expect(response.headers['content-type']).toEqual('application/json');
      expect(response.config.url).toEqual('/foo');
      done();
    }, 100);
  });
});

הם מגדירים שרת פיקטיבי ומאתחלים את פרטי התשובה הפיקטיבית שהוא ישלח, שולחים בקשת רשת עם axios ואז מחכים 100 מילי שניות ובודקים את אוביקט התשובה. אבל למה לחכות דווקא 100 מילי שניות? לפי גיט בליים האשמה היא בקומיט 46a9639. כל הבדיקות עד אותו קומיט השתמשו ב setTimeout עם זמן המתנה של 0, אבל חלקן כנראה נכשלו מדי פעם ובאותו קומיט כל הבדיקות שכללו setTimeout עלו לזמן המתנה 100 מילי שניות.

אבל שאלה יותר מעניינת היא למה בכלל להשתמש ב setTimeout שם. הרי את אותה בדיקה אפשר לתקן בצורה יותר יעילה באמצעות הזזת הקוד למקום המתאים (כלומר אחרי ה then). בקיצור ובקוד:

  it('should provide succinct object to then', function (done) {
    axios('/foo').then(function (response) {
      expect(typeof response).toEqual('object');
      expect(response.data.hello).toEqual('world');
      expect(response.status).toEqual(200);
      expect(response.headers['content-type']).toEqual('application/json');
      expect(response.config.url).toEqual('/foo');
      done();
    });

    getAjaxRequest().then(function (request) {
      request.respondWith({
        status: 200,
        responseText: '{"hello":"world"}'
      });
    });
  });

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

אבל יש שתי בעיות בגישה כזאת:

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

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

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

ואיך זה נשבר?

25/11/2022

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

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

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

סיכום כל הארגומנטים עם shift

24/11/2022

הפקודה shift ב bash "מזיזה" את הארגומנטים מקום אחד שמאלה, כך ש $1 נמחק, $2 הופך ל $1, $3 הופך ל $2 וכך הלאה. הסקריפט הבא מציג דוגמת שימוש פשוטה ב shift שפשוט מדפיסה את שני הארגומנטים הראשונים שקיבלה:

#!/bin/bash

echo $1
shift
echo $1

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

#!/bin/bash

[[ $# == 0 ]] && exit 0

echo $1
shift
exec $0 "$@"

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

#!/bin/bash

if [[ $# == 1 ]]
then
        echo $1
        exit 0
fi

(( sum = $1 + $2 ))

shift 2

exec $0 $sum "$@"

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

while getopts "c:o:l" opt; do
  case $opt in
    c)
      COUNT=$OPTARG;;
    o)
      OUTFILE=$OPTARG;;
    l)
      PADDING=1;;
  esac
done

shift $((OPTIND-1))

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

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

#!/bin/bash

for arg
do
  (( sum += arg ))
done

echo $sum

קריאה עם read מ Pipe ב Bash

23/11/2022

הפקודה read קוראת שורה מהקלט הסטנדרטי, שוברת אותה למילים (עם תו ההפרדה במשתנה IFS) ושומרת את התוצאה למשתנה אחד או יותר שאת שמו היא מקבלת בהפעלה.

לכן הקוד הבא קורא את השורה הראשונה מהקובץ /etc/shells ושומר אותו למשתנה בשם first_line:

read first_line < /etc/shells

והקוד הבא קורא רק את המילה הראשונה מאותו קובץ ושומר אותה למשתנה first_word:

read first_word rest < /etc/shells

קל לשכוח שההתנהגות של read בשילוב | אינה אינטואיטיבית, וזה לא בגלל read. כל פעם שאנחנו מפעילים פקודות בתוך Pipeline, באש פותח תת-מעטפת (Sub Shell) ומבצע את הפקודות בתוכה. הבעיה היא שמשתנים שמוגדרים באותו Sub Shell יימחקו כשה Sub Shell יסתיים, כלומר ביציאה מה Pipeline. הפקודה הזאת לדוגמה לא לוקחת את המילה הראשונה מהפלט של wc /etc/shells לתוך המשתנה lines:

wc /etc/shells | read lines

כלומר היא כן, אבל המשתנה נשמר בתוך Sub Shell ונמחק איך שהשורה נגמרת. בשביל ש read תוכל לשמור משתנה ל Shell הנוכחי שלי היא חייבת להיות מופעלת ממנה, כלומר לא מצד ימין של סימן Pipe. במצב כזה אני צריך להשתמש בפקודת <() כדי להריץ פקודה ולשמור את הפלט שלה לתוך File Handle, ואז בעזרת הפניית קלט רגילה אני יכול להעביר את השורה הראשונה מאותו File Handle ל read:

read lines words chars rest < <(wc /etc/passwd)