ירושה מרובה ב-Java
ירושה מרובה מאפשרת לך ליצור מחלקה שיורשת ממספר מחלקות-על. בניגוד לכמה שפות תכנות פופולריות מונחה עצמים, כמו C++, Java אינה מאפשרת ירושה מרובה ממחלקות. Java אינה תומכת בירושה מחלקות מרובות מכיוון שהיא עלולה להוביל לבעיית היהלומים. ובמקום לחפש דרכים לפתור בעיה זו, יש אפשרויות טובות יותר כיצד נוכל להשיג את אותה תוצאה כמו ירושה מרובה.בעיית יהלומים
כדי להבין את בעיית היהלומים ביתר קלות, הבה נניח שהירושה המרובה נתמכת ב-Java. במקרה זה, יכול להיות לנו היררכיית מחלקות כפי שמוצג בתמונה למטה. נניח שהמחלקהSuperClass
מופשטת ומוצהרת בה שיטה כלשהי. גם שיעורי בטון ClassA
וגם ClassB
.
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//ClassA own method
public void methodA(){
}
}
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//ClassB specific method
public void methodB(){
}
}
כעת נניח שאנו רוצים ליישם ClassC
ולרשת אותו מ- ClassA
ו ClassB
.
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
public void test(){
//calling super class method
doSomething();
}
}
שימו לב שהשיטה test()
קוראת לשיטת superclass doSomething()
. זה מוביל לאי בהירות מכיוון שהמהדר לא יודע איזו שיטת superclass לבצע. זהו דיאגרמת מחלקה בצורת יהלום הנקראת בעיית יהלומים. זו הסיבה העיקרית לכך ש-Java לא תומכת בירושה מרובה. שים לב שהבעיה שלעיל עם ירושה מחלקות מרובות יכולה להתרחש רק עם שלוש מחלקות שיש להן לפחות שיטה אחת משותפת.
ירושה של ממשקים מרובים
ב-Java, ירושה מרובה אינה נתמכת במחלקות, אך היא נתמכת בממשקים. וממשק אחד יכול להרחיב ממשקים רבים אחרים. להלן דוגמה פשוטה.package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
שימו לב ששני הממשקים מצהירים על אותה שיטה. כעת נוכל ליצור ממשק שמרחיב את שני הממשקים הללו, כפי שמוצג בדוגמה למטה.
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//same method is declared in InterfaceA and InterfaceB both
public void doSomething();
}
זה עובד מצוין מכיוון שממשקים מצהירים רק על שיטות והיישום יתבצע במחלקות שיורשות את הממשק. לפיכך, אין דרך לקבל אי בהירות בירושה של ריבוי ממשקים.
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();
//all the method calls below are going to same concrete implementation
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
שים לב שבכל פעם שאתה עוקף שיטת superclass או מיישם שיטת ממשק, השתמש בהערה @Override
. מה אם נרצה להשתמש בפונקציה methodA()
של מחלקה ClassA
ובפונקציה methodB()
של מחלקה ClassB
בכיתה ClassC
? הפתרון טמון בשימוש בהרכב. להלן גרסה של המחלקה ClassC
המשתמשת בקומפוזיציה כדי להגדיר הן את שיטות המחלקה והן את המתודה doSomething()
של אחד האובייקטים.
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 היא "אישור הרכב לפני ירושה". נחקור כמה מההיבטים המעדיפים גישה זו.-
נניח שיש לנו כיתת-על ושיעור שמרחיב אותו:
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } } package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
הקוד שלמעלה מקמפל ועובד היטב. אבל, מה אם נשנה את היישום של המחלקה
ClassC
כפי שמוצג להלן:package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
שימו לב שהשיטה
test()
כבר קיימת בתת המחלקה, אבל סוג ההחזרה שונה. כעת המחלקהClassD
לא תעשה קומפילציה ואם תשתמש ב-IDE כלשהו, היא תבקש ממך לשנות את סוג ההחזרה ב- superclass או subclass.כעת תארו לעצמכם מצב שבו יש לנו היררכיית ירושה מחלקה מרובת רמות ואין לנו גישה למחלקת העל. לא תהיה לנו ברירה אלא לשנות את חתימת שיטת המשנה שלנו או את שמה כדי להסיר את שגיאת ההידור. נצטרך גם לשנות את שיטת המשנה בכל המקומות שבהם היא נקראת. לפיכך, הירושה הופכת את הקוד שלנו לשביר.
הבעיה שלעיל לעולם לא תתרחש עם הרכב וזה הופך אותו ליותר אטרקטיבי לירושה.
-
בעיה נוספת עם ירושה היא שאנו חושפים את כל השיטות של מחלקת העל ללקוח ואם מחלקת העל שלנו לא מתוכננת כראוי ויש פרצות אבטחה, אז למרות שאנו מיישמים את היישום הטוב ביותר של המחלקה שלנו, אנחנו מושפעים מהיישום הלקוי של מעמד העל. קומפוזיציה עוזרת לנו במתן גישה מבוקרת לשיטות מחלקות-על, ואילו הירושה אינה מספקת שליטה על שיטות מחלקות-על. זהו גם אחד היתרונות העיקריים של הרכב מהירושה.
-
יתרון נוסף של קומפוזיציה הוא שהיא מאפשרת גמישות בשיטות הקריאה. היישום שלנו של המחלקה
ClassC
שהוצג לעיל אינו אופטימלי ומבטיח שזמן הקומפילציה קשור למתודה שתיקרא. בשינויים מינימליים נוכל להפוך את קריאת השיטה לגמישה ודינמית.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 implementation of A doSomething implementation of B
הגמישות הזו בקריאת השיטה אינה זמינה עם הורשה, מה שמוסיף יתרון נוסף לבחירת ההרכב.
-
קל יותר לעשות בדיקת יחידות עם קומפוזיציה כי אנחנו יודעים שאנחנו משתמשים בכל השיטות מ- superclass ויכולים להעתיק אותן למבחן. ואילו בירושה אנחנו תלויים יותר במעמד העל ולא מכירים את כל השיטות של מחלקת העל שישמשו. אז עלינו לבדוק את כל השיטות של מחלקת העל, שהיא עבודה נוספת עקב ירושה.
באופן אידיאלי, עלינו להשתמש בירושה רק כאשר יחסי המשנה למעמד-על מוגדרים כ"יש". בכל שאר המקרים, מומלץ להשתמש בהרכב.
GO TO FULL VERSION