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

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

אבל זה רק פסיק קטן

24/07/2020

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

// Component 1
function App() {
  const [value, setValue] = React.useState(_.random(1, 100));

  return (
    <div>
      <button onClick={() => setValue(c => c + 1)}>{value}</button>
    </div>
  );
}
// Component 2
function App() {
  const [value, setValue] = React.useState(() => _.random(1, 100));

  return (
    <div>
      <button onClick={() => setValue(c => c + 1)}>{value}</button>
    </div>
  );
}

נראה ממש דומה לא?

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

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

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

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

צריכה להיות דרך ברורה אחת (ועדיף שתהיה היחידה) לעשות את זה

23/07/2020

המשפט בכותרת הוא תרגום של המשפט הבא מתוך ה Zen של פייתון. זה המקור:

There should be one-- and preferably only one --obvious way to do it.

והוא נכון לכל השפות. נזכרתי בו במקרה כשדפדפתי בקוד הבא, דווקא בשפת רובי:

class ShoppingCart < ActiveRecord::Base
  has_many :products, :class_name => 'CartProduct', :dependent => :delete_all

    def <<(product)
        line = CartProduct.find_or_initialize_by(:product => product, :cart => self)
        # comment out to allow quantity
        # line.increment(:qty) unless line.new_record?
        line.save!
        @finalized = false

        self
    rescue ActiveRecord::RecordNotUnique
        retry
    end

    def add_with_options(product, options)
        line = CartProduct.find_or_initialize_by(:product => product, :cart => self)
        line.price = options[:price]
        line.options = options
        line.save!
        @finalized = false

        self
    rescue ActiveRecord::RecordNotUnique
        retry
    end
end

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

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

  test 'cart price is the sum of all product prices' do
    @cart = create(:cart)
    @cart.products << create(:item, price: 20)
    @cart.products << create(:item, price: 50)

    assert_equal(70, @cart.price)
  end

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

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

תיאורטית פיתרון קל מאוד במקרה כזה יהיה פשוט שהפונקציה הראשונה תקרא לפונקציה השניה, כלומר:

    def <<(product)
        add_with_options(product, {})
    end

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

במקרה שלנו אפשר לחשוב על:

class ShoppingCart < ActiveRecord::Base
    def add_product_sold_by_partner(product, partner_price, partner_options)
    end

    def add_product_from_website(product)
    end
end

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

ריאקט, קלט מספרי ועיקרון ההפתעה הקטנה ביותר

22/07/2020

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

function App() {
  const [text, setText] = React.useState('');

  return (
    <div>
      <input type="number" value={text} onChange={e => setText(e.target.value)} />
      <p>Text: {text}</p>      
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

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

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

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

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


function App() {
  const [numericInputChanges, setNumericInputChanges] = React.useState(0);
  const [textInputChanges, setTextInputChanges] = React.useState(0);
  const [numericInput, setNumericInput] = React.useState('');
  const [textInput, setTextInput] = React.useState('');

  function reset() {
    setTextInput('');
    setNumericInput('');
    setTextInputChanges(0);
    setNumericInputChanges(0);    
  }

  function numericInputChanged(e) {
    setNumericInputChanges(v => v + 1);    
    setNumericInput(e.target.value);
  }

  function textInputChanged(e) {
    setTextInputChanges(v => v + 1);
    setTextInput(e.target.value);
  }

  return (
    <div>
      <p>Numeric Input Changes: {numericInputChanges}</p>
      <p>Text Input Changes: {textInputChanges}</p>      
      <input type="text" value={textInput} onChange={textInputChanged} />
      <input type="number" value={numericInput} onChange={numericInputChanged}  />
      <button type="reset" onClick={reset} >Reset</button>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

וכן, יש גם קודפן:

הפעם אני מציג גם Counter שעולה ב-1 כל פעם שקומפוננטת ה input מפעילה את הפונקציה שכתובה ב onChange. הנה מה שקורה שם:

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

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

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

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

עריכה - ותודה לגדי על התיקון

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

שעה כל יום

21/07/2020

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

אבל שעה כל יום זה ממש אחלה. בשעה כל יום אפשר ללמוד Data Science או Web Development או אפילו גרמנית. שעה כל יום קונה לכם עולם ומלואו.

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

שעה ביום. אל מול עולם שלם שרק רוצה שתוותרו.

מה אתם מחליטים?

שימוש ב Render Props כדי להפוך קומפוננטה לגנרית יותר

20/07/2020

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

המשך קריאה

אבל אמרתי לך...

19/07/2020

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

נו, תודה.

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

עכשיו היית מצפה שאחרי כל זה אנשים היו להוטים ללמוד על ה Box Model. נו, תמשיכו לחלום.

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

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

לראות, לכתוב ולדבג

18/07/2020

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

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

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

ואז זה לא עובד.

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

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

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

הפיצול

17/07/2020

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

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

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

https://unoptimal.files.wordpress.com/2019/11/realgraph.png?w=1424

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

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

שאלת ראיונות עבודה: Docker Compose

16/07/2020

נתון קובץ docker-compose.yml הבא:

version: '3.8'

services:
  db:
    image: postgres:latest
    environment:
      POSTGRES_PASSWORD: docker
      POSTGRES_DB: wordcount_dev
      POSTGRES_USER: docker
    volumes:
      - database_data:/var/lib/postgresql/data

  webapp:
    image: pydocker1-test:latest
    depends_on:
      - db
    ports:
      - 5000:5000

volumes:
  database_data:
    driver: local

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

ואם אתם לא בטוחים מה התשובה מוזמנים לבוא היום למפגש Docker ולגלות בעצמכם.

הזדמנויות

15/07/2020

הי כולם,

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

  1. אתם יכולים ללמוד כל נושא שבעולם מהמורים הטובים בעולם והרוב בחינם או כמעט בחינם. ויש לכם את כל היום כדי להשקיע בזה. שנה של ניסיון ב Machine Learning? קיבלת! שנה של ניסיון כמפתחת Front End? אין בעיה. ביבי משלם את החשבון.

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

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

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

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