צעד ראשון עם דוקר
דוקר (Docker) הוא אחד הכלים השימושיים ביותר למפתחים של פרויקטים מכל הגדלים בשלבי הפיתוח, הבדיקות וה Production. בואו ניקח צעד ראשון יחד כדי להכיר את הכלי ולראות אם שווה לשלב אותו גם בפרויקט שלכם.
טיפים קצרים וחדשות למתכנתים
דוקר (Docker) הוא אחד הכלים השימושיים ביותר למפתחים של פרויקטים מכל הגדלים בשלבי הפיתוח, הבדיקות וה Production. בואו ניקח צעד ראשון יחד כדי להכיר את הכלי ולראות אם שווה לשלב אותו גם בפרויקט שלכם.
״אם רק היו לי עוד שעתיים ביום...״ ״אם רק היה לי עוד מתכנת בצוות...״
אנחנו אוהבים לדמיין שעם יותר משאבים היינו מספיקים לסיים את כל העבודה בזמן והמוצר היה עובד פיקס. הניסיון מוכיח ההפך - אנחנו תמיד צריכים עוד שעתיים ביום בשביל לסיים את הבאגים האחרונים. וזה בכלל לא משנה כמה זמן יש לך.
בגלל זה הכתבה הזאת של אורנה רודי נשמעה צורמת:
https://www.globes.co.il/news/article.aspx?did=1001282616
הטענה שצריך לעבוד 12 שעות ביום מבוססת על איזה צירוף מקרים מוזר בביולוגיה שלפיו סדר גודל של 12 שעות זה המקסימום שאנחנו מצליחים לעבוד. אבל למה 12 ולא 8? או 4? או 15?
האמת שאפשר בקלות לעבוד 12 שעות ביום ולא להתקדם לשום מקום. לי היו תקופות כאלה ואני בטוח שגם לכם. וכמעט תמיד אפשר למצוא מישהו שמצליח יותר ממך (בדבר שאתה רוצה להשיג) ועובד פחות שעות.
הבחירה לעשות את מה שחשוב היא הרבה יותר חשובה להצלחה שלכם מתוספת של עוד שעתיים עבודה ביום.
ג'נרטורים יכולים להציע דרך חשיבה מקורית על בעיות, כולל על בעיות של העולם האמיתי, דרך שבגלל שאנחנו לא רגילים אליה היא לא תהיה הדבר הראשון שיקפוץ לראש אבל לפעמים שווה להתאמץ. הנה דוגמא קטנה שאולי תעזור.
נניח שאנחנו רוצים לכתוב תוכנית שמקבלת 5 מספרים מהמשתמש ומדפיסה את המספר הגדול ביותר. נקודת התחלה טובה היא הפונקציה get_numeric_input שתיקח מספר מהמשתמש:
def get_numeric_input(prompt):
while True:
try:
return int(input(prompt))
except ValueError as e:
print("Sorry, only integers are allowed. Please try again")
עכשיו אני יכול להפעיל אותה בלולאה 5 פעמים ולהדפיס את המספר הגדול ביותר:
values = []
for i in range(5):
values.append(get_numeric_input("Please select a number: "))
print(max(values))
וזה עובד יופי רק הבעיה שכל ה-5 מספרים נשמרים בזיכרון. במספרים גדולים זה סתם בזבזני ומספיק לשמור רק את המספר הגדול ביותר שראינו עד עכשיו. נעדכן את הלולאה ואז הקוד מתחיל להיות מסורבל:
res = get_numeric_input("Please select a number: ")
for i in range(4):
res = max(get_numeric_input("Please select a number: "), res)
print(res)
יש כאן שם של משתנה שהייתי צריך להמציא (res), והקריאה לפונקצית ה input מופיעה פעמיים.
דרך אחרת להגיע לאותו פיתרון היא לעטוף את הפונקציה ב Generator:
def user_numeric_input(prompt):
while True:
yield get_numeric_input(prompt)
עכשיו קריאת 5 מספרים והדפסת הגדול ביניהם הופכת לשורה הבודדת:
print(reduce(max, islice(user_numeric_input("Please select a number: "), 5)))
בצורה כזאת אני לא צריך לשבור את הראש ולהמציא שם מיותר למשתנה וגם פונקציית ה input מופעלת רק ממקום אחד.
בעיה אמיתית בעבודה עם לקוחות היא זיהוי ההבדל בין באג לפיצ'ר, וזה נכון במיוחד לפרילאנסרים. אנחנו נותנים הצעת מחיר גלובאלית לפיתוח ומסכמים עם הלקוח את כל הפיצ'רים, הלקוח בטוח שהוא יקבל בדיוק את מה שכתוב במסמך האיפיון ובפועל משהו לא עובד כמו שהלקוח תכנן.
אם זה "באג" אז הפרילאנסר אשם, וצריך לתקן את המערכת "על חשבונו" ואולי אפילו לפצות את הלקוח שמרגיש מרומה.
אם זה "פיצ'ר" שהלקוח שכח לציין במסמך האיפיון אז הלקוח אשם וצריך להוסיף כסף בשביל לקבל את הפיתוח הנוסף.
הדינמיקה הזאת הרסנית לשני הצדדים.
מודל תשלום אחד יותר חכם לעבודה עם לקוחות הוא ריטיינר, שאומר שהלקוח משלם סכום קבוע כל חודש ובתמורה הפאילנסר עושה מאמץ לתקן כמה שיותר תקלות ולהוסיף כמה שיותר פיצ'רים לפי מה שהלקוח צריך באותו חודש. הבעיה כאן שלקוח יכול ליפול עליכם עם המון פיצ'רים ותיקונים עד שלא תוכלו לעמוד בעומס, או שפרילאנסר יכול לעבוד לאט ולא תקבלו ממנו תוצאות.
אפשר לשפר את זה אם מוסיפים קצת מגבלות, למשל:
אשמח לבנות לך אתר במחיר הקמה של X ש"ח. אנחנו מסכמים על תאריך (דדליין) שבו העבודה מסתיימת והאתר עולה לאוויר לא משנה מה.
אני אקצה לך גם אחרי שהאתר באוויר שעתיים-שלוש בשבוע לעדכונים ותיקונים קטנים באתר אם תצטרך. העלות היא Y ש"ח לחודש.
אני אקצה לך מדי פעם שבוע שלם של עבודה מרוכזת רק על האתר שלך כדי לתקן ולעדכן דברים יותר גדולים. עלות שבוע כזה היא Z ש"ח ואני מוכר שבועות כאלה על בסיס זמינות אז כנראה תצטרך לתאם איתי כמה שבועות מראש.
בצורה כזאת זה לא משנה אם הבעיה היא באג או פי'צר שפספסנו במסמך האיפיון. אנחנו שמים את כל האילוצים של שני הצדדים על השולחן ועושים מאמץ לבנות מערכת תמריצים בריאה יותר.
מתכנת שלא טורח לקרוא תיעוד כתב את ה Dockerfile הבא:
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install nodejs
RUN apt-get clean
מה המתכנת ניסה להשיג? למה הוא לא הצליח? ואיך הייתם מתקנים את הבעיה?
רמז? כדאי לקרוא את התיעוד של דוקר על שכבות בקישור:
את הטיפ הכי חשוב של דיבור מול קהל אני מקווה שאתם מכירים. הוא אומר שכדאי שההרצאה שלכם תהיה בנויה סביב רעיון מרכזי אחד או נקודה מרכזית אחת שאתם רוצים להעביר.
אבל זה לא מספיק, וכאן מגיע הטיפ השני:
כשאתם רוצים להעביר את הנקודה שלכם תשתמשו בידע שאתם בטוחים שיש לקהל שלכם בצורה מדויקת - לא יותר מדי ולא פחות מדי. הסתמכות על מונחים שלא בטוח שהקהל שלכם מכיר תגרום לאנשים ללכת לאיבוד, ובדיוק אותו נזק יקרה אם תתעכבו להסביר מונחים שכולם כבר מכירים.
לכן אם אני אלך להעביר הרצאה על "תכנות אסינכרוני ב Python" לקהל של אנשים שעובדים כ Data Scientists בפייתון אני אחשוב פעמיים לפני שאשתמש בירושה או Design Patterns בדוגמאות שלי, ובאותו זמן אני ארשה לעצמי לרוץ עם דוגמאות שמביאות Data Sets מהרשת וממלאות מהם מערכים ב NumPy כי אני יודע שזה משהו שהקהל שלי עושה כל יום.
ככל שתצליחו להיות יותר קרובים לקהל שלכם - כך לקהל יהיה יותר קל להקשיב ולהבין את הנקודה החשובה שבאתם להעביר.
כשאנחנו עובדים בשפה או בפריימוורק כמעט תמיד לשפה או לפריימוורק יש את הדרך שלה לעשות דברים. לפעמים ניסיון לעשות דברים בצורה קצת אחרת יעבוד, אבל הרבה פעמים הוא ייפול במקרי קצה שלא חשבנו עליהם עד הסוף.
הנה דוגמא קצרה מפייתון - בפייתון אנחנו לא אמורים להעביר מבנה נתונים שאפשר לשנות אותו בתור Default Value לפרמטר של פונקציה. אם נעשה את זה אנחנו שוברים כלל עדין של השפה שלא בהכרח ישבור את התוכניות, אבל עלול לייצר באגים מוזרים. הנה מחלקה לדוגמא שמאתחלת רשימה ורוצה לאפשר לאנשים להעביר את הרשימה כפרמטר:
class Demo:
def __init__(self, data=[]):
self.data = data
def add(self, x):
self.data.append(x)
כבר כאן אפשר לראות את הבאג - גם אם שאר התוכנית עובדת בסדר. לדוגמא הקוד הבא שמשתמש במחלקה מדפיס בדיוק את מה שרצינו:
d = Demo()
print(d.data)
e = Demo([10, 20, 30])
print(e.data)
f = Demo()
print(f.data)
אבל זה לא אומר שהקוד תקין. זה פשוט שלא דרכנו על המוקש. עם תוספת קטנה של פקודה בשורה השלישית הקוד יישבר:
d = Demo()
print(d.data)
d.add(10)
e = Demo([10, 20, 30])
print(e.data)
f = Demo()
print(f.data)
ומדפיס עכשיו:
[]
[10, 20, 30]
[10]
אז כן אתם יכולים להעתיק את הרשימה בתוך הפונקציה כדי לתקן את זה, אבל אני חושב שהרבה יותר הגיוני לכתוב את הקוד בצורה קצת אחרת כדי שהבעיה הזאת לא תהיה שם מלכתחילה. גירסא אחרת למחלקה שבורחת לגמרי מהאפשרות ליפול בבור הזה תיראה כך:
class Demo:
def __init__(self, *data):
self.data = list(data)
def add(self, x):
self.data.append(x)
ובאופן כללי אם אנחנו מקפידים לא להעביר בתור Default Value משהו שהוא Mutable לעולם לא תהיה לנו בעיה של שיתוף ערך בלי שהתכוונו.
אני חושב ששווה לשים לב לכללי אצבע כאלה כשאנחנו לומדים שפה או פריימוורק. כשאתם אוסיף אחד ועוד אחד אתם מקבלים תפיסת עולם מסוימת, סוג של רוח של השפה או הפריימוורק. וגם בתכנות קשה יותר כשהולכים נגד הרוח.
הרבה אנשים מקדמים את הרעיון הזה שזה בסדר לכתוב קוד יותר ארוך כי הוא יותר קריא או שיותר קל להרחיב אותו בהמשך. זה לפעמים נכון אבל אני חושב שצריך להיזהר מהטיעון הזה. קבלו דוגמא קצרה המבוססת על מקרה אמיתי.
מתכנת ניגש לעדכן את הקוד הבא שנראה קצת מסורבל:
def sum_all_numbers(*values):
res = 0
for v in values:
if isinstance(v, numbers.Number):
res += v
return res
ואומר לעצמו - אני יודע תכנות מונחה עצמים! אני אכתוב מחלקה שמייצגת את הערך המספרי של כל פריט במערך (הערך המספרי של מספר הוא פשוט הוא עצמו, ושל דברים אחרים הוא 0), ואז נוכל לסכום את הערכים המספריים כלומר נוכל להפוך את הפוקנציה לגירסא הממש פשוטה הזאת:
def sum_all_numbers_2(*values):
return sum([NumericValue(x) for x in values])
מדהים נכון? רק שאז ניגשים לכתוב את NumericValue ומגיעים לזה:
class NumericValue:
def __init__(self, val):
if isinstance(val, numbers.Number):
self.val = val
else:
self.val = 0
def __add__(self, other):
if isinstance(other, int):
return self.val + other
elif isinstance(other, NumericValue):
return self.val + other.val
else:
return NotImplemented
def __radd__(self, other):
return self.__add__(other)
ה if עדיין נשאר, פשוט הזזנו אותו מתחת לשטיח לתוך קוד הפונקציה __init__. אבל הבעיה שאנחנו עובדים עכשיו ממש קשה בשביל לקבל מחלקה שהפונקציה sum תסכים לעבוד איתה.
גישה הרבה יותר טובה (כאן, ובמקרים רבים נוספים) היא להישאר פשוט. אם כל מה שאתה צריך מ NumericValue זה משהו שיתנהג כמו מספר, עדיף לכתוב פונקציה שמחזירה מספר במקום מחלקה שלמה. הנה הקוד כמו שאני הייתי כותב אותו:
def numeric_value(v):
return v if isinstance(v, numbers.Number) else 0
def sum_all_numbers_3(*values):
return sum([numeric_value(v) for v in values])
print(sum_all_numbers_3(10, 20, 'f', 'g', '10', 30))
ומה שיפה כאן זה שאנחנו עדיין בעולם התכנות מונחה עצמים - פשוט במקום ללכת נגד הפלטפורמה אנחנו הולכים איתה. אנחנו לא צריכים לבנות בעצמנו מחלקה למספר, כי כבר יש כזו: אלה המספרים של פייתון. ואנחנו לא צריכים לבנות אופרטור לחיבור ולטפל בכל החיבורים בין המחלקה החדשה למחלקת המספר הרגילה, כי מספרים כבר יודעים לחבר את עצמם. כל מה שצריך זה להבין איזה מספר מתאים לאיזה ערך, וזה מה שעושה הפונקציה numeric_value.
שפת התכנות הראשונה שלמדתי נקראה Basic. אחרי Pascal, ואז C. אני זוכר שלא אהבתי את C. לא הבנתי אותה ולא התאמצתי להבין. הרגשתי שכל מה שאני צריך לבנות אני יכול לבנות בקלות ב Pascal, אז בשביל להתאמץ עם פוינטרים וסטראקטים. אני כותב דברים מדליקים, זה עובד, מה עוד צריך?
ולמרות ההתקדמות של הטכנולוגיה, ולמרות שהיום אפשר לבנות דברים הרבה יותר מתוחכמים ממה שאני כתבתי בפסקל כשרק התחלתי לתכנת, רעיון אחד חשוב לא השתנה: הרבה יותר קל לבנות לבד מערכת שנראית כמו booking.com מאשר את אתר בוקינג עצמו (ותחליפו את בוקינג בכל מערכת אחרת שאתם מכירים).
יש לזה שתי סיבות:
למערכת אמיתית יש משתמשים. הרבה מהם. ולמשתמשים יש נטיה מעצבנת לחשוף את כל מקרי הקצה שבחיים לא תגיע אליהם כשיש לך שלושה וחצי גולשים ביום באתר. רק כשיש עומס אנחנו רואים כמה היסודות חזקים.
מערכת אמיתית חיה לאורך זמן וממשיכה לקבל עדכונים ותוספות לאורך שנים. אז כן כל הכבוד שהצלחת לכתוב אתר שנראה כמו בוקינג בשלושה חודשים - אבל איפה האתר הזה יהיה עוד שנתיים? הרבה בעיות בעיצוב תוכנה מתגלות רק כשירח הדבש נגמר ודרישות חדשות מגיעות מהשטח שלא ממש מסתדרות עם האילוצים שבחרנו.
שני סוגי העומסים (קוד ומשתמשים) הם דברים שפרילאנסרים שעובדים לבד לא צריכים להתמודד איתם. ברוב המוחלט של המקרים פרילאנסר שבונה אפליקציות יבנה לעצמו את הכלים והידע כדי לבנות ללקוחות אפליקציות יחסית מהר ולהתקדם הלאה ללקוח הבא. פרילאנסרים וחברות קטנות שבונים מערכות ווב ירצו להתמקד בלתת שירות טוב ללקוחות שלהם ולא בלהוסיף עוד פיצ'רים למערכת.
אבל אלה בדיוק הדברים שחברות הייטק וסטארטאפים צריכים להתמודד איתם כל הזמן. ובגלל זה כשאותם פרילאנסרים נכנסים לעבודה בחברות אמיתיות כל ההרגלים שהם בנו בתור פרילאנסרים מתחילים לעבוד נגדם. מהר מאוד סגנון העבודה שמייצר קוד שעובד כמה שיותר מהר יוצר קוד שלא מתמודד טוב עם שינויים בדרישות מהשטח, מכניס בעיות אבטחה למערכת ולא עומד בעומסים.
אני חושב שכדאי לזכור שיש יותר ממגרש משחקים אחד בכתיבת קוד, ולכל מגרש הכללים שלו. לכן זה לא ממש עוזר להתאמן במגרש של הפרילאנסרים אם החלום שלך זה להתקדם במגרש של השכירים (או להיפך).
המקום הנכון לבדוק את המידע שלנו או להמיר אותו לפורמט שמערכת אחרת תרצה לקבל הוא ממש לפני השליחה, כמה שיותר קרוב לקצה. זה אומר:
שאנחנו ננקה שאילתות SQL בשורה שמעבירה את השאילתה לדרייבר (באמצעות Bind Variables).
שאנחנו ננקה מידע שנכתב ל HTML בקוד ה View, ממש לפני שהוא נשלח ללקוח (זאת הסיבה שריאקט מנקה את הקלט ואתם צריכים להתאמץ לעקוף את זה עם dangerouslySetInnerHTML).
שאנחנו ננקה מידע שהולך להישלח ל system ממש לפני הקריאה ל system כדי למנוע Shell Injections.
ההמרה והניקוי קרוב לקצה הם הדרך היחידה להבטיח ששינויים עתידיים בקוד לא יוכלו לדלג על קוד הניקוי, או שמקומות אחרים בהמשך ה Flow לא ילכלכו לכם את המידע בין הניקוי לפעולה המסוכנת.