14. برنامهٔ وب MVC در معماری سهلایه – مثال ۱
14.1. Présentation
تا کنون، ما خود را به مثالهایی با اهداف آموزشی محدود کردهایم. به همین دلیل، آنها باید ساده میبودند. اکنون ما یک برنامه کاربردی پایه را ارائه میدهیم که با این حال، از هر یک از برنامههای ارائهشده تا به امروز، امکانات بیشتری دارد. این برنامه به دلیل استفاده از هر سه لایه یک معماری سهلایه، قابل توجه خواهد بود:

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



![]() |
![]() |
14.2. پروژه اکلیپس
پروژهٔ کاربردی [personnes-01] نام دارد:

این پروژه سه لایه معماری سهلایهٔ برنامه را پوشش میدهد:
![]() |
- لایه [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] نمایش داده میشوند:
![]()
کلاس [Personne] به شرح زیر است:
- یک شخص با اطلاعات زیر شناسایی میشود:
- 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] شامل کلاسها و رابطهای زیر است:
![]()
- [IDao] رابطی است که توسط لایه [dao] ارائه میشود
- [DaoImpl] پیادهسازی این رابط است که در آن گروه افراد در یک شیء [ArrayList] محصور شده است
- [DaoException] نوعی استثنای بدون بررسی است که توسط لایه [dao] پرتاب میشود
رابط [IDao] به شرح زیر است:
- این رابط دارای چهار متد برای چهار عملیاتی است که میخواهیم روی گروه افراد انجام دهیم:
- getAll: برای بازیابی مجموعهای از افراد
- getOne: بازیابی یک شخص با id مشخص
- saveOne: افزودن یک شخص (id=-1) یا اصلاح یک شخص موجود (id ≠ -1)
- deleteOne: برای حذف یک شخص با id مشخص
لایه [dao] ممکن است استثناها را پرتاب کند. این استثناها از نوع [DaoException] خواهند بود:
- خط ۳: کلاس [DaoException] که از [RuntimeException] ارث میبرد، یک نوع استثنای غیربررسیشده است: کامپایلر از ما نمیخواهد که:
- این نوع استثنا را هنگام فراخوانی متدی که ممکن است آن را پرتاب کند، با یک بلوک try/catch مدیریت کنید
- کلیدواژه «throws DaoException» را در امضای متدی که ممکن است این استثنا را پرتاب کند، درج کنیم
این تکنیک نیاز به امضای متدهای رابط [IDao] با استثناءهای یک نوع خاص را از بین میبرد. بنابراین، هر پیادهسازی که استثناءهای بدون بررسی را پرتاب کند، قابل قبول خواهد بود و بدین ترتیب انعطافپذیری را به معماری میافزاید.
- خط ۶: یک کد خطا. لایه [dao] انواع مختلفی از استثناها را پرتاب خواهد کرد که هر یک با یک کد خطای متفاوت شناسایی میشوند. این امر به لایه مسئول رسیدگی به استثنا امکان میدهد تا منبع دقیق خطا را شناسایی کرده و در نتیجه اقدام مناسب را انجام دهد. راههای دیگری نیز برای دستیابی به همین نتیجه وجود دارد. یکی از آنها ایجاد یک نوع استثنای جداگانه برای هر نوع خطای ممکن است، برای مثال NomManquantException، PrenomManquantException، AgeIncorrectException، ...
- خطوط ۱۳–۱۶: سازندری (constructor) که برای ایجاد یک استثنا با کد خطا و پیام خطا استفاده میشود.
- خطوط ۸–۱۰: متدی که به کد رسیدگی به استثنا امکان میدهد کد خطا را بازیابی کند.
کلاس [DaoImpl] ) رابط [IDao] را پیادهسازی میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
ما فقط نکات اصلی این کد را تشریح خواهیم کرد. با این حال، کمی وقت را به پیچیدهترین بخشها اختصاص میدهیم.
- خط ۱۳: شیء [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] نامگذاری شدهاند. در اینجا تنها به دو مورد از آنها میپردازیم؛ خوانندگان را دعوت میکنیم تا سایر تستها را در کد منبع همراه این مقاله بررسی کنند.
- خط ۹: ارجاع به پیادهسازی لایه [dao] تحت تست
- خطوط ۱۲–۱۵: سازنده آزمایشی JUnit. این سازنده یک نمونه از نوع [DaoImpl] را از لایه [dao] که قرار است آزمایش شود، ایجاد کرده و آن را مقداردهی اولیه میکند.
متد [test1] چهار متد رابط [IDao] را به شرح زیر آزمایش میکند:
- خط ۳: فهرست افراد درخواست میشود
- خط ۶: این فهرست نمایش داده میشود
[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] طراحی شده است. به یاد داشته باشید که این متدها همگامسازی نشدهاند. کد تست به شرح زیر است:
- خطوط ۳–۶: یک شخص P بدون فرزند به لیست اضافه میشود. [id] او ثبت میشود (خط ۶).
- خطوط ۷–۱۳: N نخ راهاندازی میشوند. هر یک از آنها تعداد فرزندان شخص P را به اندازه ۱ افزایش میدهد. در نهایت، شخص P باید N فرزند داشته باشد.
- خطوط ۱۵–۱۷: متد [test4] که N نخ را راهاندازی کرده است، قبل از بررسی تعداد جدید فرزندان شخص P، منتظر پایان کار آنها میماند.
- خطوط ۱۸–۲۱: شخص P بازیابی میشود و بررسی میشود که او دارای N فرزند است.
- خطوط 22–35: شخص P حذف میشود و سپس بررسی میکنیم که دیگر در لیست وجود ندارد.
در خط ۱۱ میبینیم که رشتهها از نوع [ThreadDaoMajEnfants] هستند. سازنده این نوع سه پارامتر دارد:
- نام دادهشده به نخ گفتگو، تا از طریق لاگها قابل ردیابی باشد
- اشارهای به لایه [dao] تا نخ بتواند به آن دسترسی پیدا کند
- شناسه فردی که موضوع باید روی او کار کند
نوع [ThreadDaoMajEnfants] به شرح زیر است:
- خط ۹: [ThreadDaoMajEnfants] در واقع یک تِرد است
- خطوط ۱۸–۲۲: سازندهای که نخ را با سه مورد اطلاعات اولیه راهاندازی میکند
- نام [name] که به نخ داده شده است
- یک مرجع [dao] به لایه [dao]. توجه داشته باشید که، بار دیگر، ما با نوع رابط [IDao] کار میکنیم و نه نوع پیادهسازی [DaoImpl].
- شناسهی [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] (خط ۸۳، بخش ۱۴.۴) غیرفعال میکنیم.
- متد [test4] صد نخ ایجاد میکند (خط ۸، پاراگراف ۱۴.۵).
نتایج زیر به دست آمدهاند:

تمام پنج آزمون با موفقیت انجام شدند.
در پیکربندی دوم آزمایششده:
- دستور wait در متد [saveOne] از [DaoImpl] از حالت توضیحی خارج شده است (خط ۸۳، بخش ۱۴.۴).
- متد [test4] دو نخ ایجاد میکند (خط 8، پاراگراف 14.5).
نتایج زیر به دست آمدهاند:
![]() | ![]() |
آزمون [test4] شکست خورد. دو نخ ایجاد شدند که هر کدام مأمور افزایش یک واحدی تعداد فرزندان شخص P بودند که در ابتدا 0 فرزند داشت. بنابراین پس از اجرای دو نخ انتظار داشتیم 2 فرزند داشته باشیم، اما تنها یک فرزند وجود دارد.
بیایید به لاگهای صفحه برای [test4] نگاه کنیم تا بفهمیم چه اتفاقی افتاده است:
- خط ۱: نخ شماره ۰ کار خود را آغاز میکند
- خط ۲: یک نسخه از شخص P را بازیابی میکند و میبیند که تعداد فرزندان ۰ است
- خط ۳: با متد [run] از کلاس خود مواجه میشود و بنابراین در زمان [1145536368171] (ms) متوقف میشود
- خط ۴: نخ شماره ۱ سپس کنترل پردازنده را بازپس میگیرد و کار خود را آغاز میکند
- خط ۵: یک نسخه از شخص P دریافت شده و مشخص میشود که او ۰ فرزند دارد
- خط ۶: با متد [run] خود مواجه میشود و بنابراین متوقف میگردد
- خط ۷: نخ شماره ۰ در زمان [1145536368187] (ms)، c.a.d، ۱۶ میلیثانیه پس از از دست دادن آن، کنترل پردازنده را بازپس میگیرد.
- خط ۸: همین امر برای نخ شمارهٔ ۱ نیز صدق میکند
- خط ۹: نخ شماره ۰ بهروزرسانی خود را تکمیل کرده و تعداد فرزندان را روی ۱ تنظیم کرده است
- خط ۱۰: رشته شماره ۱ نیز همین کار را انجام داده است
سؤال این است: چرا نخ شماره ۱ توانست بهروزرسانی خود را انجام دهد، در حالی که به طور معمول دیگر نسخه صحیح شخص P را که به تازگی توسط نخ شماره ۰ بهروزرسانی شده بود، در اختیار نداشت؟
ابتدا میتوانیم یک ناهنجاری بین خطوط ۷ و ۸ مشاهده کنیم: به نظر میرسد که نخ شماره ۰ بین این دو خط کنترل پردازنده (CPU) را به نخ شماره ۱ واگذار کرده است. در آن لحظه چه کاری انجام میداد؟ در حال اجرای متد [saveOne] از لایه [dao] بود. این متد ساختار زیر را دارد (به بند 14.4 مراجعه کنید):
- رشته شماره ۰ متد [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] شامل کلاسها و رابطهای زیر است:
![]()
- [IService] رابطی است که توسط لایه [dao] ارائه میشود
- [ServiceImpl] پیادهسازی این رابط است
رابط [IService] به شرح زیر است:
این با رابط [IDao] یکسان است.
پیادهسازی [ServiceImpl] از رابط [IService] به شرح زیر است:
- خطوط ۱۰–۱۹: ویژگی [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] به شرح زیر است:
- خط ۹: لایه در حال آزمایش [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] کار میکند:
- خط ۱۲: نخ با لایه [service] کار میکند
ما تستها را با پیکربندیای که در لایه [dao] مشکل را ایجاد کرد، اجرا میکنیم:
- ما دستور wait را در متد [saveOne] از [DaoImpl] (خط ۸۳، بخش ۱۴.۴) از حالت کامنت خارج میکنیم.
- متد [test4] صد نخ ایجاد میکند (خط ۶۵، پاراگراف ۱۴.۷).
نتایج بهدستآمده به شرح زیر است:
![]() |
همگامسازی روشها در لایه [service] بود که امکان موفقیت آزمون [test4] را فراهم کرد.
14.8. لایه [web]
بیایید معماری سهسطحی برنامهمان را مرور کنیم:
![]() |
لایه [web] صفحههایی را برای کاربر فراهم میکند تا گروه افراد را مدیریت کند:
- فهرست افراد در گروه
- افزودن یک شخص به گروه
- ویرایش یک نفر در گروه
- حذف یک شخص از گروه
برای این کار، این لایه به لایه [service] متکی خواهد بود که به نوبه خود از لایه [dao] فراخوانی میکند. ما پیش از این صفحاتی را که توسط لایه [web] مدیریت میشوند، توصیف کردهایم (بخش 14.1). برای توصیف لایه وب، اکنون به ترتیب به موارد زیر میپردازیم:
- پیکربندی آن
- نماهای آن
- کنترلکنندهی آن
- برخی تستها
14.8.1. پیکربندی برنامه وب
پروژهٔ Eclipse برای برنامه به شرح زیر است:

- در پکیج [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]
این برای نمایش لیست افراد استفاده میشود:

کد آن به شرح زیر است:
<%@ 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énom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</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] استفاده میکنیم:

فایل توضیحات این کتابخانه تگ در خط ۳ تعریف شده است.
- خط ۳۷: لینک [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é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é</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 |
شناسهٔ شخص بهروزرسانیشده | همانند بالا | |
نسخهٔ آن | همان | |
نام اول او | نام وارد شده | |
نام خانوادگی او | نام خانوادگی وارد شده | |
تاریخ تولد | تاریخ تولد وارد شده | |
وضعیت تأهل | وضعیت تأهل وارد شده | |
تعداد فرزندان | تعداد فرزندان وارد شده | |
خالی | پیام خطایی که نشان میدهد افزودن یا اصلاح در طول POST با فشردن دکمه [Envoyer] ناموفق بوده است. در صورت عدم وجود خطا، خالی است. | |
خالی | نشاندهنده نام نادرست است – در غیر این صورت خالی | |
خالی | نشاندهنده نام خانوادگی نادرست است – در غیر این صورت خالی باشد | |
خالی | تاریخ تولد نادرست را نشان میدهد – در غیر این صورت خالی میماند | |
خالی | نشاندهنده تعداد نادرست فرزندان است – در غیر این صورت خالی میماند |
- ردههای ۱۱–۱۵: اگر 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 à 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] باشد، همانطور که در مثال زیر نشان داده شده است:

