תוכן עניינים
אם זה נראה כמו ברווז, מגעגע כמו ברווז, אבל דורש בטריות – אתה משתמש באבסטרקציה שגויה
היא נחשבת לפורצת דרך עבור נשים – היא מהראשונות לקבל תואר דוקטור למדעי המחשב (1968) ואחת משלוש בלבד שזכו בפרס טיורינג (2008) ב – 49 השנים בהן הוא מחולק.
ברברה ליסקוב (נולדה ב – 1939) תרמה רבות לעולם המחשוב, בפרט לתחום התכנות מונחה עצמים: מעל למאה מאמרים, שלושה ספרים, פיתוח מתודולוגיות, שפות תכנות ומערכות הפעלה ועוד.
אחד המאמרים המפורסמים ביותר שלה הוא גם העקרון השלישי בחמשת העקרונות הראשונים לעיצוב תוכנה מונחית עצמים: עקרון ההחלפה של ליסקוב.
לפני שנצא לדרך, הקדמה למצטרפים החדשים:
הפוסט הזה הוא פוסט שלישי בסדרת S.O.L.I.D / posts שבה אני מפרט על חמשת העקרונות הראשונים לעיצוב תוכנה מונחית עצמים.
אני ממליץ לקרוא על שני העקרונות הראשונים לפני שממשיכים לעקרון השלישי, שכן הם הכרחיים כדי ליישם אותו כמו שצריך:
שנתחיל?
הגדרת הקשר בין תת-סוג לסוג האב
ברברה ליסקוב הציגה לראשונה את עקרון ההחלפה (באנגלית: Liskov Substitution Principle) בשנת 1987.
המאמר שבו העקרון הוצג נכתב בשפה מאד טכנית ויבשה, עד כדי כך שאפילו בקהיליית מדעני המחשב התקשו לאמץ את העקרון:
אם עבור כל אובייקט o1 מסוג
S
ישנו אובייקט o2 מסוגT
כך שעבור כל תוכנהP
המוגדרת במונחים שלT
, ההתנהגות שלP
לא תשתנה כאשר o2 יוחלף ב – o1,
אזS
הינו תת-סוג שלT
אני הולך להמר שלפחות 80% מהקוראים לא הצליחו להבין מה מתרחש בנוסחה למעלה וזה בסדר: לפני שלמדתי את העקרון, גם אני לא הצלחתי להבין מה הגברת ליסקוב רוצה מחיי.
כשאני חושב על זה, זה מעט אירוני: עקרון ההחלפה של ליסקוב, לטעמי, הוא העקרון הפשוט ביותר מבין חמשת העקרונות.
למזלנו, העקרון נוסח מחדש על-ידי רוברט מרטין, ככה שיהיה (יותר) ידידותי לקורא.
לפני שאני מציג בפניכם את ההגדרה, חשוב לי ליישר קו ולוודא שאתם מכירים את המונחים "מחלקת בסיס" ו – "מחלקה נגזרת".
בקטע הקוד הבא:
1 2 3 4 5 6 7 |
class A { // ... } class B extends A { // ... } |
מתוארות שתי מחלקות – A
ו – B
.
A
– מחלקת בסיס: מחלקה ממנה יורשת מחלקה אחרת (B
)B
– מחלקה נגזרת: מחלקה שיורשת ממחלקה אחרת (A
)
עבור קטע הקוד למעלה, ניתן לומר כי המחלקה B
היא מחלקה נגזרת ממחלקת הבסיס A
.
עכשיו כשהמונחים הוגדרו, אני יכול לשתף איתכם ללא חשש את התרגום לניסוח-מחדש של עקרון ההחלפה של ליסקוב:
פונקציות שעושות שימוש ביכולת של עצם מסוג מחלקת בסיס חייבות להיות מסוגלות להשתמש בעצמים מסוג מחלקה נגזרת, מבלי לדעת שהן עושות זאת.
עקרון ההחלפה של ליסקוב: על ריבועים ומלבנים
במרבית המקורות, כולל במאמר המקור של רוברט מרטין, משתמשים בדוגמה של מלבן וריבוע כדי להסביר את העקרון.
מוצגת לפניכם מחלקה שמתארת מלבן:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Rectangle { protected $width; protected $height; public function setWidth($w) { $this->width = $w; } public function setHeight($h) { $this->height = $h; } public function getWidth() { return $this->width; } public function getHeight() { return this->height; } } |
בבואנו להגדיר ריבוע, נשתמש במחלקה Rectangle
כמחלקת-בסיס.
וכאן אנחנו נתקלים בבעיה. ויקיפדיה מגדירה ריבוע:
ריבוע הוא מרובע בעל ארבע צלעות שוות
מחלקת המלבן מאפשרת למשתמש להגדיר רוחב וגובה שונים; כלומר צלעות לא-שוות, בעוד שכל צלעות הריבוע שוות זו לזו.
כדי להתגבר על הבעיה, נדרוס את הפונקציות setWidth
ו – setHeight
כשניישם את המחלקה Square
:
1 2 3 4 5 6 7 8 9 10 |
class Square extends Rectangle { public function setWidth($w) { $this->width = $this->height = $w; } public function setHeight($h) { this->setWidth($h); } } |
כעת, בכל פעם שמגדירים רוחב או גובה – גם הפרמטר השני משתנה בהתאם.
כביכול, פתרנו את הבעיה – עצם מסוג ריבוע לעולם יהיה בעל צלעות שוות.
אבל מה יקרה כשנכתוב פונקציה שמטפלת בטיפוס נתונים Rectangle
? (רמז: בניינים יתחילו להתמוטט..)
ניקח לדוגמה את הפונקציה הבאה, שמקבלת כפרמטר עצם מסוג Rectangle
, קובעת רוחב (5) וגובה (4) ואז בודקת באמצעות הפקודה assert
האם רוחב כפול גובה שווה 20:
1 2 3 4 5 |
public function g(Rectangle $r) { $r->setWidth(5); $r->setHeight(4); assert ($r->getWidth() * $r->getHeight() == 20); } |
זכרו: על-פי עקרון ההחלפה של ליסקוב, על הפונקציה לדעת להתמודד עם כל עצם מסוג Rectangle
!
מי שכתב את הפונקציה g
הניח שבעת שינוי רוחב של מלבן, הגובה נשאר כפי שהוא ולהפך.
על-פניו, הנחה הגיונית וסבירה – אבל ברגע שנעביר לפונקציה Square
ולא Rectangle
– הפונקציה תחזיר ערך שגוי.
המחלקה Square
(המחלקה הנגזרת) משנה את ההתנהגות הטבעית של המחלקה Rectangle
(מחלקת הבסיס).
המשפט האחרון, גבירותיי ורבותיי, הוא תמצית עקרון ההחלפה של ליסקוב:
מחלקה מרחיבה לעולם לא תשנה את ההתנהגות הטבעית של מחלקת הבסיס
איך נפתור את הבעיה? לצערי, אני נאלץ להודיע לכם שאין פתרון קסם!
במקרה הזה, המסקנה המתבקשת היא שריבוע ומלבן הם שני עצמים שונים ללא קשר לוגי מובהק ולכן – יש להפריד ביניהם – אין הצדקה בלבסס את מחלקת הריבוע על מחלקת המלבן ויותר מזה – אין הצדקה לבצע בדיקה זהה של חישוב שטח עבור שניהם.
מישהו שלא מכיר את עקרון יישום פתוח-סגור, היה מציע להוסיף תנאי שבודק האם המלבן הוא ריבוע:
1 2 3 4 5 6 7 8 9 10 11 |
public function g(Rectangle $r) { if($r instanceof Square) { $r->setWidth(5); assert ($r->getWidth() * $r->getHeight() == 25); } else { $r->setWidth(5); $r->setHeight(4); assert ($r->getWidth() * $r->getHeight() == 20); } } |
כמובן שההצעה הזו היא לא פתרון, כי היא מאלצת אותנו ליידע את הפונקציה מהו סוג הנתונים המדוייק איתו היא עובדת ומביאה להפרה של עקרון יישום פתוח-סגור.
במאמרו, רוברט מרטין כתב (בתרגום חופשי): תקינות איננה פנימית. מה הכוונה?
אם נבחן בנפרד כל אחת מהמחלקות, Rectangle
ו – Square
, נגלה שבשורה התחתונה, שתיהן מתפקדות כצפוי:
Rectangle
מאפשרת לנו לקבוע רוחב וגובה למלבןSquare
מאפשרת לנו לקבוע אורך צלע
אבל זו הסתכלות שגויה מן היסוד, כי כאשר לא מסתכלים על התמונה הגדולה, אנחנו מתעלמים מהעובדה שהלקוח, כלומר מי שצורך את המחלקות ומשתמש בהן, יניח הנחות יסוד סבירות והגיוניות;
בדוגמה של המלבן והריבוע, הנחת היסוד שכל עצם מסוג Rectangle
(ולהזכירכם, Square
הוא כזה) יתנהג באופן עקבי, תתברר כשגויה: כמו שכבר בטח הבנתם, ההתנהגות של Square שונה בתכלית מההתנהגות של Rectangle.
כלל האצבע
כשלמדתי על עקרון ההחלפה של ליסקוב, נתקלתי בכלל האצבע הבא:
מחלקות ממומשות צריכות להיות סופיות
במחשבה ראשונה, זה נשמע קצת קיצוני. אבל כשחושבים על זה לעומק, יש כמה סיבות לא רעות למה לא להרחיב מחלקה ממומשת זה הרגל טוב:
- אנחנו כבולים ליישום של מחלקת הבסיס
- אנחנו עשויים לדרוס פונקציות קיימות
- כל הפונקציות הציבוריות זמינות גם למחלקה המרחיבה, למרות שלא בהכרח יתאימו לשימוש בה
באופן אישי, הגעתי למסקנה שכלל האצבע הזה הוא טוב, אבל גם לא טוב: כאשר אנחנו מיישמים מחלקה, עלינו להחליט האם היא ניתנת להרחבה או לא.
כלל האצבע שאני אימצתי – הוא להימנע מכללי אצבע: כאשר אני נדרש להחליט האם להרחיב מחלקה או לא וכאשר אני נדרש להחליט האם מחלקה שכתבתי היא סופית או לא, להפעיל שיקול דעת ולהגיע להחלטה בהתאם למקרה הפרטי.
מקורות
- מאמר המקור מאת רוברט מרטין


6 תגובות
יותם
24 במאי 2017 - 18:01היי,
קצת צורם לי שהמקרה הכי קלאסי להורשה (ריבוע הוא מרובע – בעברית זה מבלבל), צריך למנוע ממנו הורשה בגלל פונקציית בדיקה מאוד מוזרה (שבעצם בודקת את מנגנון ההשמה, שליפה או כפל?).
במידה ומכניסים את פונקציית חישוב השטח לתוך האובייקט נפתרה הבעיה (דבר שנראה לי מתבקש).
לי נראה שכל הורשה אפשר להתקיל ככה(בצורה מכוונת),
במיוחד היום עם מנגנוני ה-data-binding שינוי קטן יכול להוביל לשרשרת של שינויים שצריך לקחת בחשבון.
עדי
12 ביולי 2017 - 8:12מתי תעלו את שני העקורנות שנשארו?
יונתן נקסון
12 ביולי 2017 - 11:29מבטיח שלא שכחתי אותם! יעלו בהקדם האפשרי 🙂
אילנה
11 בינואר 2018 - 11:20מוסבר היטב!
מחכה לעקרונות הבאים
🙂
ד
15 באוגוסט 2018 - 16:31מתי מועלים העקרונות הבאים ?
ד
13 ביוני 2019 - 16:36יונתן, לא שכחת אותנו ? רשמת שיעלו בהקדם האפשרי.