**היום נסתכל על יירוש מרובה ב-Java. לפני זמן קצר כתבתי פוסטים כמה על ירושה, ממשק ו־הרכבה ב-Java. בפוסט הזה, נתחיל לחקור את הירושה המרובה ב-Java ולהשוות אותה להרכבה.
ירושה מרובה ב-Java
יירוש מרובה ב-Java הוא היכולת ליצור מחלקה אחת עם מספר רב של מחלקות אב. להבחנה משפות אחרות בתכנות כמו C++, Java אינה תומכת בירושה מרובה במחלקות. Java אינה תומכת בירושה מרובה במחלקות משום שזה יכול לגרום לבעיה המסובכת יהלומית, ובמקום לספק דרכים מורכבות לפתרון זה, יש דרכים טובות יותר שבאמצעותן ניתן להשיג את אותו התוצאה כמו בירושה מרובה.**
בעיה של יהלומים ב-Java
כדי להבין את הבעיה בצורה קלה, נניח שתמיכה ביירוש מרובה הייתה נתמכת ב-Java. במקרה כזה, יכולנו להיות לנו ביררכיית מחלקות כמו בדימוי התמונה למטה. נניח ש-SuperClass היא מחלקה אבסטרקטית המכילה כמה שיטות, ו-ClassA ו-ClassB הן מחלקות קונקרטיות.
SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//מתודה של ClassA
public void methodA(){
}
}
ClassB.java
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//מתודה ספציפית של ClassB
public void methodB(){
}
}
עכשיו נניח שמימוש של ClassC הוא משהו כמו שמוצג למטה והוא מרחיב גם את ClassA וגם את ClassB. ClassC.java
package com.journaldev.inheritance;
// זו תחזוקה נוספת לכדי הסבר על בעיה של יהלומים
// הקוד הזה לא יהיה תקין
public class ClassC extends ClassA, ClassB{
public void test(){
// קריאה לשיטת המחלקה האבא
doSomething();
}
}
שים לב ששיטת test()
יוצרת קריאה לשיטת האב doSomething()
. זה גורם לדו-משמעות מכיוון שהמהדר לא יודע איזו שיטת אב לבצע. עקבות לתרשים המחלקה בצורת יהלום, מתייחסות אל תופעה זו ב-Java כבעיה היהלומית. ב-Java, בעיה זו היא הסיבה המרכזית לאי תמיכה ביורשות מרובה במחלקות. שים לב שהבעיה הנלמדת עם יורשות מרובה במחלקות יכולה גם להתרחש עם שלוש מחלקות בלבד שכולן מכילות לפחות שיטה אחת משותפת.
יורשות מרובה בממשקי Java
יתכן ששם לבת שאני תמיד אומרת שיורשות מרובה אינה נתמכת במחלקות, אך היא נתמכת בממשקים. ממשק יחיד יכול להרחיב מספר ממשקים, להלן דוגמה פשוטה. InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
שים לב ששני הממשקים מצהירים על אותה שיטה, כעת נוכל ליצור ממשק המרחיב את שני הממשקים אלו כמצוין למטה. InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//אותה שיטה מוצהרת ב-InterfaceA ו-InterfaceB כאחת
public void doSomething();
}
זה בסדר לגמרי כי הממשקים מכריעים את השיטות והיישום האמיתי יתבצע על ידי קבוצת קלאסים מממשים את הממשקים. לכן אין אפשרות לכל סוג של סמיכות ברב מורשים בממשקי Java. זו הסיבה שמחלקה ב-Java יכולה לממש מספר ממשקים, דוגמת הקוד למטה. InterfacesImpl.java
package com.journaldev.inheritance;
public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {
@Override
public void doSomething() {
System.out.println("doSomething implementation of concrete class");
}
public static void main(String[] args) {
InterfaceA objA = new InterfacesImpl();
InterfaceB objB = new InterfacesImpl();
InterfaceC objC = new InterfacesImpl();
//כל קריאת שיטה למטה הולכת לאותו יישום ממשי
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
שים לב שכל פעם שאני מפרט את פעולת יישום סופרמחלקה או מממש פעולת ממשק, אני משתמש בהערת @Override. הערת המחדל היא אחת משלוש ההערות המובנות ב-Java והן צריכות להשתמש תמיד בהערת ה Override כאשר מתבצעת החלפה של שיטה.
הרכב לעזור
אז מה לעשות אם רוצים להשתמש בפונקציה methodA()
של ClassA
ובפונקציה methodB()
של ClassB
ב-ClassC
? הפתרון נמצא בשימוש ב־הרכב. הנה גרסה משופצת של ClassC
המשתמשת בהרכב כדי להשתמש בשתי המחלקות וגם בשימוש בשיטת doSomething() מאחד מהאובייקטים. ClassC.java
package com.journaldev.inheritance;
public class ClassC{
ClassA objA = new ClassA();
ClassB objB = new ClassB();
public void test(){
objA.doSomething();
}
public void methodA(){
objA.methodA();
}
public void methodB(){
objB.methodB();
}
}
הרכב נגד ירושה
אחת מקריטריוני העבודה הטובים ביותר בתכנות ב-Java הוא "להעדיף הרכב על פני ירושה". נבחן כמה מהאספקטים המעדיפים את הגישה הזו.
-
נניח שיש לנו מחלקה אב ותת-מחלקה כדלקמן:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } }
ClassD.java
package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
הקוד לעיל נכתב ועובד כשורה אך מה אם מימוש המחלקה ClassC משתנה כמפורט למטה:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
שימו לב שמתודת
test()
כבר קיימת בתת-מחלקה אך סוג ההחזרה שונה. כעת המחלקה ClassD לא תהיה ניתנת לקידום ואם תשתמשו בכלי פיתוח, הם יציעו לכם לשנות את סוג ההחזרה באחד מהמחלקות: אב או תת-מחלקה. כעת דמיינו את המצב בו יש לנו מספר שכבות של ירושה והמחלקה האב אינה נשלטת על ידינו. אין לנו ברירה אלא לשנות את החתימה של השיטה בתת-מחלקה או את שמה כדי להסיר את שגיאת ההידור. בנוסף, עלינו לבצע שינוי בכל המקומות שבהם נקראה שיטת התת-מחלקה שלנו, לכן הירושה מפנה את הקוד שלנו. הבעיה שלמעלה לעולם לא תתרחש עם הרכבה וזה מה שהופך אותה ליותר מועדפת על הירושה. -
בעיה נוספת עם הירושה היא שאנו חושפים את כל שיטות המחלקה האב ללקוח, ואם המחלקה האב אינה מעוצבת נכון ויש חורים באבטחה, אז אפילו אם אנו תופסים זהירות מוחלטת במימוש של המחלקה שלנו, ייתכן וניתקל בבעיות עקב המימוש הרע של המחלקה האב. ההרכבה עוזרת לנו לספק גישה שליטה לשיטות המחלקה האב, בעוד שהירושה אינה סופקת שליטה על שיטות המחלקה האב, וזו גם אחת מהיתרונות המרכזיים של הרכבה מעל הירושה.
-
הבעיה הנוספת עם הורשה היא שאנו חושפים את כל מתודות המחלקה-אם ללקוח, ואם המחלקה-אם שלנו לא מעוצבת כהלכה ויש בהחלקים הפנימיים שלה פרצות אבטחה, אז גם אם אנו דואגים למימוש המחלקה שלנו בצורה יסודית, אנו ניתקל בבעיה כתוצאה ממימוש גרוע של המחלקה-אם. הרכב עוזר לנו לספק גישה שליטה למתודות המחלקה-אם, בעוד שהורשה אינה סופקת שליטה כלשהי על מתודות המחלקה-אם. זה גם אחת מהיתרונות המרכזיים של הרכב מול הורשה.
package com.journaldev.inheritance; public class classc { superclass obj = null; public classc(superclass o){ this.obj = o; } public void test(){ obj.dosomething(); } public static void main(string args[]){ classc obj1 = new classc(new classa()); classc obj2 = new classc(new classb()); obj1.test(); obj2.test(); } }
הפלט של התכנית לעיל הוא:
מימוש dosomething של a מימוש dosomething של b
הגמישות הזו בקריאה למתודות אינה זמינה בהורשה ומשפרת את הפרקטיקה הטובה של לפעול עם הרכב על פני הורשה.
-
בדיקת יחידות פשוטה בהרכבה מכיוון שאנו יודעים אילו שיטות אנו משתמשים בהן מהמחלקה האב ואנו יכולים ליצור מוק עבור בדיקה. להבחנה מהורשה בה אנו תלויים כבדול במחלקה האב ולא יודעים אילו שיטות ממחלקה האב יופעלו, לכן עלינו לבדוק את כל השיטות של מחלקה האב, זהו עבודה נוספת ונצטרך לעשות אותה מיותרת בגלל הורשה.
זהו הכל לנוגע להורשה מרובה ב-Java וסקירה קצרה על הרכבה.
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java