Redux – ארכיטקטורת Flux, אבל כמו שצריך

אם עבדתם אפילו קצת עם הספריה הנהדרת React מבית פייסבוק, כנראה שהבנתם (או לפחות ניסו להסביר לכם) את ההבדל בין מאפייני רכיב (props) למצב שלו (state). ואכן בפוסט המצויין Thinking in React אנו מוצאים הסבר מפורט על איך להחליט איזה מידע שייך לכל אחד מהם (בגדול, כל מה שיכול להגיע מבחוץ יהיה בפרופס, וכל מה שישתנה ע"י הרכיב וגם באחריותו יהיה ב-state).

ארכיטקטורת Flux, שהתפרסמה גם היא ע"י פייסבוק ביחד עם React, לוקחת את הקונספט של MVC צעד אחד קדימה, ע"י הגדרת מקומות המרכזים state גלובאלי, אלו הם ה-Stores. כדי לשמור על זרימת נתונים ברורה וצפויה, שינויים למידע ב-stores לא מתבצעים ישירות, אלא ע"י יצירת אירועים הנקראים פעולות (Actions), עליהם מאזינים ה-stores ומבצעים שינויים במידע בהתאם. ה-Actions נוצרים לרוב בעקבות פעולת משתמש, לדוגמה לחיצה על כפתור. על כל אלו מנצח ה-Dispatcher שדואג שהפעולות ישוגרו בסדר הגיוני ולא בו זמנית. רכיבי ה-React עצמם מאזינים לשינויים ב-stores, ומעדכנים את ה-state של עצמם בהתאם.

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

חשוב לציין ש-Flux עצמה היא רק design pattern, ולא תשתית קוד. למעשה הקוד היחיד המסופק ע"י פייסבוק הוא ה-Dispatcher, כל השאר הן רק מוסכמות. ולמרות ש-Flux חדשנית ומשתלבת יפה עם React, היא עדיין מרגישה מעט בוסרית ולא חלקה כמו שהיינו מצפים. לדוגמה, בכל רכיב (Component) שצריך גישה למידע ב-stores, אנחנו צריכים להירשם לקבלת עדכונים, ולעדכן את ה-state בכל שינוי:

function getTodoState() {
    return { allTodos: TodoStore.getAll() };
}

var TodoApp = React.createClass({
    getInitialState: function() {
        return getTodoState();
    },
    componentDidMount: function() {
        TodoStore.addChangeListener(this._onChange);
    },
    componentWillUnmount: function() {
        TodoStore.removeChangeListener(this._onChange);
    },
    _onChange: function() {
        this.setState(getTodoState());
    }
};

כמה boilerplate! וזה עוד לפני שהתחלנו לממש את render… הדוגמה הזאת (שהיא אגב, דוגמה רשמית של Flux) גם מעלה שאלות שהתשובות עליהן לא פשוטות בכלל:

  1. מה עם רכיבים ילדים? האם לחלחל אליהם את ה-state ידנית או שגם הם יאזינו ל-store?
  2. מה קורה אם השינוי ב-store לא היה רלוונטי לרכיב? האם להתעלם ולרנדר? אולי לבדוק את השינוי? אולי PureRenderMixin?
  3. מה קורה אם יש שני stores? צריך להירשם לשניהם? או אולי לאחד אותם? ומה אם הם תלויים אחד בשני?
  4. איך בוחרים איזה מידע ישב ב-store ואיזה ב-state של רכיבי ה-React עצמם?

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

Redux להצלה

אז למרות ש-Flux הוא לא הדבר הכי נוח בעולם, העיקרון של state יחיד גלובאלי המשיך להתפתח בקהילת הקוד הפתוח, ובמהרה החלו לצוץ חלופות רבות כגון: ReFlux, Fluxible, Flummox, Fluxette (נשבע לכם שאני לא ממציא) ו-Alt. אך נראה שהמנצחת הגדולה בהתקוטטות הזאת היא ספריית Redux מאת Dan Abramov (נשמע ישראלי, אבל הוא לא).

הספרייה הזאת (וגם כמה אחרות) לוקחת כמה צעדים קדימה את העיקרון של Flux:

  1. ה-store הוא יחיד, והוא מפוצל לשני חלקים: אובייקט state יחיד לכל האפליקציה, ופונקצייה טהורה אחת בשם Reduce המקבלת את אובייקט ה-state הנוכחי ו-action כלשהו, ומחזירה state חדש.
  2. ה-state היחיד הוא היררכי, וניתן להרכיב (לשלב) Reducers שונים כך שכל אחד יטפל בחלק אחר של ה-state. כך ניתן לפתח בנפרד חלקים שונים של האפליקציה.
  3. רכיבי React לא צריכים להכיל state בכלל. כל המידע שלהם צריך להיות מסופק בתור props מרכיב האבא ו/או מה-state הראשי.

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

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

  1. פונקציית render, כמו שאנחנו כבר מכירים, שהיא לרוב פונקצייה טהורה, כלומר דטרמיניסטית ותלוייה אך ורק בקלט שלה ולא בשום מידע חיצוני.
  2. פונקציית mapStateToProps, שכשמה כן היא, מקבלת את ה-state הגלובאלי (ולעתים גם props מרכיב האבא) ומייצרת את ה-props הסופיים של הרכיב.
  3. אובייקט או פונקציה בשם mapDispatchToProps הממפה בין props של הרכיב ל-Action Creators, שהן פונקציות המחזירות אובייקט action אותו יש להעביר ל-reducer ובכך לשנות את ה-state.

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

ולמה זה טוב יותר?

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

יתרון נוסף של Redux הוא נוחות כתיבת בדיקות. מכיוון שה-Reducers הן פונקציות טהורות, הן צפויות לחלוטין ולכן קל מאוד לכתוב להן בדיקות יחידה המקבלות state ו-action, ומוודאות שה-state החדש הוא מה שציפינו. באופן זה אנו מפרידים לחלוטין את כל בדיקות ה-business logic מבדיקות ה-UI.

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

אוקיי, אני רוצה Redux, איך מתחילים?

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

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

רוצים ללמוד עוד? בבקשה:

לקריאה נוספת

יצירת תמונה מאימוג'י בפחות מ-50 שורות קוד

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

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

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

emoticon

התוכנית תייצר את התמונה הבאה (לחצו להגדלה):

emoticon.jpg.out

החומרים הדרושים

את הקוד נכתוב בשפת python, ונשתמש בספריות pillow (עיבוד תמונה) ו-numpy (מתמטיקה), וכמו כן בספרייה המובנית sys:

from PIL import Image
import numpy as np
import sys

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

emoticons3

שלב ראשון – טעינת האייקונים

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

ICON_SIZE = 20

ולאחר מכן נטען את קובץ התמונה ע"י שימוש בפונקציה Image.open של ספריית pillow.

emo_image = Image.open("emoticons.png")

לפני שנוכל לפרק את התמונה לאייקונים, אנחנו צריכים לברר כמה אייקונים יש בכל שורה ובכל עמודה, נעשה זאת ע"י שימוש במאפיין size של התמונה, שהוא tuple המכיל את רוחב התמונה וגובה התמונה, ונחלק אותו ב-ICON_SIZE (לדוגמה אם בשורה יש 80 פיקסלים ורוחב כל אייקון הוא 20 פיקסלים, אז מספר האייקונים הוא 4):

x_size, y_size = icons_image.size
x_icons = x_size / ICON_SIZE
y_icons = y_size / ICON_SIZE

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

icons = []
for row in xrange(y_icons):
    for col in xrange(x_icons):
        icons.append(crop_icon(icons_image, row, col))

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

icons = [
    crop_icon(icons_image, row, col)
    for col in range(x_icons) for row in range(y_icons)]

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

def crop_icon(img, row, col):
    return img.crop((
        col * ICON_SIZE,
        row * ICON_SIZE,
        (col + 1) * ICON_SIZE,
        (row + 1) * ICON_SIZE))

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

emoji1
שימו לב שמתחילים לספור מ-0

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

שלב שני – טעינת התמונה המקורית

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

img_filename = sys.argv[1]
img = Image.open(img_filename).convert('RGBA')
x_size, y_size = img.size

בדוגמה זו אנו מקבלים את שם התמונה משורת הפקודה ע"י שימוש ב-sys.argv, כמובן שניתן גם להשתמש בשם קבוע:

img_filename = "my_image.png"

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

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

x_size -= (x_size % ICON_SIZE)
y_size -= (y_size % ICON_SIZE)

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

שלב שלישי – מציאת האימוג'י המתאים ביותר

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

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

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

  1. מרחק מבוסס צבעים
  2. מרחק מבוסס טקסטורה
  3. מציאת וספירה של נקודות דומות (descriptors) בשתי התמונות
  4. מרחק אוקלידי פשוט (לתמונות חד מימדיות, למשל בגווני אפור בלבד)
  5. מרחק ספציפי לתחום מסויים (למשל פרצופים)
  6. שילוב של השיטות הנ"ל

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

על היסטוגרמות צבעים

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

אדום - 10 פיקסלים
כחול - 20 פיקסלים
ירוק - 7  פיקסלים

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

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

ב-python, כדי לחשב היסטוגרמה תלת-מימדית של הצבעים בתמונה נמיר אותה תחילה למערך ע"י הפונקציה getdata, ולאחר מכן נשתמש בפונקציה histogramdd של numpy:

def img_hist(img):
    arr = np.array(img.getdata(), np.uint8)
    return np.histogramdd(arr[:,:-1], bins = 6, range = [[0, 256]] * 3, weights = arr[:,3])[0]

הפרמטרים לפונקציה histogramdd הם (לפי הסדר):

  1. מערך דו מימדי עליו נרצה לחשב היסטוגרמה. המערך צריך להיות בגודל N*D כאשר N הוא מספר הנקודות בתמונה, ו-D הוא מספר המימדים (במקרה שלנו – 3). שימו לב שאנו מבצעים חיתוך של המערך, זאת כי המערך שהתקבל מ-getdata הוא בגודל N*4 (אדום, כחול, ירוק, ואלפא – פרמטר ה"שקיפות"). את אלפא אנחנו לא רוצים בהיסטוגרמה, כי הוא לא בדיוק חלק מהצבע, אבל נשתמש בו בהמשך.
  2. bins = 6 הוא מספר התאים שיהיו בהיסטוגרמה, לכל מימד. כלומר בהיסטוגרמה שלנו יהיו 6 בשלישית תאים, שהם 216 תאים המייצגים 216 טווחי צבעים. את הפרמטר בחרתי בצורה מושכלת – ערך נמוך מדי יתן תוצאות באיכות נמוכה, מכיוון שהוא יסווג צבעים שונים יחד. ערך גבוה מדי יצריך זמן ריצה גבוה יותר, ולא ישפר משמעותית את התוצאה. ניתן לשחק עם הפרמטר הזה ולראות מה קורה.
  3. range – טווח הצבעים המלא של התוצאה. חובה להגדיר פרמטר זה כדי להבטיח שכל ההיסטוגרמות תהיינה על אותם טווחים, אחרת לא תהיה משמעות להשוואה ביניהן.
  4. weights – זוכרים את הפרמטר אלפא? זהו בית אחד לכל פיקסל הקובע את רמת השקיפות שלו. פיקסל בעל פרמטר אלפא של 0 יהיה שקוף לגמרי ולמעשה אין משמעות לצבע שלו. מנגד, פיקסל עם אלפא 255 יהיה אטום לגמרי. כדי שלא נתחשב בפיקסלים שקופים בהיסטוגרמה שלנו, נעביר את מערך האלפא בפרמטר weights, הקובע את "המשקל" של כל פיקסל בהיסטוגרמה. כך ככל שפיקסל שקוף יותר, הוא ישפיע פחות על התוצאה הסופית, ופיקסלים שקופים לגמרי לא ייספרו כלל.

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

מרחק בין היסטוגרמות

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

def hist_distance(hist1, hist2):
    return np.linalg.norm(hist1 - hist2)

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

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

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

icon = min(icons, key = lambda icon: img_distance(icon, region))

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

icon_hists = map(img_hist, icons)

ופעם אחת עבור כל חלק בתמונה:

region_hist = img_hist(region)

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

icon = min(
    enumerate(icons),
    key = lambda icon: hist_distance(icon_hists[icon[0]], region_hist))[1]

שלב רביעי ואחרון – הרכבת התמונה המלאה

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

new_img = Image.new("RGB", (x_size, y_size), "white")

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

new_img = Image.new("RGB", (x_size, y_size), "white")
for row in range(y_size / ICON_SIZE):
    for col in range(x_size / ICON_SIZE):
        region_hist = img_hist(crop_icon(img, row, col))
        icon = min(
            enumerate(icons),
            key = lambda icon: hist_distance(icon_hists[icon[0]], region_hist))[1]
     new_img.paste(icon, (col * ICON_SIZE, row * ICON_SIZE))

שימו לב לשימוש בפונקציה paste של התמונה, המקבלת חלק תמונה ומיקום, ו"מדביקה" את חלק התמונה במיקום המתאים בתמונה החדשה. כדי לשמר את הרקע הלבן בתמונה החדשה, ניתן לשנות את השורה האחרונה באופן הבא:

new_img.paste(icon, (col * ICON_SIZE, row * ICON_SIZE),
              mask = icon.split()[3])

כעת כל שנשאר הוא לשמור את התמונה החדשה:

new_img.save(img_filename + '.out.png')

וניתן אף להציג אותה:

new_img.show()

I, Code emoji

מה הלאה?

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

  1. שימוש בפונקציית מרחק שונה למציאת אייקונים
  2. שימוש באייקונים בגדלים שונים / לא ריבועיים
  3. הוספת progress bar למעקב אחרי התקדמות הקוד
  4. שיפור זמן הריצה של הקוד, למשל ע"י ביצוע cache להיסטוגרמות של חלקי תמונה זהים
  5. קבלת כל הפרמטרים משורת הפקודה, או לחלופין:
  6. עטיפת הקוד באתר אינטרנט פשוט כדי שיהיה נוח לשימוש

הקוד המלא

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

איך לתחזק עשרות אלפי שורות קוד ולהישאר פרודקטיביים

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

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

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

כתבו קוד קריא

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

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

לדוגמה, קטע הקוד הבא הוא קשה מאוד לתחזוקה:

temp = datetime.now()
r = []
for x in lst:
    if temp - x.d > timedelta(10):
        continue
    r.append(x)

לעומת קטע הקוד הבא, שהתנהגותו דומה:

RECORD_TTL = timedelta(days = 10)

def discard_old_records(record_list):
    result = []
    now = datetime.now()
    for record in record_list:
        if now - record.date > RECORD_TTL:
            continue
        result.append(record)
    return result

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

העדיפו קוד קריא על הערות

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

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

לדוגמה, ההערה הבאה מיותרת לחלוטין:

# Return output data formatted as JSON
return json.dumps(output_data)

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

הנה דוגמה להערה מועילה:

# Due to a bug in service X (http://...) we should retry on a 500 error
if status_code == 500:
    raise TemporaryError

כסו את הקוד בבדיקות

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

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

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

  1. לדאוג שלפחות 70% משורות הקוד מכוסות על ידי הבדיקות.
  2. לבדוק בעדיפות עליונה חלקים חיוניים ורגישים בקוד (למשל קוד אבטחה ואימות הרשאות, שליחת מיילים ללקוחות, flow ראשי של התוכנית וכד').
  3. לבדוק בעדיפות עליונה קוד תשתיתי, עליו נסמכים חלקים אחרים בתוכנית.

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

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

השתמשו בכלי ניתוח קוד סטאטי

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

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

ניתוח קוד סטאטי חשוב אף יותר בשפות סקריפט כגון python, Javascript, PHP, Ruby, בהן אין מהדר המאתר שגיאות בזמן בניית התוכנית, ושגיאות מתגלות לרוב בזמן ריצה.

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

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

תעדו שינויים ב-source control

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

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

הנה דוגמה להודעת commit גרועה:

changing files

והנה דוגמה להודעות commit שימושית:

bugfix: handling sporadic failures on service X

החיו קוד מת

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

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

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

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

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

פרוטוקול HTTP – מדריך למתחילים

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

פרוטוקול HTTP – מה זה ולמה צריך את זה?

Hypertext transfer protocol, ובקיצור HTTP, הינו פרוטוקול שרת-לקוח מבוסס טקסט. במילים פשוטות, HTTP מגדיר שפת טקסט פשוטה המאפשרת לצרכן שירות כלשהו – הלקוח – לבקש משאבים מנותן שירות כלשהו – השרת.

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

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

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

מבנה בקשת HTTP

השלב הראשון בשיחת HTTP הוא שליחת בקשה מהלקוח לשרת. הבקשה נשלחת ע"ג חיבור TCP שנפתח בין הלקוח לשרת, ובפרט לפורט עליו מאזין השרת. באינטרנט יהיה זה בד"כ פורט 80, אך ניתן להשתמש בכל פורט.

פרוטוקול HTTP - בקשה
שליחת בקשת HTTP מהלקוח לשרת

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

  • שורת הפתיחה
  • שורות כותרת (headers)
  • גוף הבקשה (לא חובה)

שורת הפתיחה

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

  • שיטת הבקשה (method) היא מילה אחת המתארת מה מבקש הלקוח מהשרת לעשות. בפרוטוקול HTTP 1.1 קיימות 8 שיטות, שהנפוצות ביניהן הן GET המבקשת לקבל משהו מהשרת, ו-POST השולחת מידע לשרת. מבחינת הפרוטוקול עצמו, ההבדל בין השיטות הוא כמעט סמנטי בלבד (אין כמעט הבדל בין GET ל-POST), אך בפועל השרתים עצמם הם אלו שיוצרים את ההבדל כאשר הם מתייחסים בצורה שונה לגמרי לכל שיטה (GET מחזיר מידע, POST מעלה מידע).
  • כתובת המשאב המבוקש המציינת לשרת לאיזה אובייקט נרצה לגשת. הכתובת יכולה להיות אבסולוטית, לדוגמה http://icode.co.il/file.html או יחסית: /file1
  • גרסת הפרוטוקול לפיה נבנתה הבקשה, לרוב HTTP 1.1

הנה דוגמה לשורת פתיחה מלאה:

GET /index.html HTTP/1.1

כדי להמחיש כמה פשוט לקרוא פרוטוקול HTTP בקוד, להלן דוגמה לשורת פייתון המחלצת את שורת הפתיחה בבקשת HTTP, ומפרקת אותה (בהנחה שמבנה הבקשה תקין) למרכיביה:

method, url, version = HTTP_REQUEST.split('\r\n')[0].strip().split(' ')

שורות הכותרת

החלק השני בכל בקשת HTTP הוא אוסף של שורות כותרת (header lines) המכילות מידע נוסף על הבקשה (metadata) הדרוש לשרת כדי להשלים אותה. כל כותרת מורכבת משם וערך המופרדים בנקודותיים ורווח. לדוגמה:

Accept-Language: he

שורת כותרת זאת, ששמה Accept-Language וערכה he (קיצור של Hebrew) מציינת לשרת שהתשובה לבקשה יכולה להכיל מידע בעברית.

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

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

גוף הבקשה

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

לדוגמה בבקשת POST גוף הבקשה עשוי להכיל את המידע אותו נרצה לשמור בשרת (פרטי טופס, קובץ וכד'). בקשות GET לרוב לא מכילות גוף.

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

דוגמאות לבקשת HTTP

להלן דוגמה לבקשת GET המכילה שתי כותרות:

GET /index.html HTTP/1.1
Host: icode.co.il
Accept-Encoding: gzip, deflate

בדוגמה זאת אנו מבקשים מהשרת להחזיר את העמוד index.html מהאתר icode.co.il, ומוסיפים שאנו תומכים בקבלת מידע דחוס מסוג gzip.

והנה דוגמה לבקשת POST המכילה גוף. שימו לב לשורת הרווח בין הכותרות לגוף הבקשה:

POST /submit.html HTTP/1.1
Host: icode.co.il
Content-Length: 10

name=tzach

בדוגמה האחרונה, הכותרת Content-Length מכילה את האורך של גוף הבקשה בבתים, במקרה שלנו זהו האורך של הטקסט name=tzach, שהוא בדיוק 10 בתים.

בניית בקשת HTTP בקוד

בדומה לקריאת בקשת HTTP, גם הבנייה שלה היא קלה מאוד לקידוד. הנה דוגמה לבניית בקשה, בהינתן כל החלקים שלה:

request = "{method} {url} HTTP/1.1\r\n{headers}\r\n\r\n{body}".format(
    method = METHOD,
    url = URL,
    headers = '\r\n'.join('%s: %s' % item for item in HEADERS_DICT),
    body = BODY)

מבנה תגובת HTTP

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

פרוטוקול HTTP - תגובה
לאחר קבלת הבקשה, השרת שולח תגובה בחזרה ללקוח

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

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

קוד המצב

נסתכל על דוגמה לשורת מצב החוזרת מהשרת:

HTTP/1.1 200 OK

בדוגמה זאת הערך 200 (בתוספת המחרוזת OK) נקרא קוד המצב (status code) והוא מציין את התוצאה של נסיון השרת לטפל בבקשה שהתקבלה, לדוגמה: הצלחה (200), משאב לא נמצא (404) או שגיאה כללית (500).

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

  • 1XX – בקשה התקבלה אך אין עדיין תגובה (מאוד נדיר וכמעט לא בשימוש).
  • 2XX – הצלחה. הערך הנפוץ ביותר בקבוצה זאת הוא 200 OK, המציין תגובה תקינה לבקשה, אך קיימים גם ערכים אחרים כגון 201 (נוצר).
  • 3XX – המשאב נמצא אבל הועבר. לרוב התגובה תהיה 301 (הועבר באופן קבוע) או 302 (הועבר זמנית).
  • 4XX – קיימת שגיאה בבקשה עצמה, למשל התבקש משאב לא קיים (404), אין הרשאות (403), יש צורך לבצע אימות (401) ועוד.
  • 5XX – התרחשה שגיאה בשרת. שני הערכים הנפוצים הם 500 (שגיאה כללית) ו-503 (שגיאה זמנית).

שורות הכותרת

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

חלק גדול מכותרות התגובה מתחלקות לקבוצות הבאות:

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

להלן דוגמה לתגובה:

HTTP/1.1 200 OK
Date: Fri, 8 Jan 2016 11:29:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 31

<html><body>Hello</body></html>

תגובה זו מכילה שלוש שורות כותרת:

  • Date – שערכה הוא הזמן בשרת בעת יצירת התגובה.
  • Content-Type – שמציינת את סוג המידע אותו מכיל גוף התגובה, במקרה שלנו מדובר בקוד HTML בקידוד UTF-8.
  • Content-Length – אורך גוף התגובה בבתים.

גוף התגובה

לעתים קרובות השרת ירצה להחזיר ללקוח מידע כלשהו בתגובה לבקשה. מידע זה נקרא גוף התגובה, ויכול להיות מכל סוג. דוגמאות נפוצות לגוף התגובה הן קוד HTML, CSS או Javascript, מידע כלשהו בקידוד JSON או XML, קובץ בינארי כגון תמונה, gzip או מסמך.

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

נבחן דוגמה נוספת לתגובה:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 23

{"error": "unexpected"}

בדוגמה זאת השרת החזיר קוד מצב 500 המציין שגיאה בשרת. בנוסף, גוף התגובה מטיפוס JSON מכיל מידע נוסף על השגיאה. שימו לב לכותרת Content-Type המכילה את סוג המידע החוזר (הנקרא לפעמים גם MIME type או Media type).

אז מה עושים עם כל המידע הזה?

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

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

מתוך דפדפן כרום, לחצו F12 לפתיחת תפריט המפתחים, ולאחר מכן לחצו על לשונית Network וטענו מחדש את הדף:

Chrome Dev Tools

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

מה הלאה?

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

למה אתם *חייבים* ניהול גרסאות – גם כשמתכנתים בבית

כל מי שהוא חלק מצוות פיתוח בחברה יעיד שהוא משתמש בכלי ניהול גרסאות (source control) כלשהו כגון Subversion, Git, Team Foundation וכד'. ואכן כשמספר אנשים עובדים על אותו קוד היתרונות של כלי כזה הם עצומים, והוא פותר בעיות בסיסיות מאוד כמו: איפה שמור הקוד? מה קורה כששניים עובדים במקביל על אותו קובץ? איך מתעדים מה כל אחד עשה? וכד'.

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

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

כלי ניהול גרסאות במשפט אחד

את התפקיד הבסיסי של כל כלי ניהול גרסאות באשר הוא ניתן לנסח כך:

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

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

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

source-control

בדוגמה שלנו, השתמשתי בכלי ניהול הגרסאות Git, המייצר תיקייה בשם .git (שימו לב לנקודה בתחילת השם).

ניהול גרסאות למפתח יחיד

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

די לפחד משינויים

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

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

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

למה עשיתי את זה?!

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

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

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

כעת כאשר נרצה לדעת מדוע ומתי קטע קוד כלשהו נכתב, נוכל לעיין בהיסטוריית השינויים (revision history) של הקובץ ולקרוא את ההודעות שהשארנו לעצמנו. ניתן אפילו לקבל דו"ח מפורט על השינויים שבוצעו בכל שורה ושורה בקובץ ע"י פקודת annotate file, או בשמה הכן יותר – blame 🙂

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

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

רגע, מה עשיתי עכשיו?

סוג שונה של מעקב אחר שינויים הוא מעקב אחרי השינויים שעשינו מאז ה-commit האחרון. זה אולי נשמע מוזר, אבל כמה פעמים שכחתם בקוד שלכם פקודת print שלא צריכה להיות שם או console.log מיותר?

הכלים הגרפיים לניהול גרסאות כמו Tortoise Git, TortoiseSVN וכד' מאפשרות לנו לראות בצורה נוחה את כל השינויים שביצענו, ולעבור על הקוד לפני שאנחנו מבצעים commit. ואכן לפני כל commit, אני אוהב לעבור במהירות על השינויים שביצעתי, לוודא שאני לא מכניס שטויות לקוד ושהקוד עומד בסטנדרטים שהצבתי לעצמי. ברוב המקרים אני מוצא משהו לתקן, והורג באגים עוד לפני שהם נולדים.

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

Source control diff
אופס, זה לא אמור להיות פה…

עבודה על מספר גרסאות במקביל

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

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

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

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

כדי לייצר ענף חדש לרוב נשתמש בפקודה branch <new branch> שלמעשה אומרת לכלי ניהול הגרסאות "כל השינויים שאבצע מעתה צריכים להיכנס לענף new branch".

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

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

גיבוי

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

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

תהליך העבודה שלי

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

Source Control Process

התחילו כבר עכשיו!

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

  1. הורידו והתקינו את הכלי TortoiseHg.
  2. פתחו את התיקייה בה נמצא הקוד שלכם, לחצו מקש ימני ואז TortioseHg -> Create Repository Here ובחלונית שנפתחה לחצו Create. בשלב זה תיווצר תיקיה בשם .hg וקובץ נוסף.
  3. לחצו מקש ימני על תיקיית הקוד שלכם (לא על התיקייה שנוצרה), ואז Hg Commit.
  4. סמנו את קבצי הקוד אותם תרצו להוסיף לניהול הגרסאות. בד"כ יהיו אלו כל קבצי הקוד בלבד, ולא קבצים ותיקיות הנוצרים אוטומטית כמו קבצי exe, pyc וכד'.
  5. הזינו הודעה קצרה כגון "First commit" ולחצו Commit.
  6. אם נשאלתם האם להוסיף את הקבצים שסומנו, לחצו Add.

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

  1. ערכו את אחד מקבצי הקוד.
  2. לחצו מקש ימני על הקובץ או התיקיה, ולחצו Hg Commit.
  3. ברשימת הקבצים בצד שמאל תוכלו לראות את כל הקבצים שהשתנו, ובצד ימין את השינויים בכל אחד מהם.
  4. לצפייה בשינויים בתוכנה מתקדמת יותר, לחצו לחיצה כפולה על הקובץ הרצוי.
  5. כעת ניתן לכתוב הודעה ולבצע commit.
  6. לחצו מקש ימני על הקובץ ואז TortoiseHg -> Revision History לצפייה בהיסטוריית השינויים של הקובץ.

8 אוספים של אנימציות CSS3 שיוסיפו חיים לאתר שלכם

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

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

JS Bin on jsbin.com

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

Hover.css

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

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

Hover.css
Hover.css

Animate.css

אחת הספריות הותיקות יותר היא Animate.css המציעה כמה עשרות אנימציות המיועדות בעיקר לכניסה ויציאה של אלמטים מהמסך. Animate.css כוללת קבוצת אנימציות מגניבות בשם Attention Seekers (מושכי תשומת לב), המכילה אנימציות מגניבות כמו RubberBand (גומייה), שיתנו לאלמטים שלכם תחושה מציאותית וקלילה.

Animate.css

 

Animatable

ספריית Animatable היא למעשה הדגמה של אוסף כל תכונות ה-CSS התומכות באנימציה. נכון לכתיבת שורות אלו, הספרייה מציגה 39 אנימציות, שכל אחת מהן מוגדרת על מאפיין CSS אחר – שימושי מאוד כדי לקבל רעיונות חדשים לאנימציות או לדרכים בהן ניתן לממש אנימציה מסויימת.

טיפ קטן – לחצו על הכפתור "Animate all".

Animatable

SpinKit

כשהגיחה אל חיינו טכנולוגית AJAX בתחילת שנות האלפיים, כולנו למדנו להכיר את אנימציות טעינת הדף – הספינרים (spinners). הכוכבים המסתובבים האלו אמנם החלו את דרכם כתמונות gif פשוטות, אך היום קיימים ספינרים מורכבים מאוד הממומשים באמצעות אנימציות CSS, ולעתים בעזרת עשרות או מאות שורות קוד. האתר SpinKit הוא אוסף מדהים ושימושי של אנימציות טעינה שונות ומגוונות, חלקן כל כך יפות שפשוט יש חשק לגרום לדפים להיטען לאט יותר 🙂

SpinKit
SpinKit

Effeckt.css

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

Effeckt.css

Bounce.js

ספריית Bounce.js היא ספריית Javascript המאפשרת לנו לייצר אנימציות CSS ע"י ממשק נוח ופשוט (יחסית). הכח האמיתי של הספרייה הוא הקלות בה ניתן ליצור אנימציות מורכבות ע"י שילוב של מספר מאפייני CSS שונים במקביל, וצפיה מיידית בתוצאות. בנוסף הספרייה מכילה מספר שילובים מוכנים מראש שניתן להתחיל מהם.

Bounce.js

CSS Shake

כשמה כן היא, ספריית CSS Shake מספקת אחת עשרה אנימציות שונות עם מטרה אחת בלבד – להרעיד דברים! אני בעצמי עדיין לא בטוח מה עושים איתה, אבל היי – תודו שתמיד חלמתם לגרום ל-div לרעוד 😉

CSS Shake

CSS3 Animation Cheat Sheet

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

CSS3 Animations Cheat Sheet

 

שימוש בפונט להצגת אייקונים באתר שלכם

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

Flat icons
אייקונים מונוכרומטיים

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

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

SVG – תמונה וקטורית

Smart Vector Graphics, או SVG הוא פורמט תמונה וקטורי (vector) מבוסס XML. לא ארחיב עליו כאן, אך אקצר ואסביר שבשונה מפורמטים כמו Bitmap או JPEG, קובץ SVG לא מכיל מידע אודות הפיקסלים שבתמונה, אלא מתאר את האלמנטים השונים המרכיבים את התמונה כגון קווים, צורות, טקסט וכד'. הנה דוגמה לקובץ SVG.

לשימוש ב-SVG ישנם מספר יתרונות על שימוש בפורמטים שאינם וקטורים:

  • ניתן לשנות את גודל התמונה ללא פגיעה באיכות
  • קובץ התמונה הוא לרוב קטן יותר, במיוחד עבור תמונות פשוטות
  • ניתן לשנות את העיצוב באמצעות CSS
  • הקובץ הוא בפורמט טקסטואלי וניתן לייצר אותו דינאמית ואף להטמיע אותו ישירות ב-HTML

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

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

למזלנו, קיים פתרון שימושי ביותר לבעייה זו:

פונט אייקונים

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

קם חכם ואמר – מדוע צריכים הפונטים להכיל אותיות בלבד? למה שלא נייצר פונט המכיל, במקום אותיות, אייקונים? ואכן קיימים מספר פונטים שהם למעשה אוסף של אייקונים, ביניהם Font Awesome ו-Ionicons.

לפונט אייקונים יתרונות רבים:

  • בקשת HTTP אחת, ללא תלות במספר האייקונים אותם אנו צריכים.
  • תמיכה לאחור גם בדפדפנים ישנים מאוד (IE6).
  • ניתן לשנות את עיצוב האייקונים בנקל ע"י מאפייני CSS לעיצוב טקסט: font-size, color וכד'
  • השימוש בפונטים אלו הוא פשוט מאוד, ומצריך 2 שורות קוד בלבד:
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<i class="fa fa-camera-retro"></i>

השורה הראשונה מייבאת את קובץ ה-CSS של Font Awesome (שבתורו מייבא את הפונט עצמו), והשניה מייצרת תגית ריקה, שהחלק החשוב בה הוא המאפיין class, ה"ממלא" את התגית בתוכן המתאים מתוך הפונט (גם כאן לא נגלוש לפרטים).

וכך זה נראה:

JS Bin on jsbin.com

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

Fontello

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

  • ע"י בחירה ממבחר קיים של מאות אייקונים מפונטים נפוצים כגון Font Awesome, Fontelico, Entypo ועוד.
  • ע"י העלאה של קבצי SVG משלנו.

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

<link rel="stylesheet" href="path/to/fontello.css">
<i class="icon-like"></i>

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

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

האתר שלי למטה?!?

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

  1. בעיה בשרת – האתר באמת למטה
  2. בעיה מקומית במחשב / ראוטר / ספק האינטרנט שלי, והאתר בעצם למעלה

האתר הגאוני Down For Everyone or Just Me עוזר לנו להבדיל במהירות בין שני המקרים ע"י ביצוע בדיקה פשוטה של בדיקת האתר שלנו. מכיוון שהבדיקה לא מתבצעת מהמחשב שלנו, היא כנראה משקפת את המצב האמיתי של האתר.

כך זה נראה כשבדקתי אם google.com למעלה:

downforeveryoneorjustme

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

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

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

import module
module.func('yummy')

השיטה השניה היא ביצוע import ישירות לאובייקט או פונקצייה אותם אנו צריכים:

from module import func
func('meh')

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

כתיבת קוד בדיק

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

from time import sleep

def process_or_wait(value):
    try:
        # process value
        return True
    except:
        sleep(10)
        return False

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

import my_module
import unittest

class Test1(unittest.TestCase):
    def test_process_or_wait__failure(self):
        ret_val = my_module.process_or_wait('invalid')
        self.assertFalse(ret_val)

הבדיקה אכן עובדת ומוודאת שעבור ערך לא תקין, הפונקציה שלנו מחזירה False. אז מה בכל זאת הבעיה? הבעיה היא שבמקרה של כישלון, הפונקציה process_or_wait מחכה 10 שניות, כלומר הבדיקה תימשך 10 שניות!

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

import my_module
import unittest
from mock import patch

class Test1(unittest.TestCase):
    @patch('time.sleep', side_effect = lambda _ : None)
    def test_process_or_wait_failure(self, sleep_mock):
        ret_val = my_module.process_or_wait('invalid')
        self.assertFalse(ret_val)
        sleep_mock.assert_called_with(10)

נריץ את קוד הבדיקה ו… הוא לא עובד! הבדיקה עדיין אורכת 10 שניות, ובנוסף השורה האחרונה, המוודאת שפונקציית הדמה שלנו נקראה, נכשלת. הסיבה לכך נעוצה בצורה בה ביצענו import לפונקציה sleep.

המשמעות של import

בכל פעולת import משתתפים שני מודולים: המודול המייבא – זה שמכיל את פקודת ה-import, והמודול המיובא. כאשר מבצעים import ישירות לפונקציה:

from time import sleep

נוצר במודול המייבא משתנה מקומי בשם sleep המפנה (reference) לפונקציה sleep במודול time. למעשה אין קשר בין השם sleep במודול המייבא, לשם sleep במודול time (המיובא), ואכן שפת פייתון מאפשרת לנו לבחור שרירותית את השם של המשתנה המקומי במודול המייבא:

from time import sleep as zzz
print zzz # <built-in function sleep>

עכשיו נבין מה קרה בקוד הבדיקה. הפונקציה patch אכן החליפה את הפונקציה sleep במודול time המקורי, אבל היא לא החליפה את העותק המקומי של הפונקציה במודול המייבא mymodule. זאת הסיבה שקוד הבדיקה לא עבד.

אך אם היינו כותבים את הקוד הנבדק באופן הבא, הבדיקה הייתה עובדת:

import time

def process_or_wait(value):
    try:
        # process value
        return True
    except:
        time.sleep(10)
        return False

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

המסקנה היא שכדי לוודא שהקוד שלנו יהיה בדיק, אסור (אוקיי, לא מומלץ) לבצע import לפונקצייה ישירות.

וכמובן שמסקנה זאת תקפה לגבי כל* הפונקציות והאובייקטים.

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


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

@patch('mymodule.sleep', side_effect = lambda _ : None)

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

איך *באמת* לומדים תכנות

קיימות הרבה שיטות ללימוד תכנות בצורה מובנית ומסודרת:

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

אבל האמת המרה היא שאף אחת מהשיטות הללו לא באמת תלמד אתכם תכנות.

למה הכוונה?

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

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

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

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

ניסיון == כישרון

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

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

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

הדרך הנכונה ללמוד תכנות

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

כלל ראשון – תכנתו בשעות הפנאי

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

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

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

כלל שני – כל השאר הם רק עזרים

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

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


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

ומה אם אני לא יודע/ת איך להתחיל?

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