Skip to content

14. برنامهٔ وب MVC در معماری سه‌لایه – مثال ۱

14.1. Présentation

تا کنون، ما خود را به مثال‌هایی با اهداف آموزشی محدود کرده‌ایم. به همین دلیل، آن‌ها باید ساده می‌بودند. اکنون ما یک برنامه کاربردی پایه را ارائه می‌دهیم که با این حال، از هر یک از برنامه‌های ارائه‌شده تا به امروز، امکانات بیشتری دارد. این برنامه به دلیل استفاده از هر سه لایه یک معماری سه‌لایه، قابل توجه خواهد بود:

Image

از خوانندگان دعوت می‌شود تا در صورت فراموشی، اصول یک برنامه وب MVC در معماری سه‌لایه را در بند ۴ مرور کنند.

اپلیکیشن وبی که قصد داریم بنویسیم، امکان مدیریت گروهی از افراد را با استفاده از چهار عملیات فراهم می‌کند:

  • فهرست افراد گروه
  • افزودن یک شخص به گروه
  • ویرایش یک شخص در گروه
  • حذف یک شخص از گروه

این چهار عملیات پایه از یک جدول پایگاه داده آشنا هستند. ما دو نسخه از این برنامه را خواهیم نوشت:

  • در نسخهٔ ۱، لایهٔ [dao] از پایگاه داده استفاده نخواهد کرد. افراد گروه در یک شیء ساده [ArrayList] که به‌طور داخلی توسط لایه [dao] مدیریت می‌شود، ذخیره خواهند شد. این امکان را برای خواننده فراهم می‌کند تا برنامه را بدون محدودیت‌های پایگاه داده آزمایش کند.
  • در نسخهٔ ۲، گروه افراد را در یک جدول پایگاه داده قرار خواهیم داد. ما نشان خواهیم داد که این کار را می‌توان بدون تأثیرگذاری بر لایهٔ وب نسخهٔ ۱ انجام داد، که بدون تغییر باقی خواهد ماند.

اسکرین‌شات‌های زیر از صفحات نمایش داده شده توسط برنامه به کاربر را نشان می‌دهند.

Image

Image

Image

 

14.2. پروژه اکلیپس

پروژهٔ کاربردی [personnes-01] نام دارد:

Image

این پروژه سه لایه معماری سه‌لایهٔ برنامه را پوشش می‌دهد:

  • لایه [dao] در داخل بسته [istia.st.mvc.personnes.dao] قرار دارد
  • لایه [metier] یا [service] در بسته [istia.st.mvc.personnes.service] قرار دارد
  • لایه [web] یا [ui] در داخل بسته [istia.st.mvc.personnes.web] قرار دارد
  • پکیج [istia.st.mvc.personnes.entites] شامل اشیایی است که بین لایه‌های مختلف به اشتراک گذاشته شده‌اند
  • پکیج [istia.st.mvc.personnes.tests] شامل تست‌های JUnit برای لایه‌های [dao] و [service] است

ما سه لایه – [dao]، [service] و [web] – را به ترتیب بررسی خواهیم کرد. از آنجا که نوشتن آن زمان زیادی می‌برد و خواندنش ممکن است خسته‌کننده باشد، گاهی در توضیحات مختصر عمل می‌کنیم، مگر اینکه محتوا جدید باشد.

14.3. نمایندگی یک شخص

این برنامه یک گروه از افراد را مدیریت می‌کند. اسکرین‌شات‌های پاراگراف 14.1 برخی از ویژگی‌های یک شخص را نشان دادند. به‌طور رسمی، این ویژگی‌ها توسط کلاسی به نام [Personne] نمایش داده می‌شوند:

Image

کلاس [Personne] به شرح زیر است:

package istia.st.springmvc.personnes.entites;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Personne {

     //شناسهٔ منحصر به فرد شخص
    private int id;
     // نسخهٔ جاری
    private long version;
     // نام خانوادگی
    private String nom;
     // نام
    private String prenom;
     // تاریخ تولد
    private Date dateNaissance;
     //وضعیت تأهل
    private boolean marie = false;
     // تعداد فرزندان
    private int nbEnfants;

     // گیرنده‌ها و تنظیم‌کننده‌ها
...

     // سازندهٔ پیش‌فرض
    public Personne() {

    }

     // سازنده با инициализация فیلدهای شخص
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

     // سازنده برای فردی که با کپی کردن فرد دیگر ایجاد شده است
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


     // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • یک شخص با اطلاعات زیر شناسایی می‌شود:
    • id: عددی که به‌طور منحصربه‌فرد یک شخص را شناسایی می‌کند
    • نام خانوادگی: نام خانوادگی شخص
    • نام: نام آنها
    • dateNaissance: تاریخ تولد آنها
    • وضعیت تأهل: اینکه متأهل هستند یا خیر
    • nbEnfants: تعداد فرزندانشان
  • ویژگی [version] ویژگی‌ای است که به‌طور مصنوعی برای اهداف برنامه اضافه شده است. از منظر شیءگرایی، بدون شک ترجیح داده می‌شد که این ویژگی به کلاسی مشتق‌شده از [Personne] اضافه شود. نیاز به آن زمانی آشکار می‌شود که به موارد استفادهٔ وب‌اپلیکیشن توجه شود. یکی از این موارد استفاده به شرح زیر است:

در زمان T1، کاربری به نام U1 وارد حالت ویرایش برای شخص P می‌شود. در این نقطه، تعداد فرزندان 0 است. آن‌ها این عدد را به 1 تغییر می‌دهند، اما قبل از اینکه بتوانند تغییرات خود را ذخیره کنند، کاربری با شناسه U2 ویرایش همان شخص P را آغاز می‌کند. از آنجایی که U1 هنوز تغییرات خود را ذخیره نکرده است، U2 تعداد فرزندان را 0 می‌بیند. U2 نام شخص P را به حروف بزرگ تغییر می‌دهد. سپس U1 و U2 تغییرات خود را به ترتیب ذخیره می‌کنند. تغییری که توسط U2 انجام شده است، حاکم خواهد بود: نام به صورت حروف بزرگ نوشته می‌شود و تعداد فرزندان روی صفر باقی می‌ماند، حتی اگر U1 فکر کند آن را به ۱ تغییر داده است.

مفهوم نسخهٔ یک شخص به ما کمک می‌کند این مشکل را حل کنیم. بیایید دوباره همان مورد استفاده را در نظر بگیریم:

در زمان T1، کاربری به نام U1 وارد حالت ویرایش برای شخص P می‌شود. در این لحظه، تعداد فرزندان 0 است و نسخه V1 است. آن‌ها تعداد فرزندان را به ۱ تغییر می‌دهند، اما قبل از اینکه بتوانند تغییرات خود را ذخیره کنند، کاربری با شناسه U2 شروع به ویرایش همان شخص P می‌کند. از آنجا که U1 هنوز تغییرات خود را ذخیره نکرده است، U2 تعداد فرزندان را 0 و نسخه را V1 می‌بیند. U2 نام شخص P را به حروف بزرگ تغییر می‌دهد. سپس U1 و U2 تغییرات خود را به ترتیب تأیید می‌کنند. قبل از تأیید یک تغییر، بررسی می‌کنیم که کاربری که شخص P را اصلاح می‌کند، از همان نسخه‌ای استفاده کند که در حال حاضر برای شخص P ثبت شده است. این مورد برای کاربر U1 صادق است. ویرایش آنها بنابراین پذیرفته می‌شود و نسخه شخص ویرایش‌شده از V1 به V2 تغییر می‌کند تا نشان دهد که آن شخص دچار تغییر شده است. هنگام اعتبارسنجی ویرایش انجام‌شده توسط U2، متوجه خواهیم شد که آنها نسخه V1 از شخص P را در اختیار دارند، در حالی که نسخه فعلی شخص P، V2 است. سپس می‌توانیم به کاربر U2 اطلاع دهیم که شخص دیگری قبلاً تغییراتی ایجاد کرده و او باید دوباره با استفاده از نسخه جدید شخص P شروع کند. او این کار را انجام می‌دهد، نسخه‌ای از شخص P (V2) را که اکنون یک فرزند دارد بازیابی می‌کند، نام را بزرگ‌نویسی می‌کند و تغییرات را ذخیره می‌کند. ویرایش آنها در صورتی پذیرفته خواهد شد که شخص ثبت‌شده P همچنان نسخه V2 را داشته باشد. در نهایت، ویرایش‌های انجام‌شده توسط U1 و U2 در نظر گرفته خواهند شد، در حالی که در مورد استفاده بدون نسخه‌ها، یکی از ویرایش‌ها از دست می‌رفت.

  • خطوط ۳۲–۴۰: یک سازنده که قادر به مقداردهی اولیه فیلدهای یک شخص است. فیلد [version] حذف شده است.
  • خطوط ۴۳–۵۱: یک سازنده که کپی‌ای از شخص ارسال‌شده به‌عنوان پارامتر ایجاد می‌کند. در نتیجه، دو شیء با محتوای یکسان اما با اشاره به دو نشانگر متفاوت خواهیم داشت.
  • خط ۵۵: متد [toString] مجدداً تعریف شده تا رشته‌ای را که نمایانگر وضعیت شخص است بازگرداند

14.4. لایه [dao]

لایه [dao] شامل کلاس‌ها و رابط‌های زیر است:

Image

  • [IDao] رابطی است که توسط لایه [dao] ارائه می‌شود
  • [DaoImpl] پیاده‌سازی این رابط است که در آن گروه افراد در یک شیء [ArrayList] محصور شده است
  • [DaoException] نوعی استثنای بدون بررسی است که توسط لایه [dao] پرتاب می‌شود

رابط [IDao] به شرح زیر است:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // فهرست تمام افراد
    Collection getAll();
     //بازیابی یک شخص خاص
    Personne getOne(int id);
     // افزودن/ویرایش یک شخص
    void saveOne(Personne personne);
     // حذف یک شخص
    void deleteOne(int id);
}
  • این رابط دارای چهار متد برای چهار عملیاتی است که می‌خواهیم روی گروه افراد انجام دهیم:
    • getAll: برای بازیابی مجموعه‌ای از افراد
    • getOne: بازیابی یک شخص با id مشخص
    • saveOne: افزودن یک شخص (id=-1) یا اصلاح یک شخص موجود (id ≠ -1)
    • deleteOne: برای حذف یک شخص با id مشخص

لایه [dao] ممکن است استثناها را پرتاب کند. این استثناها از نوع [DaoException] خواهند بود:

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

     // کد خطا
    private int code;

    public int getCode() {
        return code;
    }

// سازنده
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • خط ۳: کلاس [DaoException] که از [RuntimeException] ارث می‌برد، یک نوع استثنای غیربررسی‌شده است: کامپایلر از ما نمی‌خواهد که:
    • این نوع استثنا را هنگام فراخوانی متدی که ممکن است آن را پرتاب کند، با یک بلوک try/catch مدیریت کنید
    • کلیدواژه «throws DaoException» را در امضای متدی که ممکن است این استثنا را پرتاب کند، درج کنیم

این تکنیک نیاز به امضای متدهای رابط [IDao] با استثناءهای یک نوع خاص را از بین می‌برد. بنابراین، هر پیاده‌سازی که استثناءهای بدون بررسی را پرتاب کند، قابل قبول خواهد بود و بدین ترتیب انعطاف‌پذیری را به معماری می‌افزاید.

  • خط ۶: یک کد خطا. لایه [dao] انواع مختلفی از استثناها را پرتاب خواهد کرد که هر یک با یک کد خطای متفاوت شناسایی می‌شوند. این امر به لایه مسئول رسیدگی به استثنا امکان می‌دهد تا منبع دقیق خطا را شناسایی کرده و در نتیجه اقدام مناسب را انجام دهد. راه‌های دیگری نیز برای دستیابی به همین نتیجه وجود دارد. یکی از آن‌ها ایجاد یک نوع استثنای جداگانه برای هر نوع خطای ممکن است، برای مثال NomManquantException، PrenomManquantException، AgeIncorrectException، ...
  • خطوط ۱۳–۱۶: سازندری (constructor) که برای ایجاد یک استثنا با کد خطا و پیام خطا استفاده می‌شود.
  • خطوط ۸–۱۰: متدی که به کد رسیدگی به استثنا امکان می‌دهد کد خطا را بازیابی کند.

کلاس [DaoImpl] ) رابط [IDao] را پیاده‌سازی می‌کند:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

     // فهرست افراد
    private ArrayList personnes = new ArrayList();

     // شماره فرد بعدی
    private int id = 0;

     // ابتدایی‌سازی‌ها
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

     // فهرست افراد
    public Collection getAll() {
        return personnes;
    }

     //بازیابی یک شخص خاص
    public Personne getOne(int id) {
         // ما به دنبال آن شخص هستیم
        int i = getPosition(id);
         //آیا آن‌ها را پیدا کرده‌ایم؟
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

     // افزودن یا ویرایش یک شخص
    public void saveOne(Personne personne) {
         //آیا پارامتر person معتبر است؟
        check(personne);
         // افزودن یا ویرایش؟
        if (personne.getId() == -1) {
             // افزودن
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
         // ویرایش – جستجوی شخص
        int i = getPosition(personne.getId());
         // آیا شخص پیدا شده است؟
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
         //آیا نسخهٔ صحیح اصلی را داریم؟
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
         //در حال انتظار ۱۰ میلی‌ثانیه
         //wait(10);
         // مشکلی نیست – بیایید تغییر را اعمال کنیم
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

     // حذف یک شخص
    public void deleteOne(int id) {
         //در جستجوی فرد
        int i = getPosition(id);
         // آیا آن‌ها را پیدا کرده‌ایم؟
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
             // حذف آن شخص
            personnes.remove(i);
        }
    }

     // مولد شناسه
    private int getNextId() {
        id++;
        return id;
    }

     // جستجو برای یک شخص
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
         //در حال مرور فهرست افراد
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
         //نتیجه؟
        return trouvé ? i : -1;
    }

     //بررسی یک شخص
    private void check(Personne p) {
         // شخص p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
         // شناسه
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
         // تاریخ تولد
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
         // تعداد فرزندان
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
         // نام خانوادگی
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
         // نام
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

     // منتظر
    private void wait(int N) {
         // انتظار برای N میلی‌ثانیه
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // نمایش ردیابی استثنا
            e.printStackTrace();
            return;
        }
    }
}

ما فقط نکات اصلی این کد را تشریح خواهیم کرد. با این حال، کمی وقت را به پیچیده‌ترین بخش‌ها اختصاص می‌دهیم.

  • خط ۱۳: شیء [ArrayList] که شامل گروه افراد خواهد بود
  • خط 16: شناسه‌ی جدیدترین فردی که اضافه شده است. هر بار که یک فرد جدید اضافه می‌شود، این شناسه ۱ افزایش می‌یابد.

کلاس [DaoImpl] به صورت یک نمونه واحد ایجاد خواهد شد. این به عنوان یک تک‌نمونه (singleton) شناخته می‌شود. یک برنامه وب به طور همزمان به کاربران خود خدمت‌رسانی می‌کند. در هر لحظه، چندین نخ (thread) روی سرور وب در حال اجرا هستند. این نخ‌ها تک‌نمونه‌ها را به اشتراک می‌گذارند:

  • آنِ لایه [dao]
  • نمونه موجود در لایه [service]
  • آن‌هایی که در لایه وب متعلق به کنترل‌کننده‌ها، اعتبارسنج‌های داده و غیره هستند

اگر یک کلاس تک‌نهاد (singleton) فیلدهای خصوصی داشته باشد، باید فوراً از خود بپرسید که چرا این فیلدها را دارد. آیا این فیلدها موجه هستند؟ به هر حال، این فیلدها بین نخ‌های (threads) مختلف به اشتراک گذاشته می‌شوند. اگر این فیلدها فقط-خواندنی (read-only) باشند، این مسئله مشکلی ایجاد نمی‌کند، به شرطی که بتوان آن‌ها را در زمانی که مطمئن هستید فقط یک نخ فعال وجود دارد، مقداردهی اولیه کرد. ما معمولاً می‌توانیم این لحظه را شناسایی کنیم. این لحظه زمانی است که وب‌اپلیکیشن راه‌اندازی می‌شود اما هنوز شروع به خدمت‌رسانی به کلاینت‌ها نکرده است. اگر این فیلدها قابل خواندن/نوشتن باشند، دسترسی به آن‌ها باید همگام‌سازی شود؛ در غیر این صورت، فاجعه اجتناب‌ناپذیر است. ما این مشکل را هنگام تست لایه [dao] نشان خواهیم داد.

  • کلاس [DaoImpl] فاقد سازنده است. بنابراین از سازنده پیش‌فرض آن استفاده خواهد شد.
  • خطوط ۱۹–۳۸: متد [init] زمانی فراخوانی می‌شود که نمونهٔ واحد (singleton) لایهٔ [dao] ایجاد شود. این متد یک لیست شامل سه نفر ایجاد می‌کند.
  • خطوط ۴۱–۴۳: متد [getAll] از رابط [IDao] را پیاده‌سازی می‌کند. این متد یک مرجع به لیست افراد را بازمی‌گرداند.
  • خطوط ۴۶–۵۵: متد [getOne] از رابط [IDao] را پیاده‌سازی می‌کند. پارامتر آن شناسه (ID) شخص مورد جستجو است.

برای بازیابی این مقدار، یک متد خصوصی به نام [getPosition] در خطوط 113–126 فراخوانی می‌شود. این متد موقعیت فرد مورد جستجو را در لیست برمی‌گرداند، یا در صورتی که فرد پیدا نشود، مقدار -1 را برمی‌گرداند.

اگر فرد پیدا شده باشد، متد [getOne] به جای خود فرد، ارجاعی (خط ۵۱) به یک کپی از آن فرد را بازمی‌گرداند. این امر به این دلیل است که وقتی کاربر می‌خواهد یک شخص را ویرایش کند، اطلاعات مربوط به آن شخص از لایه [dao] درخواست شده و در قالب یک مرجع به یک شیء [Personne] برای ویرایش به لایه [web] ارسال می‌شود. این مرجع به‌عنوان یک کانتینر ورودی در فرم ویرایش عمل خواهد کرد. هنگامی که کاربر تغییرات خود را در لایه وب ارسال می‌کند، محتوای کانتینر ورودی اصلاح می‌شود. اگر کانتینر مرجعی به شخص واقعی در لایه [ArrayList] از لایه [dao] باشد، در این صورت آن رکورد به‌روزرسانی می‌شود، حتی اگر تغییرات به لایه‌های [service] و [dao] منتقل نشده باشد. لایه دوم تنها لایه‌ای است که مجاز به مدیریت فهرست افراد است. بنابراین، لایه وب باید روی یک نسخه از فرد مورد نظر برای تغییر کار کند. در اینجا، لایه [dao] این کپی را فراهم می‌کند.

اگر شخص مورد جستجو پیدا نشود، یک استثنای [DaoException] با کد خطای 2 (خط 53) پرتاب می‌شود.

  • خطوط ۹۴–۱۰۴: متد [deleteOne] از رابط [IDao] را پیاده‌سازی می‌کند. پارامتر آن شناسه شخص مورد نظر برای حذف است. اگر شخص مورد نظر برای حذف وجود نداشته باشد، یک استثنا از نوع [DaoException] با کد خطای ۲ پرتاب می‌شود.
  • خطوط ۵۸–۹۱: متد [saveOne] از رابط [IDao] را پیاده‌سازی می‌کند. پارامتر آن یک شیء [Personne] است. اگر این شیء دارای id=-1 باشد، در این صورت این یک افزودن شخص است. در غیر این صورت، شامل اصلاح شخص با آن id در لیست با استفاده از مقادیر موجود در پارامتر است.
    • خط ۶۰: اعتبار پارامتر [Personne] توسط یک متد خصوصی [check] که در خطوط ۱۲۹–۱۵۵ تعریف شده است، بررسی می‌شود. این متد بررسی‌های پایه‌ای را بر روی مقادیر فیلدهای مختلف در [Personne] انجام می‌دهد. هرگاه ناهنجاری‌ای تشخیص داده شود، یک [DaoException] با کد خطای مشخص پرتاب می‌شود. از آنجا که متد [saveOne] این استثنا را مدیریت نمی‌کند، این خطا به متد فراخوانی‌کننده منتقل خواهد شد.
    • خط ۶۲: اگر پارامتر [Personne] دارای شناسه -۱ باشد، در این صورت این یک افزودنی است. شیء [Personne] به لیست داخلی افراد (خط 66) با اولین شناسه موجود (خط 64) و شماره نسخه 1 (خط 65) اضافه می‌شود.
    • اگر پارامتر [Personne] مقداری غیر از -1 از نوع [id] داشته باشد، این شامل اصلاح شخص در لیست داخلی با آن مقدار [id] می‌شود. ابتدا بررسی می‌کنیم (خطوط ۷۰–۷۵) که شخص مورد اصلاح وجود دارد. اگر اینطور نباشد، یک استثنای [DaoException] با کد خطای ۲ پرتاب می‌کنیم.
    • اگر شخص وجود داشته باشد، بررسی می‌کنیم که نسخهٔ فعلی او با نسخهٔ پارامتر [Personne] که شامل تغییرات اعمال‌شده بر روی نسخهٔ اصلی است، مطابقت داشته باشد. اگر اینطور نباشد، یعنی شخصی که قصد اعمال تغییرات را دارد، آخرین نسخه را در اختیار ندارد. این موضوع به آن‌ها اطلاع داده می‌شود با ایجاد یک استثنای [DaoException] با کد خطا ۳ (خطوط ۷۹–۸۰).
    • اگر همه چیز به درستی پیش برود، تغییرات در رکورد اصلی آن شخص اعمال می‌شود (خطوط ۸۵–۹۰)

واضح است که این متد نیاز به همگام‌سازی دارد. برای مثال، بین لحظه‌ای که بررسی می‌کنیم شخص مورد نظر برای اصلاح واقعاً موجود است و لحظه‌ای که اصلاح انجام می‌شود، ممکن است شخص توسط شخص دیگری از لیست حذف شده باشد. بنابراین، این متد باید به عنوان [synchronized] اعلام شود تا اطمینان حاصل شود که در هر زمان تنها یک نخ (thread) آن را اجرا می‌کند. همین امر در مورد سایر متدهای رابط [IDao] نیز صدق می‌کند. ما این کار را انجام نمی‌دهیم و ترجیح می‌دهیم این همگام‌سازی را به لایه [service] منتقل کنیم. برای برجسته کردن مسائل همگام‌سازی، در حین آزمایش لایه [dao]، اجرای [saveOne] را برای ۱۰ میلی‌ثانیه (خط ۸۳) بین لحظه‌ای که می‌دانیم می‌توانیم تغییر را اعمال کنیم و لحظه‌ای که واقعاً آن را اعمال می‌کنیم، متوقف خواهیم کرد. رشته‌ای که [saveOne] را اجرا می‌کند، سپس CPU را به رشتهٔ دیگری واگذار خواهد کرد. این کار شانس ما را برای مشاهدهٔ تضادهای دسترسی به فهرست افراد افزایش می‌دهد.

14.5. تست‌ها برای لایه [dao]

یک تست JUnit برای لایه [dao] نوشته شده است:

[TestDao] آزمون مربوط به JUnit است. برای برجسته کردن مشکلات دسترسی همزمان به فهرست افراد، رشته‌هایی از نوع [ThreadDaoMajEnfants] ایجاد می‌شوند. وظیفه آن‌ها افزایش تعداد فرزندان یک فرد مشخص به میزان ۱ است.

[TestDao] شامل پنج تست است که با شماره‌های [test1] تا [test5] نام‌گذاری شده‌اند. در اینجا تنها به دو مورد از آن‌ها می‌پردازیم؛ خوانندگان را دعوت می‌کنیم تا سایر تست‌ها را در کد منبع همراه این مقاله بررسی کنند.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

     // لایه [dao]
    private DaoImpl dao;

     // سازنده
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

     // فهرست افراد
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // اصلاح یا حذف یک عنصر موجود نیست
    public void test2() throws ParseException {
...
    }

     // مدیریت نسخه‌ی شخص
    public void test3() throws ParseException, InterruptedException {
...
    }

     //قفل‌گذاری خوش‌بینانه – دسترسی چندرشته‌ای
    public void test4() throws Exception {
...
    }

     //بررسی‌های اعتبار برای saveOne
    public void test5() throws ParseException {
    ...
}
  • خط ۹: ارجاع به پیاده‌سازی لایه [dao] تحت تست
  • خطوط ۱۲–۱۵: سازنده آزمایشی JUnit. این سازنده یک نمونه از نوع [DaoImpl] را از لایه [dao] که قرار است آزمایش شود، ایجاد کرده و آن را مقداردهی اولیه می‌کند.

متد [test1] چهار متد رابط [IDao] را به شرح زیر آزمایش می‌کند:

    public void test1() throws ParseException {
         // فهرست فعلی
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
         //نمایش
        doListe(personnes);
         // افزودن یک شخص
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
         //اعتبارسنجی – اگر شخص پیدا نشود، برنامه از کار می‌افتد
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
         // ویرایش
        p1.setNom("Y");
        dao.saveOne(p1);
         // بررسی – اگر شخص پیدا نشود، برنامه از کار می‌افتد
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
         // حذف
        dao.deleteOne(id1);
         // بررسی
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // یک خطای کد ۲ باید رخ دهد
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // فهرست افراد
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • خط ۳: فهرست افراد درخواست می‌شود
  • خط ۶: این فهرست نمایش داده می‌شود
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]

سپس تست یک شخص را اضافه می‌کند، آن را تغییر می‌دهد و حذف می‌کند. این کار از هر چهار متد رابط [IDao] استفاده می‌کند.

  • خطوط ۸–۱۰: یک شخص جدید اضافه می‌شود (id=-1).
  • خط ۱۱: شناسهٔ شخص اضافه شده بازیابی می‌شود، زیرا با افزودن به او شناسهٔ ۱ اختصاص یافته است. او قبلاً شناسه نداشت.
  • خطوط ۱۳–۱۴: از لایه [dao] درخواست یک نسخه از شخص به‌تازگی اضافه شده می‌شود. مهم است به خاطر داشته باشیم که اگر شخص مورد نظر پیدا نشود، لایه [dao] یک استثنا (exception) پرتاب می‌کند. این امر منجر به کرش (crash) در خط ۱۳ می‌شود. این مورد می‌توانست به شیوه‌ای ظریف‌تر مدیریت شود. در خط ۱۴، نام شخص یافت‌شده بررسی می‌شود.
  • خطوط ۱۶–۱۷: ما این نام را تغییر می‌دهیم و از لایه [dao] می‌خواهیم تغییرات را ذخیره کند.
  • خطوط ۱۹–۲۰: ما یک نسخه از فردی را که به تازگی از لایه [dao] اضافه شده است درخواست می‌کنیم و نام جدید او را بررسی می‌کنیم.
  • خط ۲۲: شخص اضافه شده در ابتدای تست حذف می‌شود.
  • خطوط ۲۳–۳۴: یک کپی از فردی که به‌تازگی حذف شده از لایه [dao] درخواست می‌شود. یک [DaoException] با کد ۲ باید بازگردانده شود.
  • خطوط ۳۶–۳۷: فهرست افراد دوباره درخواست می‌شود. باید همان فهرست ابتدای تست دریافت شود.

