الواجهات والأصناف المجرّدة

الواجهة مقابل الفئة المجردة

15 دقيقة الدرس 3 من 14

الواجهة مقابل الفئة المجردة

بعد أن تعرّفت على شكل كلٍّ من الأداتين، يبقى السؤال العملي: أيّهما تختار؟ تعتمد الإجابة على طبيعة علاقتين مختلفتين: ما الذي يكونه النوع في مقابل ما الذي يستطيع فعله.

الفارق الجوهري

تُنمذج الفئة المجردة علاقة هو-نوع-من. فهي تمثّل نوعًا أبًا حقيقيًا يتشارك الحالة والسلوك مع أبنائه. أمّا الواجهة فتُنمذج علاقة قادر-على (أو يتصرّف-كـ). فهي تُعلن عن قدرة يستطيع أيّ صنف غير مترابط أن يختار اعتمادها.

  • الـ Dog هو نوع من Animal — استخدم فئة مجردة.
  • الـ Dog قادر على كونه Trainable — استخدم واجهة.
  • الـ Robot قادر هو أيضًا على كونه Trainable رغم أنه ليس Animal.
قاعدة سريعة: إن وجدت نفسك تقول "X هو نوع من Y"، فاستخدم فئة مجردة. وإن قلت "X قادر على فعل Y"، فاستخدم واجهة.

الفروق التقنية الرئيسية

إلى جانب النموذج المفاهيمي، ثمّة قواعد صارمة تُقيّد اختيارك:

  • الوراثة المتعددة: يسمح Java للصنف بتطبيق واجهات متعددة لكنّه لا يسمح له بتوسيع سوى فئة مجردة واحدة. هذا القيد وحده هو السبب الأكثر شيوعًا لتفضيل الواجهات.
  • الحالة المُخزَّنة (Instance State): تستطيع الفئات المجردة أن تمتلك حقول نسخة (حالة مُخزَّنة). أمّا الواجهات فلا تحتوي إلّا على ثوابت public static final — ولا تحمل أيّ بيانات خاصة بالكائن.
  • المُنشِئات (Constructors): يمكن للفئات المجردة تعريف مُنشِئات لتهيئة الحقول المشتركة. أمّا الواجهات فلا مُنشِئات لها.
  • محدّدات الوصول: يمكن أن تكون أعضاء الفئة المجردة private أو protected أو public. أعضاء الواجهة public افتراضيًا (رغم إضافة دعم private للتوابع المساعدة في Java 9).

مثال مقارن

لنأخذ تطبيق رسم. الـ Shape هو أبٌ حقيقي — يحمل حقل اللون ومنهج describe() المشترك الذي يرثه كل شكل. أمّا Resizable وExportable فهما قدرتان لا تحتاجهما إلّا بعض الأشكال.

// فئة مجردة: حالة مشتركة + تطبيق جزئي abstract class Shape { protected String color; Shape(String color) { this.color = color; } // سلوك مشترك ومُنفَّذ public String describe() { return color + " " + getClass().getSimpleName(); } // يجب على الأصناف الفرعية توفير هذا public abstract double area(); } // واجهات القدرات interface Resizable { void resize(double factor); } interface Exportable { String toSvg(); } // Circle هو نوع من Shape، وقادر على Resizable و Exportable class Circle extends Shape implements Resizable, Exportable { private double radius; Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } @Override public void resize(double f) { radius *= f; } @Override public String toSvg() { return "<circle r=\"" + radius + "\" fill=\"" + color + "\"/>"; } } // Rectangle هو نوع من Shape لكنّه ليس Exportable class Rectangle extends Shape implements Resizable { private double w, h; Rectangle(String color, double w, double h) { super(color); this.w = w; this.h = h; } @Override public double area() { return w * h; } @Override public void resize(double f) { w *= f; h *= f; } }

لاحظ أنّ Circle وRectangle يتشاركان حقل color ومنهج describe() عبر الفئة المجردة — تلك الحالة تعيش في مكان واحد. أمّا القدرات (Resizable، Exportable) فتُضاف باستقلالية لكل صنف دون المساس بالتدرّج الهرمي.

حين تُجبرك الحالة المشتركة على الاختيار

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

// خطأ: محاولة وضع حالة مُخزَّنة مشتركة في واجهة interface BadIdea { // هذه قيمة public static final — يشترك فيها كل مُنفِّذ. // لا يمكن أن تكون حالة خاصة بكل كائن. int count = 0; } // صحيح: الحالة المشتركة لكل كائن تنتمي إلى الفئة المجردة abstract class Counter { protected int count = 0; public void increment() { count++; } public int getCount() { return count; } }

عامل "تطوّر الواجهة البرمجية"

إن كنت مالكًا لمكتبة وتحتاج إضافة منهج لاحقًا، فالأثر يختلف:

  • الفئة المجردة: أضف منهجًا مُنفَّذًا — تَرِثه الأصناف الفرعية الحالية مجانًا دون كسر أيّ شيء.
  • الواجهة (قبل Java 8): إضافة أيّ منهج كانت ستُخلّ بكل مُنفِّذ. أدخل Java 8 مناهج default تحديدًا لحلّ هذه المشكلة — يَرِث المُنفِّذون الحاليون القيمة الافتراضية تلقائيًا.
أفضل ممارسة في Java 8 وما بعده: افضّل الواجهات مع مناهج default للواجهات البرمجية العامة. يُبقي ذلك التصميم مرنًا (تعدّد التطبيقات، وراثة متعددة) ويسمح لك بتطوير الواجهة دون إجبار كل مُنفِّذ على التحديث الفوري.

دليل القرار السريع

  • تحتاج مشاركة حقول نسخة بين الأصناف الفرعية؟ ← فئة مجردة.
  • تحتاج مُنشِئًا لضمان التهيئة؟ ← فئة مجردة.
  • تريد صنفًا يرث من مصادر متعددة؟ ← واجهات (يمكن تطبيق العديد منها).
  • تُنمذج قدرة تشترك فيها أنواع غير مترابطة؟ ← واجهة.
  • تنشر واجهة برمجية لمكتبة يجب أن تبقى متوافقة مع الإصدارات السابقة؟ ← واجهة مع مناهج default.
خطأ شائع: إنشاء فئة مجردة فقط لمشاركة منهج مساعد، دون وجود علاقة is-a حقيقية. افضّل بدلًا من ذلك فئة مساعدة بمناهج ثابتة، أو واجهة صغيرة بمنهج default، حتى لا تُقيّد المُستدعين بسلسلة وراثة واحدة دون مبرّر.

الخلاصة

الفئات المجردة والواجهات أداوتان مُتكاملتان لا مُتنافستان. استخدم الفئة المجردة حين تحتاج مشاركة الحالة أو فرض تدرّج نوعي صارم. استخدم الواجهة حين تُعرّف قدرةً يستطيع اعتمادها أصناف متنوّعة وغير مترابطة — وتذكّر أنّ صنفًا واحدًا يمكنه تطبيق واجهات كثيرة. في الممارسة الفعلية، يعتمد كود Java الحديث اعتمادًا كبيرًا على الواجهات ويحتفظ بالفئات المجردة للحالات التي تنتمي فيها الحالة القابلة للتغيير أو منطق المُنشِئ فعلًا إلى الأب.