اذهب إلى المحتوى

تقدّم هذه السلسلة، هياكل البيانات 101، ثلاثة موضوعات:

  • هياكل البيانات Data Structures: سنناقش هياكل البيانات التي يُوفِّرها إطار التجميعات في لغة جافا Java Collections Framework والتي تُختصرُ إلى JCF، وسنتعلم كيفية استخدام بعض هياكل البيانات مثل القوائم والخرائط وسنرى طريقة عملها.
  • تحليل الخوارزميات ِAlgorithms: سنتعرض لتقنياتٍ تساعد على تحليل الشيفرة وعلى التنبؤ بسرعة تنفيذها ومقدار الذاكرة الذي تتطلَّبه.
  • استرجاع المعلومات Information retrieval: سنَستخدِم الموضوعين السابقين: هياكل البيانات والخوارزميات لإنشاء محرك بحثٍ بسيطٍ عبر الإنترنت، وذلك لنستفيد منهما عمليًّا ونجعل التمارين أكثر تشويقًا.

وسنناقش تلك الموضوعات وفقًا للترتيب التالي:

  • سنبدأ بالواجهة List، وسنكتب صنفين ينفذ كلٌ منهما تلك الواجهة بطريقة مختلفة، ثم سنوازن بين هذين الصنفين اللذين كتبناهما وبين صنفي الجافا ArrayList وLinkedList.
  • بعد ذلك، سنقدِّم هياكل بيانات شجريّة الشكل، ونبدأ بكتابة شيفرة التطبيق الأول. حيث سيقرأ هذا التطبيق صفحاتٍ من موقع Wikipedia، ثم يُحلِّل محتوياتها ويعطي النتيجة على هيئة شجرة، وفي النهاية سيمر عبر تلك الشجرة بحثًا عن روابطَ ومزايا أخرى. سنَستخدِم تلك الأدوات لاختبار الفرضيّة الشهيرة "الطريق إلى الفلسفة" Getting to Philosophy.
  • سنتطرق للواجهة Map وصنف الجافا HashMap المُنفِّذ لها، ثم سنكتب أصنافًا تُنفِّذ تلك الواجهة باستخدام جدول hash وشجرة بحثٍ ثنائيّة.
  • أخيرًا، سنستخدِم تلك الأصناف وبعض الأصناف الأخرى التي سنتناولها عبر الكتاب لتنفيذ محرك بحث عبر الإنترنت. سيكون هذا المحرك بمنزلة زاحف crawler يبحث عن الصفحات ويقرؤها، كما أنه سيُفهرِس ويُخزِّن محتويات صفحات الإنترنت بهيئةٍ تُمكِّنه من إجراء عملية البحث فيها بكفاءة، كما أنه سيَعمَل مثل مُسترجِع للمعلومات، أي أنه سيَستقبِل استفساراتٍ من المُستخدِم ويعيد النتائج ذات الصلة.

ولنبدأ الآن.

لماذا هنالك نوعان من الصنف List؟

عندما يبدأ المبرمجون باستخدام إطار عمل جافا للتجميعات، فإنهم عادةً يحتارون أي الصنفين يختارون ArrayList أم LinkedList. فلماذا تُوفِّر جافا تنفيذين implementations للواجهة List؟ وكيف ينبغي الاختيار بينهما؟ سنجيب عن تلك الأسئلة خلال الفصول القليلة القادمة.

سنبدأ باستعراض الواجهات والأصناف المُنفِّذَة لها، وسنُقدِّم فكرة البرمجة إلى واجهة.

سننفِّذ في التمارين القليلة الأولى أصنافًا مشابهةً للصنفين ArrayList وLinkedList، لكي نتمكَّن من فهم طريقة عملهما، وسنرى أن لكل منهما عيوبًا ومميزاتٍ، فبعض العمليات تكون أسرع وتحتاج إلى مساحة أقل عند استخدام الصنف ArrayList، وبعضها الآخر يكون أسرع وأصغر عند استخدام الصنف LinkedList، وبهذا يمكن القول: إن تحديد الصنف الأفضل لتطبيقٍ معيّنٍ يعتمد على نوعية العمليات الأكثر استخدامًا ضمن ذلك التطبيق.

الواجهات في لغة جافا

تُحدّد الواجهة بلغة جافا مجموعةً من التوابع methods، ولا بُدّ لأي صنفٍ يُنفِّذ تلك الواجهة أن يُوفِّر تلك التوابع. على سبيل المثال، انظر إلى شيفرة الواجهة Comparable المُعرَّفة ضمن الحزمة java.lang:

public interface Comparable<T> {
    public int compareTo(T o);
}

يَستخدِم تعريف تلك الواجهة معاملَ نوعٍ type parameter اسمه T، وبذلك تكون تلك الواجهة من النوع المُعمَّم generic type، وينبغي لأي صنفٍ يُنفِّذ تلك الواجهة أن:

  • يُحدد النوع الذي يشير إليه معامل النوع T.
  • يُوفِّر تابعًا اسمه compareTo يَستقبِل كائنًا كمعامل parameter ويعيد قيمةً من النوع int.

