رضوى العربي

تَسمَح بعض اللغات البرمجية كائنية التوجه (object-oriented programming)، مثل C++‎، للصَنْف بأن يَتمدَّد (extend) من أكثر من مُجرّد صَنْف أعلى (superclass) واحد، وهو ما يُعرَف باسم الوراثة المُتعدّدة (multiple inheritance). بالرسم التالي مثلًا، يَتمدَّد الصنف E من صنفين أعليين (superclasses) مباشرةً، هما الصنفين A و B، بينما يَتمدَّد الصنف F من ثلاثة أصناف أعلين (superclasses) مباشرةً:

001Multiple_Inheritance.png

أراد مُصمِّمي الجافا أن يجعلوا اللغة بسيطة على نحو معقول، ولمّا وجدوا أن مزايا الوراثة المُتعدّدة (multiple inheritance) لا تَستحِقّ ما يُقابِلها من تعقيد مُتزايد، فإنهم لم يُدعِّموها باللغة. ومع هذا، تُوفِّر الجافا ما يُعرَف باسم الواجهات (interfaces) والتي يُمكِن اِستخدَامها لتحقيق الكثير من أهداف الوراثة المُتعدّدة. لقد تَعرَّضنا -بالقسم ٤.٥- لواجهات نوع الدالة (functional interfaces) وعلاقتها بتعبيرات لامدا (lambda expressions)، ورأينا أنها تُخصِّص تابعًا (method) وحيدًا. في المقابل، يُمكِن للواجهات (interfaces) أن تَكُون أكثر تعقيدًا بمراحل كما أن لها استخدامات آخرى كثيرة.

