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

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

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

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)

ללמוד מהטובים ביותר

22/11/2022

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

האמונה הזאת מסבכת אותנו בשני מקומות:

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

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

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

דינו מאפשר הרצת חבילות גם מ npm

21/11/2022

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

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

import express from "npm:express@4.18.2";

function twice(x: number) {
  return x * 2;
}

const app = express();

app.get("/", (req, res) => {
  res.send("Welcome to the Dinosaur API!");
});

app.get("/twice", (req, res) => {
  const n = Number(req.query.n || 0);
  res.send(`${n} * 2 = ${twice(n)}`);
});

app.listen(8000);

ובשביל להריץ, אם יש לכם כבר דינו 1.28 ומעלה מותקן מפעילים:

$ deno run --allow-net --allow-read --allow-env main.ts

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

ביצועים ושפות מודרניות

20/11/2022

אלגוריתם Fisher-Yates לערבוב מערך מציע לבצע את הצעדים הבאים:

  1. בחרו אינדקס אקראי במערך.
  2. מחקו את האיבר באינדקס שבחרתם וכתבו אותו בסוף הרשימה.
  3. המשיכו עד שמחקתם את כל האיברים.

זה אלגוריתם אלגנטי וקל למימוש בכל שפה, לדוגמה ב JavaScript:

function shuffle(arr) {
  let end = arr.length;
  while (end >= 0) {
    const nextIndex = Math.floor(Math.random() * end);
    arr.push(arr[nextIndex]);
    arr.splice(nextIndex, 1);
    end -= 1;
  }
  return arr;
}

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

function shuffle(arr) {
  for (let i=arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    const item = arr[i];
    arr[i] = arr[j];
    arr[j] = item;
  }

  return arr;
}

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