وفي مثالٍ على ما نقول، انظر إلى الشيفرة المصدرية للصنف java.lang.Integer فيما يلي:

public final class Integer extends Number implements Comparable<Integer> {

    public int compareTo(Integer anotherInteger) {
        int thisVal = this.value;
        int anotherVal = anotherInteger.value;
        return (thisVal<anotherVal ? -1 : (thisVal==anotherVal ? 0 : 1));
    }

    // other methods omitted
}

يمتدُّ هذا الصنف من الصنف Number، وبالتالي فإنه يَرِث التوابع ومتغيرات النُّسَخ instance variables المُعرَّفة في ذلك الصنف، كما أنه يُنفِّذ أيضًا الواجهة Comparable<Integer>‎، ولذلك فإنه يُوفِّر تابعًا اسمه compareTo ويَستقبِل معاملًا من النوع Integer ويُعيد قيمةً من النوع int.

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

لاحِظ أنّ تنفيذ التابع compareTo الوارد في الأعلى يَستخدِم عاملًا ثلاثيًّا ternary operator يُكتَب أحيانًا على النحو التالي ‎?:‎. إذا لم يكن لديك فكرةٌ عن العوامل الثلاثية، فيُمكِنك قراءة مقال القيم والأنواع والعوامل في جافاسكربت.

الواجهة List

يحتوي إطار التجميعات في لغة جافا JCF‎ على واجهة اسمها List، ويُوفِّر تنفيذين لها هما ArrayList وLinkedList.

تُعرِّف تلك الواجهة ما ينبغي أن يكون عليه الكائن لكي يُمثِل قائمةً من النوع List، ومن ثمّ فلا بُدّ أن يُوفِّر أي صنفٍ يُنفِّذ تلك الواجهة مجموعةً محددةً من التوابع، منها add وget وremove، بالإضافة إلى 20 تابعًا آخر.

يوفّر كلا الصنفين ArrayList وLinkedList تلك التوابع، وبالتالي يُمكِن التبديل بينهما. ويَعنِي ذلك أنه في حالة وجود تابعٍ مُصمَّمٍ ليَعمَل مع كائنٍ من النوع List، فإن بإمكانه العمل أيضًا مع كائنٍ من النوع ArrayList أو من النوع LinkedList، أو من أيّ نوعٍ آخرَ يُنفِّذ الواجهة List.

يُوضِّح المثال التالي تلك الفكرة:

public class ListClientExample {
    private List list;

    public ListClientExample() {
        list = new LinkedList();
    }

    private List getList() {
        return list;        
    }

    public static void main(String[] args) {
        ListClientExample lce = new ListClientExample();
        List list = lce.getList();
        System.out.println(list);
    }
}

كما نرى، لا يقوم الصنف ListClientExample بعملٍ مفيد، غير أنه يحتوي على بعض العناصر الضرورية لتغليف قائمة من النوع List، إذ يتضمَّن متغير نسخةٍ من النوع List. سنستخدِم هذا الصنف لتوضيح فكرةٍ معينة، ثم سنحتاج إلى استخدامه في التمرين الأول.

يُهيئ باني الصنف ListClientExample القائمة list باستنساخ instantiating -أي بإنشاء- كائنٍ جديدٍ من النوع LinkedList، بينما يعيد الجالب getList مرجعًا reference إلى الكائن الداخليّ المُمثِل للقائمة، في حين يحتوي التابع main على أسطرٍ قليلةٍ من الشيفرة لاختبار تلك التوابع.

النقطة الأساسية التي أردنا الإشارة إليها في هذا المثال هو أنه يحاول استخدام List ، دون أن يلجأ لتحديد نوع القائمة هل هي LinkedList أم ArrayList ما لم تكن هناك ضرورة، فكما نرى متغير النسخة كيف أنه مُعرَّف ليكون من النوع List، كما أن التابع getList يعيد قيمةً من النوع List، دون التطرق لنوع القائمة في أيٍّ منهما. وبالتالي إذا غيرّت رأيك مستقبلًا وقررت أن تَستخدِم كائنًا من النوع ArrayList، فكل ما ستحتاج إليه هو تعديل الباني دون الحاجة لإجراء أي تعديلاتٍ أخرى.

تُطلَق على هذا الأسلوب تسميةُ البرمجة المعتمدة على الواجهات أو البرمجة إلى واجهة. تجدر الإشارة هنا إلى أنّ الكلام هنا عن الواجهات بمفهومها العام وليس مقتصرًا على الواجهات بلغة جافا.

في أسلوب البرمجة المعتمدة على الواجهات، تعتمد الشيفرة المكتوبة على الواجهات فقط مثل List، ولا تعتمد على تنفيذاتٍ معيّنةٍ لتلك الواجهات، مثل ArrayList. وبهذا، ستعمل الشيفرة حتى لو تغيّر التنفيذ المعتمد عليها في المستقبل.

