Skip to content

3. [TD]: کلاس‌ها

کلمات کلیدی: کلاس، رابط، ارث‌بری، استثنا، چندریختی

مطالعهٔ پیشنهادی:

  • بخش‌های ۲.۱، ۲.۲، ۲.۴ و ۲.۷ از فصل ۲ کتاب [ref1]: کلاس‌ها و رابط‌ها
  • بخش‌های ۳.۳ (کلاس String)، ۳.۵ (کلاس ArrayList) و ۳.۶ (کلاس Arrays)

در بخش اول تمرین ELECTIONS، از هیچ کلاسی استفاده نشد. ما راه‌حلی را به همان شیوه‌ای که در زبان C انجام می‌دادیم، ساختیم. اکنون مفهوم کلاس‌های جاوا را معرفی خواهیم کرد.

3.1. Support

 

پوشه [support / chap-03] حاوی پروژه اکلیپس برای این فصل است.

از این پس، ما با JDK نسخهٔ ۱.۸ کار خواهیم کرد، زیرا برخی از پروژه‌های بعدی به این نسخه از JDK نیاز دارند. برای اینکه بفهمید کدام نسخه از JDK در حال استفاده است، مراحل زیر را دنبال کنید:

  • به [4]، JRE (محیط اجرایی جاوا) در حال استفاده است. این JRE در واقع یک JDK (Java Development Kit) است، در اینجا [jdk1.8.0_60]. اگر این یک JDK نیست یا نسخه‌ای کمتر از 1.8 دارید، به شرح زیر عمل کنید: [5-21];
  • به [8]، JRE که در حال حاضر به‌طور پیش‌فرض توسط Eclipse استفاده می‌شود؛
  • به [11]، انواع JDK و JRE که در حال حاضر توسط اکلیپس شناسایی می‌شوند؛
  • از [15]، به جای JRE، یک JDK را انتخاب کنید. این سند از پروژه‌های Maven استفاده می‌کند که به JDK نیاز دارند؛
  • در [21]، یک JDK با نسخه >=1.8 وجود دارد;
  • در [22-23]، به فکت‌های (نگاه‌های مختلف از یک پروژهٔ Eclipse) پروژه دسترسی پیدا کنید؛
  • در [24]، بررسی کنید که از نسخه‌ای از جاوا با نسخه >=1.8 استفاده می‌کنید؛

3.2. کلاس [ListeElectorale]

در زبان C، احتمالاً از یک ساختار برای نمایش فهرست نامزدهای انتخابات استفاده می‌کردیم. این ساختار ممکن بود شکل زیر را داشته باشد:

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

مفهوم «ساختار» در زبان جاوا وجود ندارد. باید آن را با مفهوم «کلاس» جایگزین کرد. بنابراین تصمیم می‌گیریم کلاسی برای ذخیره اطلاعات درباره فهرست کوتاه ایجاد کنیم. این کلاس ساختار پایه‌ای زیر را خواهد داشت:


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() {
   ...
    }
}
  • خط ۸: یک شناسه‌ی منحصربه‌فرد برای یک لیست. این مورد در اینجا ضروری نیست اما برای استفاده‌ی آینده ارائه شده است.
  • خط ۱۳: نام فهرست.
  • خط ۱۷: تعداد آراء برای فهرست
  • خط ۲۱: تعداد کرسی‌های کسب‌شده توسط فهرست
  • خط ۲۵: یک مقدار بولی که نشان می‌دهد آیا لیست حذف شده است (درصد آرای کسب‌شده زیر آستانه انتخاباتی) یا خیر.

