עקרון החלפת ליסקוב בג'אווה

1. סקירה כללית

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

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

2. העיקרון הפתוח / סגור

כדי להבין את עקרון ההחלפה של ליסקוב, עלינו להבין תחילה את העקרון הפתוח / סגור ("O" מ- SOLID).

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

3. מקרה שימוש לדוגמא

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

3.1. ללא העיקרון הפתוח / סגור

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

ה BankingAppWithdrawalService משרת את פונקציונליות המשיכה למשתמשיה:

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

3.2. שימוש בעקרון פתוח / סגור כדי להרחיב את הקוד

בואו נעצב מחדש את הפיתרון כך שיעמוד בעקרון הפתוח / סגור. נסגור BankingAppWithdrawalService משינוי כאשר יש צורך בסוגי חשבונות חדשים, באמצעות חֶשְׁבּוֹן מחלקת בסיס במקום:

הנה, הצגנו תקציר חדש חֶשְׁבּוֹן מחלק את זה חשבון נוכחי ו חסכון חשבון לְהַאֲרִיך.

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

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

3.3. קוד Java

בואו נסתכל על הדוגמה הזו ב- Java. ראשית, בואו נגדיר את חֶשְׁבּוֹן מעמד:

חשבון בכיתת מופשט ציבורי חשבון {פיקדון ריק מופשט מוגן (סכום BigDecimal); / ** * מפחית את יתרת החשבון בסכום שצוין * הסכום הנתון> 0 והחשבון עומד בקריטריוני היתרה הזמינים *. * * סכום @param * / משיכת חלל מופשטת מוגנת (סכום BigDecimal); } 

ובואו נגדיר את BankingAppWithdrawalService:

בכיתה ציבורית BankingAppWithdrawalService {חשבון חשבון פרטי; BankingAppWithdrawalService ציבורי (חשבון חשבון) {this.account = חשבון; } משיכת חלל ציבורי (סכום BigDecimal) {account.withdraw (סכום); }}

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

3.4. סוג חשבון חדש

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

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

אז בואו נעשה FixedTermDepositAccount תת מחלקה של חֶשְׁבּוֹן:

מחלקה ציבורית FixedTermDepositAccount מאריך את החשבון {// שיטות נשללות ...}

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

זה אומר שהחדש FixedTermDepositAccount הכיתה לא יכולה לספק משמעותית את לָסֶגֶת שיטה ש חֶשְׁבּוֹן מגדיר. פיתרון נפוץ אחד לכך הוא ביצוע FixedTermDepositAccount לזרוק לא נתמךOperationException בשיטה שהיא לא יכולה למלא:

מחלקה ציבורית FixedTermDepositAccount מרחיב חשבון {@Override פיקדון בטל מוגן (סכום BigDecimal) {// הפקדה לחשבון זה} @Outride בטל בטל משיכה (סכום BigDecimal) {זרוק חדש לא נתמךOperationException ("משיכות לא נתמכות על ידי FixedTermDepositAccount !!"); }}

3.5. בדיקה באמצעות סוג חשבון חדש

בעוד שהכיתה החדשה עובדת בסדר, בואו ננסה להשתמש בה עם ה- BankingAppWithdrawalService:

חשבון myFixedTermDepositAccount = FixedTermDepositAccount חדש (); myFixedTermDepositAccount.deposit (BigDecimal חדש (1000.00)); BankingAppWithdrawalService winningService = חדש BankingAppWithdrawalService (myFixedTermDepositAccount); winningService.withdraw (BigDecimal חדש (100.00));

באופן לא מפתיע, האפליקציה הבנקאית קורסת עם השגיאה:

משיכות אינן נתמכות על ידי FixedTermDepositAccount !!

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

3.6. מה השתבש?

ה BankingAppWithdrawalService הוא לקוח של חֶשְׁבּוֹן מעמד. הוא מצפה ששניהם חֶשְׁבּוֹן ותתי הסוגים שלה מבטיחים את ההתנהגות שה- חֶשְׁבּוֹן המחלקה ציין עבור שלה לָסֶגֶת שיטה:

/ ** * מפחית את יתרת החשבון בסכום שצוין * הסכום הנתון הניתן> 0 והחשבון עומד בקריטריוני היתרה הזמינים *. * * סכום @ param * / משיכת בטל ריק מוגן (סכום BigDecimal);

עם זאת, בכך שאינו תומך ב- לָסֶגֶת השיטה, FixedTermDepositAccount מפר את מפרט השיטה הזה. לכן, איננו יכולים להחליף באופן מהימן FixedTermDepositAccount ל חֶשְׁבּוֹן.

במילים אחרות, ה FixedTermDepositAccount הפר את עקרון ההחלפה של ליסקוב.

3.7. האם איננו יכולים לטפל בשגיאה ב BankingAppWithdrawalService?

נוכל לתקן את העיצוב כך שהלקוח של חֶשְׁבּוֹןשל לָסֶגֶת השיטה צריכה להיות מודעת לשגיאה אפשרית בקריאתה. עם זאת, פירוש הדבר שלקוחות צריכים להיות בעלי ידע מיוחד בהתנהגות תת-סוג לא צפויה. זה מתחיל לשבור את העיקרון פתוח / סגור.

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

בואו נסתכל בפירוט על עקרון ההחלפה של ליסקוב.

4. עקרון ההחלפה של ליסקוב

4.1. הַגדָרָה

רוברט סי מרטין מסכם זאת:

סוגי המשנה חייבים להחליף את סוגי הבסיס שלהם.

ברברה ליסקוב, שהגדירה אותו בשנת 1988, סיפקה הגדרה מתמטית יותר:

אם עבור כל אובייקט o1 מסוג S קיים אובייקט o2 מסוג T כך שלכל התוכניות P המוגדרות במונחי T, ההתנהגות של P אינה משתנה כאשר o1 מוחלף ל- o2 אז S הוא תת-סוג של T.

בואו ונבין קצת יותר את ההגדרות האלה.

4.2. מתי ניתן להחליף תת-סוג לסוג-העל שלו?

תת-סוג אינו הופך אוטומטית להחלפה לסוג העל שלו. כדי להיות מוחלף, תת הסוג חייב להתנהג כמו סוג העל שלו.

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

הקלדת משנה ב- Java מחייבת את מאפייני המחלקה הבסיסית והשיטות זמינות בתת-מחלקה.

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

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

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

5. Refactoring

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

5.1. גורם השורש

בדוגמה, שלנו FixedTermDepositAccount לא היה תת-סוג התנהגותי של חֶשְׁבּוֹן.

העיצוב של חֶשְׁבּוֹן הנחתי באופן שגוי שכולם חֶשְׁבּוֹן סוגים מאפשרים משיכות. כתוצאה מכך, כל תתי הסוגים של חֶשְׁבּוֹן, לְרַבּוֹת FixedTermDepositAccount שאינו תומך בנסיגות, ירש את לָסֶגֶת שיטה.

אם כי יכולנו לעקוף זאת על ידי הארכת החוזה של חֶשְׁבּוֹן, ישנם פתרונות חלופיים.

5.2. תרשים כיתות מתוקן

בואו נעצב את ההיררכיה של החשבון שלנו אחרת:

מכיוון שכל החשבונות אינם תומכים במשיכות, העברנו את לָסֶגֶת שיטה מה- חֶשְׁבּוֹן כיתה לתת-מופע חדש מופשט חשבון נשלף. שניהם חשבון נוכחי ו חשבון חסכון לאפשר משיכות. אז עכשיו הם הפכו למקטעי משנה של החדש חשבון נשלף.

זה אומר BankingAppWithdrawalService יכול לסמוך על סוג החשבון הנכון שיספק את לָסֶגֶת פוּנקצִיָה.

5.3. משוחזר BankingAppWithdrawalService

BankingAppWithdrawalService עכשיו צריך להשתמש ב- חשבון נשלף:

מעמד ציבורי BankingAppWithdrawalService {פרטי WithdrawableAccount pullableAccount; BankingAppWithdrawalService (WithdrawableAccount pullableAccount) ציבורי {this.withdrawableAccount = pullableAccount; } משיכת חלל ציבורית (סכום BigDecimal) {pullableAccount.withdraw (סכום); }}

בנוגע ל FixedTermDepositAccount, אנו שומרים חֶשְׁבּוֹן כמעמד ההורים שלה. כתוצאה מכך, הוא יורש רק את לְהַפְקִיד התנהגות שהיא יכולה למלא בצורה אמינה וכבר לא יורשת את לָסֶגֶת שיטה שהיא לא רוצה. עיצוב חדש זה נמנע מהבעיות שראינו קודם.

6. כללים

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

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

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

עלינו לציין כמה מינוחים כאן. סוג רחב הוא כללי יותר - לְהִתְנַגֵד למשל יכול להיות כל אובייקט Java והוא רחב יותר מאשר, למשל, CharSequence, איפה חוּט הוא מאוד ספציפי ולכן צר יותר.

6.1. כלל חתימה - סוגי טיעוני שיטה

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

כללי דריסת השיטה של ​​Java תומכים בכלל זה על ידי אכיפה כי סוגי הארגומנטים הנדרשים תואמים בדיוק לשיטת supertype.

6.2. כלל חתימה - סוגי החזרה

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

מחלקה מופשטת ציבורית Foo {תקציר ציבורי Number createNumber (); // שיטות אחרות} 

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

בר המחלקה הציבורית מרחיב את Foo {@Override Integer public generateNumber () {החזר מספר שלם חדש (10); } // שיטות אחרות}

כי מספר שלם הוא מספר, קוד לקוח שמצפה מספר יכול להחליף פו עם בָּר בלי שום בעיות.

מצד שני, אם השיטה הנדרשת ב- בָּר היו מחזירים סוג רחב יותר מ מספר, למשל לְהִתְנַגֵד, שיכול לכלול כל תת סוג של לְהִתְנַגֵד לְמָשָׁל א מַשָׂאִית. כל קוד לקוח שהסתמך על סוג ההחזר של מספר לא יכול היה להתמודד עם מַשָׂאִית!

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

6.3. כלל חתימה - חריגים

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

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

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

6.4. כלל מאפיינים - משתנה בכיתה

משתנה בכיתה הוא קביעה הנוגעת לתכונות האובייקט שחייבות להיות נכונות לכל המצבים החוקיים של האובייקט.

בואו נסתכל על דוגמה:

מכונית מופשטת ציבורית מכונית {מוגבלת int מוגבלת; // משתנה: מהירות <הגבלה; מהירות int מוגנת; // תנאי פוסט: מהירות <הגבלת ריק מופשט מוגן להאיץ (); // שיטות אחרות ...}

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

בואו נגדיר תת-מחלקה של אוטו המשמר את המשתנה בכיתה:

המעמד הציבורי HybridCar מרחיב את המכונית {/ / משתנה: תשלום> = 0; חיוב פרטי פרטי; @Override // postcondition: speed <limit מוגבל ריק להאיץ () {// Accelerate HybridCar להבטיח מהירות <limit} // שיטות אחרות ...}

בדוגמה זו, המשתנה ב אוטו נשמר על ידי הנדרסים להאיץ שיטה ב מכונית היברידית. ה מכונית היברידית מגדיר בנוסף את משתנה הכיתה שלה תשלום> = 0וזה בסדר גמור.

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

6.5. כלל נכסים - אילוץ היסטוריה

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

בואו נסתכל על דוגמה:

מכונית מופשטת ציבורית מכונית {// מותר לקבוע פעם אחת בעת היצירה. // הערך יכול להתגבר רק לאחר מכן. // לא ניתן לאפס את הערך. קילומטראז 'מוגן; רכב ציבורי (קילומטראז ') {this.mileage = קילומטראז'; } // מאפיינים ושיטות אחרות ...}

ה אוטו מחלקה מציינת מגבלה על ה- מִספָּר הַמַיִלִים תכונה. ה מִספָּר הַמַיִלִים ניתן להגדיר נכס פעם אחת בלבד בזמן היצירה ולא ניתן לאפס אותו לאחר מכן.

בואו נגדיר כעת א מכונית צעצוע שמתארך אוטו:

מחלקה ציבורית ToyCar מרחיב את המכונית {reset public public () {mileage = 0; } // מאפיינים ושיטות אחרות}

ה מכונית צעצוע יש שיטה נוספת אִתחוּל שמאפס את ה- מִספָּר הַמַיִלִים תכונה. בכך, ה מכונית צעצוע התעלם מהאילוץ שהטילה הוריו על מִספָּר הַמַיִלִים תכונה. זה שובר כל קוד לקוח שמסתמך על האילוץ. כך, מכונית צעצוע אינו תחליף ל אוטו.

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

6.6. כלל שיטות - תנאים מוקדמים

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

Class public Foo {// תנאי מוקדם: 0 <num <= 5 public void doStuff (int num) {if (num 5) {throw new IllegalArgumentException ("קלט מחוץ לטווח 1-5"); } // קצת היגיון כאן ...}}

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

בואו נעקוף את ה- לעשות דברים שיטה עם תנאי מוקדם מוחלש:

Bar class class Bar מרחיב את Foo {@Override // תנאי מוקדם: 0 <num <= 10 public void doStuff (int num) {if (num 10) {throw new IllegalArgumentException ("קלט מחוץ לטווח 1-10"); } // קצת היגיון כאן ...}}

כאן, התנאי המוקדם נחלש בביטול לעשות דברים שיטה ל 0 <num <= 10, המאפשר מגוון רחב יותר של ערכים עבור מספר. כל הערכים של מספר שתקפים ל Foo.doStuff תקפים ל בר.dוסטוף גם כן. כתוצאה מכך, לקוח של Foo.doStuff לא מבחין בהבדל כשהוא מחליף פו עם בָּר.

לעומת זאת, כאשר תת-סוג מחזק את התנאי המוקדם (למשל. 0 <num <= 3 בדוגמה שלנו), הוא מחיל מגבלות מחמירות יותר מאשר סוג העל. לדוגמה, ערכים 4 & 5 עבור מספר תקפים ל Foo.doStuff, אך אינם תקפים עוד עבור בר.dוסטוף.

זה ישבור את קוד הלקוח שאינו מצפה לאילוץ הדוק החדש הזה.

6.7. כלל שיטות - תנאי פוסט

תנאי לאחר הוא תנאי שיש לעמוד בו לאחר ביצוע שיטה.

בואו נסתכל על דוגמה:

מחלקה מופשטת ציבורית מכונית {מהירות int מוגנת; // מצב שלאחר מכן: על המהירות להפחית את בלם הריק המופשט המוגן (); // שיטות אחרות ...} 

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

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

המעמד הציבורי HybridCar מרחיב את המכונית {// כמה נכסים ושיטות אחרות ...@Override // postcondition: מהירות חייבת להפחית // postcondition: טעינה חייבת להגדיל בלם ריק מוגן () {// הפעל בלם HybridCar}}

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

לעומת זאת, אם מכונית היברידית אמורים להחליש את מצבם של הפוסלים בֶּלֶם שיטה, זה כבר לא היה מבטיח שה- מְהִירוּת יצטמצם. זה עלול לשבור את קוד הלקוח בהינתן מכונית היברידית כתחליף ל אוטו.

7. ריחות קוד

כיצד נוכל להבחין בתת-סוג שאינו מתחליף לסוג-העל שלו בעולם האמיתי?

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

7.1. תת-סוג משליך חריג להתנהגות שאינו יכול למלא

ראינו דוגמה לכך בדוגמת היישום הבנקאי שלנו קודם לכן.

לפני רפקטור, ה- חֶשְׁבּוֹן בכיתה הייתה שיטה נוספת לָסֶגֶת כי המשנה שלה FixedTermDepositAccount לא רצה. ה FixedTermDepositAccount הכיתה עבדה סביב זה על ידי השלכת ה- לא נתמךOperationException בשביל ה לָסֶגֶת שיטה. עם זאת, זה היה רק ​​פריצה כדי לכסות על חולשה בדוגמנות של היררכיית הירושה.

7.2. תת-סוג אינו מספק יישום להתנהגות שאינה יכולה למלא

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

הנה דוגמה. בואו נגדיר א מערכת קבצים מִמְשָׁק:

ממשק ציבורי FileSystem {File [] listFiles (נתיב מחרוזת); בטל deleteFile (נתיב מחרוזת) זורק IOException; } 

בואו נגדיר א ReadOnlyFileSystem שמיישם מערכת קבצים:

מחלקה ציבורית ReadOnlyFileSystem מיישם את FileSystem {public File [] listFiles (נתיב מחרוזת) {// קוד לרשימת קבצים להחזיר קובץ חדש [0]; } בטל ציבורי בטל deleteFile (נתיב מחרוזת) זורק IOException {// אל תעשה כלום. // פעולת deleteFile אינה נתמכת במערכת קבצים לקריאה בלבד}}

הנה ה ReadOnlyFileSystem אינו תומך ב- מחק קובץ פעולה ולכן לא מספק יישום.

7.3. הלקוח יודע על סוגי משנה

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

בואו נמחיש זאת באמצעות FilePurgingJob:

מחלקה ציבורית FilePurgingJob {fileSystem fileSystem פרטית; FilePurgingJob (FileSystem fileSystem) ציבורי {this.fileSystem = fileSystem; } public void purgeOldestFile (נתיב מחרוזת) {if (! (fileSystem instanceof ReadOnlyFileSystem)) {// קוד לאיתור fileSystem.deleteFile הקובץ העתיק ביותר (path); }}}

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

7.4. שיטת תת-סוג מחזירה תמיד את אותו ערך

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

מחלקה ציבורית ToyCar מרחיבה את המכונית {@Override מוגנת int getRemainingFuel () {להחזיר 0; }} 

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

8. מסקנה

במאמר זה, הסתכלנו על עקרון העיצוב SOLID של החלפת Liskov.

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

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

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

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

כמו תמיד, קוד הדוגמא ממאמר זה זמין ב- GitHub.


$config[zx-auto] not found$config[zx-overlay] not found