وفي المقابل، إذا تغيّرت الواجهة، فلا بُدّ أيضًا من تعديل الشيفرة التي تعتمد على تلك الواجهة، ولهذا السبب يتجنَّب مطورو المكتبات تعديل الواجهات إلا عند الضرورة القصوى.

تمرين 1

نظرًا لأن هذا التمرين هو الأول، فقد حرصنا على تبسيطه. انسَخ الشيفرة الموجودة في القسم السابق، وأجرِ التبديل التالي: ضع الصنف ArrayList بدلًا من الصنف LinkedList. لاحظ هنا أن الشيفرة تُطبِّق مبدأ البرمجة إلى واجهة، ولذا فإنك لن تحتاج لتعديل أكثر من سطرٍ واحدٍ فقط وإضافة تعليمة import.

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

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

إذا لم تكن قد حمَّلت الشيفرة المصدرية للكتاب إلى الآن، فانظر إلى التعليمات في القسم 0.1.

ستَجِد الملفات والمجلدات التالية داخل مجلد اسمه code:

  • build.xml: هو ملف Ant يساعد على تصريف الشيفرة وتشغيلها.
  • lib: يحتوي على المكتبات اللازمة لتشغيل الأمثلة (مكتبة JUnit فقط في هذا التمرين).
  • src: يحتوي على الشيفرة المصدرية.

إذا ذهبت إلى المجلد src/com/allendowney/thinkdast فستجد ملفات الشيفرة التالية الخاصة بهذا التمرين:

  • ListClientExample.java: يحتوي على الشيفرة المصدرية الموجودة في القسم السابق.
  • ListClientExampleTest.java: يحتوي على اختبارات JUnit للصنف ListClientExample.

راجع الصنف ListClientExample وبعد أن تتأكَّد أنك فهمت كيف يعمل، صرِّفه وشغّله؛ وإذا كنت تستخدم أداة Ant، فاذهب إلى مجلد code ونفِّذ الأمر ant ListClientExample.

ربما تتلقى تحذيرًا يشبه التالي:

List is a raw type. References to generic type List<E> 
should be parameterized.

سبب ظهور هذا التحذير هو أننا لم نُحدّد نوع عناصر القائمة، وقد فعلنا ذلك بهدف تبسيط المثال، لكن يُمكِن حل إشكاليّة هذا التحذير بتعديل كل List أو LinkedList إلى List<Integer>‎ أو LinkedList<Integer>‎ على الترتيب.

يُجرِي الصنف ListClientExampleTest اختبارً واحدًا، يُنشِئ من خلاله كائنًا من النوع ListClientExample، ويَستدعِي تابعه الجالب getList، ثم يَفحَص ما إذا كانت القيمة المعادة منه هي كائن من النوع ArrayList. سيفشَل هذا الاختبار في البداية لأن التابع سيعيد قيمةً من النوع LinkedList لا من النوع ArrayList، لهذا شغِّل الاختبار ولاحظ كيف أنه سيفشل.

اقتباس

ملحوظة: قد يكون هذا الاختبار مناسبًا لهذا التمرين على وجه الخصوص، ولكنه بشكلٍ عامٍّ ليس مثالًا جيدًا على الاختبارات؛ فالاختبارات الجيدة ينبغي أن تتأكَّد من تلبية الصنف الذي يجري اختباره لمتطلبات الواجهة، لا أن تكون هذه الاختبارات مبنيةً على تفاصيل التنفيذ.

والآن لنعدّل الشيفرة كما يلي: ضعLinkedList بدلًا من ArrayList ضمن الصنف ListClientExample، وربما تحتاج أيضًا إلى إضافة تعليمة import. صرِّف الصنف ListClientExample وشغِّله، ثم شغِّل الاختبار مرةً أخرى. يُفترضُ أن ينجح الاختبار بعد هذا التعديل.

إن سبب نجاح هذا الاختبار هو تعديلك للتسمية LinkedList في باني الصنف، دون تعديلٍ لاسم الواجهة List في أي مكانٍ آخر. لكن ماذا سيحدث لو فعلت؟ دعنا نجرب. عدِّل اسم الواجهة List في مكان واحد أو أكثر إلى الصنف ArrayList، عندها ستجد أن البرنامج ما يزال بإمكانه العمل بشكل صحيح، ولكنه الآن يحدد تفاصيلَ زائدةً عن الحاجة، وبالتالي إذا أردت أن تُبدِّل الواجهة مرةً أخرى في المستقبل، فستضطّر إلى إجراء تعديلاتٍ أكثر على الشيفرة.

تُرى، ماذا سيحدث لو اِستخدَمت List بدلًا من ArrayList داخل باني الصنف ListClientExample؟ ولماذا لا تستطيع إنشاء نسخة من List؟

ترجمة -بتصرّف- للفصل Chapter 1: Interfaces من كتاب Think Data Structures: Algorithms and Information Retrieval in Java.

اقرأ أيضًا


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...