کد صفحه [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] تعریف شده است:
![]()
Structure ture و راهاندازی کنترلر
اسکلت کنترلکننده [Application] به شرح زیر است:
- خطوط ۲۰–۳۶: پارامترهای مورد انتظار از فایل [web.xml] بازیابی میشوند.
- خطوط 39–41: پارامتر [urlErreurs] باید موجود باشد، زیرا URL نمای [erreurs] را مشخص میکند که قادر به نمایش هرگونه خطای инициализация است. اگر این فایل وجود نداشته باشد، برنامه با اجرای [ServletException] (خط ۴۰) خاتمه مییابد. این استثنا به سرور وب منتقل شده و توسط تگ <error-page> در فایل [web.xml] مدیریت میشود. بنابراین نما [exception.jsp] نمایش داده میشود:

لینک [Retour à la liste] بالا غیرفعال است. استفاده از آن تا زمانی که برنامه اصلاح و دوباره بارگذاری نشود، همان پاسخ را بازخواهد گرداند. همانطور که قبلاً دیدیم، این برای انواع دیگر خطاها مفید است.
- خط ۴۳: یک نمونه [DaoImpl] ایجاد میکند که لایه [dao] را پیادهسازی میکند
- خط ۴۴: این نمونه را مقداردهی اولیه میکند (ایجاد یک لیست اولیه از سه نفر)
- خط ۴۶: یک نمونه از [ServiceImpl] ایجاد میکند که لایه [service] را پیادهسازی میکند
- خط ۴۷: لایه [service] را با ارائه یک مرجع به لایه [dao] راهاندازی میکند
پس از اینکه کنترلکننده راهاندازی شد، متدهای آن یک مرجع [service] به لایه [service] (خط 15) دارند، که از آن برای اجرای عملیات درخواستی کاربر استفاده خواهند کرد. این اقدامات توسط متد [doGet] رهگیری شده و برای پردازش به یک متد خاص کنترلکننده ارسال میشوند:
URL | متد HTTP | متد کنترلکننده |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
روش [doGet]
هدف این متد هدایت پردازش عملیات درخواستشده توسط کاربر به متد صحیح است. کد آن به شرح زیر است:
- خطوط ۷–۱۳: بررسی میکنیم که لیست خطاهای инициализация خالی باشد. اگر اینطور نباشد، نمای [erreurs(erreurs)] را نمایش میدهیم که خطا(ها) را گزارش خواهد کرد.
- خط ۱۵: متد [get] یا [post] که توسط کلاینت برای ارسال درخواست خود استفاده میشود، بازیابی میگردد.
- خط ۱۷: مقدار پارامتر [action] را از درخواست بازیابی میکنیم.
- خطوط ۲۳–۲۷: پردازش درخواست [GET /do/list] که درخواست فهرستی از افراد را دارد.
- خطوط ۲۸–۳۲: پردازش درخواست [GET /do/delete] که درخواست حذف یک شخص را دارد.
- خطوط ۳۳–۳۷: پردازش درخواست [GET /do/edit] که فرم بهروزرسانی یک شخص را درخواست میکند.
- خطوط ۳۸–۴۲: پردازش درخواست [POST /do/validate] که درخواست اعتبارسنجی شخص بهروزشده را دارد.
- خط ۴۴: اگر عمل درخواستی یکی از پنج مورد قبلی نباشد، آنگاه مانند [GET /do/list] در نظر گرفته میشود.
روش [doListPersonnes]
این متد درخواست [GET /do/list] را پردازش میکند که فهرستی از افراد را درخواست میکند:

کد آن به شرح زیر است:
- خط ۵: لایه [service] برای دریافت فهرست افراد گروه پرسوجو میشود و این فهرست در مدل تحت کلید «people» قرار میگیرد.
- خط ۷: نمای [list.jsp]، که در بند 14.8.2 توصیف شده است، نمایش داده میشود.
متد [doDeletePersonne]
این متد پرسوجوی [GET /do/delete?id=XX] را پردازش میکند که درخواست حذف شخص با شناسه XX را دارد. URL [/do/delete?id=XX] مربوط به لینکهای [Supprimer] در نما [list.jsp] است:

که کد آن به شرح زیر است:
در خط ۱۲، ما URL [/do/delete?id=XX] را برای لینک [Supprimer] مشاهده میکنیم. متد [doDeletePersonne] که مسئول رسیدگی به این URL است، باید شخص با id=XX را حذف کرده و سپس لیست جدید افراد گروه را نمایش دهد. کد آن به شرح زیر است:
- خط ۵: URL در حال پردازش به شکل [/do/delete?id=XX] است. مقدار [XX] از پارامتر [id] بازیابی میشود.
- خط ۷: به لایه [service] دستور داده میشود تا شخص با شناسهٔ بهدستآمده را حذف کند. ما هیچگونه بررسیای انجام نمیدهیم. اگر شخصی که قصد حذف آن را داریم وجود نداشته باشد، لایه [dao] یک استثنا (exception) ایجاد میکند که توسط لایه [service] به سمت بالا منتقل میشود. ما اینجا در کنترلر نیز این مورد را مدیریت نمیکنیم. بنابراین این خطا تا وبسرور منتقل میشود که با پیکربندی، صفحه [exception.jsp] را که در بند 14.8.2 شرح داده شده است، نمایش خواهد داد:

- خط ۹: اگر حذف با موفقیت انجام شود (بدون استثنا)، به کلاینت دستور داده میشود که به 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] است:

که کد آن به شرح زیر است:
در خط ۱۱، میتوانیم URL [/do/edit?id=XX] را برای لینک [Modifier] و در خط ۱۷، URL [/do/edit?id=-1] را برای لینک [Ajout] مشاهده کنیم. متد [doEditPersonne] باید فرم ویرایش را برای شخص با شناسه XX نمایش دهد یا اگر ورودی جدید باشد، یک فرم خالی نمایش دهد.
![]() | ![]() |
کد متد [doEditPersonne] به شرح زیر است:
- GET یک URL از نوع [/do/edit?id=XX] را هدف قرار میدهد. در خط ۵، مقدار [id] را بازیابی میکنیم. سپس دو سناریوی ممکن وجود دارد:
- اگر id برابر -1 نباشد، این یک بهروزرسانی است و باید یک فرم نمایش داده شود که از قبل با جزئیات شخص مورد نظر برای بهروزرسانی پر شده باشد. در خط ۱۰، این شخص از لایه [service] بازیابی میشود.
- اگر 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] فراخوانی میشود:

بیایید فیلدهای ورودی فرم HTML را که در نمای بالا نشان داده شده است، به یاد بیاوریم:
درخواست POST شامل پارامترهای [prenom, nom, dateNaissance, marie, nbEnfants, id, version] است و به URL [/do/validate] ارسال میشود (خط 1). این درخواست توسط متد زیر [doValidatePersonne] پردازش میشود:
- خطوط ۸–۱۴: پارامتر [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] را درخواست میکند:

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

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

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

کاربر U1 تغییرات را اعمال و ذخیره میکند:
![]() |
کاربر U2 همین کار را انجام میدهد:
![]() |
کاربر U2 از طریق لینک [Annuler] در فرم به فهرست افراد بازمیگردد:

آنها شخص [Lemarchand] را که توسط U1 اصلاح شده است، پیدا میکنند. اکنون U2، [Lemarchand] را حذف میکند:
![]() |
U1 هنوز فهرست خود را دارد و میخواهد دوباره [Lemarchand] را ویرایش کند:
![]() |
U1 از لینک [Retour à la liste] استفاده میکند تا ببیند موضوع چیست:

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

















