שלום אורח התחבר

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

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

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

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

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

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
  },
})

אז בכל קבצי הבדיקות שלכם תוכלו להסתמך על זה שיש describe, it ו expect ואולי עוד כמה משתנים גלובאליים. ברירת המחדל היא false ואז צריך לייבא הכל לבד עם שורה כזאת בתחילת קובץ בדיקות:

import { describe, it, expect } from 'vitest';

עד לפה אין סיבה להתרגש, אבל מסתבר שלהזרקת ה globals יש עוד אפקט והוא הרבה יותר מורגש: בעת בדיקת קומפוננטות ריאקט עם react-testing-library, הספריה תנקה את ה DOM בין בדיקה לבדיקה אם ה globals מוזרקים, אבל לא תנקה בלעדיהם.

במילים אחרות בקובץ בדיקות כזה:

import { describe, it, expect } from 'vitest';
import { screen, render } from '@testing-library/react';
import App from './App';

describe('App Changes Colors', () => {
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
});

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

הפונקציה הרלוונטית מ testing-library שמטפלת בסיפור המחיקה נקראת cleanup וזה מה שמוסבר גם בתיעוד שלה:

    Please note that this is done automatically if the testing framework you're using supports the afterEach global and it is injected to your testing environment (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.

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

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

נתבונן בקוד הבא:

function BrokenHugeList() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }
  console.log(`1 seach = ${deferredSearch}`);
  const searchResults = items.filter(i => i.includes(deferredSearch));
  console.log('2');

  return (
    <div>
      <input type="text" value={search} onChange={handleChange} />
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
}

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

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

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

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

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

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

const HugeList = React.memo(function HugeList(props) {
  const { search } = props;
  console.log(`3, search = ${search}`);
  const searchResults = items.filter(i => i.includes(search));
  console.log('4');

  return (
    <div>
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
});

function App() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }

  return (
    <>
      <input type="text" value={search} onChange={handleChange} /> 
      <HugeList search={deferredSearch} />
      <hr />
    </>
  );
}

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

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

אבל הערך האמיתי הוא בתמונה הגדולה - למה בכלל אנחנו בודקים? איזה ערך מקבלים מכל סוג בדיקות? כמה זה עולה לנו?

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

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

המשך קריאה...

השקענו כבר כל כך הרבה...

כל הלקוחות שלנו מחכים לפיצ'ר הזה...

זה ממש כמעט עובד...


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

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

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


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

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

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

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

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

המשך קריאה...

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

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

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

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

נקסט!

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

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

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

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

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

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

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

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

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

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

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

הם טועים.

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

המשך קריאה...

זה יעבוד גם מחר בבוקר?

זה יעבוד גם אחרי שידרוג גירסה?

זה יעבוד גם אחרי Refactoring?

זה יעבוד גם עם נתונים של פרודקשן?

זה יעבוד גם כשנעבור למערכת הפעלה אחרת?

זה יעבוד גם עבור משתמשים מהודו?

זה יעבוד גם אחרי שנחזור מגיבוי?

זה יעבוד גם כשיהיה עומס על המערכת?

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