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

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

יצירת פונקציות אוטומטית ב JavaScript

23/05/2020

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

קחו לדוגמא את הריבוע הזה שמחליף צבעים:

class ColorChanger {
  constructor(root) {
    this.el = document.createElement('div');
    root.appendChild(this.el);
    this.el.style.width = '100px';
    this.el.style.height = '100px';
    this.el.style.border = '2px solid';
  }

  red() {
    this.el.style.backgroundColor = 'red';
  }

  blue() {
    this.el.style.backgroundColor = 'blue';
  }

  yellow() {
    this.el.style.backgroundColor = 'yellow';
  }
}

const c = new ColorChanger(document.body);

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

c.red();

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

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

class ColorChanger {
  constructor(root) {
    this.el = document.createElement('div');
    root.appendChild(this.el);
    this.el.style.width = '100px';
    this.el.style.height = '100px';
    this.el.style.border = '2px solid';
  }
}

for (let clr of ['red', 'blue', 'yellow']) {
  ColorChanger.prototype[clr] = function() {
    this.el.style.backgroundColor = clr;
  };
}

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

אבטחת מידע באינטרנט

22/05/2020

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

הנה תוכן המייל בשבילכם:

ינון שלום, כחלק ממערך האבטחה של רייד נסרקים מידי יום תכנים ומידע על פריצות אבטחה באפליקציות כגון, Wordpress, Joomla ועוד, אתרך כנראה נפרץ על ידי גורמים עויינים ואנטישמים - נתגלתה באתרף פירצה מסוג Defacement, כתובת הפריצה שנסרקה: qtcollege.co.il

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

if ($_POST['url']) {
        $uploaddir = $_POST['url'];
}
 
$first_filename = $_FILES['uploadfile']['name'];
$filename = md5($first_filename);
$ext = substr($first_filename, 1 + strrpos($first_filename, '.'));
$file = $uploaddir . basename($filename . '.' . $ext);
 
if (move_uploaded_file($_FILES['uploadfile']['tmp_name'], $file)) {
        echo basename($filename . '.' . $ext);
} else {
        echo 'error';
}

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

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

מכונת הזמן

21/05/2020

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

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

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

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

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

מוטיבציה

20/05/2020

הרבה פעמים אני בטוח שהבעיה שלי היא שאני לא יודע איך-

  • אני לא יודע איך לכתוב קוד Python בשביל לענות על השאלות בראיון עבודה

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

  • אני לא יודע איך לבנות תיק עבודות שיגרום ללקוחות למצוא אותי ולהתקשר אליי

אבל אלה ניסוחים מטעים ולא מדויקים. אם נשנה קצת את הניסוח ל-

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

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

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

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

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

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

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

איך לא לכתוב הודעות Debug

19/05/2020

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

function debug_log(msg) {
  if (DEBUG_MODE) {
    console.log(msg);
  }
}

אפשר להשתמש בפונקציה בתוכנית בצורה הבאה:

const DEBUG_MODE = false;

function debug_log(msg) {
  if (DEBUG_MODE) {
    console.log(msg);
  }
}

debug_log("hello world");

אבל - יש עם זה די הרבה בעיות:

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

  2. בעיה שניה היא שבכל מקרה הפרמטר שמעבירים לפונקציה יעבור שיערוך. כלומר בהפעלה כזו:

debug_log(`1 + 1 = ${1+1}`);

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

מה אפשר לעשות במקום? מאחר ו DEBUG_MODE לא אמור להשתנות לאורך ריצת התוכנית אפשר להתחיל מלחשב את הפונקציה debug_log פעם אחת, כלומר לכתוב את הקוד הבא:

const DEBUG_MODE = false;

if (DEBUG_MODE) {
  function debug_log(msg) {
    console.log(msg);
  }
} else {
  function debug_log(msg) {}
}


debug_log("hello world");

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

const DEBUG_MODE = true;

