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

تنفيذ واجهات متعددة

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

تنفيذ واجهات متعددة

من أقوى ميزات الواجهات في Java أنّ الصنف الواحد يمكنه تنفيذ أيّ عدد من الواجهات. هذا يتجاوز قيد الوراثة الأحادية في الأصناف، ويتيح لك تركيب سلوكيّات من عقود مستقلة متعددة. في هذا الدرس ستتعلم كيفية ربط واجهات متعددة، والأهمّ، كيفية التعامل مع التعارضات التي تنشأ حين تُعرّف إحدى الواجهتين تابع default بنفس التوقيع.

لماذا ننفّذ واجهات متعددة؟

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

المبدأ التصميمي الأساسي: افضّل الواجهات الصغيرة المركّزة على الواجهات الضخمة. الصنف الذي ينفّذ ثلاث واجهات صغيرة أسهل للفهم والاختبار بكثير من الصنف الذي يرث من صنف أساسي ضخم.

الصياغة الأساسية

اذكر كل واجهة بعد الكلمة المحجوزة implements مفصولةً بفاصلة:

interface Printable { void print(); } interface Exportable { void exportToCsv(); } interface Auditable { String lastModifiedBy(); } class Report implements Printable, Exportable, Auditable { private final String author; Report(String author) { this.author = author; } @Override public void print() { System.out.println("Printing report..."); } @Override public void exportToCsv() { System.out.println("Exporting report to CSV..."); } @Override public String lastModifiedBy() { return author; } }

يفرض المُصرِّف تنفيذ كل تابع مجرّد من كل واجهة. إن أغفلت تنفيذ تابع واحد لن يُصرَّف الصنف.

تعدد الأشكال عبر واجهات متعددة

يمكن الآن إسناد كائن Report لمتغيّرات من أيٍّ من الثلاث أنواع. كل مرجع يكشف فقط التوابع الخاصة بنوعه المُعلَن:

Report report = new Report("Alice"); Printable p = report; p.print(); // صحيح Exportable e = report; e.exportToCsv(); // صحيح Auditable a = report; System.out.println(a.lastModifiedBy()); // "Alice"

هذا هو تعدد الأشكال الاعتيادي. الكائن الفعلي دائمًا Report، لكن مرجع الواجهة يُقيّد القدرات التي يراها المستدعي — أداة مفيدة لتضييق النطاق.

تعارض توابع default

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

interface Logger { default String tag() { return "[LOG]"; } } interface Tracer { default String tag() { return "[TRACE]"; } } // لن يُصرَّف هذا كما هو؛ يجب تجاوز tag(): class Instrumented implements Logger, Tracer { @Override public String tag() { // الخيار أ — اكتب تنفيذك الخاص: return "[INSTRUMENTED]"; } }

إن كنت تفضّل التفويض لأحد التنفيذات الافتراضية الموجودة بدلًا من كتابة تنفيذ جديد، استخدم الصياغة الخاصة InterfaceName.super.method():

class Instrumented implements Logger, Tracer { @Override public String tag() { // الخيار ب — فوّض إلى تنفيذ Logger الافتراضي: return Logger.super.tag() + " " + Tracer.super.tag(); } }
نسيان حل التعارض خطأ في وقت التصريف. لن تختار Java واجهةً على الأخرى بصمت قطّ. هذا مقصود — الاختيار الصامت يجعل الكود غير متوقّع. يجعل متطلب التجاوز القرار واضحًا في الشفرة المصدرية.

مثال عملي: مكوّن نظام إضافات

إليك سيناريو أكثر واقعية. تخيّل نظام إضافات حيث يجب على كل إضافة دعم خطّافات دورة الحياة وكذلك الوعي بالإصدار:

interface Lifecycle { void onLoad(); void onUnload(); default String status() { return "active"; } } interface Versioned { String version(); default String status() { return "v" + version(); } } class DatabasePlugin implements Lifecycle, Versioned { @Override public void onLoad() { System.out.println("DB plugin loaded"); } @Override public void onUnload() { System.out.println("DB plugin unloaded"); } @Override public String version() { return "2.1.0"; } // يجب التجاوز لأن كلتا الواجهتين تعرّفان status(): @Override public String status() { return Lifecycle.super.status() + " | " + Versioned.super.status(); // تطبع "active | v2.1.0" } }

ملخّص القواعد

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

الخلاصة

تنفيذ واجهات متعددة هو إجابة Java عن الوراثة المتعددة. تُدرج كل الواجهات بعد implements، وتوفّر جسمًا لكل تابع مجرّد، وتحلّ صراحةً أيّ تعارض في أسماء توابع default بتجاوز التابع المتعارِض. تُتيح لك الصياغة InterfaceName.super.method() استدعاء التنفيذ الافتراضي لأيٍّ من الواجهتين كلبنة بناء داخل تجاوزك. تمنحك هذه القواعد معًا تصاميم مرنة قابلة للتركيب دون غموض مشاكل الوراثة المتعددة من الأصناف في لغات أخرى.