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

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

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

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

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

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

רק בבקשה אל תעצרו ב"לא יודע". זה לא בריא לכם וזה לא בריא לקוד שלכם.

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

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

class Critter
  @@instance = Critter.new
  def self.instance
    @@instance
  end

  def val
    5
  end
end

ואכן יש לנו מחלקה Critter עם מתודה בשם instance שמחזירה תמיד את אותו אוביקט:

c = Critter.instance
d = Critter.instance

puts c == d # true

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

require 'singleton'

class Critter
  include Singleton
  def val
    5
  end
end

c = Critter.instance
d = Critter.instance

puts c.val
puts d == c

הבעיות מתחילות כשננסה להסתיר את הפונקציה new. כאן לרובי יש סוג של פיתרון באמצעות הפונקציה private_class_method. כך נראה Critter שמסתיר את פונקציית new שלו:

class Critter
  include Singleton
  def val
    5
  end
  private_class_method :new
end

ובאמת מי שינסה לקרוא עכשיו ל Critter.new יקבל את השגיאה:

NoMethodError: private method `new' called for Critter:Class

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

require 'singleton'

class Critter
  include Singleton
  def val
    5
  end
  private_class_method :new
end

c = Critter.instance
d = Critter.send(:new)

puts d.val # print 5
puts d == c # false

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

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

for fname in *.old
do
    mv "$fname" "${fname%old}new"
done

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

בקוד זה נראה כך:

glob("*.old").each do |fname|
    File.rename(fname, fname.chomp("old") + "new")
end

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

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

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

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

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

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

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

נתחיל עם מה שעובד - כל עוד אנחנו בקובץ יחיד אפשר להשתמש ב const כדי לתת שם לערך קבוע:

const MESSAGE_SIZE = 48;

ומכאן ועד סוף הקובץ לא משנה מה תעשו השם MESSAGE_SIZE תמיד יתייחס למספר הקבוע 48. אבל העסק מסתבך מהר מאוד כשאנחנו רוצים לשתף מידע בין מספר קבצים. נניח שהקובץ utils.js מגדיר הפעם את הקבוע ומייצא אותו:

// utils.js
const MESSAGE_SIZE = 48;
exports.MESSAGE_SIZE = MESSAGE_SIZE;

והקובץ app.js מייבא את הקבוע:

const { MESSAGE_SIZE } = require('./utils');
console.log(MESSAGE_SIZE);

זה עבד לא רע והדפיס 48, אבל הולך להישבר די בקלות. נסו לכתוב במקום את הקוד הבא ב app.js:

const utils = require('./utils');
utils.MESSAGE_SIZE = 999;

console.log(utils.MESSAGE_SIZE);

הקוד ידפיס 999 וגם כל קובץ אחר שיטען את הקבוע מ utils יקבל עכשיו את הערך 999. מסתבר שברגע שאנחנו עוברים להשתמש במנגנון ה exports מה שאנחנו בעצם מייצאים הוא אוביקט. המילה const לא מונעת שינויים בשדות של האוביקט וכך כל מי שרוצה יכול לשנות את ה"קבועים" שלנו.

מה אפשר לעשות? אז אומנם const לא תעזור לשמור על שדות של אוביקטים משינויים, אבל Object.freeze דווקא כן. אם נפעיל אותה לפני ה export נוכל לקבל קבועים של ממש.

החליפו את תוכן הקובץ utils.js עם הקוד הבא:

module.exports = Object.freeze({
  MESSAGE_SIZE: 48,
});

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

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

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

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

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

דוגמא? יאללה בשמחה-

המראיין: מהי בעיית N+1 בעת שליפת נתונים מ DB, ואיך היית מתמודד איתה?

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

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

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

אבל כמובן שזה משנה.

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

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

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

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

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

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

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

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

require 'set'

s = Set.new([10, 20, 30])
t = Set.new([30, 40])
puts (s | t).inspect
puts s.merge(t).inspect

בשני המקרים תוצאת ההדפסה היא:

#<Set: {10, 20, 30, 40}>

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

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

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

עכשיו אתם ועדיין ברובי- מה ההבדל בין ארבעת אלה? מה יכול להשתבש? ואיך?

arr = [10, 20, 30]

arr.inject(0) { |acc, val| acc + val }
arr.inject { |acc, val| acc + val }
arr.inject do |acc, val| acc + val; end
arr.inject(0) do |acc, val| acc + val; end