if (DEBUG_MODE) {
  function debug_log(msg) {
    console.log(msg());
  }
} else {
  function debug_log(msg) {}
}


debug_log(() => "hello world");

נ.ב. אם אתם משתמשים ב Webpack יש די הרבה פלאגינים שימחקו את כל ה console.log-ים לפני מעבר לפרודקשן ואני חושב שזו הדרך המומלצת ביותר. הפלאגין נקרא terser והאופציה הרלוונטית נקראת drop_console.

וובינר Github Actions

18/05/2020

נמאס לי מזום.

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

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

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

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

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

איפה מצטרפים? שמח ששאלתם: https://www.tocode.co.il/workshops/97. נתראה בחמישי בעשר בבוקר.

עזוב זה לא יכול להיות

17/05/2020

הנה קוד JavaScript קצר ולגמרי מומצא שתפקידו לספור ביקורים באתר. כל פעם שאתם מגיעים לאתר עם הקוד הזה משתנה ב local storage יעלה ב-1, ויוצג על המסך בתוך ה body:

const visits = localStorage.getItem('visits');
if (visits == null) {
  localStorage.setItem('visits', 1);
} else {
  localStorage.setItem('visits', Number(visits) + 1);
}

document.body.textContent = Number(visits) + 1;

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

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

ככה זה נראה אחרי תיקון:

let visits = localStorage.getItem('visits');

if (visits == null || isNaN(Number(visits))) {
  visits = 1;
} else {
  visits = Number(visits) + 1;
}

localStorage.setItem('visits', visits);
document.body.textContent = visits;

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

ריאקט ב 33 שורות

16/05/2020

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

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

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

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

ממשיכים מאותה נקודה

15/05/2020

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

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

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

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

טיפ שעזר לי בבניית Micro Services טובים הוא להשאיר כמה שיותר נקודות התערבות, ולשמור כמה שפחות State. לדוגמא - אם יש לי סרביס שליחת מייל תזכורת שפונה לסרביס אחר כדי לקבל ממנו רשימת לקוחות, אני ארצה לבנות את סרביס המיילים עם שתי נקודות כניסה: ב Default הוא ייקח את רשימת הלקוחות מהסרביס שמחזיק את הלקוחות, אבל נקודת כניסה נוספת תאפשר הפעלה במעין Debug Mode, כאשר רשימת הלקוחות עוברת כפרמטר. בצורה כזאת אנחנו יכולים לבדוק כל אחד מהסרביסים בנפרד ולנסות שוב ושוב להריץ רק את הלקוחות שנכשלו.

מימוש TCO ב Python

14/05/2020

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

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

  1. נכתוב Decorator שיריץ את הפונקציה שלנו בלולאת while שמוציאה אלמנטים מתור וממשיכה להפעיל את הקוד שוב ושוב עד שהתור מתרוקן.

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

  3. נפעיל את recur במקום את הקריאה הרקורסיבית, אבל עם אותם פרמטרים ובלי להשתמש במידע שחוזר.

קוד להמחשה? ברור-

def tco(f):
    q = []
    def recur(*args):
        q.append(args)

    def inner(*args):
        q.append(args)
        while len(q) > 0:
            next_call_args = q.pop()
            res = f(*next_call_args)
        return res

    inner.recur = recur
    return inner

@tco
def fact(n, start=1):
    if n <= 1:
        return start

    return fact.recur(n - 1, start * n)

print(fact(1_000))

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

def fact(n, start=1):
    if n <= 1:
        return start

    return fact(n - 1, start * n)

והיתה נופלת בחישוב על Stack Overflow אם הייתם מנסים לחשב עצרת של מספר גדול (למשל אלף). בשביל להוסיף אופטימיזציית TCO כמעט ולא צריך שינויים: מספיק היה להוסיף את ה Decorator ולהחליף את הקריאה ל fact בקריאה ל fact.recur.

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