Skip to content

3. [TD]: الفئات

الكلمات الرئيسية: فئة، واجهة، وراثة، استثناء، تعدد الأشكال

قراءات موصى بها:

  • الفقرات 2.1 و 2.2 و 2.4 و 2.7 من الفصل 2 من [ref1]: الفئات والواجهات
  • الفقرات 3.3 (فئة String)، 3.5 (فئة ArrayList)، 3.6 (فئة Arrays)

في الجزء 1 من التمرين ELECTIONS لم يتم استخدام أي فئة. لقد قمنا ببناء حل كما كنا سنبنيه بلغة C. نقدم الآن مفهوم فئة Java.

3.1. Support

 

يحتوي المجلد [support / chap-03] على مشروع Eclipse الخاص بهذا الفصل.

سنعمل من الآن فصاعدًا مع JDK 1.8 لأن بعض المشاريع التالية تتطلب هذا الإصدار. لمعرفة الإصدار المستخدم، اتبع الخطوات التالية:

  • في [4]، JRE (Java Runtime Environment) المستخدم. هذا JRE هو في الواقع هنا JDK (Java Development Kit)، وهنا [jdk1.8.0_60]. إذا لم يكن هذا هو JDK أو إذا كان لديك إصدار أقل من 1.8، فاتبع الخطوات التالية [5-21]؛
  • إلى [8]، وهو JRE المستخدم حاليًا بشكل افتراضي بواسطة Eclipse؛
  • إلى [11]، مختلف JDK و JRE المعروفة حاليًا لدى Eclipse؛
  • في [15]، اختر JDK بدلاً من JRE. يستخدم هذا المستند مشاريع Maven التي تحتاج إلى JDK؛
  • في [21]، لدينا JDK من الإصدار >=1.8؛
  • في [22-23]، يمكنك الوصول إلى جوانب (طرق عرض مختلفة لنفس مشروع Eclipse) المشروع؛
  • في [24]، تحقق من أنك تستخدم إصدار Java >=1.8؛

3.2. الفئة [ListeElectorale]

في لغة C، كنا سنستخدم على الأرجح بنية لتقديم قائمة المرشحين في الانتخابات. وكان من الممكن أن تكون بالشكل التالي:

struct t_liste
   {
     char nom[15];
     long voix;
     int  elimine;
     int sieges;
   };

مفهوم البنية غير موجود في لغة Java. يجب استبداله بمفهوم الفئة. لذلك قررنا إنشاء فئة لتخزين المعلومات حول قائمة المرشحين. سيكون هيكلها كما يلي:


package istia.st.elections;

public class ListeElectorale {

    /**
     * identité de la liste
     */
    private int id;

    /**
     * nom de la liste
     */
    private String nom;
    /**
     * nombre de voix de la liste
     */
    private int voix;
    /**
     * nombre de sièges de la liste
     */
    private int sieges;
    /**
     * indique si la liste est éliminée ou non
     */
    private boolean elimine;

    /**
     * constructeur par défaut
     */
    public ListeElectorale() {
    }

    /**
     *
     * @param nom String : le nom de la liste
     * @param voix int : son nombre de voix
     * @param sieges int : son nombre de sieges
     * @param elimine boolean : son état éliminé ou non
     */
    public ListeElectorale(int id,String nom, int voix, int sieges, boolean elimine) {
...
    }

    /**
     *
     * @return int : l'identifiant de la liste
     */
    public int getId() {
...
    }

    /**
     * initialise l'identifiant de liste
     * @param id int : identifiant de la liste
     * @throws ElectionsException si id<1
     */
    public void setId(int id) {
...
    }

    /**
     *
     * @return String : le nom de la liste
     */
    public String getNom() {
...
    }

    /**
     * initialise le nom de la liste
     * @param nom String : nom de la liste
     *  @throws ElectionsException si le nom est vide ou blanc
     */
    public void setNom(String nom) {
...
    }

    /**
     *
     * @return int : le nombre de voix de la liste
     */
    public int getVoix() {
 ...
    }