من غَيْر المُحتمَل أن تحتاج إلى كتابة واجهات (interfaces) خاصة بك حاليًا؛ فهي ضرورية فقط للبرامج المُعقَّدة نسبيًا، ولكن هنالك عدة واجهات (interfaces) مُستخدَمة بحزم جافا القياسية (Java's standard packages) بطرائق مُهِمّة وتحتاج إلى تَعلُّم طريقة اِستخدَامها.

تعريف الواجهات (interfaces) وتنفيذها (implementation)

لقد تَعرَّضنا لمصطلح "الواجهة (interface)" ضِمْن أكثر من سياق، سواء فيما يَتَعلَّق بالصناديق السوداء (black boxes) في العموم أو فيما يَتَعلَّق بالبرامج الفرعية (subroutines) على وجه الخصوص. تَتكوَّن واجهة أي برنامج فرعي (subroutine interface) من اسمه، ونوعه المُعاد (return type)، وعدد مُعامِلاته (parameters) وأنواعها. تُمثِل تلك المعلومات كل ما أنت بحاجة إلى مَعرِفته لكي تَتَمكَّن من استدعاء البرنامج الفرعي. بالإضافة إلى ذلك، يَمتلك أي برنامج فرعي جزءًا تّنْفيذيًا (implementation)، هو كتلة الشيفرة المُعرِّفة له (defines) والتي تُنفَّذ عند استدعاءه.

بلغة الجافا، كلمة interface هي كلمة محجوزة تَحمِل معنًى تقنيًا إضافيًا. وفقًا لهذا المعنى، تَتكوَّن الواجهة من مجموعة من واجهات توابع النُسخ (instance method interfaces) بدون أجزائها التّنفيذية (implementations). يستطيع أي صنف أن يُنفِّذ (implement) واجهة معينة بتوفير الأجزاء التّنْفيذية (implementation) لجميع التوابع المُخصَّصة ضِمْن تلك الواجهة. اُنظر المثال التالي لواجهة (interface) بسيطة جدًا بلغة الجافا:

public interface Strokeable {
   public void stroke(GraphicsContext g);
}

تبدو الشيفرة بالأعلى مشابهة لتعريف صنف (class definition) باستثناء حَذْف الجزء التّنْفيذي (implementation) للتابع stroke()‎. إذا أراد صنف معين أن يُنفِّذ تلك الواجهة Strokeable، فلابُدّ له من أن يُوفِّر جزءًا تّنْفيذيًا للتابع stroke()‎ كما قد يَتَضمَّن أي توابع أو متغيرات آخرى. اُنظر الشيفرة التالية على سبيل المثال:

public class Line implements Strokeable {
    public void stroke(GraphicsContext g) {
        . . . // ارسم خطًا
    }
    . . . // توابع ومتغيرات وبواني آخرى
}

لكي يُنفِّذ صنف واجهةً (interface) معينةً، ينبغي عليه أن يَفعَل أكثر من مُجرّد توفير الأجزاء التّنْفيذية (implementation) لجميع التوابع ضِمْن تلك الواجهة، فعليه تحديدًا أن يُعلن صراحةً عن تّنفيذه (implements) لتلك الواجهة باستخدام الكلمة المحجوزة implements كالمثال بالأعلى. لابُدّ لأي صنف حقيقي (concrete class) يَرغَب بتّنْفيذ الواجهة Strokeable من أن يُعرِّف تابع نسخة اسمه stroke()‎، لذا سيَتضمَّن أي كائن (object) مُنشَئ من هذا الصنف التابع stroke()‎. يُعدّ الكائن مُنفِّذًا (implement) لواجهة معينة إذا كان ينتمي لصنف يُنفِّذ (implements) تلك الواجهة، فمثلًا، يُنفِّذ أي كائن من النوع Line الواجهة Strokeable.

في حين يستطيع الصنف أن يَتمدَّد (extend) من صنف واحد فقط، فإنه في المقابل يستطيع أن يُنفِّذ (implements) أي عدد من الواجهات (interfaces). وفي الواقع، يُمكِن للصنف أن يَتمدَّد (extend) من صنف آخر، وأن يُنفِّذ واجهة واحدة أو أكثر بنفس ذات الوقت، لذلك نستطيع كتابة التالي مثلًا:

class FilledCircle extends Circle 
                        implements Strokeable, Fillable {
   . . .
}

على الرغم من أن الواجهات (interfaces) ليست أصنافًا (classes)، فإنها تُشبهها إلى حد كبير. في الواقع، أي واجهة (interface) هي أَشْبه ما تَكُون بصنف مُجرّد (abstract class) لا يُستخدَم لإنشاء كائنات، وإنما كقاعدة لإنشاء أصناف فرعية (subclasses). تُعدّ البرامج الفرعية (subroutines) ضِمْن أي واجهة توابعًا مجردةً (abstract methods)، والتي لابُدّ لأيّ صنف حقيقي (concrete class) يَرغَب بتّنْفيذ تلك الواجهة من أن يُنفِّذها (implement). تستطيع الموازنة بين الواجهة Strokeable والصَنْف المُجرّد (abstract class) التالي:

public abstract class AbstractStrokeable {
   public abstract void stroke(GraphicsContext g);
}

يَكمُن الفرق بينهما في أن الصنف الذي يَتمدَّد (extend) من الصنف AbstractStrokeable لا يُمكِنه أن يَتَمدَّد من أي صنف آخر. أما الصنف الذي يُنفِّذ الواجهة Strokeable يستطيع أن يَتَمدَّد من أي صنف آخر كما يستطيع أن يُنفِّذ (implement) أي واجهات (interfaces) آخرى. بالإضافة إلى ذلك، يُمكِن لأي صنف مُجرّد (abstract class) أن يَتَضمَّن توابعًا غير مُجرّدة (non-abstract) وآخرى مُجرّدة (abstract). في المقابل، تستطيع أي واجهة (interface) أن تَتَضمَّن توابعًا مُجرّدة فقط، لذا فهي أَشْبه ما تَكُون بصنف مُجرّد نقي (pure).

ينبغي أن تُصرِّح عن التوابع ضِمْن أي واجهة (interface) على أساس كَوْنها -أي التوابع- عامة public ومُجردّة abstract. ولمّا كان هذا هو الخيار الوحيد المُتاح، فإن تَخْصِيص هذين المُبدِّلين (modifiers) ضِمْن التّصْريح (declaration) ليس ضروريًا.

إلى جانب التّصريح (method declarations) عن التوابع، يُمكِن لأي واجهة (interface) أن تُصرِّح عن وجود مُتْغيِّرات (variable declarations)، وينبغي عندها أن تُصرِّح عنها على أساس كَوْنها عامة public، وساكنة static، ونهائية final، ولذا فإنها تَصيِر عامة وساكنة ونهائية بأي صنف يُنفِّذ (implements) تلك الواجهة. ولمّا كان هذا هو الخيار الوحيد المُتاح للتّصْريح عنها، فإن تَخْصِيص تلك المُبدِّلات (modifiers) ضِمْن التّصْريح (declaration) ليس ضروريًا. اُنظر المثال التالي:

public interface ConversionFactors {
    int INCHES_PER_FOOT = 12;
    int FEET_PER_YARD = 3;
    int YARDS_PER_MILE = 1760;
}

هذه هي الطريقة المناسبة لتعريف (define) ثوابت مُسماة (named constants) يُمكِن اِستخدَامها بعدة أصناف. يُمكِن لأي صنف يُنفِّذ (implements) الواجهة ConversionFactors أن يَستخدِم الثوابت المُعرَّفة بتلك الواجهة (interface) كما لو كانت مُعرَّفة بالصنف.

لاحِظ أن أي مُتْغيِّر مُعرَّف ضِمْن واجهة (interface) هو بالنهاية ثابت (constant) وليس مُتْغيِّرًا على الإطلاق. وفي العموم، لا يُمكِن لأي واجهة (interface) أن تُضيف مُتْغيِّرات نُسخ (instance variables) إلى الأصناف التي تُنفِّذها (implement).

يُمكِن لأي واجهة (interface) أن تَتَمدَّد (extend) من واجهة واحدة أو أكثر. على سبيل المثال، إذا كان لدينا الواجهة Strokeable المُعطاة بالأعلى، بالإضافة إلى الواجهة Fillable والتي تُعرِّف التابع fill(g)‎، نستطيع عندها تعريف الواجهة التالية:

public interface Drawable extends Strokeable, Fillable {
    // المزيد من التوابع أو الثوابت
}

ينبغي لأي صَنْف حقيقي (concrete class) يُنفِّذ الواجهة Drawable من أن يُوفِّر الأجزاء التّنْفيذية (implementations) لكُلًا من التابع stroke()‎ من الواجهة Strokeable، والتابع draw()‎ من الواجهة Fillable، بالإضافة إلى أي توابع مُجرّدة (abstract methods) آخرى قد تُخصِّصها الواجهة Drawable مباشرة. عادة ما تُعرَّف (define) الواجهة (interface) ضِمْن ملف ‎.java الخاص بها، والذي لابُدّ أن يَكُون له نفس اسم الواجهة. فمثلًا، تُعرَّف الواجهة Strokeable بملف اسمه Strokeable.java. وبالمثل من الأصناف (classes)، يُمكِن للواجهة (interface) أن تقع ضِمْن حزمة (package)، كما يُمكِنها أن تَستورِد (import) أشياءً من حزم آخرى.

التوابع الافتراضية (default methods)

بداية من الإصدار الثامن من الجافا، تستطيع الواجهات (interfaces) أن تَتَضمَّن ما يعرف باسم "التوابع الافتراضية (default methods)"، والتي تَملُك جزءًا تّنْفيذيًا (implementation) بعكس التوابع المُجرّدة (abstract methods) المُعتادة. تُورَث التوابع الافتراضية من الواجهات (interfaces) إلى أصنافها المُنفِّذة (implement) بنفس الطريقة التي تُورَث بها التوابع العادية من الأصناف إلى أصنافها الفرعية. لذا عندما يُنفِّذ (implement) صنف معين واجهةً تَحتوِي على توابع افتراضية، فإنه لا يَكُون مضطرًا لأن يُوفِّر جزءًا تّنْفيذيًا (implementation) لأي تابع افتراضي (default method) ضِمْن الواجهة، مع أن بإمكانه القيام بذلك إذا كان لديه تّنْفيذًا (implementation) مُختلفًا. تَدفَع التوابع الافتراضية (default methods) لغة الجافا خطوة للأمام بطريق دَعْم الوراثة المُتعدّدة (multiple inheritance)، ولكنها مع ذلك ليست وراثة مُتعدّدة بحق؛ لأن الواجهات لا تستطيع تعريف مُتْغيِّرات نُسخ (instance variables). تستطيع التوابع الافتراضية (default methods) استدعاء التوابع المجردة (abstract methods) المُعرَّفة بنفس الواجهة، لكنها لا تستطيع الإشارة إلى أي مُتْغيِّر نسخة (instance variable).

ملحوظة: تستطيع واجهات نوع الدالة (functional interfaces) أيضًا أن تَحتوِي على توابع افتراضية (default methods) بالإضافة إلى التابع المُجرّد (abstract method) الوحيد الذي بإمكانها تَخْصِيصه.

ينبغي أن تُصرِّح عن التوابع الافتراضية (default methods) على أساس كَوْنها عامة public، ولمّا كان ذلك هو الخيار الوحيد المُتاح، فإن تَخْصِيص المبدل public ضِمْن التّصْريح (declaration) ليس ضروريًا. في المقابل، لابُدّ من كتابة المُبدِّل default بشكل صريح أثناء التّصْريح عن أي تابع افتراضي (default method). اُنظر المثال التالي:

public interface Readable { // تمثل مصدر إدخال

    public char readChar();  // اقرأ المحرف التالي المُدْخَل

    default public String readLine() { //اقرأ حتى نهاية السطر

        StringBuilder line = new StringBuilder();
        char ch = readChar();
        while (ch != '\n') {
            line.append(ch);
            ch = readChar();
        }
        return line.toString();
    }

}

لابُدّ لأي صنف حقيقي (concrete class) يُنفِّذ الواجهة (interface) -بالأعلى- من أن يُوفِّر تّنْفيذًا (implementation) للتابع readChar()‎. في المقابل، سيَرِث ذلك الصنف تعريف readLine()‎ من الواجهة، ولكنه قد يُوفِّر تعريفًا (definition) جديدًا إذا كان ذلك ضروريًا. عندما يَتَضمَّن صنف معين تّنفيذًا (implementation) لتابع افتراضي (default method)، فإن ذلك التّنْفيذ الجديد يُعيد تعريف (overrides) التابع الافتراضي الموجود بالواجهة (interface).

بالمثال السابق، يَستدعِي التابع الافتراضي readLine()‎ التابع المُجرّد readChar()‎، والذي يَتوفَّر تعريفه (definition) فقط من خلال الأصناف المُنفِّذة للواجهة، ولهذا تُعدّ الإشارة إلى readChar()‎ مُتعدِّدة الأشكال (polymorphic). كُتب التّنْفيذ الافتراضي للتابع readLine()‎ بحيث يَكُون ملائمًا لأي صنف يُنفِّذ الواجهة Readable. اُنظر الشيفرة التالية والتي تَتَضمَّن صنفًا يُنفِّذ الواجهة Readable كما يَحتوِي على البرنامج main()‎ لاختبار الصَنْف:

public class Stars implements Readable {

    public char readChar() {
        if (Math.random() > 0.02)
           return '*';
        else
           return '\n';
    }

    public static void main(String[] args) {
        Stars stars = new Stars();
        for (int i = 0 ; i < 10; i++ ) {
            // اِستدعي التابع  الافتراضي
            String line = stars.readLine();  
            System.out.println( line );
        }
    }

}

تُوفِّر التوابع الافتراضية (default methods) إمكانية شبيهة لما يُعرَف باسم "المخلوط (mixin)" المُدعَّم ببعض اللغات البرمجية الآخرى، والتي تَعنِي المقدرة على خَلْط وظائف مصادر آخرى إلى داخل الصنف. لمّا كان بإمكان أي صنف أن يُنفِّذ أي عدد من الواجهات (interfaces)، فإنه يستطيع خلط وظائف عدة مصادر آخرى مختلفة.

الواجهات كأنواع

كما هو الحال مع الأصناف المُجرّدة (abstract classes)، لا يُمكِنك إنشاء كائن فعليّ من واجهة (interface)، ولكن تستطيع التّصْريح (declare) عن مُتْغيِّر نوعه عبارة عن واجهة. لنَفْترِض مثلًا أن لدينا الواجهة Strokeable المُعرَّفة بالأعلى، ويُنفِّذها كُلًا من الصنفين Line و Circle، يُمكِنك عندها كتابة التالي:

// صرح عن متغير من النوع‫ Strokeable والذي يمكنه الإشارة إلى أي
// كائن ينفذ تلك الواجهة
Strokeable figure;  

figure = new Line();  // ‫يشير إلى كائن من الصنف Line 
figure.stroke(g);   // ‫اِستدعي التابع stroke() من الصنف Line

figure = new Circle();   // ‫يشير الآن إلى كائن من الصنف Circle
figure.stroke(g);   // ‫اِستدعي التابع stroke() من الصنف Circle

يُمكِن لأي مُتْغيِّر من النوع Strokeable أن يُشير إلى أي كائن طالما كان صَنْفه يُنفِّذ الواجهة Strokeable. لمّا كان figure مُتْغيِّرًا من النوع Strokeable، ولأن أي كائن من النوع Strokeable يَحتوِي على التابع stroke()‎، فحتمًا سيَحتوِي الكائن الذي يُشير إليه المُتْغيِّر figure على التابع stroke()‎، ولهذا فإن التَعْليمَة figure.stroke(g)‎ صالحة تمامًا.

تُستخدَم الأنواع (types) في العموم إما للتّصْريح (declare) عن مُتْغيِّر، أو لتَخْصِيص نوع معامل برنامج فرعي (routine)، أو لتَخْصِيص النوع المُعاد (return type) من دالة (function). النوع في العموم إما أن يَكُون صنفًا أو واجهة (interface) أو أحد الأنواع البسيطة (primitive) الثمانية المَبنية مُسْبَقًا (built-in). ليس هنالك من أيّ احتمال آخر، ربما باستثناء بعض الحالات الخاصة كأنواع التعداد (enum) والتي هي بمثابة نوع خاص من الأصناف. من بين كل تلك الأنواع، الأصناف هي الوحيدة التي يُمكِن اِستخدَامها لإنشاء كائنات (objects).

يُمكِنك أيضًا اِستخدَام الواجهات (interface) لتَخْصِيص النوع الأساسي (base type) لمصفوفة. فمثلًا، تستطيع أن تُصرِّح عن مُتْغيِّر أو أن تُنشِئ مصفوفة باستخدام نوع المصفوفة Strokeable[]‎، وفي تلك الحالة، يُمكِن لعناصر تلك المصفوفة الإشارة إلى أي كائن طالما كان يُنفِّذ الواجهة Strokeable. اُنظر الشيفرة التالية:

Strokeable[] listOfFigures;
listOfFigures = new Strokeable[10];
listOfFigures[0] = new Line();
listOfFigures[1] = new Circle();
listOfFigures[2] = new Line();
  .
  .
  .

تَملُك جميع عناصر تلك المصفوفة التابع stroke()‎، مما يَعنِي إمكانية كتابة تعبيرات مثل listOfFigures.stroke(g)‎.

ترجمة -بتصرّف- للقسم Section 7: Interfaces من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.



1 شخص أعجب بهذا


تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن