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

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

למה אני לא מתלהב מ React Forget

13/03/2024

אם עדיין לא שמעתם ככל הנראה ריאקט 19 תשוחרר עם תמיכה ב React Forget הקומפיילר החדש מבית ריאקט שאמור להוסיף useMemo אוטומטית לתוכניות שלנו. הנימוקים שלהם לפיתוח הפיצ׳ר (מתוך הפוסט) היו-

  1. כתיבת useMemo ידנית מלכלכת את הקוד.

  2. קל לטעות כשצריך להחליט ידנית למה לעשות Memoization.

  3. תוספת הקוד מעמיסה על התחזוקה.

הנה משפט המפתח מתוך הפוסט-

Our vision is for React to automatically re-render just the right parts of the UI when state changes, without compromising on React’s core mental model.

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

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

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

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

  3. הוספת מנגנונים כמו React Forget רק מרחיקה אותנו מפיתרון הבעיות האמיתיות של ריאקט. במובן הזה סיגנלים היו יכולים להיות תוספת הרבה יותר מועילה ומעניינת.

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

מאיפה הגיעה ה Z בסוף הזמן?

12/03/2024

בשביל לקחת בסקאלה (או Java) את השעה הנוכחית ולשמור אותה כמחרוזת אוכל להשתמש בקוד הבא:

import java.time.*
import java.time.format.DateTimeFormatter

LocalTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)

והמחרוזת שתתקבל עשויה להיראות כך:

"16:51:44.748375"

אבל כשמסתכלים בבסיסי נתונים וברשת אנחנו נתקלים במחרוזות דומות שמסתיימות באות Z לדוגמה:

14:54:10.140198Z

למה הם התכוונו? מאיפה ה Z הגיע? ואיך אני גם מקבל אחד?

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

OffsetTime.now(ZoneId.of("UTC")).format(DateTimeFormatter.ISO_OFFSET_TIME)

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

scala> OffsetTime.now(ZoneId.of("Israel")).format(DateTimeFormatter.ISO_OFFSET_TIME);

val res5: String = 16:57:24.769026+02:00

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

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

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

על גיטהאב, חתימה על קומיטים ולמי אכפת

11/03/2024

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

git config --replace-all user.name elite-hacker
git config --replace-all user.email hacker@theboss.com

ואז הכניסו קומיט למערכת. יכול להיות גם ריק, והפעילו git log. מה הפרטים שאתם רואים בקומיט?

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

הדרך של גיט לאמת את הזהות שלכם בקומיט היא חתימה על קומיטים. כאן יש מדריך איך לקנפג הכל בגיטהאב: https://gist.github.com/troyfontaine/18c9146295168ee9ca2b30c00bd1b41e

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

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

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

  3. האקרים ממילא יודעים לזייף את החתימות שלנו אז בשביל מה להתאמץ.

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

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

  2. דווקא די פשוט. המדריך בלינק שהדבקתי למעלה.

  3. אז למה לעשות להם חיים קלים? שיתאמצו קצת ההאקרים.

עוד לא חותמים על קומיטים? היום הוא יום מצוין לקנפג את זה.

טיפוס של GUI

10/03/2024

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

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

את הבדיקות כותבים לפני או אחרי הקוד?

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

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

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

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

שלוש נקודות על SQL-ים מסובכים מדי

09/03/2024

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

  1. לפעמים זה שווה את המאמץ. יש שאילתות SQL שאחרי שמצליחים לכתוב אותן מבינים משהו חדש על SQL. כן יש דבר כזה Window Functions ואם צריך להשקיע עכשיו שעתיים בללמוד איך הן עובדות זה ממש שווה את ההשקעה.

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

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

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

מה עושים במקום התיעוד הלא רלוונטי

08/03/2024

אה זה מה Readme? תתעלם הוא לא מעודכן

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

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

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

קונספציות

07/03/2024

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

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

docker run -dit --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4

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

אני מודה כשראיתי את ההתנהגות הזאת לא האמנתי. הייתי בטוח שאני עושה משהו לא בסדר במקום אחר. אחרי כמה שעות הלכתי לגוגל ומצאתי שהסיפור מתועד למשל כאן: https://blog.jarrousse.org/2023/03/18/how-to-use-ufw-firewall-with-docker-containers/

וכאן: https://www.baeldung.com/linux/docker-container-published-port-ignoring-ufw-rules

וכאן: https://www.howtogeek.com/devops/how-to-use-docker-with-a-ufw-firewall/

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

index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()

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

index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()
mgmt.setConsistency(index, ConsistencyModifier.LOCK)

וגם זה מתועד יפה כשיודעים איפה לחפש: https://docs.janusgraph.org/advanced-topics/eventual-consistency/

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

היום למדתי: סטיב ווזניאק לא משעמם

06/03/2024

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

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

קוד הגרלת השמות של דוקר כולל את רשימת כל המדענים והחוקרים שהם יכולים להציע עם הסבר קצר על כל אחד ואתם יכולים גם למצוא אותו בקישור: https://github.com/moby/moby/blob/39f7b2b6d0156811d9683c6cb0743118ae516a11/pkg/namesgenerator/names-generator.go#L852-L863

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

func GetRandomName(retry int) string {
begin:
    name := left[rand.Intn(len(left))] + "_" + right[rand.Intn(len(right))] //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand)
    if name == "boring_wozniak" /* Steve Wozniak is not boring */ {
        goto begin
    }

    if retry > 0 {
        name += strconv.Itoa(rand.Intn(10)) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand)
    }
    return name
}

כמה זה באמת עולה?

05/03/2024

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

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

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

טיפ ריאקט: עדיף לוותר על טרנרי בתוך JSX

04/03/2024

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

function App() {
  const [text, setText] = useState(0);
  return (
    <div>
      <button onClick={() => setText(t => (t + 1) % 2)}>Toggle</button>
      {text == 0 ? <Text1 /> : <Text2 />}
    </div>
  )
}

אבל מהר מאוד הופך למפלצת.

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

function Text({id}) {
  if (id === 0) {
    return <Text1 />
  } else {
    return <Text2 />
  }
} 


function App() {
  const [text, setText] = useState(0);
  return (
    <div>
      <button onClick={() => setText(t => (t + 1) % 2)}>Toggle</button>
      <Text id={text} />
    </div>
  )
}

בשלב הבא אם נצטרך מנגנון יותר גנרי (למשל כי נצטרך שוב לבחור בין כמה קומפוננטות) נוכל להפוך את Text לפונקציה כללית ונקבל:

function Text1({color}) {
  return <p style={{color}}>Text 1</p>
}

function Text2({color}) {
  return <p style={{color}}>Text 2</p>
}

const Toggle = (...components) => (props) => {
  const cls = components[props.id]
  return React.createElement(cls, props);
} 

const Text = Toggle(Text1, Text2);

function App() {
  const [text, setText] = useState(0);
  return (
    <div>
      <button onClick={() => setText(t => (t + 1) % 2)}>Toggle</button>
      <Text id={text} color="red" />
    </div>
  )
}