    /**
     * initialise le nombre de voix de la liste
     * @param voix int : le nombre de voix de la liste
     */
    public void setVoix(int voix) {
 ...
    }

    /**
     *
     * @return int : le nombre de sièges de la liste
     */
    public int getSieges() {
 ...
    }

    /**
     * fixe le nombre de sièges de la liste
     * @param sieges int : le nombre de sièges de la liste
     */
    public void setSieges(int sieges) {
...
    }

    /**
     *
     * @return boolean : valeur du champ elimine
     */
    public boolean isElimine() {
  ...
    }

    /**
     *
     * @param sieges int
     */
    public void setElimine(boolean elimine) {
 ...
    }

    /**
     *
     * @return String : identité de la liste électorale
     */
    public String toString() {
   ...
    }
}
  • السطر 8: رقم يحدد القائمة بشكل فريد. ليس ضروريًا هنا ولكنه مخصص للاستخدام في المستقبل.
  • السطر 13: اسم القائمة.
  • السطر 17: عدد الأصوات التي حصلت عليها القائمة
  • السطر 21: عدد المقاعد التي حصلت عليها القائمة
  • السطر 25: قيمة منطقية تشير إلى ما إذا كانت القائمة قد تم استبعادها (نسبة الأصوات التي حصلت عليها أقل من العتبة الانتخابية) أم لا.

يمكن تهيئة كل حقل خاص باسم [xyz] بواسطة طريقة تسمى [setXyz]. وتسمح الطريقة [getXyz] بالحصول على قيمة الحقل الخاص [xyz]. في الحالة الخاصة التي يكون فيها [xyz] حقلًا من النوع المنطقي، يمكن استبدال الطريقة [getXyz] بالطريقة [isXyz]. تتبع التسمية الخاصة لهذه الطرق معيار ترميز يسمى معيار JavaBean. وبالتالي، نحدد الطرق العامة التالية:

  • getId (السطر 48)، setId (السطر 57)
  • getNom (السطر 65)، setNom (السطر 74)
  • getVoix (السطر 82)، setVoix (السطر 90)
  • getSieges (السطر 98)، setSieges (السطر 106)
  • isElimine (السطر 114)، setElimine (السطر 122)
  • السطران 30-31: يحددان منشئًا بدون معلمات. يتيح هذا المنشئ إنشاء كائن [ListeElectorale] دون تهيئته. يمكن بعد ذلك تهيئة هذا الكائن باستخدام طرق set.
  • السطور 40-42: تحدد منشئًا يسمح بإنشاء كائن [ListeElectorale] مع تهيئة حقوله الخمسة الخاصة.
  • السطور 130-132: تحدد الطريقة [toString] التي تُرجع سلسلة أحرف تعطي قيم الحقول الخمسة للكائن.

قد يكون برنامج اختبار الفئة ListeElectorale كما يلي:


package istia.st.elections.tests;

import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // إنشاء قائمة انتخابية
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // عرض هوية القائمة
        System.out.println("listeElectorale1=" + listeElectorale1);
        // تعديل عدد المقاعد
        listeElectorale1.setSieges(2);
        // عرض هوية القائمة 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // قائمة انتخابية جديدة
        ListeElectorale listeElectorale2 = listeElectorale1;
        // عرض هوية القائمة 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // تعديل عدد المقاعد
        listeElectorale2.setSieges(3);
        // عرض هوية القائمتين
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

قد تكون بيئة Eclipse لهذا الاختبار كما يلي:

  • [1]: يُسمى المشروع [elections-02A]
  • [2]: سيتم وضع التطبيق في حزمة، هنا [istia.st.elections]
  • [3]: [ListeElectorale.java] هو كود المصدر لفئة [ListeElectorale]
  • [4]: سيتم وضع فئات الاختبار في حزمة، هنا [istia.st.elections.tests]
  • [5]: فئة الاختبار [MainTest1ListeElectorale]

الشاشة التي تظهر بعد تنفيذ البرنامج أعلاه هي كما يلي:

Image


المهمة المطلوبة: باستخدام ما سبق، أكمل كود الفئة ListeElectorale.


3.3. إنشاء فئة استثناء [ElectionsException]

من بين فئات الاستثناء المختلفة في لغة Java، هناك فئة تسمى [RuntimeException]. هذه الفئة مشتقة من الفئة [Exception]، وهي أصل جميع فئات الاستثناء. تتمثل خصوصية مثيلات [RuntimeException] أو المثيلات المشتقة منها في أنه ليس من الضروري إعلانها أو إدارتها. وتسمى هذه الاستثناءات بالاستثناءات غير الخاضعة للرقابة.

لنأخذ مثالاً أول. الفئة [BufferedReader] هي فئة تسمح مثيلاتها بقراءة أسطر النص في تدفق البيانات. وهي تمتلك طريقة [readLine] التي يكون توقيعها كما يلي:

public String readLine()throws IOException

نرى أن الطريقة يمكن أن تطلق استثناءً من النوع [IOException]. شجرة هذه الفئة هي كما يلي:

1
2
3
4
java.lang.Object
  java.lang.Throwable
      java.lang.Exception
          java.io.IOException

تشتق الفئة [IOException] من الفئة [Exception] (السطر 3). يفرض علينا المُجمِّع إدارة وإعلان الاستثناءات من النوع [java.lang.Exception] أو مشتقاتها (باستثناء الفرع [RuntimeException] الذي سنعرضه لاحقًا). وبالتالي، لقراءة سطر نصي تمت كتابته على لوحة المفاتيح، سنضطر إلى كتابة شيء مثل:

1
2
3
4
5
6
7
8
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // إدارة الاستثناء
    ....
}

لنأخذ مثالاً آخر. لتحويل سلسلة كاملة، يمكننا استخدام الطريقة الثابتة [Integer.parseInt] التي توقيعها كما يلي:

public static int parseInt(String s) throws NumberFormatException

الحجة [s] هي سلسلة الأحرف المراد تحويلها إلى عدد صحيح. نرى أن الطريقة يمكن أن تطلق استثناءً من النوع [NumberFormatException]. شجرة هذه الفئة هي كما يلي:

1
2
3
4
5
6
java.lang.Object
  java.lang.Throwable
      java.lang.Exception
          java.lang.RuntimeException
              java.lang.IllegalArgumentException
                  java.lang.NumberFormatException

تشتق الفئة [NumberFormatException] من الفئة [RuntimeException] (السطر 4). لا يجبرنا المُجمع على إدارة وإعلان الاستثناءات من النوع [java.lang.RuntimeException] أو مشتقاتها. وبالتالي، يمكننا كتابة شيء مثل:

1
2
3
4
5
6
7
8
9
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // إدارة الاستثناء
    ....
}
int age=Integer.parseInt(ligne);

لسنا مضطرين إلى وضع بند [try - catch] لإدارة الاستثناء المحتمل الذي يولده [Integer.parseInt] (السطر 9).

هناك مزايا وعيوب لإنشاء واستخدام فئات الاستثناءات المشتقة من [RuntimeException]:

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

قررنا إنشاء فئة خاصة تجمع كل الاستثناءات التي قد تحدث في تطبيقنا ELECTIONS. ستسمى [ElectionsException] وستشتق من الفئة [RuntimeException]. وها هو كودها:


package istia.st.elections;

