היום למדתי: הגדרת פרוטוקול ב Python
מנגנון Type Hints של פייתון מציע דרך להוסיף הגדרת טיפוסים למשתנים ופונקציות שלנו. כלים כמו mypy יכולים אז לעבור על הקוד ולזהות מתי ניסינו לעשות משהו שלא יעבוד בגלל בעיית טיפוסים ולהציל אותנו מבאגים טפשיים.
טיפים קצרים וחדשות למתכנתים
מנגנון Type Hints של פייתון מציע דרך להוסיף הגדרת טיפוסים למשתנים ופונקציות שלנו. כלים כמו mypy יכולים אז לעבור על הקוד ולזהות מתי ניסינו לעשות משהו שלא יעבוד בגלל בעיית טיפוסים ולהציל אותנו מבאגים טפשיים.
בתיעוד של Redux מוצגת הדוגמה הבאה ל Reducer:
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + action.payload }
case 'counter/decremented':
return { value: state.value - action.payload }
default:
return state
}
}
היא עובדת, אבל לא הולכת להזדקן יפה. על כל פעולה חדשה ביישום נצטרך עוד בלוק ב switch/case הזה, ולאורך זמן הבלוק יגדל למימדים מפלצתיים, יהפך לקשה לתחזוקה וכמעט בלתי אפשרי לשינוי. האתגרים המרכזיים של מבנה ה switch/case הקלאסי של רידאקס הם:
אין Context - מי שיסתכל על הקוד הזה בעוד שבועיים לא ידע מתי פעולה מסוימת נקראת, מה החשיבות שלה ביישום באופן כללי.
אין טיפוסים - לא ברור מה צריך להיות הערך של action.payload ואיך הוא מחובר ל action.type בלי לקרוא ולהבין את הקוד עצמו.
שימוש במחרוזות בתור טיפוסים של actions-ים אומר שאם מישהו ירצה לשנות שם של פעולות הוא יצטרך לעבור על כל הקבצים ביישום כדי לחפש את כל המקומות בהם המחרוזת מופיעה.
אז אין ספק שעם 10 שורות הכל עובד מעולה אבל ב 500 שורות כבר אי אפשר לתחזק את הקוד. ועכשיו השאלה מתי עוצרים? מתי משכתבים את היסודות כי הלוגיקה של המערכת שלנו נהייתה מסובכת מדי? אני יכול לחשוב על 3 אפשרויות:
אפשר לחפש מהתחלה בסיס שיגדל טוב - לדוגמה אפשר להתחיל את הפרויקט עם Redux Toolkit ו TypeScript ולקבל יסודות טובים יותר מהשניה הראשונה.
אפשר לזהות שקטעי קוד מסוימים "מתחילים" להיות מכוערים או קשים לתחזוקה ולשכתב את היסודות איפשהו סביב הנקודה הזאת.
אפשר לחכות עד שהקוד יהיה לגמרי כדור ענק של בוץ ואז לבנות אותו מחדש עם יסודות טובים יותר.
לכל גישה יש את היתרונות והחסרונות שלה, הדבר החשוב הוא לבחור את הגישה בצורה מפורשת בתחילת הפיתוח כדי שנדע מה מצפה לנו.
יושב מתכנת בחברת תוכנה כלשהי ומקבל דיווח על באג - הבילדים נשברים. אחרי קצת מחקר מתגלית הבעיה: סקריפט הבניה של התוכנה שלהם מוריד קוד מגיטהאב בתור קובץ tar.gz ובודק Checksum על הקובץ שירד כדי לראות שקיבל את הקובץ הנכון. רק אם ה Checksum מתאים הוא מריץ את הבניה. ומסתבר שלאחרונה ה Checksum-ים על קבצי ה tag.gz התחילו להשתנות.
פרטים בקישור: https://github.com/orgs/community/discussions/45830
מה שקרה זה שגיטהאב שינו את שיטת הכיווץ שלהם, ולכן למרות שהקוד עצמו זהה, הקובץ המכווץ שסקריפט הבניה מקבל שונה מאיך שהוא היה בעבר. וכן תקלות מסוג זה קרו גם לי אינספור פעמים בעבר.
וזה רגע שצריך לתפוס.
כי ברגע שאתה מתחיל לבדוק מסביב אתה מגלה שיש הרבה מאוד אנשים שלא חושבים שלגיטהאב יש איזושהי מחויבות לגבי האופן בו הם מכווצים את הקוד בריפו, ואף פעם לא היו חולמים לחשב Checksum על קובץ כזה או להתבסס עליו בשום סקריפט בניה. הנה כמה מהתגובות ברדיט:
It's been known for forever that these files aren't stable
GitHub doesn't guarantee the stability of checksums for automatically generated archives. annndd its gonee
This is why I generally upload tarballs to my releases. Artifacts attached to a release won't change.
הסיבה שאני אוהב באגים כאלה היא שהם חושפים בעיה באינטואיציה שלי, וכך הם נותנים לי הזדמנות להבין משהו לגבי מגבלות ודברים שאפשר או אי אפשר להסתמך עליהם, וגם לגבי התיעוד ואיך לקרוא ולהבין אותו. כשאנחנו נתקלים בכזה באג, הדבר החכם לעשות הוא לחזור לתיעוד ולחפש כמה שיותר רמזים שאפשר היה להבין מהם מה החוזה האמיתי של ה API ועל מה מותר ואסור להסתמך. אינטואיציה טובה יותר היא אחת הדרכים להפוך למפתחים טובים יותר.
סיפרתי לכם כבר על התעודות שהוספתי לאתר, ואיך כתבתי שאילתת SQL לא מאוד מתוחכמת כדי להבין אם משתמש ראה את כל השיעורים בקורס כדי לדעת אם אפשר לשלוח תעודה.
אלא שאז כמה אנשים ביקשו אפשרות להדפיס תעודות ישנות, כלומר תעודות על קורסים שהם כבר לא יכולים לגשת אליהם כי המנוי שלהם נגמר. הפיתרון לזה היה לייצר מסך "רשימת תעודות" שיראה לכם במקום אחד את כל הקורסים שסיימתם עם אפשרות להוריד תעודה לכל קורס. אבל עכשיו יש לנו בעיה חדשה - השאילתה שבודקת אם תלמיד סיים קורס תלויה במזהה הקורס. הפעלה שלה בלולאה על כל הקורסים מייצרת את באג N+1 הידוע לשימצה. הגיע הזמן לעדכן את השאילתה כדי להוציא את כל הקורסים שתלמיד סיים.
האתגר בקצרה ברמת הטבלאות נשמע כך-
יש טבלה בשם bundle שמחזיקה נתונים על קורסים. אנחנו רוצים לקבל את ה id של כל הקורסים שתלמיד סיים.
יש טבלה בשם lessons שמחזיקה את כל השיעורים. לכל שיעור גם יש id ובגלל ששיעור שייך לקורס יש לו גם bundle_id
.
יש טבלה בשם user_in_lessons
שבשביל הסיפור שלנו מספיק לחשוב שהיא מחזיקה שתי עמודות user_id
ו lesson_id
. אם יש בה שורה למשתמש ושיעור מסוים זה אומר שהמשתמש ראה את כל השיעור.
טבלה אחרונה בסיפור היא users
. אנחנו מתחילים עם id
מסוים מטבלה זו.
המסלול בגדול הוא לקחת את המשתמש, להוציא את כל השיעורים שהוא עשה מסודרים לפי קורסים, לבדוק באיזה קורסים הוא עשה את כל השיעורים ולהחזיר רשימה של כל הקורסים האלה. ואת המשימה הזאת רק באנגלית הדבקתי בחלון השיחה של Chat GPT. התשובה באמת נראית כמו שאילתה:
SELECT bundles.id
FROM bundles
JOIN lessons ON bundles.id = lessons.bundle_id
WHERE NOT EXISTS (
SELECT *
FROM lessons AS l
LEFT JOIN user_in_lesson AS uil ON l.id = uil.lesson_id
WHERE uil.user_id = [user_id] AND l.bundle_id = bundles.id
) IS NULL;
ונכון, היא לא עובדת, אבל היי יש פה רעיון מעניין, באמת אפשר להשתמש ב Sub Queries כדי לרוץ על כל הקורסים בלולאה בתוך בסיס הנתונים. רק צריך לתקן את תתי השאילתות ומגיעים לגירסה שבסוף הכנסתי לאתר (מתורגמת כבר לריילס):
@bundles = Bundle
.joins(:lessons)
.where("
(SELECT count(*)
FROM lessons AS l
LEFT JOIN user_in_lessons AS uil ON l.id = uil.lesson_id
WHERE uil.user_id = ? AND l.bundle_id = bundles.id)
=
(SELECT count(*) FROM lessons where lessons.bundle_id = bundles.id)", user_id).distinct
במשתמשי הבדיקה שלי זה עבד, ואתם מוזמנים גם להיכנס למסך "החשבון שלי" באתר ולראות אם יש קורסים שסיימתם, ואם השאילתה שלי הצליחה למצוא אותם.
הדבר החשוב לקחת מהסיפור הזה הוא המצב של Chat GPT היום. הרבה מדברים על הפחד מהרובוטים אבל המציאות כמובן יותר מורכבת. הבאגים של Chat GPT הם חלק מהותי מהטכנולוגיה, אלה פיצ'רים לא באגים. העבודה שלנו כמתכנתים היא להבין איך להשתמש בו כדי לעבוד מהר יותר ולקבל עוד רעיונות לפיתרונות. המעבר מרעיון לביצוע ותיקון הבאגים הוא עדיין משימה שלנו.
ג'אווהסקריפט כל הזמן מתקדמת ולמרות שכבר ירדנו מרכבת ההרים שהיתה ES6, עדיין יש שיפורים קטנים שהופכים את החיים לקלים יותר כל יום. אז נכון כשחושבים על ES13 מיד קופץ לראש סימן הסולמית והיכולת להגדיר פונקציות פרטיות, אבל האמת שבזה אני לא רואה סיבה להשתמש. הנה 3 פיצ'רים אחרים שדווקא כן יכולים להיכנס לקוד שלי-
בדרך כלל כשחושבים על קוד "קריא" אנחנו חושבים על קוד שמי שקורא אותו יכול להבין מה הוא עושה יחסית בקלות, ועל קוד לא קריא בתור קוד שצריך מאוד להתאמץ כדי להבין מה קורה בו. כשמסתכלים על פונקציה בודדת קל לראות איזה דברים הופכים קוד ללא קריא למשל כשהפונקציה ארוכה מדי, כוללת המון תנאים ושמות המשתנים הם פשוט רצף אקראי של אותיות. במערכת גדולה כבר הרבה יותר קשה לחשוב על קוד קריא דרך היכולת "לקרוא" אותו, פשוט כי יש יותר מדי ממנו. וככל שיש יותר קוד ככה יהיה יותר קשה להבין מה כל חלק עושה ובמה הוא תלוי.
דרך נוספת לדבר על קוד קריא שאולי יותר מתאימה למערכות גדולות תהיה לבדוק כמה מסובך לענות על שאלה ספציפית לגבי הקוד, ואז להכין לעצמנו רשימה של שאלות שחשוב לנו שיהיה קל לענות עליהן. בניגוד ל"כמה הקוד מובן", כשיש לנו שאלה ספציפית בראש הקריטריון הופך ליותר קשיח.
בשביל הדוגמה נדמיין Service שכתוב בריאקט ומתחבר לשרת Backend, אבל גם מתחבר לכל מיני APIs ברשת. בקוד יש כמה מאות קומפוננטות, חלקן לוקחות מידע מה Backend דרך SWR, חלקן מעדכנות מידע באמצעות fetch וקומפוננטות אחרות משתמשות בממשקים ייעודיים של APIs חיצוניים כדי לתקשר עם אותם APIs. ועכשיו נשאל "איפה אני מוצא רשימה של כל בקשות הרשת שה Service שלי מסוגל להוציא?"
במקרה הטוב אם הקוד קריא ביחס לשאלה הזאת יהיה לנו במערכת קובץ אחד שמטפל בכל הבקשות שיוצאות מהיישום ומהווה "נקודת יציאה" לכל Backend או API. הקומפוננטות תמיד יעברו דרך נקודת יציאה זו וכך בשביל לענות על השאלה צריך רק לפתוח את הקובץ.
במקרה הפחות טוב כל קומפוננטה תקרא בדרך שלה ל API או ה Backend שהיא עובדת מולו, ונצטרך לעבור על הקומפוננטות אחת אחת כדי לאסוף את כל הקריאות.
קשה מאוד לכתוב קוד שיהיה קריא ביחס לכל שאלה, אבל לאורך זמן ככל שנאסוף יותר שאלות שמעניינות אותנו נוכל לכוון את הקוד שלנו כדי שיהיה קריא ביחס לאותן שאלות, וכך לשפר את איכות המערכת ומהירות הפיתוח.
אחת הדוגמאות הראשונות שאני מראה ל Web Sockets היא בניה של לוח מודעות משותף לכמה גולשים - כל פעם שגולש אחד שולח הודעה, ההודעה מיד מופיעה על המסך של כל האחרים. והיום ננסה לבנות את אותו לוח מודעות בלי לכתוב שורה של JavaScript, רק באמצעות המנגנונים המובנים ב Rails.
מה קורה כשיש באג במערכת שאתה לא מצליח למצוא? נכון, כותבים מעקף. מעקף הוא מנגנון שבגדול אומר "אין לי מושג למה הגענו לכאן, אבל אם כבר אתה כאן אז עדיף שתפנה ימינה". אנחנו מגיעים למעקפים דרך התרסקויות או באגים - פונקציה אמורה לקבל מספר ופתאום מופעלת עם מחרוזת ולכן מתרסקת. אחרי שלא הצלחת למצוא שום נתיב בקוד שגורם לפונקציה לקבל מחרוזת הצעד הבא הוא לעדכן את קוד הפונקציה לטפל גם במחרוזות, למשל:
function isInCanvas(x, y) {
if (typeof x === 'string') {
x = Number(x);
}
if (typeof y === 'string') {
y = Number(y);
}
// check if (x,y) is in the canvas
}
וכן יש שתי בעיות עם מעקפים:
הם מקשים על קריאת הקוד - יום אחד בעוד חודשיים (או שבועיים) כשמישהו יגיע להסתכל על הפונקציה הוא לא יבין למה היא צריכה להתמודד גם עם מחרוזות.
הם מתרבים ויוצרים תרבות של מעקפים - לאורך זמן מנהלים ומתכנתים מתמכרים לרעיון המעקפים ומוסיפים מעקף במקום להשקיע את הזמן בחיפוש הבאג האמיתי, מה שרק מחמיר את הבעיה כי אותו באג יכול להשפיע על עוד מקומות במערכת, ואז גם בהם נצטרך להוסיף מעקפים.
החיים זה לא ספר לימוד וברור שבמערכת אמיתית תצטרכו להכניס יותר ממעקף אחד. כדאי כשאתם עושים את זה להיות ברורים, להוסיף הערה בקוד שמסבירה מה מצב המעקף ומה מצאתם עד עכשיו והרציניים באמת גם יוסיפו ל Backlog ב Jira משימה חדשה להסיר את המעקף, כשיהיה זמן כמובן.
אם את עובדת עם React ולא יודעת איך לכתוב Custom Hook שווה לעצור עכשיו וללמוד את זה, מאחר ו Custom Hook הוא אחד המנגנונים הבסיסיים של שיתוף קוד בין קומפוננטות, ובכל פרויקט ריאקט מגיע הרגע שמישהו יכתוב אחד.
אבל אם באותו React את לא יודעת מה זה Server Side Rendering או למה להשתמש ב useDeferredValue
, לא קרה כלום. זה אולי יהיה מכשול בראיונות עבודה אבל בחיים האמיתיים יש הרבה פרויקטים שלא צריכים את הפיצ'רים האלה, ויותר מזה - יש הרבה פרויקטים בהם בגלל שינסו להטמיע את הפיצ'רים האלה הפרויקט רק יהפוך ליותר קשה לתחזוקה.
הייתי שמח אם בתיעוד של ספריה היינו רואים ליד כל פונקציה איזה דירוג שאומר כמה כדאי להשתמש בה - מ"אם אתה לא משתמש במנגנון הזה הקוד שלך שבור" ל-"אם אתה משתמש במנגנון הזה הקוד שלך שבור". עד שזה יקרה האחריות עלינו להפעיל שיקול דעת, וכדאי לזכור, רק בגלל שהפיצ'ר קיים לא אומר שהוא רלוונטי (או אי פעם יהיה) לעולם שלכם.
בדיוק כשחשבתי שמצאתי את האור ו TypeScript יתקן לי את כל הבאגים יחסינו הגיעו למשבר. הכל התחיל בפונקציה תמימה שלוקחת מחרוזת, מחליפה בתוכה כמה סימנים ומחזירה את הטקסט המוחלף:
function replaceByDictionary(text: string, replacements: Record<string, string>) {
let interpolatedText = text;
for (const [key, value] of Object.entries(replacements)) {
interpolatedText = interpolatedText.replace(key, value);
}
}
בלי לשים לב לבאג המשכתי להשתמש בפונקציה כדי לשלוח את הטקסט המוחלף לשרת:
const replacedText = replaceByDictionary("I love TypeScript", { "I": "Everyone" });
await fetch(`https://tocode.requestcatcher.com/test?text=${replacedText}`, { method: 'POST' });
רק כדי להישאר עם לב (וקוד) שבור כשגיליתי שהבקשה שתכל'ס נשלחה לשרת היתה עם ה URL:
POST /test?text=undefined
מה קרה כאן? למה טייפסקריפט פספס? התשובה פשוטה - הסקת טיפוסים אוטומטית גרמה לטייפסקריפט להגדיר את replacedText
בתור void, ואופרטור הגרש ההפוך יודע לקבל משתני void בתוך מחרוזות. חוקי לגמרי, ובכלל לא מה שהתכוונתי.
הפיתרון? כמו תמיד בתכנות אין פיתרונות קסם, אבל כן יש שתי אפשרויות מרכזיות:
אפשר להחליט שאנחנו לא אוהבים את הסקת הטיפוסים של טייפסקריפט, ומגדירים את replacedText
בתור string בעצמנו. זה אומר לכתוב:
const replacedText: string = replaceByDictionary("I love TypeScript", { "I": "Everyone" });
ועל זה טייפסקריפט כבר יצעק.
או (ולדעתי עדיף) להחליט להוציא לפונקציה את הקוד ששולח טקסט לשרת, ואז נקבל:
async function postText(text: string) {
await fetch(`https://tocode.requestcatcher.com/test?text=${replacedText}`, { method: 'POST' });
}
const replacedText = replaceByDictionary("I love TypeScript", { "I": "Everyone" });
await postText(replacedText);
בגישה כזאת אנחנו גם שומרים על הגמישות לשנות את קוד השליחה לשרת בעתיד, גם מפרידים בין הלוגיקה (לעדכן את הטקסט) לבין מה עושים עם התוצאה (שולחים לשרת), והכי חשוב, מקבלים מטייפסקריפט את ההגנה שרצינו.