التوريث المتعدد في جافا

اليوم سنتناول موضوع التوريث المتعدد في جافا. قبل فترة قصيرة كتبت عدة مقالات حول التوريث، الواجهة، و التركيب في جافا. في هذا المقال، سنتحدث عن التوريث المتعدد في جافا ونقارن بين التركيب والتوريث.

التوريث المتعدد في جافا

التوريث المتعدد في جافا هو القدرة على إنشاء فئة واحدة تمتلك عدة فئات أساسية. على عكس بعض لغات البرمجة الموجهة نحو الكائنات الأخرى مثل سي بلس بلس، لا توفر جافا دعمًا للتوريث المتعدد في الفئات. جافا لا تدعم التوراث المتعددة في الفئات لأنه يمكن أن يؤدي إلى مشكلة الماس وبدلاً من تقديم طريقة معقدة لحلها، هناك طرق أفضل من خلالها يمكننا تحقيق نفس النتيجة كالتوراث المتعدد.

مشكلة الماس في جافا

لفهم مشكلة الماس بسهولة، لنفترض أن الوراثة المتعددة مدعومة في جافا. في هذه الحالة، يمكن أن يكون لدينا تسلسل تصنيفي للصفوف مثل الصورة أدناه. لنفترض أن 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");
	}
	
	//طريقة خاصة لفئة A
	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");
	}
	
	//طريقة خاصة لفئة B
	public void methodB(){
		
	}
}

الآن لنفترض أن تكون تنفيذ ClassC شيئًا مثل ما يلي وأنها تمتد كل من ClassA و ClassB. ClassC.java

package com.journaldev.inheritance;

// هذا مجرد افتراض لشرح مشكلة الماس
//هذا الكود لن يتم تجميعه
public class ClassC extends ClassA, ClassB{

	public void test(){
		//استدعاء طريقة الفئة الأساسية
		doSomething();
	}

}

يرجى ملاحظة أن الطريقة test() تقوم بالاتصال بأسلوب الفئة الأم doSomething(). يؤدي ذلك إلى الغموض حيث لا يعرف المترجم أي أسلوب فئة أم يجب تنفيذه. بسبب الرسم البياني للفئة على شكل الماس، يُشار إلى هذه المشكلة باسم “مشكلة الماس” في لغة البرمجة جافا. مشكلة الماس في جافا هي السبب الرئيسي الذي يجعل جافا لا تدعم التوريث المتعدد في الفئات. لاحظ أن المشكلة أعلاه مع التوريث المتعدد للفئات قد تأتي أيضًا مع وجود ثلاث فئات فقط حيث تحتوي كل واحدة على أقل شيء واحد من الأساليب المشتركة.

التوريث المتعدد في واجهات جافا

قد لاحظت أنني دائمًا أقول أن التوريث المتعدد غير مدعوم في الفئات ولكنه مدعوم في الواجهات. يمكن لواجهة واحدة أن تمتد إلى واجهات متعددة، كما هو موضح في المثال البسيط أدناه. 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 التعليق. تعليق Override هو واحد من ثلاث تعليقات مدمجة java annotations ويجب علينا دائما استخدام تعليق 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();
	}
}

التكوين مقابل الوراثة

واحدة من أفضل الممارسات في برمجة جافا هي “تفضيل التكوين على الوراثة”. سننظر في بعض الجوانب التي تفضل هذا النهج.

  1. نفترض أن لدينا فئة رئيسية وفئة فرعية كما يلي: 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 وإذا كنت تستخدم أي بيئة تطوير متكاملة، فسوف تقترح عليك تغيير نوع العائد في الفئة الأم أو الفرعية. الآن تخيل الوضع حيث لدينا عدة مستويات من التوريث والفئة الأم ليست تحت سيطرتنا. لن يكون لدينا خيار سوى تغيير توقيع طريقة الفئة الفرعية أو اسمها لإزالة خطأ التجميع. أيضًا، سيتعين علينا إجراء تغيير في جميع الأماكن التي كانت تُستدعى فيها طريقة الفئة الفرعية لدينا، لذا يجعل التوريث كودنا هشًا. لن يحدث المشكلة أعلاه أبدًا مع التكوين وهذا ما يجعله أكثر ميلاً على التوريث.

  2. مشكلة أخرى مع التوريث هي أننا نعرض جميع طرق الفئة الأم للعميل، وإذا لم تكن فئتنا الأم مصممة بشكل جيد وتحتوي على ثغرات أمان، فإننا حتى إذا اعتنينا تمامًا بتنفيذ فئتنا، سنتأثر بالتنفيذ السيء للفئة الأم. يساعدنا التركيب في توفير وصول مراقب لطرق الفئة الأم بينما لا يوفر التوريث أي تحكم في طرق الفئة الأم، وهذا أيضًا أحد المزايا الرئيسية للتركيب على التوريث.

  3. فائدة أخرى مع التكوين هي أنه يوفر مرونة في استدعاء الطرق. التنفيذ السابق لـ ClassC ليس مثاليًا ويوفر ربطًا في وقت الترجمة مع الطريقة التي سيتم استدعاؤها، مع التغيير الدقيق يمكننا جعل استدعاء الطريقة مرنًا وجعله ديناميكيًا. ClassC.java

    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
    

    هذه المرونة في استدعاء الطريقة غير متاحة في التوريث وتعزز الممارسة الجيدة لصالح التكوين على التوريث.

  4. اختبار الوحدة سهل في التكوين لأننا نعلم جميع الطرق التي نستخدمها من الفئة الأم ويمكننا تجسيمها للاختبار، بينما في التوريث نعتمد بشكل كبير على الفئة الأم ولا نعرف جميع الطرق التي ستستخدم من الفئة الأم، لذا نحتاج إلى اختبار جميع طرق الفئة الأم، وهذا هو عمل إضافي ونحتاج إلى القيام به بدون داعٍ بسبب التوريث.

هذا كل شيء بخصوص التورث المتعدد في جافا ونظرة موجزة على التكوين.

Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java