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

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

בדיקות יחידה טובות

09/12/2022

אלה כמה קריטריונים שאתם יכולים לאמץ כדי לראות אם בדיקות היחידה שלכם עובדות בשבילכם, או אתם בשבילן-

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

  2. הבדיקה נכשלת כשאתם שוברים משהו, וכשהיא נכשלת אתם מבינים בדיוק מה שברתם.

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

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

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

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

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

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

היום למדתי: סימון פונקציה בתור Deprecated ב TypeScript

08/12/2022

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

עם טייפסקריפט ו VS Code סימון כזה הוא יחסית פשוט:

  1. אנחנו מוסיפים את המילה @deprecated בהערת JSDoc מעל הגדרת הפונקציה אותה רוצים לסמן בתור Deprecated.

  2. באופן אוטומטי כש VS Code ימצא שהשתמשתי באותה פונקציה הוא יסמן קו באמצע של הקוד שמפעיל את הפונקציה (strike through) וכשאני אעבור על הסימון עם העכבר אני אוכל לראות את כל ההערה, שם בדרך כלל תהיה המלצה במה להשתמש במקום.

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

/**
 * @deprecated The method should not be used. Use `bar` instead.
 */
function foo(x: number) {
    return x + x;
}

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

const x = foo(10);
const y = bar(20);

ואם אין לכם פרויקט טייפסקריפט זמין אפשר לראות את האפקט גם לייב ב TypeScript Playground בקישור הזה.

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

07/12/2022

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

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

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

טיפ ריסלקט - שימו לב כשאתם מפעילים filter או map בתוך Selector

06/12/2022

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

import { createSelector } from "@reduxjs/toolkit";

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

  1. בתוך קוד קומפוננטה אני קורא ל useSelector כדי "לחבר" בין קוד הקומפוננטה לנתיב מסוים באוביקט ה State ב Redux.

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

const todos = useSelector((state: AppState) => state.todos);

כדי לגשת לכל מערך ה todos שיש בסטייט.

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

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

const todosCount = useSelector((state: AppState) => state.todos.length);

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

  1. הספריה Reselect מאפשרת לנו לתאר את הקשר בין Selector-ים שונים, וכך לחסוך חישובים מיותרים. סלקטור יחושב מחדש רק אם חלק מהתלויות שלו השתנו.

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

import { createSelector } from "@reduxjs/toolkit";
import { AppState } from "./store";

const todos = (state: AppState) => state.todos;

export const todosThatStartWithA = createSelector(todos, (todos) =>
  todos.filter((t) => t.message.startsWith("a"))
);

export const finishedTodosThatStartWithA = createSelector(
  todosThatStartWithA,
  (todos) => todos.filter((t) => t.completed)
);

export const numberOfFinishedTodosThatStartWithA = createSelector(
  finishedTodosThatStartWithA,
  (finishedTodosThatStartWithA) => {
    console.log(`Recalculating the length`);
    return finishedTodosThatStartWithA.length;
  }
);

יש לי כל מיני שאלות שקשורות ל todos, למשל:

  1. מי ה todos במערך שההודעה שלהם מתחילה ב a ?

  2. מי ה todos במערך שההודעה שלהם מתחילה ב a, והם מסומנים בתור completed ?

  3. כמה todos יש במערך שגם ההודעה שלהם מתחילה ב a וגם מסומנים בתור completed ?

השימוש ב createSelector מספק דרך נוחה לשלב כמה Selectors, וגם לחשב מחדש כל Selector רק כשהתלויות שלו באמת משתנות. אבל בואו לא נתבלבל, אפילו Reselect אינו קוסם.

למרות שהכתיב נותן הרגשה כאילו ה Selectors תלויים אחד בשני, בפועל בגלל ששני ה Selectors שבאמצע משתמשים ב filter (ולכן מייצרים מערך חדש), מצב התלויות הוא שכל ה Selectors תלויים ב state.todos. ברגע שמערך ה todos משתנה מכל סיבה שהיא, כל ה Selectors יחושבו מחדש, כי todosThatStartWithA ו finishedTodosThatStartWithA תמיד מחזירים מערך חדש. אם נריץ את הקוד נגלה שכל שינוי ב todos, גם אם ה todos שהשתנו לא מתחילים ב a, עדיין גרם לחישוב מחדש של ה Selector האחרון.

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

הצצה לעולם פוסט GPT3

05/12/2022

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

Implement a program that reads a file and writes its lines backwards

וקיבלתי את הקוד הבא בפייתון:

import sys

def main():
    if len(sys.argv) != 2:
        print("Usage: python reverse.py <file>")
        sys.exit(1)

    file = sys.argv[1]
    with open(file, 'r') as f:
        lines = f.readlines()
        lines.reverse()
        for line in lines:
            print(line, end='')

if __name__ == "__main__":
    main()

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

אז מה, להחליף מקצוע? נדמה לי שהמסקנה היא בדיוק הפוכה:

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

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

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

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

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

create a unix pipeline to sum all the numbers read from stdin

הוא הציע שני רעיונות, כל אחד עם הבעיות שלו:

# Option 1
cat | tr ' ' '\n' | awk '{ sum += $1 } END { print sum }'

# Option 2
cat | paste -sd+ | bc

בראשון ה cut וה tr מיותרים; השני לא עובד על מק ואיתו כנראה על עוד כמה וריאנטים של יוניקס כי paste אצלנו לא קוראת מ STDIN.

ולמרות הכל הסיפור הזה מרשים ומרגש. אם עוד לא קשקשתם עם GPT3 לכו להירשם עכשיו. טירוף מה שהולך שם.

ושוב awk הציל את היום

04/12/2022

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

המשך קריאה

איך קוד מתקלקל (דוגמת טייפסקריפט + ריאקט)

03/12/2022

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

type PinkProps = {
    page?: number;
    itemsPerPage?: number;
    items: Array<{ id: number, text: string }>;
};

function Pink({ page=1, itemsPerPage=25, items }: PinkProps) {
    // component implementation ...
}

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

type OrangeProps = Pick<PinkProps, "page"|"itemsPerPage"> & {
    items: Array<{ id: number, visible: boolean }>
};

function Orange({ page=1, itemsPerPage=25, items}: OrangeProps) {
    // component implementation ...
}

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

function isValid(item: OrangeProps['items'][number]) {
    // validate the item from orange props
}

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

type PinkProps = {
    page?: number;
    itemsPerPage?: number;
    items: Array<{ id: number, text: string }>;
};

type OrangeProps = Pick<PinkProps, "page"|"itemsPerPage"> & {
    items: Array<{ id: number, visible: boolean }>
};

function isValid(item: OrangeProps['items'][number]) {
    // validate the item from orange props
}

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

type Paginated = {
    page?: number;
    itemsPerPage?: number;
}

type TextItem = {
    id: number;
    text: string;
}

type ToggleItem = {
    id: number;
    visible: boolean;
}

type PinkProps = Paginated & {
    items: Array<TextItem>;
};

type OrangeProps = Paginated & {
    items: Array<ToggleItem>;
}

function isValid(item: ToggleItem) {
    // validate the item from orange props
}

function Pink({ page=1, itemsPerPage=25, items }: PinkProps) {}


function Orange({ page=1, itemsPerPage=25, items}: OrangeProps) {}

ארוך יותר, כן, אבל הרבה יותר ברור וקל להרחבה.

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

פיתרון 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 בין עדכונים מהשרת.

המשך קריאה