روش [test4] با هدف برجسته کردن مشکلات دسترسی همزمان به متدهای لایه [dao] طراحی شده است. به یاد داشته باشید که این متدها همگام‌سازی نشده‌اند. کد تست به شرح زیر است:

    public void test4() throws Exception {
         // افزودن یک شخص
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // ایجاد N نخ برای به‌روزرسانی تعداد فرزندان
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // در انتظار پایان نخ‌ها
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // بازیابی شخص
        p1 = dao.getOne(id1);
         // آنها باید N فرزند داشته باشند
        assertEquals(N, p1.getNbEnfants());
         // حذف شخص p1
        dao.deleteOne(p1.getId());
         //بررسی
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         //باید خطای کد ۲ رخ دهد
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • خطوط ۳–۶: یک شخص P بدون فرزند به لیست اضافه می‌شود. [id] او ثبت می‌شود (خط ۶).
  • خطوط ۷–۱۳: N نخ راه‌اندازی می‌شوند. هر یک از آن‌ها تعداد فرزندان شخص P را به اندازه ۱ افزایش می‌دهد. در نهایت، شخص P باید N فرزند داشته باشد.
  • خطوط ۱۵–۱۷: متد [test4] که N نخ را راه‌اندازی کرده است، قبل از بررسی تعداد جدید فرزندان شخص P، منتظر پایان کار آن‌ها می‌ماند.
  • خطوط ۱۸–۲۱: شخص P بازیابی می‌شود و بررسی می‌شود که او دارای N فرزند است.
  • خطوط 22–35: شخص P حذف می‌شود و سپس بررسی می‌کنیم که دیگر در لیست وجود ندارد.

در خط ۱۱ می‌بینیم که رشته‌ها از نوع [ThreadDaoMajEnfants] هستند. سازنده این نوع سه پارامتر دارد:

  1. نام داده‌شده به نخ گفتگو، تا از طریق لاگ‌ها قابل ردیابی باشد
  2. اشاره‌ای به لایه [dao] تا نخ بتواند به آن دسترسی پیدا کند
  3. شناسه فردی که موضوع باید روی او کار کند

نوع [ThreadDaoMajEnfants] به شرح زیر است:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // نام نخ
    private String name;
     // مرجع در لایه [dao]
    private IDao dao;
     //شناسه فردی که قرار است روی او کار کنیم
    private int idPersonne;

     // سازنده
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // بدنهٔ نخ
    public void run() {
         // ردیابی
        suivi("lancé");
         // حلقه تا زمانی که با موفقیت یک واحد افزایش دهیم
         // تعداد فرزندان شخص idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // یک نسخه از شخص را از idPersonne بازیابی کنید
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             //پیگیری
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
             // ۱۰ میلی‌ثانیه صبر کنید قبل از واگذاری پردازنده
            try {
                 //پیگیری
                suivi("début attente");
                 // موقتا متوقف شدن برای واگذاری پردازنده
                Thread.sleep(10);
                 //پیگیری
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // انتظار کامل – در حال تلاش برای اعتبارسنجی کپی
             // در این میان، ممکن است سایر نخ‌ها نسخهٔ اصلی را تغییر داده باشند
            int codeErreur = 0;
            try {
                 // تعداد فرزندان این کپی را ۱ افزایش می‌دهد
                personne.setNbEnfants(nbEnfants + 1);
                 // در حال تلاش برای تغییر نسخه اصلی
                dao.saveOne(personne);
                 // موفقیت‌آمیز – نسخهٔ اصلی اصلاح شد
                fini = true;
            } catch (DaoException ex) {
                 // در حال بازیابی کد خطا
                codeErreur = ex.getCode();
                 //باید خطای نسخهٔ ۳ باشد – در غیر این صورت، دوباره تلاش می‌کنیم
                 // استثناء
                if (codeErreur != 3) {
                    throw ex;
                } else {
                     //پیگیری
                    suivi(ex.getMessage());
                }
                 // نسخهٔ اصلی تغییر کرده است – دوباره از ابتدا شروع کنید
            }
        }
         //پیگیری
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     //پیگیری
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • خط ۹: [ThreadDaoMajEnfants] در واقع یک تِرد است
  • خطوط ۱۸–۲۲: سازنده‌ای که نخ را با سه مورد اطلاعات اولیه راه‌اندازی می‌کند
    1. نام [name] که به نخ داده شده است
    2. یک مرجع [dao] به لایه [dao]. توجه داشته باشید که، بار دیگر، ما با نوع رابط [IDao] کار می‌کنیم و نه نوع پیاده‌سازی [DaoImpl].
    3. شناسه‌ی [id] مربوط به شخصی که نخ روی آن اجرا می‌شود

وقتی [test4] یک نخ [ThreadDaoMajEnfants] را راه‌اندازی می‌کند (خط ۱۲ از test4)، متد [run] (خط ۲۵) آن نخ اجرا می‌شود:

  • خطوط ۷۸–۸۱: متد خصوصی [suivi] ثبت خروجی صفحه را فعال می‌کند. متد [run] از این قابلیت استفاده می‌کند تا اجرای نخ را قابل ردیابی سازد.
  • این نخ تلاش می‌کند تعداد فرزندان شخص P با شناسه [id] را یک واحد افزایش دهد. این به‌روزرسانی ممکن است به چندین تلاش نیاز داشته باشد. بیایید دو نخ [TH1] و [TH2] را در نظر بگیریم. [TH1] از لایه [dao] یک نسخه از شخص P درخواست می‌کند. آن را دریافت می‌کند و متوجه می‌شود که نسخهٔ V1 را دارد. [TH1] متوقف می‌شود. [TH2] که در حال دنبال کردن آن بود، همین کار را انجام می‌دهد و همان نسخه، V1، از شخص P را به دست می‌آورد. [TH2] متوقف می‌شود. [TH2] کنترل را بازپس می‌گیرد، تعداد فرزندان P را افزایش می‌دهد و تغییرات خود را ذخیره می‌کند. می‌دانیم که در این نقطه، این تغییرات ذخیره شده‌اند و نسخه P به V2 تغییر خواهد کرد. [TH1] کار خود را به پایان رسانده است. [TH2] کنترل را از سر می‌گیرد و همین کار را انجام می‌دهد. به‌روزرسانی آن برای P رد خواهد شد زیرا نسخه‌ای از P با نسخه V1 در اختیار دارد، در حالی که نسخه اصلی P اکنون در نسخه V2 قرار دارد. بنابراین [TH2] باید کل چرخه [lecture -> mise à jour -> sauvegarde] را تکرار کند. به همین دلیل است که حلقه را در خطوط ۳۲–۷۲ می‌بینیم. در داخل این حلقه، نخ:
  • یک نسخه از شخص P را برای اصلاح درخواست می‌کند (خط ۳۴)
  • برای ۱۰ میلی‌ثانیه منتظر می‌ماند (خط ۴۳). این کار مصنوعی است و هدف آن ایجاد وقفه در اجرای نخ بین خواندن شخص P و به‌روزرسانی واقعی او در فهرست افراد است تا احتمال بروز تعارضات افزایش یابد.
  • تعداد فرزندان P را افزایش می‌دهد (خط ۵۴) و P را ذخیره می‌کند (خط ۵۶). اگر نخ نسخه صحیح P را نداشته باشد، یک استثنا توسط لایه [dao] پرتاب خواهد شد. سپس کد استثنا بازیابی می‌شود (خط ۶۱) تا بررسی شود که آیا واقعاً کد ۳ (نسخه نادرست P) است یا خیر. اگر اینطور نباشد، استثنا مجدداً به متد فراخوانی‌کننده پرتاب می‌شود که در نهایت متد تست [test4] است. اگر با استثناء‌ای با کد ۳ مواجه شویم، چرخه [lecture -> mise à jour -> sauvegarde] مجدداً آغاز می‌شود. اگر استثناء‌ای رخ ندهد، به‌روزرسانی کامل شده و کار نخ (thread) به پایان می‌رسد.

تست‌ها چه چیزی را نشان می‌دهند؟

در پیکربندی اول آزمایش‌شده:

  • ما دستور wait را در متد [saveOne] از [DaoImpl] (خط ۸۳، بخش ۱۴.۴) غیرفعال می‌کنیم.
         // منتظر ۱۰ میلی‌ثانیه
         //wait(10);
  • متد [test4] صد نخ ایجاد می‌کند (خط ۸، پاراگراف ۱۴.۵).
         // ایجاد N نخ برای به‌روزرسانی تعداد فرزندان
        final int N = 100;

نتایج زیر به دست آمده‌اند:

Image

تمام پنج آزمون با موفقیت انجام شدند.

در پیکربندی دوم آزمایش‌شده:

  • دستور wait در متد [saveOne] از [DaoImpl] از حالت توضیحی خارج شده است (خط ۸۳، بخش ۱۴.۴).
         //در انتظار ۱۰ میلی‌ثانیه
        wait(10);
  • متد [test4] دو نخ ایجاد می‌کند (خط 8، پاراگراف 14.5).
         //ایجاد N نخ برای به‌روزرسانی تعداد فرزندان
        final int N = 2;

نتایج زیر به دست آمده‌اند:

آزمون [test4] شکست خورد. دو نخ ایجاد شدند که هر کدام مأمور افزایش یک واحدی تعداد فرزندان شخص P بودند که در ابتدا 0 فرزند داشت. بنابراین پس از اجرای دو نخ انتظار داشتیم 2 فرزند داشته باشیم، اما تنها یک فرزند وجود دارد.

بیایید به لاگ‌های صفحه برای [test4] نگاه کنیم تا بفهمیم چه اتفاقی افتاده است:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • خط ۱: نخ شماره ۰ کار خود را آغاز می‌کند
  • خط ۲: یک نسخه از شخص P را بازیابی می‌کند و می‌بیند که تعداد فرزندان ۰ است
  • خط ۳: با متد [run] از کلاس خود مواجه می‌شود و بنابراین در زمان [1145536368171] (ms) متوقف می‌شود
  • خط ۴: نخ شماره ۱ سپس کنترل پردازنده را بازپس می‌گیرد و کار خود را آغاز می‌کند
  • خط ۵: یک نسخه از شخص P دریافت شده و مشخص می‌شود که او ۰ فرزند دارد
  • خط ۶: با متد [run] خود مواجه می‌شود و بنابراین متوقف می‌گردد
  • خط ۷: نخ شماره ۰ در زمان [1145536368187] (ms)، c.a.d، ۱۶ میلی‌ثانیه پس از از دست دادن آن، کنترل پردازنده را بازپس می‌گیرد.
  • خط ۸: همین امر برای نخ شمارهٔ ۱ نیز صدق می‌کند
  • خط ۹: نخ شماره ۰ به‌روزرسانی خود را تکمیل کرده و تعداد فرزندان را روی ۱ تنظیم کرده است
  • خط ۱۰: رشته شماره ۱ نیز همین کار را انجام داده است

سؤال این است: چرا نخ شماره ۱ توانست به‌روزرسانی خود را انجام دهد، در حالی که به طور معمول دیگر نسخه صحیح شخص P را که به تازگی توسط نخ شماره ۰ به‌روزرسانی شده بود، در اختیار نداشت؟

ابتدا می‌توانیم یک ناهنجاری بین خطوط ۷ و ۸ مشاهده کنیم: به نظر می‌رسد که نخ شماره ۰ بین این دو خط کنترل پردازنده (CPU) را به نخ شماره ۱ واگذار کرده است. در آن لحظه چه کاری انجام می‌داد؟ در حال اجرای متد [saveOne] از لایه [dao] بود. این متد ساختار زیر را دارد (به بند 14.4 مراجعه کنید):

    public void saveOne(Personne personne) {
...
         // اصلاح – جستجو برای شخص
....
         //آیا نسخهٔ صحیح نسخهٔ اصلی را داریم؟
...
         // در حال انتظار ۱۰ میلی‌ثانیه
        wait(10);
         // همه چیز واضح است – در حال اعمال تغییر
    ...
}
  • رشته شماره ۰ متد [saveOne] را اجرا کرد و به خط ۸ رسید، جایی که مجبور شد پردازنده را واگذار کند. در این میان، نسخه شخص P را که ۱ بود خوانده بود، زیرا شخص P هنوز به‌روزرسانی نشده بود.
  • وقتی پردازنده آزاد شد، نخ شمارهٔ ۱ آن را تصاحب کرد. سپس [saveOne] را اجرا کرد و به خط ۸ رسید، جایی که مجبور شد پردازنده را آزاد کند. در این میان، نسخهٔ شخص P را خواند که ۱ بود، زیرا شخص P هنوز به‌روزرسانی نشده بود.
  • با آزاد شدن پردازنده، نخ شماره ۰ آن را تصاحب کرد. از خط ۹ به بعد، به‌روزرسانی خود را انجام داد و تعداد فرزندان را روی ۱ تنظیم کرد. سپس متد [run] از رشته شماره ۰ پایان یافت و آن رشته لاگی را نمایش داد که بیان می‌کرد تعداد فرزندان را روی ۱ تنظیم کرده است (خط ۹).
  • وقتی پردازنده آزاد شد، نخ شماره ۱ آن را به ارث برد. از خط ۹ به بعد، به‌روزرسانی خود را انجام داد و تعداد فرزندان را روی ۱ تنظیم کرد. چرا ۱؟ چون نسخه‌ای از P را در اختیار دارد که تعداد فرزندانش روی ۰ تنظیم شده است. این موضوع در لاگ (خط ۵) ذکر شده است. سپس متد [run] در نخ شماره ۱ به پایان رسید و این نخ پیام لاگ را نمایش داد که در آن ذکر شده بود که تعداد فرزندان را روی ۱ تنظیم کرده است (خط ۱۰).

مشکل از کجا ناشی می‌شود؟ این مشکل از این واقعیت نشأت می‌گیرد که نخ شماره ۰ فرصت نکرد تغییر خود را ثبت (commit) کند و در نتیجه نسخه شخص P را قبل از آنکه نخ شماره ۱ تلاش کند آن نسخه را بخواند تا بررسی کند که آیا شخص P تغییر کرده است، به‌روزرسانی نکند. این سناریو بعید است اما غیرممکن نیست. ما مجبور شدیم تا برای ایجاد این مشکل با تنها دو نخ، نخ شماره ۰ را وادار به از دست دادن CPU کنیم. بدون این راه‌حل، پیکربندی قبلی نتوانسته بود همین سناریو را با ۱۰۰ نخ بازتولید کند. تست [test4] موفقیت‌آمیز بود.

راه حل چیست؟ بدون شک چندین راه حل وجود دارد. یکی از آنها که پیاده‌سازی آن ساده است، همگام‌سازی متد [saveOne] است:


    public synchronized void saveOne(Personne personne)

کلیدواژه [synchronized] تضمین می‌کند که تنها یک نخ در هر زمان می‌تواند این متد را اجرا کند. بنابراین، به نخ شماره ۱ تنها پس از خروج نخ شماره ۰ از [saveOne] اجازه اجرای آن داده می‌شود. بنابراین می‌توانیم مطمئن باشیم که نسخه شخص P تا زمانی که نخ شماره ۱ وارد [saveOne] شود، تغییر کرده است. سپس به‌روزرسانی آن رد خواهد شد زیرا نسخه صحیح P را نخواهد داشت.

این‌ها چهار متد لایه [dao] هستند که نیاز به همگام‌سازی دارند. با این حال، ما تصمیم گرفته‌ایم این لایه را همان‌طور که توصیف شده حفظ کرده و همگام‌سازی را به لایه [service] منتقل کنیم. دلایل متعددی برای این کار وجود دارد:

  • ما فرض می‌کنیم که دسترسی به لایه [dao] همیشه از طریق لایه [service] انجام می‌شود. این موضوع در اپلیکیشن وب ما صادق است.
  • همچنین ممکن است به دلایل دیگری غیر از دلایلی که ما را به همگام‌سازی متدهای لایه [dao] وادار می‌کند، همگام‌سازی دسترسی به متدهای لایه [service] نیز ضروری باشد. در این حالت، نیازی به همگام‌سازی متدهای لایه [dao] نیست. اگر مطمئن باشیم که:
  • تمام دسترسی‌ها به لایه [dao] از طریق لایه [service] انجام می‌شود
  • فقط یک نخ در هر لحظه از لایه [service] استفاده می‌کند

در این صورت می‌توانیم مطمئن باشیم که متدهای لایه [dao] همزمان توسط دو نخ اجرا نخواهند شد.

اکنون به لایه [service] می‌پردازیم.

14.6. لایه [service]

لایه [service] شامل کلاس‌ها و رابط‌های زیر است:

Image

  • [IService] رابطی است که توسط لایه [dao] ارائه می‌شود
  • [ServiceImpl] پیاده‌سازی این رابط است

رابط [IService] به شرح زیر است:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // فهرست همه افراد
    Collection getAll();
     //بازیابی یک فرد مشخص
    Personne getOne(int id);
     // افزودن/ویرایش یک شخص
    void saveOne(Personne personne);
     // حذف یک شخص
    void deleteOne(int id);
}

این با رابط [IDao] یکسان است.

پیاده‌سازی [ServiceImpl] از رابط [IService] به شرح زیر است:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

     //لایه [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // فهرست افراد
    public synchronized Collection getAll() {
        return dao.getAll();
    }

     //بازیابی یک شخص خاص
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

     // افزودن یا ویرایش یک شخص
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // حذف یک شخص
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • خطوط ۱۰–۱۹: ویژگی [IDao dao] مرجعی به لایه [dao] است. این ویژگی توسط Spring IoC مقداردهی اولیه خواهد شد.
  • خطوط 22–24: پیاده‌سازی متد [getAll] از رابط [IService]. این متد به سادگی درخواست را به لایه [dao] واگذار می‌کند.
  • خطوط ۲۷–۲۹: پیاده‌سازی متد [getOne] از رابط [IService]. این متد به سادگی درخواست را به لایه [dao] واگذار می‌کند.
  • خطوط ۳۲–۳۴: پیاده‌سازی متد [saveOne] از رابط [IService]. این متد به سادگی درخواست را به لایه [dao] واگذار می‌کند.
  • خطوط ۳۷–۳۹: پیاده‌سازی متد [deleteOne] از رابط [IService]. این متد به سادگی درخواست را به لایه [dao] واگذار می‌کند.
  • تمام متدها با استفاده از کلمه کلیدی `synchronized` همگام‌سازی شده‌اند، که تضمین می‌کند تنها یک نخ در هر لحظه می‌تواند از لایه `[service]` و در نتیجه لایه `[dao]` استفاده کند.

14.7. آزمایش لایه [service]

یک تست JUnit برای لایه [service] نوشته شده است:

[TestService] آزمون مربوط به JUnit است. آزمون‌های انجام‌شده دقیقاً مشابه آزمون‌های انجام‌شده برای لایه [dao] هستند. اسکلت [TestService] به شرح زیر است:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

     //لایه [service]
    private ServiceImpl service;

     // سازنده
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

     //فهرست افراد
    private void doListe(Collection personnes) {
...
    }

     //test1
    public void test1() throws ParseException {
         // فهرست فعلی
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
         // نمایش
        doListe(personnes);
         // افزودن شخص
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
         // بررسی – اگر شخص پیدا نشود، برنامه از کار می‌افتد
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

     // اصلاح یا حذف یک مورد غیرموجود
    public void test2() throws ParseException {
...
    }

     // مدیریت نسخه‌های شخص
    public void test3() throws ParseException, InterruptedException {
...
    }

     // قفل خوش‌بینانه – دسترسی چندرشته‌ای
    public void test4() throws Exception {
         //افزودن یک شخص
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
         // ایجاد N نخ برای به‌روزرسانی تعداد فرزندان
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

     //بررسی‌های اعتبار برای saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • خط ۹: لایه در حال آزمایش [service] از نوع [ServiceImpl] است.
  • خطوط ۱۱–۱۵: سازنده تست JUnit یک نمونه از لایه [service] را برای تست ایجاد می‌کند (خط ۱۲)، یک نمونه از لایه [dao] ایجاد می‌کند (خط ۱۳) و به لایه [service] دستور می‌دهد که از این لایه [dao] استفاده کند (خط ۱۴).

متد [test1]، چهار متد رابط [IService] را دقیقاً به همان شیوه‌ای که متد تست لایه [dao] با همین نام انجام می‌دهد، آزمایش می‌کند. تنها تفاوت این است که به جای لایه [dao]، به لایه [service] (خطوط 25، 32، 35) دسترسی پیدا می‌شود.

روش [test4] با هدف برجسته کردن مشکلات دسترسی همزمان به متدهای لایه [service] طراحی شده است. بار دیگر، این با روش آزمون [test4] در لایه [dao] یکسان است. با این حال، چند تفاوت وجود دارد:

  • به جای لایه [dao]، لایه [service] فراخوانی می‌شود (خط ۵۵)
  • یک مرجع به لایه [service] به جای مرجع به لایه [dao] به تارها ارسال می‌شود (خط ۶۱)

نوع [ThreadServiceMajEnfants] نیز تقریباً با نوع [ThreadDaoMajEnfants] یکسان است، با این تفاوت که به جای لایه [dao]، با لایه [service] کار می‌کند:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

     // نام نخ
    private String name;
     // مرجع در لایه [service]
    private IService service;
     //شناسهٔ فردی که قرار است روی او کار کنیم
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

     // ردیابی
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • خط ۱۲: نخ با لایه [service] کار می‌کند

ما تست‌ها را با پیکربندی‌ای که در لایه [dao] مشکل را ایجاد کرد، اجرا می‌کنیم:

  • ما دستور wait را در متد [saveOne] از [DaoImpl] (خط ۸۳، بخش ۱۴.۴) از حالت کامنت خارج می‌کنیم.
         // منتظر ۱۰ میلی‌ثانیه بمان
        wait(10);
  • متد [test4] صد نخ ایجاد می‌کند (خط ۶۵، پاراگراف ۱۴.۷).
         // ایجاد N نخ برای به‌روزرسانی تعداد فرزندان
        final int N = 100;

نتایج به‌دست‌آمده به شرح زیر است:

همگام‌سازی روش‌ها در لایه [service] بود که امکان موفقیت آزمون [test4] را فراهم کرد.

14.8. لایه [web]

بیایید معماری سه‌سطحی برنامه‌مان را مرور کنیم:

لایه [web] صفحه‌هایی را برای کاربر فراهم می‌کند تا گروه افراد را مدیریت کند:

  • فهرست افراد در گروه
  • افزودن یک شخص به گروه
  • ویرایش یک نفر در گروه
  • حذف یک شخص از گروه

برای این کار، این لایه به لایه [service] متکی خواهد بود که به نوبه خود از لایه [dao] فراخوانی می‌کند. ما پیش از این صفحاتی را که توسط لایه [web] مدیریت می‌شوند، توصیف کرده‌ایم (بخش 14.1). برای توصیف لایه وب، اکنون به ترتیب به موارد زیر می‌پردازیم:

  • پیکربندی آن
  • نماهای آن
  • کنترل‌کننده‌ی آن
  • برخی تست‌ها

14.8.1. پیکربندی برنامه وب

پروژهٔ Eclipse برای برنامه به شرح زیر است:

Image

  • در پکیج [istia.st.mvc.personnes.web]، کنترلر [Application] را خواهید یافت.
  • صفحات JSP و JSTL در [WEB-INF/vues] قرار دارند.
  • پوشه [lib] شامل کتابخانه‌های شخص ثالث مورد نیاز برنامه است. این کتابخانه‌ها را می‌توان در پوشه [Web App Libraries] یافت.

[web.xml]


فایل [web.xml] فایلی است که توسط وب‌سرور برای بارگذاری برنامه استفاده می‌شود. محتویات آن به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>mvc-personnes-01</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>
            istia.st.mvc.personnes.web.Application
        </servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  نگاشت ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  خانه فایل‌ها -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  صفحه خطای غیرمنتظره -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • خطوط ۲۷–۳۰: URLهای [/do/*] توسط سرولِت [personnes] پردازش خواهند شد
  • خطوط ۹–۱۲: سرولت [personnes] نمونه‌ای از کلاس [Application] است، کلاسی که ما قصد داریم ایجاد کنیم.
  • خطوط ۱۳–۲۴: سه پارامتر [urlList, urlEdit, urlErreurs] را تعریف می‌کنند که آدرس صفحات JSP را برای نماهای [list, edit, erreurs] مشخص می‌نمایند.
  • خطوط ۳۲–۳۴: برنامه یک صفحهٔ اصلی پیش‌فرض به آدرس [index.jsp] دارد که در ریشهٔ پوشهٔ برنامهٔ وب قرار دارد.
  • خطوط ۳۶–۳۹: برنامه دارای یک صفحهٔ خطای پیش‌فرض است که زمانی نمایش داده می‌شود که وب‌سرور با استثنایی مواجه شود که توسط برنامه مدیریت نشده باشد.
    • خط ۳۷: تگ <exception-type> نوع استثنایی را که توسط دستور <error-page> مدیریت می‌شود مشخص می‌کند؛ در اینجا نوع [java.lang.Exception] و زیرنوع‌های آن، یعنی همه استثناها، است.
    • خط ۳۸: تگ <location> صفحه‌ی JSP را مشخص می‌کند که هنگام رخ دادن استثنایی از نوع تعریف‌شده توسط <exception-type> نمایش داده شود. استثنای رخ‌داده در این صفحه در ابجکت با نام 'exception' در دسترس است اگر صفحه شامل دستور زیر باشد:

<%@ page isErrorPage="true" %>
  • (ادامه)
    • اگر <exception-type> یک نوع T1 را مشخص کند و یک استثنای از نوع T2—که از T1 مشتق نشده باشد—به وب سرور بازگردانده شود، سرور یک صفحه استثنای اختصاصی برای کلاینت ارسال می‌کند که عموماً چندان کاربرپسند نیست. از این رو، ارزش تگ <error-page> در فایل [web.xml] اهمیت پیدا می‌کند.

[index.jsp]


این صفحه زمانی نمایش داده می‌شود که کاربر بدون مشخص کردن URL، مستقیماً زمینهٔ برنامه را درخواست کند، c.a.d. در اینجا، [/personnes-01]. محتوای آن به شرح زیر است:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp] کلاینت را به URL [/do/list] هدایت می‌کند. این URL فهرست افراد گروه را نمایش می‌دهد.

14.8.2. صفحات JSP / JSTL در برنامه


نما [list.jsp]


این برای نمایش لیست افراد استفاده می‌شود:

Image

کد آن به شرح زیر است:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbEnfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

  • این نما یک عنصر را در قالب خود دریافت می‌کند:
  • عنصر [personnes] که با یک شیء از نوع [ArrayList] مرتبط است، که به نوبه خود شامل اشیاء از نوع [Personne] می‌باشد
  • خطوط ۲۲–۳۴: لیست ${personnes} به صورت حلقه‌ای بررسی می‌شود تا جدولی از نوع HTML که شامل افراد گروه است، نمایش داده شود.
  • خط ۳۱: URL مورد اشاره توسط لینک [Modifier] توسط فیلد [id] از شخص فعلی تنظیم می‌شود تا کنترلر مرتبط با URL [/do/edit] بداند کدام شخص را ویرایش کند.
  • خط ۳۲: همین امر در مورد لینک [Supprimer] نیز صدق می‌کند.
  • خط ۲۸: برای نمایش تاریخ تولد شخص در قالب JJ/MM/AAAA، ما از تگ در کتابخانه تگ [DateTime] در پروژه Apache [Jakarta Taglibs] استفاده می‌کنیم:

Image

فایل توضیحات این کتابخانه تگ در خط ۳ تعریف شده است.

  • خط ۳۷: لینک [Ajout] برای افزودن شخص جدید به URL [/do/edit] اشاره می‌کند، درست مانند لینک [Modifier] در خط ۳۱. این مقدار -1 پارامتر [id] است که نشان می‌دهد این یک افزودن است نه یک ویرایش.

نما [edit.jsp]


این برای نمایش فرم افزودن شخص جدید یا ویرایش شخص موجود استفاده می‌شود:

کد مربوط به نما [edit.jsp] به شرح زیر است:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit != ''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${dateNaissance}" name="dateNaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbEnfants}" name="nbEnfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

این نما یک فرم برای افزودن شخص جدید یا به‌روزرسانی شخص موجود را نمایش می‌دهد. از این پس، برای ساده‌سازی متن، از اصطلاح واحد [mise à jour] استفاده خواهیم کرد. دکمه [Valider] (خط ۷۳) فرم POST را در URL [/do/validate] (خط ۱۶) فراخوانی می‌کند. اگر POST با شکست مواجه شود، نمای [edit.jsp] مجدداً نمایش داده می‌شود و خطاهای رخ‌داده‌شده را نشان می‌دهد؛ در غیر این صورت، نمای [list.jsp] نمایش داده می‌شود.

  • نما [edit.jsp] که هم روی GET و هم روی POST که شکست می‌خورد نمایش داده می‌شود، در قالب خود عناصر زیر را دریافت می‌کند:
ویژگی
GET
POST
id
شناسهٔ شخص به‌روزرسانی‌شده
همانند بالا
version
نسخهٔ آن
همان
prenom
نام اول او
نام وارد شده
nom
نام خانوادگی او
نام خانوادگی وارد شده
dateNaissance
تاریخ تولد
تاریخ تولد وارد شده
marie
وضعیت تأهل
وضعیت تأهل وارد شده
nbEnfants
تعداد فرزندان
تعداد فرزندان وارد شده
erreurEdit
خالی
پیام خطایی که نشان می‌دهد افزودن یا اصلاح در طول POST با فشردن دکمه [Envoyer] ناموفق بوده است. در صورت عدم وجود خطا، خالی است.
erreurPrenom
خالی
نشان‌دهنده نام نادرست است – در غیر این صورت خالی
erreurNom
خالی
نشان‌دهنده نام خانوادگی نادرست است – در غیر این صورت خالی باشد
erreurDateNaissance
خالی
تاریخ تولد نادرست را نشان می‌دهد – در غیر این صورت خالی می‌ماند
erreurNbEnfants
خالی
نشان‌دهنده تعداد نادرست فرزندان است – در غیر این صورت خالی می‌ماند
  • رده‌های ۱۱–۱۵: اگر POST در فرم با خطا مواجه شود، [erreurEdit!=''] بازگردانده شده و یک پیام خطا نمایش داده می‌شود.
  • خط ۱۶: فرم به URL [/do/validate] ارسال خواهد شد
  • خط ۲۰: عنصر [id] قالب نمایش داده می‌شود
  • خط ۲۴: عنصر [version] از قالب نمایش داده می‌شود
  • خطوط ۲۶–۳۲: وارد کردن نام کوچک شخص:
    • هنگامی که فرم برای اولین بار نمایش داده می‌شود (GET)، ${first_name} مقدار فعلی فیلد [prenom] از شیء به‌روزرسانی‌شده [Personne] را نمایش می‌دهد، و ${erreurPrenom} خالی است.
    • اگر پس از POST خطایی رخ دهد، مقدار وارد شده ${first_name} به همراه هر پیام خطا ${erreurPrenom} دوباره نمایش داده می‌شود.
  • خطوط ۳۳–۳۹: وارد کردن نام خانوادگی شخص
  • خطوط ۴۰–۴۶: وارد کردن تاریخ تولد شخص
  • خطوط ۴۷–۶۱: وارد کردن وضعیت تأهل شخص با استفاده از دکمه رادیویی. مقدار فیلد [marie] در شیء [Personne] برای تعیین اینکه کدام یک از دو دکمه رادیویی باید انتخاب شود، استفاده می‌شود.
  • خطوط ۶۲–۶۸: وارد کردن تعداد فرزندان شخص
  • خط ۷۱: یک فیلد مخفی HTML با نام [id] که مقدار آن فیلد [id] برای فردی است که در حال به‌روزرسانی است؛ مقدار -۱ برای افزودن، و هر مقدار دیگری برای اصلاح.
  • خط ۷۲: یک فیلد مخفی HTML با نام [version]، با مقداری برابر با فیلد [id] شخص در حال به‌روزرسانی.
  • خط ۷۳: دکمه [Valider] از نوع [Submit] در فرم
  • خط ۷۴: یک پیوند برای بازگشت به فهرست افراد. این پیوند با برچسب [Annuler] نام‌گذاری شده است زیرا به کاربر اجازه می‌دهد بدون ارسال فرم، آن را ترک کند.

نما [exception.jsp]


این برای نمایش صفحه‌ای استفاده می‌شود که نشان می‌دهد یک استثنا رخ داده که توسط برنامه مدیریت نشده و به سرور وب ارجاع داده شده است.

برای مثال، بیایید فردی را که در گروه وجود ندارد حذف کنیم:

کد نمای [exception.jsp] به شرح زیر است:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>

  • این ویو یک کلید را در قالب خود دریافت می‌کند: عنصر [exception]، که همان استثنایی است که توسط وب‌سرور رهگیری شده است. برای اینکه این عنصر توسط وب‌سرور در قالب صفحه JSP گنجانده شود، صفحه باید تگ را در خط ۳ تعریف کرده باشد.
  • خط ۶: کد وضعیت HTTP پاسخ روی 200 تنظیم شده است. این اولین هدر HTTP در پاسخ است. کد 200 به کلاینت نشان می‌دهد که درخواست او برآورده شده است. به طور کلی، یک سند HTML در پاسخ سرور گنجانده شده است. در اینجا نیز همین‌طور است. اگر کد وضعیت پاسخ HTTP روی 200 تنظیم نشده باشد، در اینجا مقدار 500 را خواهد داشت که نشان‌دهنده وقوع یک خطا است. این به این دلیل است که سرور وب، پس از دریافت یک استثنای مدیریت‌نشده، این را به عنوان یک وضعیت غیرعادی تشخیص داده و آن را با کد 500 گزارش می‌کند. پاسخ مربوط به کد وضعیت 500 بسته به مرورگر متفاوت است: فایرفاکس سندی را که ممکن است همراه این پاسخ باشد نمایش می‌دهد، در حالی که مرورگرهای دیگر این سند را نادیده گرفته و صفحهٔ خود را نمایش می‌دهند. به همین دلیل کد وضعیت 500 را با کد وضعیت 200 جایگزین کرده‌ایم.
  • خط ۱۶: متن استثنا نمایش داده می‌شود
  • خط ۱۸: به کاربر لینکی برای بازگشت به فهرست افراد ارائه می‌شود

نما [erreurs.jsp]


این برای نمایش صفحه‌ای است که خطاهای راه‌اندازی برنامه، c.a.d، و خطاهای شناسایی‌شده در حین اجرای متد [init] سرویس‌لت کنترلر را گزارش می‌کند. این می‌تواند، برای مثال، عدم وجود یک پارامتر در فایل [web.xml] باشد، همانطور که در مثال زیر نشان داده شده است:

Image

کد صفحه [erreurs.jsp] به شرح زیر است:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

این صفحه در قالب خود یک عنصر [erreurs] را دریافت می‌کند که یک شی از نوع [ArrayList] است و شامل اشیایی از نوع [String] می‌باشد؛ این اشیاء پیام‌های خطا هستند. این پیام‌ها توسط حلقه در خطوط ۱۳ تا ۱۵ نمایش داده می‌شوند.

14.8.3. کنترل‌کنندهٔ برنامه

کنترل‌کننده [Application] در بسته [istia.st.mvc.personnes.web] تعریف شده است:

Image


Structure ture و راه‌اندازی کنترلر


اسکلت کنترل‌کننده [Application] به شرح زیر است:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
     //پارامترهای نمونه
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

     // سرویس
    ServiceImpl service=null;

     // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
         //بازیابی پارامترهای راه‌اندازی سرولت
        ServletConfig config = getServletConfig();
         // پردازش سایر پارامترهای راه‌اندازی
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
             // مقدار پارامتر
            valeur = config.getInitParameter(paramètres[i]);
             // آیا پارامتر موجود است؟
            if (valeur == null) {
                 //خطا ثبت می‌شود
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                 //مقدار پارامتر ذخیره می‌شود
                params.put(paramètres[i], valeur);
            }
        }
         // URL نمای [erreurs] به‌طور ویژه پردازش می‌شود
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
         //نمونه‌سازی لایه [dao]
        DaoImpl dao = new DaoImpl();
        dao.init();
         //نمونه‌سازی لایه [service]
        service = new ServiceImpl();
        service.setDao(dao);
    }

     // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

     // نمایش فهرست افراد
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // ویرایش / افزودن یک شخص
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     //تأیید ویرایش یا افزودن یک شخص
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     //تأیید تغییرات یا افزودن شخص
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     //نمایش فرم از پیش پرشده
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

     //ارسال
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
         //کنترل به GET واگذار می‌شود
        doGet(request, response);    }
}
  • خطوط ۲۰–۳۶: پارامترهای مورد انتظار از فایل [web.xml] بازیابی می‌شوند.
  • خطوط 39–41: پارامتر [urlErreurs] باید موجود باشد، زیرا URL نمای [erreurs] را مشخص می‌کند که قادر به نمایش هرگونه خطای инициализация است. اگر این فایل وجود نداشته باشد، برنامه با اجرای [ServletException] (خط ۴۰) خاتمه می‌یابد. این استثنا به سرور وب منتقل شده و توسط تگ <error-page> در فایل [web.xml] مدیریت می‌شود. بنابراین نما [exception.jsp] نمایش داده می‌شود:

Image

لینک [Retour à la liste] بالا غیرفعال است. استفاده از آن تا زمانی که برنامه اصلاح و دوباره بارگذاری نشود، همان پاسخ را بازخواهد گرداند. همانطور که قبلاً دیدیم، این برای انواع دیگر خطاها مفید است.

  • خط ۴۳: یک نمونه [DaoImpl] ایجاد می‌کند که لایه [dao] را پیاده‌سازی می‌کند
  • خط ۴۴: این نمونه را مقداردهی اولیه می‌کند (ایجاد یک لیست اولیه از سه نفر)
  • خط ۴۶: یک نمونه از [ServiceImpl] ایجاد می‌کند که لایه [service] را پیاده‌سازی می‌کند
  • خط ۴۷: لایه [service] را با ارائه یک مرجع به لایه [dao] راه‌اندازی می‌کند

پس از اینکه کنترل‌کننده راه‌اندازی شد، متدهای آن یک مرجع [service] به لایه [service] (خط 15) دارند، که از آن برای اجرای عملیات درخواستی کاربر استفاده خواهند کرد. این اقدامات توسط متد [doGet] رهگیری شده و برای پردازش به یک متد خاص کنترل‌کننده ارسال می‌شوند:

URL
متد HTTP
متد کنترل‌کننده
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

روش [doGet]


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

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

         //بررسی نحوهٔ راه‌اندازی سرولت
        if (erreursInitialisation.size() != 0) {
             //کنترل به صفحهٔ خطا منتقل می‌شود
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
             //پایان
            return;
        }
         // متد مورد استفاده برای ارسال درخواست را بازیابی می‌کند
        String méthode = request.getMethod().toLowerCase();
         // اکشن قابل اجرا را بازیابی می‌کند
        String action = request.getPathInfo();
         // اقدام؟
        if (action == null) {
            action = "/list";
        }
         // اجرای اقدام
        if (méthode.equals("get") && action.equals("/list")) {
             // فهرست افراد
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
             // حذف یک شخص
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
             //نمایش فرم برای افزودن/ویرایش یک شخص
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
             //اعتبارسنجی فرم برای افزودن/ویرایش یک شخص
            doValidatePersonne(request, response);
            return;
        }
         //موارد دیگر
        doListPersonnes(request, response);
    }
  • خطوط ۷–۱۳: بررسی می‌کنیم که لیست خطاهای инициализация خالی باشد. اگر اینطور نباشد، نمای [erreurs(erreurs)] را نمایش می‌دهیم که خطا(ها) را گزارش خواهد کرد.
  • خط ۱۵: متد [get] یا [post] که توسط کلاینت برای ارسال درخواست خود استفاده می‌شود، بازیابی می‌گردد.
  • خط ۱۷: مقدار پارامتر [action] را از درخواست بازیابی می‌کنیم.
  • خطوط ۲۳–۲۷: پردازش درخواست [GET /do/list] که درخواست فهرستی از افراد را دارد.
  • خطوط ۲۸–۳۲: پردازش درخواست [GET /do/delete] که درخواست حذف یک شخص را دارد.
  • خطوط ۳۳–۳۷: پردازش درخواست [GET /do/edit] که فرم به‌روزرسانی یک شخص را درخواست می‌کند.
  • خطوط ۳۸–۴۲: پردازش درخواست [POST /do/validate] که درخواست اعتبارسنجی شخص به‌روزشده را دارد.
  • خط ۴۴: اگر عمل درخواستی یکی از پنج مورد قبلی نباشد، آنگاه مانند [GET /do/list] در نظر گرفته می‌شود.

روش [doListPersonnes]


این متد درخواست [GET /do/list] را پردازش می‌کند که فهرستی از افراد را درخواست می‌کند:

Image

کد آن به شرح زیر است:

1
2
3
4
5
6
7
8
9
     //نمایش لیست افراد
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // قالب نما [list]
        request.setAttribute("personnes", service.getAll());
         //نمایش نما [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • خط ۵: لایه [service] برای دریافت فهرست افراد گروه پرس‌وجو می‌شود و این فهرست در مدل تحت کلید «people» قرار می‌گیرد.
  • خط ۷: نمای [list.jsp]، که در بند 14.8.2 توصیف شده است، نمایش داده می‌شود.

متد [doDeletePersonne]


این متد پرس‌وجوی [GET /do/delete?id=XX] را پردازش می‌کند که درخواست حذف شخص با شناسه XX را دارد. URL [/do/delete?id=XX] مربوط به لینک‌های [Supprimer] در نما [list.jsp] است:

Image

که کد آن به شرح زیر است:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

در خط ۱۲، ما URL [/do/delete?id=XX] را برای لینک [Supprimer] مشاهده می‌کنیم. متد [doDeletePersonne] که مسئول رسیدگی به این URL است، باید شخص با id=XX را حذف کرده و سپس لیست جدید افراد گروه را نمایش دهد. کد آن به شرح زیر است:

     //اعتبارسنجی تغییرات یا افزودن یک شخص
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         //بازیابی شناسهٔ شخص
        int id = Integer.parseInt(request.getParameter("id"));
         // حذف شخص
        service.deleteOne(id);
         // ارسال مجدد به فهرست افراد
        response.sendRedirect("list");
    }
  • خط ۵: URL در حال پردازش به شکل [/do/delete?id=XX] است. مقدار [XX] از پارامتر [id] بازیابی می‌شود.
  • خط ۷: به لایه [service] دستور داده می‌شود تا شخص با شناسهٔ به‌دست‌آمده را حذف کند. ما هیچ‌گونه بررسی‌ای انجام نمی‌دهیم. اگر شخصی که قصد حذف آن را داریم وجود نداشته باشد، لایه [dao] یک استثنا (exception) ایجاد می‌کند که توسط لایه [service] به سمت بالا منتقل می‌شود. ما اینجا در کنترلر نیز این مورد را مدیریت نمی‌کنیم. بنابراین این خطا تا وب‌سرور منتقل می‌شود که با پیکربندی، صفحه [exception.jsp] را که در بند 14.8.2 شرح داده شده است، نمایش خواهد داد:

Image

  • خط ۹: اگر حذف با موفقیت انجام شود (بدون استثنا)، به کلاینت دستور داده می‌شود که به URL نسبی [list] هدایت (redirect) کند. از آنجایی که صفحهٔ به تازگی پردازش شده [/do/delete] است، URL هدایت مجدد [/do/list] خواهد بود. بنابراین مرورگر به [GET /do/list] هدایت می‌شود که فهرست افراد را نمایش می‌دهد.

متد [doEditPersonne]


این متد درخواست [GET /do/edit?id=XX] را که فرم به‌روزرسانی شخص با شناسه XX را درخواست می‌کند، مدیریت می‌کند. URL [/do/edit?id=XX] همان URL برای لینک‌های [Modifier] و [Ajout] در نمای [list.jsp] است:

Image

که کد آن به شرح زیر است:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

در خط ۱۱، می‌توانیم URL [/do/edit?id=XX] را برای لینک [Modifier] و در خط ۱۷، URL [/do/edit?id=-1] را برای لینک [Ajout] مشاهده کنیم. متد [doEditPersonne] باید فرم ویرایش را برای شخص با شناسه XX نمایش دهد یا اگر ورودی جدید باشد، یک فرم خالی نمایش دهد.

کد متد [doEditPersonne] به شرح زیر است:

     //ویرایش یا افزودن یک شخص
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         //بازیابی شناسهٔ شخص
        int id = Integer.parseInt(request.getParameter("id"));
         // افزودن یا ویرایش؟
        Personne personne = null;
        if (id != -1) {
             // ویرایش – شخص مورد ویرایش را بازیابی می‌کند
            personne = service.getOne(id);
        } else {
             // افزودن – ایجاد یک شخص خالی
            personne = new Personne();
            personne.setId(-1);
        }
         //شیء [Personne] در قالب نما [edit] قرار می‌گیرد
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
         //نمایش نما [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • GET یک URL از نوع [/do/edit?id=XX] را هدف قرار می‌دهد. در خط ۵، مقدار [id] را بازیابی می‌کنیم. سپس دو سناریوی ممکن وجود دارد:
  1. اگر id برابر -1 نباشد، این یک به‌روزرسانی است و باید یک فرم نمایش داده شود که از قبل با جزئیات شخص مورد نظر برای به‌روزرسانی پر شده باشد. در خط ۱۰، این شخص از لایه [service] بازیابی می‌شود.
  2. اگر id برابر با -1 باشد، این یک افزودن است و باید یک فرم خالی نمایش داده شود. برای این کار، یک رکورد شخص خالی در خطوط 13–14 ایجاد می‌شود.
  • شیء حاصل [Personne] در قالب صفحه [edit.jsp] که در بند 14.8.2 توصیف شده است، قرار می‌گیرد. این قالب شامل عناصر زیر است: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. این عناصر در خطوط 17–30 مقداردهی اولیه می‌شوند، به استثنای آنهایی که مقدارشان رشته خالی [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants] است. مشخص است که اگر آنها در قالب وجود نداشته باشند، کتابخانه JSTL برای مقدارشان یک رشته خالی نمایش خواهد داد. اگرچه عنصر [erreurEdit] نیز مقدار رشته‌ی خالی دارد، با این حال مقداردهی اولیه می‌شود زیرا در صفحه‌ی [edit.jsp]، مقدار آن بررسی می‌شود.
  • هنگامی که مدل آماده شد، کنترل به صفحه [edit.jsp]، خطوط ۳۲–۳۳، واگذار می‌شود که نمای [edit] را تولید خواهد کرد.

روش [doValidatePersonne]


این متد درخواست [POST /do/validate] را مدیریت می‌کند که فرم به‌روزرسانی را اعتبارسنجی می‌کند. این POST توسط دکمه [Valider] فراخوانی می‌شود:

Image

بیایید فیلدهای ورودی فرم HTML را که در نمای بالا نشان داده شده است، به یاد بیاوریم:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

درخواست POST شامل پارامترهای [prenom, nom, dateNaissance, marie, nbEnfants, id, version] است و به URL [/do/validate] ارسال می‌شود (خط 1). این درخواست توسط متد زیر [doValidatePersonne] پردازش می‌شود:

//اعتبارسنجی اصلاح یا افزودن یک شخص
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         //بازیابی داده‌های ارسال‌شده
        boolean formulaireErroné = false;
        boolean erreur;
         // نام
        String prenom = request.getParameter("prenom").trim();
         //آیا نام اول معتبر است؟
        if (prenom.length() == 0) {
             // ثبت خطا
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
         // نام خانوادگی
        String nom = request.getParameter("nom").trim();
         //آیا نام اول معتبر است؟
        if (nom.length() == 0) {
             // خطا ثبت شد
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
         // تاریخ تولد
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
             // خطا ثبت شد
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
         // وضعیت تأهل
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
         // تعداد فرزندان
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
             // توجه به خطا
            erreur = true;
        }
         //تعداد نادرست فرزندان؟
        if (erreur) {
             //خطا گزارش شد
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
         //شناسهٔ شخص
        int id = Integer.parseInt(request.getParameter("id"));
         // نسخه
        long version = Long.parseLong(request.getParameter("version"));
         //آیا در فرم خطایی وجود دارد؟
        if (formulaireErroné) {
             // بارگذاری مجدد فرم با پیام‌های خطا
            showFormulaire(request, response, "");
             // انجام شد
            return;
        }
         // فرم صحیح است – شخص ذخیره شد
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
             //ذخیره شد
            service.saveOne(personne);
        } catch (DaoException ex) {
             //فرم با پیام خطا مجدداً نمایش داده می‌شود
            showFormulaire(request, response, ex.getMessage());
             //انجام شد
            return;
        }
         // ارسال به لیست افراد
        response.sendRedirect("list");
    }

     //نمایش فرم از پیش پرشده
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
         // قالب نما را آماده کنید [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
         //نمایش نما [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • خطوط ۸–۱۴: پارامتر [prenom] از درخواست POST بازیابی شده و اعتبار آن بررسی می‌شود. اگر نادرست تشخیص داده شود، عنصر [erreurPrenom] با یک پیام خطا مقداردهی اولیه شده و در ویژگی‌های پرس‌وجو قرار می‌گیرد.
  • خطوط 16–22: همین رویه برای پارامتر [nom] دنبال می‌شود
  • خطوط 24–32: همان رویه برای پارامتر [dateNaissance] دنبال می‌شود
  • خط ۳۴: پارامتر [marie] بازیابی می‌شود. ما اعتبار آن را بررسی نمی‌کنیم زیرا اصولاً از مقدار یک دکمه رادیویی می‌آید. با این حال، هیچ چیزی مانع از آن نمی‌شود که یک برنامه یک [POST /personnes-01/do/validate] را به همراه یک پارامتر ساختگی [marie] تولید کند. بنابراین باید اعتبار این پارامتر را بررسی کنیم. در اینجا، ما به مدیریت خطاهای خود تکیه می‌کنیم که در صورتی که کنترل‌کننده خود خطاها را مدیریت نکند، صفحه [exception.jsp] را نمایش می‌دهد. بنابراین، اگر تبدیل پارامتر [marie] به یک مقدار بولی در خط ۳۴ با شکست مواجه شود، یک استثنا پرتاب خواهد شد و در نتیجه صفحه [exception.jsp] برای کلاینت ارسال می‌شود. این رفتار برای ما قابل قبول است.
  • خطوط ۳۴–۵۴: ما پارامتر [nbEnfants] را بازیابی کرده و مقدار آن را بررسی می‌کنیم.
  • خط ۵۶: ما پارامتر [id] را بدون بررسی مقدار آن بازیابی می‌کنیم
  • خط ۵۸: ما همین کار را برای پارامتر [version] انجام می‌دهیم
  • خطوط ۶۰–۶۵: اگر فرم حاوی خطا باشد، با پیام‌های خطای تولیدشده قبلی مجدداً نمایش داده می‌شود
  • خطوط ۶۷–۶۹: اگر معتبر باشد، یک شیء جدید [Personne] با استفاده از عناصر فرم ایجاد می‌شود
  • خطوط ۷۰–۷۸: شخص ذخیره می‌شود. عملیات ذخیره ممکن است با شکست مواجه شود. در یک محیط چندکاربره، شخص مورد ویرایش ممکن است حذف شده باشد یا قبلاً توسط شخص دیگری ویرایش شده باشد. در این صورت، لایه [dao] یک استثنا پرتاب می‌کند که در اینجا مدیریت می‌شود.
  • خط ۸۰: اگر هیچ استثنایی رخ نداده باشد، کلاینت به URL [/do/list] برای نمایش وضعیت جدید گروه هدایت می‌شود.
  • خط ۷۵: اگر در حین ذخیره کردن خطایی رخ داده باشد، درخواست می‌کنیم که فرم اولیه مجدداً نمایش داده شود و پیام خطای حاصل از استثنا (پارامتر سوم) به آن ارسال گردد.

متد [showFormulaire] (خطوط 84–101) قالب مورد نیاز برای صفحه [edit.jsp] را با استفاده از مقادیر وارد شده (request.getParameter(" ... ")) ایجاد می‌کند. شایان ذکر است که پیام‌های خطا قبلاً توسط متد [doValidatePersonne] در قالب درج شده‌اند. صفحه [edit.jsp] در خطوط 99–100 نمایش داده می‌شود.

14.9. آزمایش برنامه وب

تعدادی آزمون در بخش 14.1 ارائه شدند. از خواننده دعوت می‌شود تا آن‌ها را مجدداً اجرا کند. در اینجا تصاویر بیشتری را نشان می‌دهیم که موارد تداخل دسترسی به داده‌ها را در یک محیط چندکاربره نشان می‌دهند:

[Firefox] مرورگر کاربر U1 خواهد بود. این کاربر URL [http://localhost:8080/personnes-01] را درخواست می‌کند:

Image

[IE] مرورگر کاربر U2 است. این کاربر همان URL را درخواست می‌کند:

Image

کاربر U1 وارد صفحه ویرایش شخص [Lemarchand] می‌شود:

Image

کاربر U2 نیز همین کار را انجام می‌دهد:

Image

کاربر U1 تغییرات را اعمال و ذخیره می‌کند:

کاربر U2 همین کار را انجام می‌دهد:

کاربر U2 از طریق لینک [Annuler] در فرم به فهرست افراد بازمی‌گردد:

Image

آن‌ها شخص [Lemarchand] را که توسط U1 اصلاح شده است، پیدا می‌کنند. اکنون U2، [Lemarchand] را حذف می‌کند:

U1 هنوز فهرست خود را دارد و می‌خواهد دوباره [Lemarchand] را ویرایش کند:

U1 از لینک [Retour à la liste] استفاده می‌کند تا ببیند موضوع چیست:

Image

او متوجه می‌شود که [Lemarchand] واقعاً دیگر در فهرست نیست...

14.10. Conclusion

ما معماری MVC را در یک معماری سه‌لایه ([web, metier, dao]) با استفاده از یک مثال پایه برای مدیریت فهرستی از افراد پیاده‌سازی کرده‌ایم. این امر به ما امکان داد تا مفاهیم ارائه‌شده در بخش‌های قبلی را به کار ببریم. در نسخه مورد بررسی، فهرست افراد در حافظه نگهداری می‌شد. به‌زودی نسخه‌هایی را بررسی خواهیم کرد که در آن‌ها این فهرست در یک جدول پایگاه داده ذخیره می‌شود.

اما ابتدا ابزاری به نام Spring IoC را معرفی می‌کنیم که ادغام لایه‌های مختلف یک برنامه ntier را آسان‌تر می‌کند.