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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  3. פחד - האם תהיה לי עבודה בטכנולוגיה החדשה? האם אי פעם אצליח להיות טוב ב"למידת מכונה" כמו שאני טוב ב Java? האם Swift זה לא עוד טרנד חולף שעוד שנתיים יעלם? עדיף כבר להמשיך במה שאני עושה היום.

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

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

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

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

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

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

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

private String[] userAgents = new String[] {
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", // 13.5%
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", // 6.6%
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0", // 6.4%
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", // 6.2%
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36", // 5.2%
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36" // 4.8%
};

connection.setRequestProperty("User-Agent", userAgents[(int) Math.round(Math.random() * (userAgents.length - 1))]);

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

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


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

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

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

אחת הסיבות שדברים נשברים בלי אזהרה מוקדמת היא יצירתיות של כותבי פריימוורקס (שלרוב לא מפורסמת בדף הראשי של הפריימוורק). הדוגמה היום היא מ Jest, ושימו לב ל Issue הבא: https://github.com/facebook/jest/issues/5818

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

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

בחזרה לג'סט ול Issue שבקישור - מסתבר שג'סט יריץ את הבדיקות שלכם במקביל רק אם הוא חושב שיש מספיק בדיקות בשביל זה ושהבדיקות מספיק איטיות בשביל שבאמת תרוויחו משהו מהרצה מקבילית. הוא גם לא יריץ במקביל בדיקות שכתובות באותו קובץ בדיקה.

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

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

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

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

רוב הזמן יישומי Front End לא עושים עבודה חישובית יותר מדי קשה, ולכן את רוב שיפורי הביצועים בריאקט אפשר לפתור עם צמצום קריאות מהשרת או צמצום render-ים. אבל מדי פעם כן יש לנו קומפוננטה שצריכה לעשות עבודה חישובית, וכשזה קורה כדאי לדעת מה לעשות - והתשובה הפשוטה היא Web Worker.

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

import { useState } from 'react';

function isPrime(n) {
  for (let i=2; i < n/2; i++) {
    if (n % i === 0) {
      return false;
    }
  }
  return true;
}

function calculatePrimesUntilMillion() {
  let count = 0;
  console.log('Start');

  for (let i=2; i < 1000000; i++) {
    if (isPrime(i)) {
      count += 1;
    }
  }
  console.log('Ready');
  return count;
}

function App() {
  const [_, forceRender] = useState(0);

  return (
    <div className="App">
      <p>There are {calculatePrimesUntilMillion()} prime numbers &lt; 1,000,000</p>
      <button onClick={() => forceRender(v => !v)}>Calculate again</button>
    </div>
  );
}

export default App;

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

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

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

אני משתמש ב create-react-app בגירסה החדשה ביותר והוא משתמש ב webpack 5 ושם התמיכה מ Web Worker היא מובנית. בשביל להפוך את הפרויקט שלי להשתמש ב Web Worker אני צריך:

  1. ליצור קובץ חדש בשם primes.js עם התוכן הבא:
function isPrime(n) {
  for (let i=2; i < n/2; i++) {
    if (n % i === 0) {
      return false;
    }
  }
  return true;
}

function calculatePrimesUntilMillion() {
  let count = 0;
  console.log('Start');

  for (let i=2; i < 1000000; i++) {
    if (isPrime(i)) {
      count += 1;
      postMessage({ count });
    }
  }
  console.log('Ready');
  return count;
}

onmessage = function(_ev) {
  const count = calculatePrimesUntilMillion();
  postMessage({ count });
}

  1. לעדכן את קוד הקומפוננטה כדי לטעון את ה Worker:
const worker = new Worker(new URL('./primes.js', import.meta.url));

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

function App() {
  const [forceRender, setForceRender] = useState(0);
  const [primesCount, setPrimesCount] = useState(0);

  useEffect(() => {
    worker.onmessage = function(ev) {
      setPrimesCount(ev.data.count);
    };

    worker.postMessage({});
  }, [forceRender]);

  return (
    <div className="App">
      <p>There are {primesCount} prime numbers &lt; 1,000,000</p>
      <button onClick={() => setForceRender(v => !v)}>Calculate again</button>
    </div>
  );
}

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

אם אתם עובדים בגירסאות ישנות יותר של create-react-app או וובפאק, שווה לבדוק את הספריה https://github.com/developit/workerize-loader שמאפשרת לטעון Web Worker גם בוובפאק 4.

כשאנחנו מקשיבים למה שקורה בתעשיה או מסתכלים ברשת על כל מיני Best Practices אנחנו עלולים לטעות ולחשוב ש-

  1. כולם כותבים Micro Services, מונוליט זה מיושן.

  2. אצל כולם יש בדיקות (יחידה + קצה לקצה).

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

  4. לכולם יש גיבויים מתוקתקים ופעם בחודש הם עושים תרגיל שחזור מגיבוי.

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

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

  7. כולם מחזיקים תיעוד עדכני של המערכת כך שמתכנתים חדשים יכולים בקלות להיכנס לקוד.

  8. כולם מוחקים קוד שלא משתמשים בו.

  9. כולם שולטים בטכנולוגיות בהן הם עובדים.

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

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

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

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

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

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

  4. זה מעניין אותי - כי כשאני מתעניין במשהו ההשקעה נראית הרבה יותר קלה.

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

  6. כי אנשים כמוני עושים דברים כאלה - כשאני מצפה מעצמי להתנהגות מסוימת שמתאימה לדימוי העצמי שלי.

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