public class ElectionsException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public ElectionsException() {
        super();
    }

    public ElectionsException(String message) {
        super(message);
    }

    public ElectionsException(Throwable cause) {
        super(cause);
    }

    public ElectionsException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • السطر 1: نضع الفئة في الحزمة [istia.st.elections
  • السطر 3: الفئة مشتقة من [RuntimeException]. وبالتالي فهي غير خاضعة للرقابة؛
  • السطر 4: معرف تسلسل يمكن تجاهله في الوقت الحالي؛
  • سنستخدم في تطبيقنا نوعين من المنشئ:
    • المنشئ الكلاسيكي في الأسطر 15-17 كما يلي:
throw new ElectionsException("Le nombre de sièges doit être >0")

في هذه الحالة، يمكن للطريقة التي تستدعي طريقة تطلق مثل هذا الاستثناء أن تديرها على النحو التالي:


        // اختبار الاستثناء
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
  • (تابع)
    • أو ذلك الموجود في الأسطر 14-20 والمخصص لإعادة توجيه استثناء حدث بالفعل، عن طريق تغليفه في استثناء من النوع [ElectionsException]:

    try {
        ...;
        } catch (SQLException ex) {
            // تغليف الاستثناء
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

تتميز هذه الطريقة الثانية بميزة الاحتفاظ بالمعلومات التي قد تحتوي عليها الاستثناء الأول. في هذه الحالة، يمكن للطريقة التي تستدعي طريقة تطلق مثل هذا الاستثناء أن تديره على النحو التالي:


        try {
            ...;
        } catch (ElectionsException ex) {
            System.out.println(ex.getMessage() + ", Cause : "+ ex.getCause().getMessage());
            System.exit(1);
        }

المهمة المطلوبة: أعد كتابة كود الفئة ListeElectorale بحيث تطلق طرق set استثناءً من النوع [ElectionsException] إذا كانت التهيئة المطلوبة غير صحيحة، مثل تهيئة الاسم بسلسلة فارغة.


قد يكون مشروع Eclipse لاختبار هذه النسخة الجديدة كما يلي:

  • [1]: يُسمى المشروع [elections-02B]
  • [2]: يتم وضع التطبيق في حزمة، هنا [istia.st.elections]
  • [3]: الفئات [ListeElectorale] و [ElectionsException]
  • [4]: فئات الاختبار موضوعة في حزمة، هنا [istia.st.elections.tests]
  • [5]: فئة الاختبار [MainTest1ListeElectorale]

تم تعديل فئة الاختبار [MainTest1ListeElectorale] التي تمت دراستها سابقًا بشكل طفيف لاختبار حالات الاستثناء:


package istia.st.elections.tests;

import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // إنشاء قائمة انتخابية
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // عرض هوية القائمة
        System.out.println("listeElectorale1=" + listeElectorale1);
        // تعديل عدد المقاعد
        listeElectorale1.setSieges(2);
        // عرض هوية القائمة 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // قائمة انتخابية جديدة
        ListeElectorale listeElectorale2 = listeElectorale1;
        // عرض هوية القائمة 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // تعديل عدد المقاعد
        listeElectorale2.setSieges(3);
        // عرض هوية القائمتين
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // اختبار الاستثناء
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }

    }
}
  • السطر 28: نحاول تهيئة عدد المقاعد بقيمة غير مسموح بها
  • السطر 30: في حالة وجود استثناء، يتم عرضه

يؤدي تنفيذ الاختبار إلى النتائج التالية:

Image

نلاحظ أن الفئة [ListeElectorale] قد أحدثت استثناءً بالفعل عندما حاولنا تهيئة عدد المقاعد بقيمة غير صالحة (السطر 28 من الكود).

3.4. فئة اختبار وحدة

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

يتطور التطبيق بمرور الوقت. مع كل تطور، يجب التحقق من أن التطبيق لا "يتراجع"، c.a.d. وأنه يستمر في اجتياز اختبارات الأداء التي تم إجراؤها عند كتابته في البداية. نسمي هذه الاختبارات اختبارات "عدم التراجع". قد يتطلب تطبيق مهم بعض الشيء مئات الاختبارات. في الواقع، يتم اختبار كل طريقة في كل فئة من فئات التطبيق. ويُطلق على هذه الاختبارات اسم "الاختبارات الوحدوية". وقد تتطلب هذه الاختبارات مشاركة عدد كبير من المطورين إذا لم يتم أتمتتها.

