יואל ספולסקי (Joel Spolsky), יהודי אמריקאי ישראלי, אחד המייסדים של Stack Overflow ואושיית תכנות מפורסמת, כתב ב-2002 מאמר על בעיית האבסטרקציות (ההפשטות) הדולפות, מאמר שהיה לאבן פינה כאשר דנים באבסטרקציות בתכנות וביכולת שלהם להסתיר את המימוש הפנימי שלהם. משפט המפתח של המאמר הוא "All non-trivial abstractions, to some degree, are leaky", ובתרגום חופשי לעברית: "כל האבסטרקציות הלא טריוויאליות דולפות במידה כלשהי".
במאמר זה נסביר לכם מהי אבסטרקציה דולפת, נזכיר כמה דוגמאות שהביא ספולסקי (TCP ו-SQL) אשר נגעו במקרים בהם זמן הריצה או האפשרות להריץ את התוכנית הושפעו כתוצאה מדליפת האבסטרקציה. אנו נוסיף על הדוגמאות הללו מקרים בהם דליפת האבסטרקציה משפיעה גם על נכונות התוצאה של הקוד שאנו כותבים.

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

פרוטוקול TCP
אחת הדוגמאות הקלות להבנה המוזכרות במאמר של ספולסקי היא הפרוטוקול TCP.
הפרוטוקול שקדם ל-TCP הוא הפרוטוקול IP, בו כדי לשלוח הודעות ממחשב למחשב עושים משהו שדומה לאופן בו אנחנו שולחים דואר באמצעות דואר ישראל: אנחנו שמים את המכתב במעטפה, כותבים את כתובת היעד ומקווים לטוב. אף אחד לא מבטיח לנו שהמכתב יגיע ליעדו, או שהוא יגיע שלם ללא נזק ושהוא יגיע תוך פרק זמן סביר. באופן דומה, כאשר שולחים ממחשב למחשב הודעות באמצעות הפרוטוקול IP ההודעות נשלחות לרשת עם כתובת היעד שלהן, אך דבר לא מבטיח לנו שהן תגענה כלל, שהן תגענה כאשר הן תקינות או שההגעה תשמור על הסדר בו הן נשלחו.
מכיוון שקשה לדמיין תקשורת טובה בין מחשבים תחת התנאים שמבטיח הפרוטוקול IP, נוצר הפרוטוקול TCP שאמור לסתום את החורים שהותיר IP. על פי הפרוטוקול, שולחים את ההודעות דרך הפרוטוקול IP הלא אמין, אך בעזרת מנגנון של אישורי קבלה וניסיונות שליחה חוזרים של הודעות שנכשלו ניתן להבטיח כי כל ההודעות תגענה ליעדן שלמות ובסדר בו הן נשלחו.
הפרוטוקול TCP בא לחפות על החסרונות של IP באמצעות אבסטרקציה לדרך בה ההודעות נשלחות בפועל – בדיוק באותו הפרוטוקול הבעייתי IP. האבסטרקציה אמורה להסתיר מהמשתמש את העובדה שהרשת בבסיסה היא מקום בו לא ניתן להבטיח שליחה נאותה של הודעות ממחשב למחשב. אך האבסטרקציה הזאת היא אבסטרקציה דולפת מכיוון שבפועל אין אפשרות להסתיר מהמשתמש את העובדה הזאת: כאשר הרשת עמוסה או תקולה הפרוטוקול TCP ייאלץ לשלוח הודעות שוב ושוב מה שישפיע במקרה הטוב על מהירות העברת הנתונים, ובמקרה הגרוע על עצם האפשרות להעביר נתונים. הפרוטוקול TCP, למרות ההבטחה שלו להעברת נתונים מושלמת, לא מצליח להסתיר את העובדה שהוא ממומש על גבי פרוטוקול שאינו אמין והמשתמש בו חייב להכיר זאת ולהתכונן בהתאם.
שאילתות SQL
דוגמה נוספת לאבסטרקציות דולפות שמביא ספולסקי היא שאילתות בבסיס נתונים. SQL מאפשרת למפתח להגדיר בצורה הצהרתית אילו נתונים הוא צריך בלי לחשוב על הצורה בה השרת מאחזר אותם בפועל. למרות זאת, אם לא נבין כיצד עובד מנוע ה-SQL נוכל לעשות טעויות רבות שיעלו לנו בביצועים לא טובים. ניקח לדוגמה את שתי השאילתות הבאות:
קל לראות כי שתי השאילתות שקולות לחלוטין ובכל המקרים יאוחזרו אותם נתונים משתיהן. למרות זאת, ייתכן שמנוע ה-SQL לא יבין שהתנאי השלישי הוא מיותר ועבור כל שורה יבדוק גם את התנאי הזה, מה שיגזול זמן וכוח חישוב. כך שעל אף שמנוע ה-SQL מצהיר שאפשר רק להגדיר מה הנתונים שאנחנו צריכים והוא ידאג לאחזר אותם, עדיין פרטי המימוש של המנוע דולפים ואנחנו כמשתמשים מושפעים מצורת המימוש.
ORM
ORM הוא Object-relational mapping – טכניקה להמרת טבלאות בבסיסי נתונים לאובייקטים, והוא מאפשר לתשאל את הנתונים בשפת התכנות בה אנו כותבים. כמעט לכל שפת תכנות קיים ORM ולרוב קיימים כמה כאלו. ה-ORM הוא שכבה המשמשת הפשטה מעל מסד הנתונים שאמורה לתת לנו את האפשרות להשתמש במסדי נתונים גם כאשר אנו לא בקיאים ב-SQL. מכיוון שההפשטה הזו רחוקה מלהיות טריוויאלית, מטבע הדברים לעיתים קרובות היא דולפת ולמשתמשים אין מנוס אלא לדעת איך בנוי ה-ORM ותחת אילו מגבלות, וכתוצאה מכך אילו שאילתות SQL נוצרות בפועל. בלי ההבנה הזו המפתח נעשה שיגרמו לשאילתות להיות איטיות מאוד או אפילו לקבל תוצאות אחרות מהרצוי.
נתבונן בקוד הבא, במיוחד בשורות המסומנות ב-1 ו-2 ובפלט שלהן המתואר אחריהן:
אנו רוצים לקבל את כל ה-User
שאין עבורם ערך בשדה name
ומשתמשים בספרייה SQLAlchemy כדי להמיר השוואות פייתוניות לשאילתות SQL. אלא שלספרייה אין כל דרך למנוע את ההתנהגות הבסיסית של פייתון: כאשר האינטרפרטר של פייתון רואה את הביטוי User.name is None
בדוגמה הראשונה, הוא מפרש את הביטוי כ-False ומעביר ל-SQLAlchemy את הערך אחרי החישוב, מה שהופך לשאילתה שהתנאי בה נכשל תמיד. כדי לתת לספרייה את האפשרות לדחות את חישוב הביטוי ולהעביר אותו למנוע ה-SQL, אנחנו צריכים להשתמש בפונקציה _is
, כפי שממומש בדוגמה השנייה. כך שעל אף ש-ה-ORM מתיימר להסתיר מהמפתחים את השימוש ב-SQL ולתת להם לכתוב בפייתון כרגיל עדיין צפים ההבדלים בין קוד SQL לקוד פייתון אמיתי, ומשתמשים נאיביים שסוברים ש-ORM מסוגל לחולל קסמים יקבלו שאילתה שגויה.
חישוב נקודה צפה
אחד הדברים הבסיסיים להם קיימת אבסטרקציה בשפות התכנות העיליות הוא חישוב אריתמטי. אנו מבקשים מהמערכת שתחזיר לנו את התוצאה של חישוב כלשהו, נניח 1+1, ומצפים לקבלת התשובה הנכונה. ברור לנו שבדרך לתוצאת החישוב הפשוט מתבצעת מאחורי הקלעים עבודה רבה, אך שפת התכנות אמורה למסך לנו את הפרטים הטכניים הללו. אך במקרים רבים אפילו ביצוע של חישוב פשוט עלול להיפגע כתוצאה מהעבודה שהאבסטרקציה דולפת. ניקח למשל את הקלט והפלט הבאים בשפת פייתון:
מה קרה כאן?
אנו יודעים שבייצוג השבר העשרוני יש שברים שלא יכולים להיות מיוצגים באופן מדויק. למשל, ⅓ מיוצג על ידי השבר העשרוני האינסופי …0.33333. אנו יכולים לרשום עוד ועוד ספרות ולשפר את הדיוק של השבר, אבל בסופו של דבר נצטרך להתפשר על רמת דיוק מסוימת ולומר שהערך שקיבלנו קרוב בשבילנו מספיק לערך האמיתי של השבר.
בדיוק כמו בייצוג העשרוני המוכר לנו מחיי היום יום, כך גם בייצוג הבינארי בו נשמרים הערכים בזכרון המחשב, יש שברים שלא יכולים להיות מיוצגים בדיוק. למשל, המספר העשרוני 0.1 מיוצג בשיטה הבינארית על ידי השבר האינסופי …0.0001100110011. למעשה כל שבר שהמכנה שלו איננו חזקה של 2 לא יכול להיות מיוצג בדיוק בשיטת ההצגה הבינארית של הנקודה הצפה. גם כאן, אנחנו יכולים להקצות ביטים נוספים כדי להבטיח רמת דיוק גדולה יותר, אך לעולם לא נוכל להגיע לדיוק מושלם.
מכיוון שגם 0.1 וגם 0.2 אינם מיוצגים בדיוק מושלם, ממילא גם תוצאת החיבור שלהם אינה מדויקת. ניתן לראות כאן איזה מספר מיוצג בדיוק כאשר אנו מזינים שבר למערכות שונות.
ההתנהגות הזאת אינה ייחודית לפייתון. בדוגמת קוד הפשוטה הזו ניתן לראות תוצאה זהה ב-JAVA וכאן ב-JavaScript. באתר שמוקדש בדיוק לנושא הזה, בכתובת החיננית https://0.30000000000000004.com, ניתן לראות איזו תוצאה מחזיר החישוב שראינו בכמה שפות תכנות שונות.
מכאן שהאבסטרקציה של החישובים האריתמטיים לא מצליחה להסתיר פרטי מימוש רבים שלה: שהמספרים בזיכרון מיוצגים בייצוג בינארי ושלייצוג מוקצה מספר מסוים של ביטים שמשפיע על הדיוק האפשרי.
מה אפשר לעשות?
אז אחרי שאנו מודעים לפרטי המימוש שמשפיעים על תוצאת החישוב שלנו, מה אנחנו יכולים לעשות? אפשר לשקול כמה גישות. במקום בו אנו צריכים להשוות בין ערכים של נקודה צפה נוכל להשתמש בהשוואה עד כדי אפסילון במקום בהשוואה מדויקת:
אנו עדיין צריכים להיות מודעים לרמת הדיוק של ערכי הנקודה הצפה של המערכת שלנו כדי לדעת לבחור אפסילון שמתאים גם לדיוק הזה וגם לצרכי הקוד שלנו.
אפשר לנקוט גישה דומה ולעגל את המספרים:
גם כאן, צריכים להיזהר בבחירת הפרמטר שקובע כמה ספרות אחרי הנקודה העשרונית יישמרו אחרי תהליך העיגול ולהיזהר שפעולות עיגול חוזרות ונשנות לא יגרמו לסחיפה משמעותית של הערך שאנו שומרים מהערך האמיתי.
שיטה נוספת שאפשר לשקול היא שימוש בספריות ייעודיות לחישובי שברים. למשל, הנה מימוש של החישוב לעיל בפייתון בספריית fractions או בספריות דומות. נשים לב שהפתרון אינו אידיאלי: הוא נראה מסורבל ומועד לטעויות, וגם זמן הריצה שלו ארוך יותר משמעותית.
שפות התכנות אמורות להסתיר מאיתנו את המימוש של מבנה המחשב הפנימי המימוש של פקודות המכונה אבל בפועל המימוש מחלחל למעלה למשתמשים והם צריכים להכיר איך ממומשות הפעולות האריתמטיות.
הערה לסיום
על הדוגמאות לעיל התבוננו מנקודת המבט של המשתמשים באבסטרקציה, והבנו כמה חשוב להכיר גם את שכבות המימוש, במיוחד כאשר הן אינן טריוויאליות. כצרכנים של מוצרים ושירותים אנחנו צריכים להיות מודעים לכך שמתישהו האבסטרקציה תדלוף ולא להאמין להבטחות שווא. עם זאת, חשוב לזכור זאת גם כאשר אנו מפתחים את שכבת האבסטרקציה. בתכנון ובכתיבת האבסטרקציה נדאג שהמשתמשים שלנו יהיו מודעים לכמה שפחות פרטי מימוש וכמובן שנרצה להימנע ממצב בו המימוש שלנו עצמו מטיל על המשתמשים בו את הצורך להכיר את פרטיו.
מצויין כרגיל
אחלה פוסט!!!!!!1 לגבי הדוגמא ב*ORM* אני לא מסכים, אין פה דליפה כלל, בסוף שלחת ב*filter*. לדעתי דליפה בORM היא ב*יחסים*. לדוגמא user.friends יכול לגרום לשאילתא יקרה והמתכנת צריך לדעת להזהר מכך.
אתה צודק. חשבנו להביא דוגמאות של דליפות ברמות שונות של הבנה, ואכן כאן ההתנהגות מפתיעה רק מישהו שחושב ש-ORM הוא קסם. הדגשנו את זה בפוסט עקב ההערה שלך.
כתוב מצוין וברור. תודה!
תודה על הפידבק יחזקאל.
אחלה מאמר.
מרגיש שORM עושה לנו חיים קשים בCSR 🙂
תודה רבה.
וואו מאמר מאד מעניין
תודה אסף.
החיים הקלים!!
כתיבה מעולה, והסבר מיוחד!!
לגבי חישובי נקודה עשרונית: ברוב השפות העיליות, כחלק מה type system הסטנדרטי, יש גם Decimal. זה טיפוס מדויק יותר, אבל משמעותית איטי יותר.