Skip to content

4. [TD]: معماری‌های لایه‌ای

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

4.1. Introduction

بیایید مروری بر آنچه انجام داده‌ایم داشته باشیم:

  • در بخش اول تمرین ELECTIONS، از هیچ کلاسی استفاده نشد. ما راه‌حلی را به همان شیوه‌ای ساختیم که در زبان C انجام می‌دادیم.
  • در بخش دوم تمرین، دو کلاس معرفی شدند:
    • [ListeElectorale]، که نمایانگر ویژگی‌های (id, name, votes, seats, eliminated) یک فهرست نامزدها است
    • [ElectionsException]، کلاسی برای استثناهای بدون رسیدگی (unhandled). این نوع استثنا هر زمان که یک خطای مرگبار در برنامه انتخابات رخ دهد، استفاده می‌شود. این استثنا بدون رسیدگی (c.a.d) است، به این معنی که توسعه‌دهنده ملزم به رسیدگی به آن با یک بلوک try-catch نیست.

تاکنون، محاسبه نتایج انتخابات توسط متدی به نام [main] از کلاسی به نام [MainElections] انجام شده است

package istia.st.elections;

import java.io.*;

public class MainElections {

   // برخی داده‌ها
  private static final double barre = 0.05;

  // ----------------------------------------------------------------------
   // رویهٔ اصلی
  public static void main(String[] arguments) throws IOException {

     //آماده‌سازی جریان ورودی صفحه‌کلید
    BufferedReader clavier = new BufferedReader(new InputStreamReader(System.in));

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

راه حل قبلی شامل سه فاز استاندارد است:

  • استخراج داده‌ها، خطوط 17–18
  • محاسبه نتیجه، خطوط ۱۹–۲۰
  • نمایش و/یا ذخیره‌سازی نتایج، خطوط 21–22

فقط فاز ۲ واقعاً ثابت است. فاز ۱ ممکن است متغیر باشد: داده‌ها ممکن است از صفحه‌کلید، مانند مثال‌های مورد مطالعه، از یک فایل متنی، یک رابط کاربری گرافیکی، یک پایگاه داده، شبکه و غیره بیایند. به همین ترتیب، راه‌های زیادی برای ارائه نتایج در فاز ۳ وجود دارد: نمایش آن‌ها روی صفحه نمایش، مانند مثال‌های مورد مطالعه؛ ذخیره آن‌ها در یک فایل یا پایگاه داده؛ ارسال آن‌ها از طریق شبکه؛ و غیره.

به‌طور کلی‌تر، یک برنامه کاربردی اغلب می‌تواند به صورت سه لایه مدل‌سازی شود که هر کدام نقش مشخصی دارند:

این معماری همچنین با نام «معماری سه‌لایه» نیز شناخته می‌شود. اصطلاح «سه لایه» معمولاً به معماری‌ای اشاره دارد که در آن هر لایه روی یک ماشین مجزا قرار دارد. هنگامی که لایه‌ها روی یک ماشین واحد قرار می‌گیرند، این معماری به یک معماری «سه لایه» تبدیل می‌شود.

  • لایه [metier] شامل قواعد کسب‌وکار برنامه است. برای برنامه انتخابات ما، این‌ها قواعدی هستند که پس از مشخص شدن آرای کسب‌شده توسط هر فهرست، امکان محاسبه کرسی‌های به‌دست‌آمده توسط فهرست‌های مختلف را فراهم می‌کنند. این لایه برای عملکرد نیازمند داده‌ها است. برای مثال، در برنامه انتخابات:
  • فهرست‌ها، هر کدام با نام و تعداد آرای خود
  • تعداد کرسی‌های مورد تصدی
  • آستانهٔ انتخاباتی که هر فهرستی زیر آن حذف می‌شود

در نمودار بالا، داده‌ها می‌توانند از دو منبع تأمین شوند:

  • لایه دسترسی به داده‌ها یا [dao] (DAO = شیء دسترسی به داده‌ها) برای داده‌هایی که قبلاً در فایل‌ها یا پایگاه‌های داده ذخیره شده‌اند. این می‌تواند در اینجا برای نام فهرست‌ها، تعداد کرسی‌های مورد نظر و آستانه انتخاباتی صادق باشد. در واقع، این اطلاعات پیش از خود انتخابات مشخص است.
  • لایه رابط کاربری یا [ui] (UI = رابط کاربری) برای داده‌های وارد شده توسط کاربر یا نمایش داده شده به کاربر. این امر می‌تواند در اینجا در مورد آرای فهرست‌ها، که تنها در لحظه آخر مشخص می‌شوند، و همچنین نمایش نتایج انتخابات صادق باشد.
  • به‌طور کلی، لایه [dao] دسترسی به داده‌های پایدار (فایل‌ها، پایگاه‌های داده) یا داده‌های ناپایدار (شبکه، حسگرها و غیره) را مدیریت می‌کند.
  • لایه [ui]، از سوی خود، تعاملات با کاربر را در صورت وجود، مدیریت می‌کند.
  • این سه لایه از طریق استفاده از رابط‌های جاوا مستقل شده‌اند.
  • روش‌های مختلفی برای یکپارچه‌سازی این لایه‌ها در داخل برنامه وجود دارد. ما از ابزاری به نام «اسپرینگ» استفاده خواهیم کرد. در نمودار، این ابزار بر سایر لایه‌ها سوار شده است.

ما به سراغ برنامه [Elections] که قبلاً توسعه یافته است، بازمی‌گردیم تا به آن معماری سه‌لایه بدهیم. برای این کار، لایه‌های [ui, metier, dao] را یکی‌یکی بررسی می‌کنیم، با شروع از لایه [dao] که داده‌های پایدار را مدیریت می‌کند.

قبل از انجام این کار، باید رابط‌های لایه‌های مختلف برنامه [Elections] را تعریف کنیم.

4.2. رابط‌های برنامه [Elections]

به یاد داشته باشید که یک رابط مجموعه‌ای از امضاهای متد را تعریف می‌کند. کلاس‌هایی که این رابط را پیاده‌سازی می‌کنند، بدنه این متدها را فراهم می‌کنند.

بیایید به معماری سه‌لایهٔ برنامهٔ خود بازگردیم:

در این نوع معماری، اغلب کاربر است که ابتکار عمل را به دست می‌گیرد. آنها در [1] درخواستی را ارسال کرده و در [8] پاسخی دریافت می‌کنند. این چرخه، چرخه درخواست-پاسخ نامیده می‌شود. بیایید مثال محاسبه تعداد کرسی‌های کسب‌شده در شب انتخابات را در نظر بگیریم. این کار به چندین مرحله نیاز دارد:

  1. لایه [ui] باید از کاربر درباره تعداد آرای کسب‌شده توسط هر یک از فهرست‌ها سؤال کند. برای این کار، باید نام فهرست‌های رقیب را به کاربر نمایش دهد. سپس کاربر به سادگی باید تعداد آرا را در مقابل هر فهرست وارد کرده و محاسبه تخصیص کرسی‌ها را درخواست کند.
  2. لایه [ui] نام فهرست‌ها را ندارد. این نام‌ها در منبع داده در سمت راست نمودار ذخیره شده‌اند. این لایه برای بازیابی آن‌ها از مسیر [2, 3, 4, 5, 6, 7] استفاده خواهد کرد. عملیات [2] درخواست فهرست‌ها و عملیات [7] پاسخ به آن درخواست است. پس از انجام این کار، می‌توان آن‌ها را از طریق [8] به کاربر نمایش داد.
  3. کاربر تعداد آرای کسب‌شده توسط هر فهرست را به لایه [ui] ارسال می‌کند. این همان عملیات [1] است که در بالا ذکر شد. در این مرحله، کاربر فقط با لایه [ui] تعامل دارد. این لایه است که به‌ویژه اعتبار داده‌های وارد شده را بررسی می‌کند. پس از انجام این کار، کاربر فهرست کرسی‌های کسب‌شده توسط هر یک از فهرست‌ها را درخواست خواهد کرد.
  4. لایه [ui] از لایه کسب‌وکار می‌خواهد که تعداد کرسی‌ها را محاسبه کند. برای این کار، داده‌هایی را که از کاربر دریافت کرده است، به آن منتقل می‌کند. این عملیات [2] است.
  5. لایه [metier] برای انجام وظیفه خود به اطلاعات خاصی نیاز دارد. این لایه از قبل فهرست‌ها را از عملیات (b) در اختیار دارد. همچنین به تعداد صندلی‌های قابل پر شدن و آستانه انتخاباتی نیاز دارد. این لایه این اطلاعات را از طریق مسیر [3, 4, 5, 6] از لایه [dao] درخواست می‌کند. [3] درخواست اولیه و [6] پاسخ به آن درخواست است.
  6. پس از دریافت تمام داده‌های مورد نیاز، لایه [metier] تعداد کرسی‌های کسب‌شده توسط هر فهرست را محاسبه می‌کند.
  7. لایه [metier] اکنون می‌تواند به درخواست ارسال‌شده توسط لایه [ui] در (d) پاسخ دهد. این مسیر [7] است.
  8. لایه [ui] این نتایج را قالب‌بندی می‌کند تا آن‌ها را در قالبی مناسب به کاربر ارائه دهد و سپس نمایش دهد. این مسیر [8] است.
  9. ممکن است تصور شود که این نتایج باید در یک فایل یا پایگاه داده ذخیره شوند. این کار می‌تواند به طور خودکار انجام شود. در این صورت، پس از عملیات (f)، لایه [metier] به لایه [dao] دستور می‌دهد که نتایج را ذخیره کند. این مسیر [3, 4, 5, 6] خواهد بود. این کار همچنین می‌تواند تنها به درخواست کاربر انجام شود. مسیر [1-8] توسط چرخه درخواست–پاسخ استفاده خواهد شد.

از این توضیح برمی‌آید که یک لایه باید از منابع لایه سمت راست خود استفاده کند، نه از منابع لایه سمت چپ خود. بیایید دو لایه مجاور را در نظر بگیریم:

لایه [A] درخواست‌هایی را به لایه [B] ارسال می‌کند. در ساده‌ترین موارد، یک لایه توسط یک کلاس واحد پیاده‌سازی می‌شود. یک برنامه کاربردی در طول زمان تکامل می‌یابد. بنابراین، لایه [B] ممکن است کلاس‌های پیاده‌سازی متفاوتی مانند [B1, B2, ...] داشته باشد. اگر لایه [B] همان لایه [dao] باشد، لایه دوم ممکن است یک پیاده‌سازی اولیه به نام [B1] داشته باشد که داده‌ها را از یک فایل بازیابی می‌کند. چند سال بعد، ممکن است بخواهیم داده‌ها را در یک پایگاه داده ذخیره کنیم. سپس یک کلاس پیاده‌سازی دوم به نام [B2] ایجاد خواهیم کرد. اگر در برنامه اصلی، لایه [A] مستقیماً با کلاس [B1] کار می‌کرد، مجبور می‌شدیم کد لایه [A] را تا حدی بازنویسی کنیم. برای مثال، فرض کنیم که ما چیزی شبیه به کد زیر را در لایه [A] نوشته بودیم:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • خط ۱: یک نمونه از کلاس [B1] ایجاد می‌شود
  • خط ۳: از این نمونه درخواست داده می‌شود

اگر فرض کنیم که کلاس پیاده‌سازی جدید [B2] از متدهایی با امضای یکسان با متدهای کلاس [B1] استفاده می‌کند، باید تمام نمونه‌های [B1] را به [B2] تغییر دهیم. این یک سناریوی بسیار مطلوب است و در صورتی که به این امضاهای متد توجه نشده باشد، بسیار بعید است. در عمل، این امر رایج است که کلاس‌های [B1] و [B2] امضاهای متد یکسانی نداشته باشند، به این معنی که بخش قابل توجهی از لایه [A] باید به طور کامل بازنویسی شود.

این وضعیت را می‌توان با معرفی یک رابط بین لایه‌های [A] و [B] بهبود بخشید. این بدان معناست که امضاهای متدی که توسط لایه [B] به لایه [A] ارائه می‌شوند، در یک رابط ثابت شده‌اند. نمودار قبلی در این صورت به شکل زیر درمی‌آید:

لایه [A] دیگر مستقیماً با لایه [B] ارتباط برقرار نمی‌کند، بلکه با رابط آن، [IB]، ارتباط برقرار می‌کند. بنابراین، در کد مربوط به لایه [A]، کلاس پیاده‌سازی [Bi] لایه [B] تنها یک بار، هنگام پیاده‌سازی رابط [IB]، ظاهر می‌شود. با این اوصاف، در کد از رابط [IB] – و نه کلاس پیاده‌سازی آن – استفاده می‌شود. کد قبلی به صورت زیر درمی‌آید:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • خط ۱: یک نمونه از `[ib]` که رابط `[IB]` را پیاده‌سازی می‌کند، با نمونه‌سازی کلاس `[B1]` ایجاد می‌شود
  • خط ۳: داده از نمونه [ib] درخواست می‌شود

اکنون، اگر پیاده‌سازی [B1] لایه [B] را با یک پیاده‌سازی [B2] جایگزین کنیم، و هر دوی این پیاده‌سازی‌ها به همان رابط [IB] پایبند باشند، آنگاه تنها خط ۱ لایه [A] نیاز به اصلاح دارد و هیچ خط دیگری نه. این یک مزیت عمده است که به خودی خود، استفاده سیستماتیک از رابط‌ها بین دو لایه را توجیه می‌کند.

می‌توانیم حتی فراتر رفته و لایه [A] را کاملاً مستقل از لایه [B] کنیم. در کد بالا، خط ۱ مشکل‌ساز است زیرا ارجاع به کلاس [B1] را به‌صورت سخت‌کد (hard-code) درج می‌کند. به‌طور ایده‌آل، لایه [A] باید بتواند از یک پیاده‌سازی از رابط [IB] بدون نیاز به مشخص کردن نام کلاس استفاده کند. این امر با نمودار ما در بالا سازگار خواهد بود. می‌توانیم ببینیم که لایه [A] با رابط [IB] تعامل دارد و هیچ دلیل آشکاری وجود ندارد که چرا باید نام کلاسی را که این رابط را پیاده‌سازی می‌کند، بداند. این جزئیات برای لایه [A] بی‌فایده است.

چارچوب Spring (http://www.springframework.org) این امکان را فراهم می‌کند. معماری قبلی به شرح زیر تکامل می‌یابد:

لایهٔ میان‌لایهٔ [Spring] به یک لایه امکان می‌دهد تا از طریق پیکربندی، مرجعی به لایهٔ سمت راست خود به‌دست آورد، بدون اینکه نیاز باشد نام کلاس پیاده‌سازی آن لایه را بداند. این نام در فایل‌های پیکربندی مشخص می‌شود، نه در کد جاوا. کد جاوا برای لایه [A] سپس به شکل زیر درمی‌آید:

1
2
3
IB ib; // ابتکاریزه شده توسط Spring
..
ib.getData(...);
  • خط ۱: یک نمونه از [ib] که رابط [IB] لایه [B] را پیاده‌سازی می‌کند. این نمونه توسط Spring بر اساس اطلاعاتی که در یک فایل پیکربندی یافت می‌شود، ایجاد می‌شود. Spring ایجاد موارد زیر را بر عهده می‌گیرد:
    • نمونه [b]، که لایه [B] را پیاده‌سازی می‌کند
    • اِنسام [a] که لایه [A] را پیاده‌سازی می‌کند. این اِنسام مقداردهی اولیه خواهد شد. میدان [ib] در بالا، مرجع [b] شیء پیاده‌ساز لایه [B] را دریافت خواهد کرد
  • خط ۳: داده از نمونه [ib] درخواست می‌شود

اکنون می‌توانیم ببینیم که کلاس پیاده‌سازی [B1] لایه B در هیچ کجای کد لایه [A] ظاهر نمی‌شود. وقتی پیاده‌سازی [B1] با پیاده‌سازی جدید [B2] جایگزین شود، هیچ تغییری در کد کلاس [A] رخ نخواهد داد. ما به سادگی فایل‌های پیکربندی Spring را تغییر می‌دهیم تا به جای [B1]، [B2] را نمونه‌سازی کند.

ترکیب اسپرینگ و رابط‌های جاوا با مستقل کردن لایه‌های برنامه از یکدیگر، بهبود چشمگیری در نگهداری برنامه به همراه دارد. این راه‌حلی است که برای برنامه [Elections] استفاده خواهیم کرد.

بیایید به معماری سه‌لایهٔ برنامهٔ خود بازگردیم:

در موارد ساده، می‌توانیم با لایه [metier] شروع کنیم تا رابط‌های برنامه را شناسایی کنیم. برای کارکرد، برنامه به داده‌ها نیاز دارد:

  • که از قبل در فایل‌ها، پایگاه‌های داده یا از طریق شبکه در دسترس هستند. این داده‌ها توسط لایه [dao] فراهم می‌شوند.
  • هنوز در دسترس نیست. در این صورت، این لایه توسط لایه [ui] که آن را از کاربر برنامه دریافت می‌کند، تأمین می‌شود.

لایه [dao] باید چه رابطی را برای لایه [metier] فراهم کند؟ چه تعاملاتی بین این دو لایه ممکن است؟ لایه [dao] باید داده‌های زیر را برای لایه [metier] فراهم کند:

  • تعداد کرسی‌های مورد پر شدن
  • آستانهٔ انتخاباتی که بر اساس آن یک فهرست حذف می‌شود
  • نام فهرست‌ها

در واقع این اطلاعات پیش از انتخابات مشخص است و بنابراین می‌توان آن را ذخیره کرد. در جهت [metier] → [dao]، لایه [metier] ممکن است از لایه [dao] بخواهد که نتایج انتخابات، به‌ویژه کرسی‌های کسب‌شده توسط فهرست‌های مختلف، را ثبت کند.

با این اطلاعات، می‌توانیم یک تعریف اولیه از رابط برای لایه [dao] ارائه دهیم:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}
  • خط ۱: این رابط [IElectionsDao] نامیده می‌شود. این رابط چهار متد را تعریف می‌کند:
    • سه متد برای خواندن داده‌ها از منبع داده: [getSeuilElectoral, getNbSiegesAPourvoir, getListesElectorales]. این سه متد به لایه [metier] امکان می‌دهند داده‌های توصیف‌کننده انتخابات جاری را بازیابی کند.
    • یک روش برای نوشتن داده‌ها به منبع داده: [setListesElectorales]. این روش به لایه [metier] امکان می‌دهد تا درخواست ثبت نتایج محاسبه‌شده را بدهد.

بیایید به معماری سه‌لایهٔ برنامهٔ خود بازگردیم:

لایه [metier] باید چه رابطی را برای لایه [ui] فراهم کند؟ بیایید تعاملات ممکن بین این دو لایه را بررسی کنیم.

  1. نقش لایه [ui] این خواهد بود که از کاربر برای رأی‌گیری به نفع فهرست‌های رقیب مختلف درخواست کند. برای انجام این کار، باید تعداد فهرست‌ها را بداند. این لایه می‌تواند این اطلاعات را از لایه [metier] درخواست کند، که به نوبه خود می‌تواند جدول فهرست‌های رقیب را از لایه [dao] درخواست نماید. اگر لایه [metier] این آرایه را در اختیار داشته باشد، می‌تواند آن را به لایه [ui] منتقل کند. این لایه سپس نام فهرست‌ها را در اختیار خواهد داشت و می‌تواند با پرسیدن سؤالاتی مانند «تعداد آراء برای فهرست A»، پیام‌های خود را به کاربر دقیق‌تر کند.
  2. به محض اینکه لایه [ui] آرای تمام فهرست‌ها را به دست آورد، محاسبه تخصیص کرسی‌ها را از لایه [metier] درخواست خواهد کرد. این لایه قادر خواهد بود محاسبه را انجام دهد و نتیجه را به لایه [ui] بازگرداند.
  3. لایه [ui] سپس قادر خواهد بود این نتایج را به کاربر ارائه دهد. کاربر همچنین می‌تواند درخواست کند که این نتایج ذخیره شوند.
  4. لایه [ui] ممکن است بخواهد اطلاعات بیشتری را نیز به کاربر ارائه دهد، مانند آستانه انتخاباتی یا تعداد کرسی‌های قابل تخصیص.

با این اطلاعات، می‌توانیم یک تعریف اولیه از رابط کاربری لایه [metier] ارائه دهیم:


public interface IElectionsMetier {

    public ListeElectorale[] getListesElectorales();

    public int getNbSiegesAPourvoir();

    public double getSeuilElectoral();

    public void recordResultats(ListeElectorale[] listesElectorales);

    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}
  • خط ۱: رابط [IElectionsMetier] نامیده می‌شود. این رابط متدهای زیر را تعریف می‌کند:
    • خط ۳: متد [getListesElectorales] که به لایه [ui] امکان بازیابی آرایه لیست‌های رقیب را می‌دهد؛
    • خط ۵: متد [getNbSiegesAPourvoir] تعداد کرسی‌های قابل پر شدن را بازیابی می‌کند؛
    • خط ۷: متد [getSeuilElectoral] آستانه انتخاباتی را بازیابی می‌کند؛
    • خط ۱۱: متد [calculerSieges] (خط ۳۶) که به لایه [ui] امکان می‌دهد پس از مشخص شدن تعداد آرای فهرست‌های مختلف، محاسبه کرسی‌ها را درخواست کند. پارامتر، آرایهٔ فهرست‌های رقیب است، بدون کرسی‌های آن‌ها و بدون بولی «حذف‌شده». نتیجهٔ بازگشتی، همین آرایه است، این بار با مقداردهی اولیهٔ فیلدهای [sièges, elimine
    • خط ۹: متدی [recordResultats] که به لایه [ui] امکان می‌دهد ثبت نتایج را درخواست کند.

توجه: به دلیل موقعیتش، لایه [métier] برخی از متدهای لایه [DAO] را برای در دسترس قرار دادن آن‌ها در لایه [UI] مجدداً استفاده می‌کند. به دلیل این تکرار، ممکن است وسوسه شوید که همه چیز را در یک لایه واحد گروه بندی کنید که هم منطق کسب‌وکار و هم دسترسی به داده‌ها را ترکیب کند. گاهی به این لایه واحد، «مدل» گفته می‌شود که «M» در مخفف MVC (مدل–نما–کنترل‌کننده) است. MVC یک الگوی طراحی است که معمولاً در برنامه‌های وب استفاده می‌شود.

بیایید امضای متد [calculerSieges] را بررسی کنیم:


public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

همان‌طور که در بالا گفته شد: «پارامتر آرایه‌ای از لیست‌های رقیب است، بدون جایگاه‌های آن‌ها و بدون بولین حذف‌شده. نتیجه همین آرایه است، اما این بار با فیلدهای [sièges, elimine].» امضای متد می‌تواند به شکل زیر نیز باشد:


public void calculerSieges(ListeElectorale[] listesElectorales);

پارامتر [listesElectorales] یک مرجع شیء است، که در این مورد یک آرایه است. هر عنصر آن نیز به نوبه خود یک مرجع شیء است، که در این مورد از نوع [ListeElectorale] است. متد [calculerSieges] فیلدهای [sieges, elimine] هر یک از این اشیاء را تغییر خواهد داد. متد فراخواننده یک نشانگر [listesElectorales] را در اختیار دارد که:

  • قبل از فراخوانی، یک اشاره به آرایه‌ای از اشیاء [ListeElectorale] است که فیلدهای آن [sieges, elimine] مقداردهی نشده‌اند؛
  • پس از فراخوانی، ارجاع (همان ارجاع) به آرایه‌ای از اشیاء [ListeElectorale] است که فیلدهای [sieges, elimine] آن مقداردهی شده‌اند؛

پس چرا از این امضا استفاده کنیم:


public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

هنگام نوشتن یک رابط، ارزش دارد به خاطر داشته باشید که می‌تواند در دو زمینهٔ متفاوت استفاده شود: local و distant. در زمینه local، متد فراخوانی‌کننده و متد فراخوانی‌شونده در همان JVM (ماشین مجازی جاو) اجرا می‌شوند:

اگر لایه [ui] متد calculerSieges لایه [DAO] را فراخوانی کند، در واقع به پارامتر [[${#httpServletRequest.remoteUser}]] ارجاع دارد[] listesElectorales] را که به متد ارسال می‌کند.

در زمینه distant، متد فراخوانی‌کننده و متد فراخوانی‌شونده در زمینه‌های JVM مختلف اجرا می‌شوند:

در مثال بالا، لایه [ui] در JVM 1 و لایه [métier] در JVM 2 بر روی دو ماشین مختلف اجرا می‌شوند. این دو لایه مستقیماً با یکدیگر ارتباط برقرار نمی‌کنند. بین آن‌ها لایه‌ای قرار دارد که آن را لایه ارتباطی [1] می‌نامیم. این لایه شامل یک لایه انتقال، [2]، و یک لایه دریافت، [3]، است. توسعه‌دهنده معمولاً نیازی به نوشتن این لایه‌های ارتباطی ندارد. آنها به‌طور خودکار توسط ابزارهای نرم‌افزاری تولید می‌شوند. لایه [metier] طوری نوشته می‌شود که گویی در همان JVM که لایه [DAO] در آن اجرا می‌شود، در حال اجراست. بنابراین هیچ تغییری در کد ایجاد نمی‌شود.

مکانیزم ارتباط بین لایه [ui] و لایه [métier] به شرح زیر است:

  • لایه [ui] متد calculerSieges لایه [métier] را فراخوانی می‌کند و پارامتر [ListeElectorale را به آن ارسال می‌نماید[] listesElectorales1];
  • در واقع این پارامتر به لایه انتقال [2] ارسال می‌شود. این لایه مقدار پارامتر listesElectorales1 را به جای مرجع آن، از طریق شبکه منتقل می‌کند. شکل دقیق این مقدار به پروتکل ارتباطی مورد استفاده بستگی دارد؛
  • لایه دریافت‌کننده [3] این مقدار را بازیابی کرده و از آن برای بازسازی یک شیء [ListeElectorale[] listesElectorales2] استفاده می‌کند که بازتاب‌دهنده پارامتر اولیه ارسال‌شده توسط لایه [metier] است. اکنون ما دو شیء یکسان (از نظر محتوا) در دو لایه JVM مختلف داریم: listesElectorales1 و listesElectorales2.
  • لایه دریافت‌کننده، شیء listesElectorales2 را به متد calculerSieges در لایه [métier] ارسال می‌کند که آن را در پایگاه داده ذخیره خواهد کرد. پس از این عملیات، مرجع listesElectorales2 به آرایه‌ای از اشیاء [ListeElectorale] اشاره می‌کند که فیلدهای [sieges, elimine] آن‌ها مقداردهی شده‌اند. . این امر برای شیء listesElectorales1 صدق نمی‌کند، که لایه [ui] به آن ارجاع دارد. اگر بخواهیم لایه [ui] به شیء listesElectorales2 ارجاع دهد، باید شیء دوم را به آن پاس کنیم. بنابراین برای متد [calculerSieges] باید از امضای زیر استفاده کنیم:

public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
  • با این امضا، متد calculerSieges مرجع listesElectorales2 را بازخواهد گرداند. این نتیجه به لایه دریافت‌کننده [3] بازگردانده می‌شود، که لایه [métier] را فراخوانی کرده بود. لایهٔ اخیر مقدار (نه مرجع) listesElectorales2 را به لایهٔ ارسال‌کننده [2] بازمی‌گرداند؛
  • لایه ارسال‌کننده [2] این مقدار را بازیابی کرده و از آن برای بازسازی یک شی [ListeElectorale[] listesElectorales3] تصویر نتیجه بازگردانده‌شده توسط متد calculerSieges از لایه [métier].
  • شیء [ListeElectorale[] listesElectorales3] به متد لایه [ui] ارسال می‌شود، که فراخوانی آن از متد calculerSieges در لایه [DAO]، کل این مکانیزم را آغاز کرده بود؛

در طول این فرایند، اشیاء از نوع [ListeElectorale] بین لایه‌های [2] و [3] منتقل می‌شوند:

  • هنگامی که لایه [2] مقدار یک شیء [ListeElectorale] را به لایه [3] منتقل می‌کند، گفته می‌شود که شیء سریالی شده است. شکل دقیق این سریالی‌سازی به پروتکل ارتباطی مورد استفاده بستگی دارد؛
  • هنگامی که لایه [3] مقدار یک شیء [ListeElectorale] را برای بازآفرینی یک شیء [ListeElectorale] بازیابی می‌کند، گفته می‌شود که شیء سریالی‌سازی معکوس شده است؛

برای اینکه یک شیء تحت این سریالیزاسیون/دسریالیزاسیون قرار گیرد، برخی پروتکل‌ها نیاز دارند که شیء، رابط [Serializable] را پیاده‌سازی کند. این رابط صرفاً یک نشانگر است؛ هیچ متدی برای پیاده‌سازی وجود ندارد. بنابراین، کلاس [ListeElectorale] اکنون به صورت زیر اعلام می‌شود:


public abstract class ListeElectorale implements Serializable {
    private static final long serialVersionUID = 1L;
  • میدان در خط ۲ اجباری است. می‌توان آن را بدون تغییر باقی گذاشت و برای هر کلاسی از نوع [Serializable] استفاده کرد.

4.3. کلاس استثنا

بیایید به رابط لایه [DAO] بازگردیم:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}

این متدها با پایگاه داده کار می‌کنند و ممکن است با خطاهای مختلفی مواجه شوند، مانند در دسترس نبودن SGBD. هنگام نوشتن یک متد، باید همیشه موارد خطا را پیش‌بینی کرد. این موارد معمولاً با یک استثنا (exception) اعلام می‌شوند. ما قبلاً با کلاس [ElectionsException] در بخش 3.3 آشنا شده‌ایم. ما به استفاده از آن ادامه خواهیم داد، اما آن را به شرح زیر گسترش خواهیم داد:


package ...;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

//کلاس استثنا برای برنامه انتخابات
// استثناء مدیریت نشده است

public class ElectionsException extends RuntimeException implements Serializable {

    // سریال ID
    private static final long serialVersionUID = 1L;

    // fields محلی
    private int code;
    private List<String> erreurs;

    // سازنده‌ها
    public ElectionsException() {
        super();
    }

    public ElectionsException(int code, Throwable e) {
        // والد
        super(e);
        // محلی
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message, Throwable e) {
        // والد
        super(message,e);
        // محلی
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message) {
        // والد
        super(message);
        // محلی
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public ElectionsException(int code, List<String> erreurs) {
        // parent
        super();
        // محلی
        this.code = code;
        this.erreurs = erreurs;
    }

    // فهرست پیام‌های خطا برای یک استثنا
    private List<String> getErreursForException(Throwable th) {
        // فهرست پیام‌های خطا برای استثنا را بازیابی می‌کند
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // پیام را تنها در صورتی بازیابی می‌کند که != null و خالی نباشد
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // علت بعدی
            cause = cause.getCause();
        }
        return erreurs;
    }

    // گیرنده و تنظیم‌کننده
...
}
  • خطوط ۱۶–۱۷: نوع [ElectionsException] دربرگیرنده است:
    • یک کد خطا، خط ۱۶؛
    • یک فهرست از پیام‌های خطا، خط 17؛

این کلاس از پنج سازنده پشتیبانی می‌کند:

  • خط ۲۰: ElectionsException()
  • خط 24: ElectionsException(int code, Throwable e): پارامتر دوم از نوع [Throwable] است که کلاس پایه (superclass) کلاس [Exception] است. این سازنده اجازه می‌دهد استثناء e با یک کد خطا جاسازی شود. نوع [Throwable] (و در نتیجه نوع Exception) امکان جاسازی یک یا چند استثناء را فراهم می‌کند. ایده این است:
    • برای گرفتن استثنایی که رخ می‌دهد؛
    • به آن با قرار دادن در یک استثنای جدید، پیامی افزود؛
    • به دوباره پرتاب کردن استثنای جدید؛
try{
...
}catch (Exception1 e1){
   throw new Exception2(«un message»,e1);
}

پوشان‌سازی در خط ۳۴ از طریق دستور [super(message,e)] انجام می‌شود. این فرآیند پوشان‌سازی می‌تواند تکرار شود و استثنای اولیه می‌تواند با پیام‌های مختلف غنی‌سازی شود. به این مجموعه، «ستون استثنا» گفته می‌شود. متد [private List<String> getErreursForException(Throwable th)] به شما امکان می‌دهد پیام‌های مختلف مرتبط با استثناءهای پوشانده‌شده را بازیابی کنید:

  • (ادامه)
    • (ادامه)
      • استثنای محصور شده با استفاده از متد Throwable [Throwable].getCause(); بازیابی می‌شود.
      • پیام مرتبط با یک استثنا با استفاده از متد String [Throwable].getMessage(); به دست می‌آید؛
  • خطوط ۲۸–۲۹: فیلدهای [code, erreurs] ساخته می‌شوند؛
  • خط ۳۲: public ElectionsException(int code, String message, Throwable e): این سازنده مشابه سازنده قبلی است، با این تفاوت که استثناءی مورد نظر را با یک کد و یک پیام غنی‌سازی می‌کند؛
  • خط ۴۰: public ElectionsException(int code, String message): سازنده بدون جبران‌سازی استثنا؛
  • خط ۵۰: public ElectionsException(int code, List<String> errors): سازنده بدون مدیریت خطا یا پیام؛

کلاس [ElectionsException] به صورت زیر قابل استفاده است:

try{
...
}catch (Exception1 e1){
   throw new ElectionsException(un_code,un_message,e1);
}

جایی که پیام ممکن است موجود باشد یا نباشد. پس از ایجاد، استثنای [ElectionsException] قرار نیست استثناهای بیشتری را در بر گیرد. در مثال بالا، این کلاس استثنای e1 و استثناهایی را که e1 در بر می‌گیرد، محصور می‌کند. فراتر از این، هیچ سطح دیگری از محصورسازی وجود ندارد.

کلاس [ElectionsException] همچنین می‌تواند به صورت زیر استفاده شود:

// کدی که احتمالاً با خطا مواجه می‌شود (اما نه به شکل یک استثنا)
...
if(erreur){
    throw new ElectionsException(un_code,un_message);
}