هر فیلد خصوصی با نام [xyz] را می‌توان با استفاده از متدی به نام [setXyz] مقداردهی اولیه کرد. متد [getXyz] به نوبه خود، مقدار فیلد خصوصی [xyz] را بازیابی می‌کند. در مورد خاص که [xyz] یک فیلد بولی باشد، متد [getXyz] می‌تواند با متد [isXyz] جایگزین شود. نام‌گذاری خاص این متدها مطابق با استاندارد کدنویسی‌ای است که به نام استاندارد JavaBean شناخته می‌شود. بنابراین، متدهای عمومی زیر را تعریف می‌کنیم:

  • getId (خط ۴۸)، setId (خط ۵۷)
  • getNom (خط ۶۵)، setNom (خط ۷۴)
  • getVoix (خط ۸۲)، setVoix (خط ۹۰)
  • getSieges (خط ۹۸)، setSieges (خط ۱۰۶)
  • isElimine (خط ۱۱۴)، setElimine (خط ۱۲۲)
  • خطوط ۳۰–۳۱: یک سازنده بدون پارامتر تعریف کنید. این امکان را می‌دهد که یک شیء [ListeElectorale] بدون مقداردهی اولیه ایجاد شود. سپس می‌توان آن را با استفاده از متدهای set مقداردهی اولیه کرد.
  • خطوط ۴۰–۴۲: یک سازنده تعریف کنید که یک شیء [ListeElectorale] را ایجاد کرده و همزمان پنج فیلد خصوصی آن را مقداردهی اولیه می‌کند.
  • خطوط ۱۳۰–۱۳۲: متد [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);
        // نمایش جزئیات فهرست ۱
        System.out.println("listeElectorale1=" + listeElectorale1);
        // فهرست انتخاباتی جدید
        ListeElectorale listeElectorale2 = listeElectorale1;
        // نمایش فهرست هویت ۲
        System.out.println("listeElectorale2=" + listeElectorale2);
        // تغییر در تعداد کرسی‌ها
        listeElectorale2.setSieges(3);
        //نمایش هویت‌های دو فهرست
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

محیط اکلیپس برای این تست می‌تواند به شرح زیر باشد:

  • [1]: پروژه [elections-02A] نامیده می‌شود
  • [2]: برنامه در یک بسته قرار داده خواهد شد، در این مورد [istia.st.elections]
  • [3]: [ListeElectorale.java] کد منبع کلاس [ListeElectorale] است
  • [4]: کلاس‌های تست در یک پکیج قرار داده خواهند شد، در این مورد [istia.st.elections.tests]
  • [5]: کلاس تست [MainTest1ListeElectorale]

خروجی صفحه پس از اجرای برنامه فوق به شرح زیر است:

Image


وظیفه: با استفاده از اطلاعات بالا، کد کلاس ListeElectorale را تکمیل کنید.


3.3. ایجاد یک کلاس استثنا [ElectionsException]

در میان کلاس‌های استثنای مختلف در زبان جاوا، کلاسی به نام [RuntimeException] وجود دارد. این کلاس از کلاس [Exception] که ریشه تمام کلاس‌های استثنا است، ارث می‌برد. ویژگی متمایز نمونه‌های [RuntimeException] یا نمونه‌های مشتق‌شده از آن این است که نیازی به اعلام یا مدیریت شدن ندارند. این موارد به عنوان استثناهای گرفته‌نشده (uncaught exceptions) شناخته می‌شوند.

بیایید به یک مثال اولیه نگاه کنیم. کلاس [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] (خط ۳) ارث می‌برد. کامپایلر از ما می‌خواهد که استثناءهای نوع [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] (خط ۴) ارث می‌برد. کامپایلر از ما نمی‌خواهد که با خطاهای (exceptions) نوع [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);

ما ملزم نیستیم برای رسیدگی به هر استثنایی که توسط [Integer.parseInt] (خط 9) ایجاد می‌شود، یک بند [try - catch] را درج کنیم.

ایجاد و استفاده از کلاس‌های استثنا مشتق‌شده از [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);
    }
}
  • خط ۱: ما کلاس را در پکیج [istia.st.elections] قرار می‌دهیم؛
  • خط ۳: کلاس از [RuntimeException] ارث می‌برد. بنابراین بررسی نمی‌شود؛
  • خط ۴: یک شناسه‌ی سریالی‌سازی که در حال حاضر می‌توانیم آن را نادیده بگیریم؛
  • در برنامهٔ ما، از دو نوع سازنده استفاده خواهیم کرد:
    • سازنده استاندارد در خطوط ۱۵–۱۷، همان‌طور که در زیر نشان داده شده است:
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() + "]");
        }
  • (ادامه)
    • یا آن در خطوط ۱۴–۲۰، که برای انتشار استثنایی که قبلاً رخ داده است با محصور کردن آن در یک استثنای از نوع [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] را پرتاب کنند.


پروژه تست اکلیپس برای این نسخه جدید می‌تواند به صورت زیر باشد:

  • [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);
        //نمایش جزئیات فهرست ۱
        System.out.println("listeElectorale1=" + listeElectorale1);
        // فهرست جدید رأی‌دهندگان
        ListeElectorale listeElectorale2 = listeElectorale1;
        // نمایش فهرست هویت ۲
        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() + "]");
        }

    }
}
  • خط ۲۸: تلاشی برای مقداردهی اولیه تعداد صندلی‌ها با یک مقدار نامعتبر انجام می‌شود
  • خط ۳۰: اگر خطایی رخ دهد، نمایش داده می‌شود

اجرای تست نتایج زیر را تولید می‌کند:

Image

مشاهده می‌شود که کلاس [ListeElectorale] در واقع زمانی که تلاش شد تعداد صندلی‌ها با یک مقدار نامعتبر مقداردهی اولیه شود (خط ۲۸ کد)، یک استثنا تولید کرد.

3.4. یک کلاس تست واحد

نوع قبلی تست به تأیید بصری متکی است. ما بررسی می‌کنیم که آنچه روی صفحه نمایش داده می‌شود با آنچه انتظار می‌رود مطابقت دارد. این روش در یک محیط حرفه‌ای توصیه نمی‌شود. تست‌ها باید تا حد امکان خودکار باشند و هدفشان این باشد که به هیچ دخالت انسانی نیازی نداشته باشند. به هر حال، انسان‌ها مستعد خستگی هستند و توانایی آنها برای تأیید تست‌ها با گذشت روز کاهش می‌یابد.

یک برنامه کاربردی در طول زمان تکامل می‌یابد. با هر تغییر، باید تأیید کنیم که برنامه دچار «پس‌رفت» (regression) نمی‌شود و همچنان تست‌های عملکردی را که هنگام نوشتن اولیه آن انجام شده بود، با موفقیت پشت سر می‌گذارد. این تست‌ها به عنوان «تست‌های عدم پس‌رفت» (non-regression) شناخته می‌شوند. یک برنامه نسبتاً بزرگ ممکن است به صدها تست نیاز داشته باشد. در واقع، هر متد در هر کلاس از برنامه تست می‌شود. این‌ها به عنوان تست‌های واحد (unit tests) شناخته می‌شوند. اگر این تست‌ها خودکار نشده باشند، می‌توانند زمان زیادی از توسعه‌دهندگان بگیرند.

ابزارهایی برای خودکارسازی تست توسعه داده شده‌اند. یکی از آن‌ها [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);
    }

}
  • خط ۱: کلاس در پکیج [istia.st.elections.tests] قرار داده شده است؛
  • خط ۱۱: متدی که با anotation [@Before] علامت‌گذاری شده است، قبل از هر تست واحد اجرا می‌شود؛
  • خط ۱۶: متدی که با anotation [@After] علامت‌گذاری شده است، پس از هر تست واحد اجرا می‌شود؛
  • خط ۲۱: متدی که با انوتیشن [@Test] علامت‌گذاری شده، متدی است که توسط تست واحد بررسی می‌شود. متدهایی که با [@Test] علامت‌گذاری شده‌اند، یکی پس از دیگری اجرا خواهند شد، مگر اینکه خلاف آن توسط تست‌کننده مشخص شود، که می‌تواند خود متدهای مورد نظر برای تست را انتخاب کند. قبل از هر اجرای متد [@Test]، متد [@Before] اجرا می‌شود. پس از هر اجرای متد [@Test]، متد [@After] اجرا می‌شود؛
  • خطوط ۲۲–۲۵: تعریف یک روش تست [t1]؛
  • خط ۱۸: یکی از متدهای [Assert.assert*] برای بررسی صحت‌ها. متدهای زیر از [assert] در دسترس هستند:
    • assertEquals(expression1, expression2): بررسی می‌کند که مقادیر دو عبارت برابر هستند. انواع عبارت‌های زیادی پذیرفته می‌شوند (int, String, float, double, boolean, char, short). اگر دو عبارت برابر نباشند، یک استثنای [AssertionFailedError ] پرتاب می‌شود،
    • assertEquals(real1, real2, delta): بررسی می‌کند که دو عدد حقیقی تا فاصلهٔ delta برابر باشند، c.a.d abs(real1 - real2) ≤ delta. برای مثال، می‌توان نوشت assertEquals(real1, real2, 1E-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: این شرط باید برقرار شود؛
  • خط ۳۰: این فرض باید شکست بخورد؛

در محیط اکلیپس، یک کلاس تست به نام 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]، این همان پیامی است که در بالا نشان داده شده است. پیام، شماره خطی را که خطا در آن رخ داده است (خط ۳۰) نشان می‌دهد. در خط ۳۰، فراخوانی که با شکست مواجه شد، عبارت بود از:


    Assert.assertEquals(1, 2);

پارامتر اول «مقدار مورد انتظار» و پارامتر دوم «مقدار واقعی» نامیده می‌شود. پیام خطای مربوط به [test2] در بالا نشان می‌دهد که مقدار مورد انتظار ۲ بوده اما مقدار واقعی ۳ بوده است.

در نهایت، پیام‌های نوشته شده به کنسول توسط روش‌های تست مختلف به شرح زیر بود:

Image

این پیام‌ها نشان می‌دهند که متدهای [@Before] و [@After] واقعاً قبل و بعد از هر روش تست، فراخوانی شده‌اند.

کلاس‌های تست لزوماً توسط خود توسعه‌دهندگان نوشته نمی‌شوند. ممکن است توسط افرادی که مشخصات برنامه را تدوین کرده‌اند نوشته شوند. برخی از روش‌های توسعه، که به عنوان TDD (توسعه مبتنی بر آزمون) شناخته می‌شوند، نوشتن کلاس‌های آزمون را حتی پیش از نوشتن کلاس‌های مورد آزمون توصیه می‌کنند. این امر گاهی می‌تواند به روشن‌تر شدن مشخصاتی کمک کند که در غیر این صورت ممکن است به روش‌های مختلفی تفسیر شوند.

بیایید یک تست TDD به نام [JUnitTest1ListeElectorale] برای کلاس [ListeElectorale] ایجاد کنیم. در اکلیپس، طبق توضیحات قبلی پیش می‌رویم:

کد تولیدشده توسط جادوگر را به شرح زیر تکمیل می‌کنیم:


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: نسخهٔ ۲

مطالعهٔ پیشنهادی:

  • بخش‌های ۲.۱، ۲.۲، ۲.۴ و ۲.۷ از فصل ۲ کتاب [1]: کلاس‌ها و رابط‌ها
  • بخش‌های ۳.۳ (کلاس String)، ۳.۵ (کلاس ArrayList) و ۳.۶ (کلاس Arrays)

ما می‌خواهیم برنامه [Elections] را با افزودن محدودیت‌های جدید زیر بازنویسی کنیم:

  • کلاس [ListeElectorale] برای نمایش لیست نامزدها استفاده خواهد شد
  • برنامه از کاربر اطلاعات زیر را درخواست خواهد کرد:
  • تعداد صندلی‌های مورد پر شدن
  • نام‌ها و صداهای فهرست‌ها. از پیش مشخص نیست که چند فهرست وجود دارد. آخرین فهرست با نامی برابر با رشتهٔ «*» شناسایی خواهد شد.
  • از آنجا که تعداد فهرست‌ها از پیش مشخص نیست، ابتدا در یک شیء از نوع [ArrayList] ذخیره می‌شوند. سپس، پس از وارد شدن همه فهرست‌ها، به یک آرایه از فهرست‌ها منتقل خواهند شد.
  • نتایج به ترتیب نزولی تعداد کرسی‌های کسب‌شده نمایش داده خواهند شد.

برای مرتب‌سازی آرایه T، متدهای استاتیک مختلفی از کلاس [Arrays] در دسترس هستند:

  • Arrays.sort(T): آرایه T را بر اساس یک ترتیب طبیعی مرتب می‌کند، در صورتی که چنین ترتیبی داشته باشد (صعودی برای اعداد و تاریخ‌ها، الفبایی برای رشته‌ها و غیره).
  • Arrays.sort(T,comparator): برای مرتب‌سازی آرایه‌های T که ترتیب طبیعی ندارند. این مورد در اینجا برای آرایهٔ لیست‌ها صادق است، که باید بر اساس یک فیلد خاص در لیست مرتب شود: تعداد کرسی‌های به دست آمده.

در متد Arrays.sort(T,comparator)، پارامتر comparator یک شیء است که رابط Comparator زیر را پیاده‌سازی می‌کند:

Image

  • متد `compare` برای مقایسه دو عنصر از آرایه T استفاده می‌شود.
  • متد `equals` تعیین می‌کند که آیا دو شیء برابر هستند یا خیر

هر دو متد انواع شیء obj1 و obj2 را مقایسه می‌کنند. انتخاب obj1<obj2، obj1=obj2 یا obj1>obj2 بستگی به رابطهٔ ترتیب‌دهی دارد که می‌خواهید بین این دو شیء برقرار کنید. این بر عهدهٔ توسعه‌دهنده‌ای است که این رابط را پیاده‌سازی می‌کند تا مشخص کند چگونه می‌توانیم بدانیم که:

  • obj1 کوچکتر از obj2 است
  • obj1 بزرگ‌تر از obj2 است
  • obj1 برابر با obj2 است

کلاس Object، که تمام کلاس‌های جاوا از آن مشتق شده‌اند، از پیش متد [equals] را دارد. برای مرتب‌سازی یک آرایه T از اشیاء از نوع O، متد [equals] در کلاس O مورد نیاز نیست. بنابراین می‌توانیم پیاده‌سازی پیش‌فرض ارائه‌شده توسط کلاس Object را حفظ کنیم. فقط متد [compare] نیاز به پیاده‌سازی دارد. این متد به طور مکرر توسط متد [Arrays.sort] فراخوانی می‌شود. هر بار، متد دوم، obj1 و obj2 — دو عنصر آرایه T که باید مرتب شوند — را به عنوان پارامتر به متد compare ارسال می‌کند. در مورد ما، این عناصر از نوع [ListeElectorale] خواهند بود. به چندریختی (polymorphism) که در اینجا به کار رفته توجه کنید. متد [compare] به گونه‌ای تعریف شده است که پارامترهایی با نوع [Object] را می‌پذیرد. این بدان معناست که می‌تواند پارامترهایی از نوع [Object] یا انواع مشتق‌شده (چندریختی) را بپذیرد. از آنجایی که [Object] کلاس والدین تمام کلاس‌های جاوا است، پارامترهای واقعی ممکن است از نوع [ListeElectorale] باشند.

برای مرتب‌سازی صعودی، متد [compare] باید بازگرداند:

  • -1 اگر obj1 کوچکتر از obj2 باشد
  • +۱ اگر obj1 بزرگ‌تر از obj2 باشد
  • 0 اگر obj1 با obj2 برابر باشد

برای مرتب‌سازی نزولی، مقادیر +1 و -1 معکوس می‌شوند. اصطلاحات «کمتر از»، «بزرگ‌تر از» و «مساوی» بیانگر یک رابطهٔ ترتیبی هستند. برای اشیاء از نوع [ListeElectorale]، رابطهٔ list1 «کمتر از» list2 زمانی برقرار است که list1 رأی‌های کمتری نسبت به list2 داشته باشد.

در همان فایل منبع کلاس [MainElections]، می‌توان یک کلاس دوم اضافه کرد:

//کلاس مقایسه فهرست رأی‌دهندگان
class CompareListesElectorales implements Comparator {

     //مقایسه دو فهرست رأی‌دهندگان بر اساس تعداد آرا
    public int compare(Object obj1, Object obj2) {
         // بازیابی فهرست‌های رأی‌دهندگان
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
         // مقایسه آراء در این دو فهرست انتخاباتی
....        
    }
}
  • خط ۲: کلاس به صورت عمومی (public) تعریف نشده است. در یک فایل منبع جاوا، ممکن است چندین کلاس وجود داشته باشد، اما فقط یکی می‌تواند ویژگی عمومی را داشته باشد – کلاسی که نام آن با نام فایل منبع یکسان است.

در روش قبلی compare، پارامترها از نوع Object, هستند که نیاز دارد در خطوط ۷ و ۸ پارامترهای متد از نوع Object به نوع ListeElectorale تبدیل شوند. امضای متد compare توسط رابط Comparator تعیین می‌شود که برای مقایسهٔ اشیاء دلخواه نوشته شده است. از نسخه JDK 1.5، یک رابط عمومی Comparator:Comparator<T> وجود دارد، که در آن T می‌تواند هر نوع جاوا باشد. متد compare از رابط Comparator<T> اشیاء از نوع T را به جای اشیاء از نوع Object مقایسه می‌کند و بدین ترتیب از تبدیل‌های نوع قبلی جلوگیری می‌کند. کلاس مقایسه برای اشیاء از نوع ListeElectorale ممکن است به این صورت باشد:


//کلاس مقایسه فهرست‌های انتخاباتی
class CompareListesElectorales implements Comparator<ListeElectorale> {

    //مقایسه دو فهرست انتخاباتی بر اساس تعداد کرسی‌ها
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • خط ۲: کلاس رابط Comparator<ListeElectorale> را پیاده‌سازی می‌کند
  • خطوط ۵–۶: پارامترهای متد compare از نوع ListeElectorale هستند. تبدیل نوع اکنون غیرضروری است.

JDK نسخهٔ ۱.۵ مفهوم کلاس‌ها و اینترفیس‌های عمومی را برای کلاس‌ها و اینترفیس‌های مختلف در JDK نسخهٔ ۱.۴ معرفی کرد، که در ابتدا تنها با اشیاء از نوع Object کار می‌کرد. این موضوع در مورد لیست‌ها، دیکشنری‌ها و غیره نیز صدق می‌کند.

قبلاً اشاره کردیم که به دلیل نامشخص بودن تعداد لیست‌ها، نمی‌توانستند در یک آرایه ذخیره شوند. آن‌ها می‌توانند در یک شیء ArrayList ذخیره شوند که مفهوم «لیست اشیاء» را پیاده‌سازی می‌کند. این کلاس اشیاء از نوع Object را ذخیره می‌کند. از نسخه JDK 1.5، لیست‌های شیء نوع‌دار وجود دارند. بنابراین، یک شیء ArrayList<ListeElectorale> برای ذخیره لیست‌ها قبل از انتقال آن‌ها به یک آرایه استفاده خواهد شد. اگر آرایه با نام tListes باشد، با استفاده از عبارت زیر مرتب خواهد شد:


//مرتب‌سازی فهرست‌ها
Arrays.sort(tListes, new CompareListesElectorales());

که در آن CompareListesElectorales کلاسی است که رابط Comparator<ListeElectorale> را پیاده‌سازی می‌کند.


وظیفه: بازنویسی برنامه [Elections] با در نظر گرفتن این مشخصات جدید.


پروژه Eclipse می‌تواند به صورت زیر باشد:

یک مثال از اجرای [1] به شرح زیر است:

Image