تم تطوير أدوات لأتمتة الاختبارات. إحدى هذه الأدوات تسمى [JUnit]. وهي مكتبة من الفئات مخصصة لإدارة الاختبارات. سنستخدم هذه الأداة لاختبار الفئة [ListeElectorale].

يكون شكل برنامج الاختبار JUnit (الإصدارات 4.x) كما يلي:


package istia.st.elections.tests;

import org.junit.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class JUnitEssai {

    @Before
    public void avant() throws Exception {
        System.out.println("tearUp");
    }

    @After
    public void après() throws Exception {
        System.out.println("tearDown");
    }

    @Test
    public void t1() {
        System.out.println("test1");
        Assert.assertEquals(1, 1);
    }

    @Test
    public void t2() {
        System.out.println("test2");
        Assert.assertEquals(1, 2);
    }

}
  • السطر 1: تم وضع الفئة في الحزمة [istia.st.elections.tests
  • السطر 11: يتم تنفيذ الطريقة المُعلَّمة بالتعليق التوضيحي [@Before] قبل كل اختبار وحدة؛
  • السطر 16: يتم تنفيذ الطريقة المُعلَّمة بعلامة [@After] بعد كل اختبار وحدة؛
  • السطر 21: الطريقة المُعلَّمة بعلامة [@Test] هي طريقة يتم اختبارها بواسطة الاختبار الفردي. سيتم تنفيذ الطرق المُعلَّمة بعلامة [@Test] واحدة تلو الأخرى، ما لم يحدد المختبر خلاف ذلك، حيث يمكنه اختيار الطرق المراد اختبارها بنفسه. قبل كل تنفيذ لطريقة [@Test]، يتم تنفيذ الطريقة [@Before]. بعد كل تنفيذ لطريقة [@Test]، يتم تنفيذ الطريقة [@After
  • الأسطر 22-25: تحدد طريقة اختبار [t1]؛
  • السطر 18: إحدى طرق [Assert.assert*] التي تسمح بالتحقق من التأكيدات. توجد طرق [assert] التالية:
    • assertEquals(expression1, expression2): تتحقق من أن قيم التعبيرين متساوية. يتم قبول العديد من أنواع التعبيرات (int، String، float، double، boolean، char، short). إذا لم تكن التعبيرات متساوية، يتم إطلاق استثناء من النوع [AssertionFailedError
    • assertEquals(عدد حقيقي1، عدد حقيقي2، دلتا): يتحقق من أن عددين حقيقيين متساويان بفارق لا يتجاوز دلتا، c.a.d abs(عدد حقيقي1-عدد حقيقي2) <= دلتا. يمكننا على سبيل المثال كتابة assertEquals(عدد حقيقي1، عدد حقيقي2، 1E-6) للتحقق من أن القيمتين متساويتان بفارق 10-6،
    • assertEquals(message, expression1, expression2) و assertEquals(message, real1, real2, delta) هي متغيرات تسمح بتحديد رسالة الخطأ المرتبطة بالاستثناء من النوع [AssertionFailedError] الذي يتم إطلاقه عند فشل الطريقة [assertEquals
    • assertNotNull(Object) و assertNotNull(message, Object): تتحقق من أن Object لا يساوي null،
    • assertNull(Object) و assertNull(message, Object): تتحقق من أن Object يساوي null،
    • assertSame(Object1, Object2) و assertSame(message, Object1, Object2): يتحقق من أن المراجع Object1 و Object2 تشير إلى نفس الكائن،
    • assertNotSame(Object1, Object2) و assertNotSame(message, Object1, Object2): يتحقق من أن المراجع Object1 و Object2 لا تشير إلى نفس الكائن؛
  • السطر 24: يجب أن تنجح هذه المقارنة؛
  • السطر 30: يجب أن تفشل هذه المقارنة؛

في بيئة Eclipse، يمكن إنشاء فئة اختبار JUnit بالطريقة التالية:

  • [1]: انقر بزر الماوس الأيمن على الحزمة التي تريد إضافة فئة الاختبار إليها، ثم اختر الخيار [JUnit / New / JUnit Test Case]
  • [1]: اختيار إصدار JUnit؛
  • [2]: اختيار المجلد الذي يجب إنشاء فئة الاختبار فيه؛
  • [3]: اختيار الحزمة التي يجب إنشاء فئة الاختبار فيها؛
  • [4]: اسم فئة الاختبار؛
  • [5]: اختيار الطرق التي سيتم تضمينها في الفئة التي سيتم إنشاؤها؛
  • [6]: تم إنشاء الفئة JUnitEssai

يقوم المساعد السابق بإنشاء فئة شبه فارغة:


package istia.st.elections.tests;

import org.junit.Assert;
import org.junit.After;
import org.junit.Before;

public class JUnitEssai {

    @Before
    public void setUp() throws Exception {        
    }

    @After
    public void tearDown() throws Exception {
    }
}

دعونا نكمل ونعدل الكود السابق على النحو التالي:


package istia.st.elections.tests;

import org.junit.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class JUnitEssai2 {

    @Before
    public void avant() throws Exception {
        System.out.println("tearUp");
    }

    @After
    public void après() throws Exception {
        System.out.println("tearDown");
    }

    @Test
    public void t1() {
        System.out.println("test1");
        Assert.assertEquals(1, 1);
    }

    @Test
    public void t2() {
        System.out.println("test2");
        Assert.assertEquals(1, 2);
    }

}

في Eclipse، يمكن تنفيذ الفئة بالنقر بزر الماوس الأيمن على فئة الاختبار، ثم اختيار الخيار [Run as / JUnit test]:

Image

النتائج التي تم الحصول عليها عند تنفيذ هذا الاختبار هي التالية:

Image

فيما سبق، فشلت الطريقة [test2]. في كل مرة يفشل فيها اختبار، ترتبط به رسالة خطأ. بالنسبة لـ [test2]، هذه هي الرسالة المعروضة أعلاه. تشير الرسالة إلى رقم السطر الذي حدث فيه الخطأ (السطر 30). في السطر 30، كان الاستدعاء الذي فشل هو:


    Assert.assertEquals(1, 2);

يُطلق على المعلمة الأولى اسم القيمة المتوقعة، وعلى الثانية اسم القيمة الفعلية. تشير رسالة الخطأ الخاصة بـ [test2] أعلاه إلى أن القيمة المتوقعة كانت 2، لكن القيمة الفعلية كانت 3.

وأخيرًا، كانت الرسائل التي كتبتها طرق الاختبار المختلفة على وحدة التحكم كما يلي:

Image

تُظهر هذه الرسائل أن الطريقتين [@Before] و [@After] قد تم استدعاؤهما بالفعل، على التوالي، قبل وبعد كل طريقة اختبار.

لا يتم كتابة فئات الاختبار بالضرورة من قبل المطورين أنفسهم. يمكن أن يتم كتابتها من قبل الأشخاص الذين كتبوا مواصفات التطبيق. توصي بعض أساليب التطوير المعروفة باسم TDD (التطوير القائم على الاختبار) بكتابة فئات الاختبار قبل كتابة الفئات المراد اختبارها. وهذا يساعد أحيانًا في توضيح المواصفات التي قد تُفسر بطرق متعددة لولا ذلك.

لنقم بإنشاء اختبار JUnit 4، يسمى [JUnitTest1ListeElectorale]، لفئة [ListeElectorale]. في Eclipse، سنقوم بذلك كما هو موضح سابقًا:

نكمل الكود الذي تم إنشاؤه بواسطة المساعد بالطريقة التالية:


package istia.st.elections.tests;

import org.junit.Assert;
import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;

import org.junit.Test;

public class JUnitTest1ListeElectorale {

    @Test
    public void t1() {
        // إنشاء قائمة انتخابية
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // التحقق
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // التحقق من صحة المعرف
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // التحقق من صحة الاسم
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // التحقق من صحة الأصوات
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // التحقق من صحة المقاعد
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }

}

يؤدي تنفيذ الاختبار إلى النتيجة التالية:

Image

نجحت الاختبارات. سنعتبر الآن أن لدينا فئة [ListeElectorale] جاهزة للعمل.

3.5. MainElections: الإصدار 2

قراءات موصى بها:

  • الفقرات 2.1 و 2.2 و 2.4 و 2.7 من الفصل 2 من [1]: الفئات والواجهات
  • الفقرات 3.3 (فئة String)، 3.5 (فئة ArrayList)، 3.6 (فئة Arrays)

نرغب في إعادة كتابة التطبيق [Elections] بإضافة القيود الجديدة التالية:

  • سنستخدم الفئة [ListeElectorale] لتمثيل قائمة المرشحين
  • سيطلب التطبيق من لوحة المفاتيح المعلومات التالية:
  • عدد المقاعد الشاغرة
  • أسماء القوائم وأصواتها. لا يُعرف مسبقًا عدد القوائم. وسيتم تمييز القائمة الأخيرة باسم يساوي السلسلة "*".
  • نظرًا لعدم معرفة عدد القوائم مسبقًا، سيتم تخزينها أولاً في كائن من النوع [ArrayList]. ثم، بعد إدخال جميع القوائم، سيتم نقلها إلى مصفوفة قوائم.
  • سيتم عرض النتائج بترتيب تنازلي حسب عدد المقاعد التي تم الحصول عليها.

لترتيب مصفوفة T، تتوفر طرق ثابتة مختلفة من الفئة [Arrays]:

  • Arrays.sort(T): يفرز المصفوفة T وفقًا لترتيب طبيعي إن وجد (تصاعدي للأرقام والتواريخ، أبجدي للسلاسل، ...)
  • Arrays.sort(T,comparateur): لفرز المصفوفات T التي ليس لها ترتيب طبيعي. وهذا هو الحال هنا بالنسبة لمصفوفة القوائم التي يجب فرزها وفقًا لحقل معين في القائمة: عدد المقاعد التي تم الحصول عليها.

في الطريقة Arrays.sort(T,comparateur)، المعلمة comparateur هي كائن ينفذ الواجهة Comparator التالية:

Image

  • تسمح الطريقة compare بمقارنة عنصرين من المصفوفة T
  • تسمح الطريقة equals بتحديد ما إذا كان كائنان متساويين

تقارن الطريقتان أنواع Object obj1 و obj2. يعتمد تحديد ما إذا كان obj1<obj2 أو obj1=obj2 أو obj1>obj2 على علاقة الترتيب التي نريد إنشاءها بين الكائنين. يقع على عاتق المطور الذي ينفذ هذه الواجهة تحديد كيفية معرفة ما إذا كان:

  • obj1 أصغر من obj2
  • obj1 أكبر من obj2
  • obj1 يساوي obj2

تحتوي فئة Object التي تنحدر منها جميع فئات Java بالفعل على طريقة [equals]. لفرز مصفوفة T من الكائنات من النوع O، لا تكون طريقة [equals] الخاصة بفئة O مفيدة. لذلك يمكن ترك التنفيذ الافتراضي الذي توفره فئة Object. في هذه الحالة، يجب تنفيذ الطريقة [compare] فقط. يتم استدعاء هذه الطريقة بشكل متكرر بواسطة الطريقة [Arrays.sort]. ستقوم هذه الطريقة في كل مرة بتمرير obj1 و obj2 كمعلمات لطريقة compare، وهما عنصران من المصفوفة T المراد فرزها. في حالتنا، ستكون هذه العناصر من النوع [ListeElectorale]. تجدر الإشارة هنا إلى تعدد الأشكال في العمل. يتم تعريف الطريقة [compare] على أنها تستقبل معلمات من النوع [Object]. وهذا يعني أنها يمكن أن تستقبل معلمات من النوع [Object] أو مشتقة (تعدد الأشكال). وبما أن [Object] هي الفئة الأم لجميع فئات Java، فإن المعلمات الفعلية يمكن أن تكون من النوع [ListeElectorale].

للترتيب التصاعدي، يجب أن تُرجع الطريقة [compare]:

  • -1 إذا كان obj1 أصغر من obj2
  • +1 إذا كان obj1 أكبر من obj2
  • 0 إذا كان obj1 يساوي obj2

للترتيب التنازلي، يتم عكس القيمتين +1 و -1. تعبر مصطلحات "أصغر من" و"أكبر من" و"يساوي" عن علاقة ترتيب. بالنسبة للكائنات من النوع [ListeElectorale]، تكون العلاقة قائمة1 "أصغر من" قائمة2 إذا كان عدد الأصوات في قائمة1 أقل من عدد الأصوات في قائمة2.

في نفس ملف المصدر الخاص بالفئة [MainElections]، يمكن إضافة فئة ثانية:

// فئة مقارنة القوائم الانتخابية
class CompareListesElectorales implements Comparator {

     // مقارنة قائمتين انتخابيتين حسب عدد الأصوات
    public int compare(Object obj1, Object obj2) {
         // استرداد القوائم الانتخابية
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
         // مقارنة الأصوات في هاتين القائمتين
....        
    }
}
  • السطر 2: الفئة غير مُعلنة على أنها عامة. في ملف مصدر Java، قد يكون هناك عدة فئات، لكن فئة واحدة فقط يمكن أن تحمل السمة العامة، وهي تلك التي تحمل اسم ملف المصدر.

في الأسلوب compare السابق، المعلمات من النوع Object,، مما يفرض على السطرين 7 و8 إجراء تحويل نوع لمعلمات الطريقة من النوع Object إلى النوع ListeElectorale. يتم فرض توقيع الطريقة compare بواسطة الواجهة Comparator التي تمت كتابتها لمقارنة أي كائنات. منذ الإصدار JDK 1.5، توجد واجهة عامة Comparator: Comparator<T> حيث T هو أي نوع Java. تقارن الطريقة compare للواجهة Comparator<T> كائنات من النوع T وليس من النوع Object، مما يتجنب عمليات التحويل السابقة. قد تبدو فئة مقارنة الكائنات من النوع ListeElectorale كما يلي:


// فئة مقارنة القوائم الانتخابية
class CompareListesElectorales implements Comparator<ListeElectorale> {

    // مقارنة بين قائمتين انتخابيتين حسب عدد المقاعد
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • السطر 2: تنفذ الفئة واجهة Comparator<ListeElectorale>
  • السطران 5-6: معلمات الطريقة compare هي من النوع ListeElectorale. لم يعد التحويل بين الأنواع ضروريًا الآن.

أدخلت النسخة 1.5 من JDK مفهوم الفئة/الواجهة العامة لمختلف الفئات/الواجهات في النسخة 1.4 من JDK التي كانت تتعامل في البداية فقط مع كائنات من النوع Object. وهذا هو الحال بالنسبة للقوائم والقواميس، ...

ذكرنا سابقًا أنه نظرًا لعدم معرفة عدد القوائم، لم يكن من الممكن تخزينها في مصفوفة. يمكن تخزينها في كائن ArrayList الذي ينفذ مفهوم "قائمة الكائنات". تخزن هذه الفئة كائنات من النوع Object. منذ الإصدار JDK 1.5، توجد قوائم من الكائنات المحددة النوع. وبالتالي، سنستخدم كائن ArrayList<ListeElectorale> لتخزين القوائم قبل نقلها إلى مصفوفة. إذا كانت هذه المصفوفة تسمى tListes، فسيتم فرزها باستخدام الأمر:


// فرز القوائم
Arrays.sort(tListes, new CompareListesElectorales());

حيث CompareListesElectorales هي الفئة التي تنفذ واجهة Comparator<ListeElectorale>.


المهمة المطلوبة: أعد كتابة التطبيق [Elections] مع مراعاة هذه المواصفات الجديدة.


قد يكون مشروع Eclipse كما يلي:

فيما يلي مثال على تشغيل [1]:

Image