Skip to content

4. [TD]: البنى الطبقية

الكلمات الرئيسية: بنية متعددة الطبقات، Spring، حقن التبعيات.

4.1. Introduction

لنتذكر ما تم إنجازه:

  • في الجزء 1 من التمرين ELECTIONS لم يتم استخدام أي فئة. قمنا ببناء حل كما كنا سنبنيه بلغة C.
  • في الجزء الثاني من التمرين، تم إدخال فئتين:
    • [ListeElectorale] التي تمثل سمات (id، اسم، أصوات، مقاعد، استبعاد) قائمة المرشحين
    • [ElectionsException] فئة استثناءات غير خاضعة للرقابة. يُستخدم هذا النوع من الاستثناءات في كل مرة تحدث فيها خطأ فادح في تطبيق الانتخابات. وهي غير خاضعة للرقابة، 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
  • حساب الحل، الأسطر 19-20
  • عرض و/أو حفظ النتائج، السطور 21-22

المرحلة 2 هي الوحيدة الثابتة حقًا. أما المرحلة 1 فيمكن أن تختلف: فقد تأتي البيانات من لوحة المفاتيح كما في الأمثلة التي درسناها، أو من ملف نصي، أو من واجهة رسومية، أو من قاعدة بيانات، أو من الشبكة، ... وبالمثل، هناك طرق متعددة لعرض النتائج في المرحلة 3: عرضها على الشاشة كما حدث في الأمثلة التي تمت دراستها، أو تسجيلها في ملف، أو في قاعدة بيانات، أو إرسالها عبر الشبكة، ...

بشكل عام، يمكن غالبًا نمذجة التطبيق في ثلاث طبقات لكل منها دور محدد بوضوح:

يُطلق على هذه البنية أيضًا اسم "بنية ثلاثية الطبقات"، وهي ترجمة من الإنجليزية "three tier architecture". يشير مصطلح "ثلاث طبقات" عادةً إلى بنية حيث توجد كل طبقة على جهاز مختلف. عندما تكون الطبقات على نفس الجهاز، تصبح البنية "بنية ثلاثية الطبقات".

  • الطبقة [metier] هي التي تحتوي على قواعد العمل الخاصة بالتطبيق. بالنسبة لتطبيق الانتخابات الخاص بنا، هذه هي القواعد التي تسمح بحساب المقاعد التي حصلت عليها القوائم المختلفة، بمجرد معرفة الأصوات التي حصلت عليها كل قائمة. تحتاج هذه الطبقة إلى بيانات لتعمل. على سبيل المثال في تطبيق الانتخابات:
  • القوائم مع اسم كل منها وعدد الأصوات التي حصلت عليها
  • عدد المقاعد الشاغرة
  • الحد الأدنى للأصوات الذي إذا لم تحصل القائمة عليه، يتم استبعادها

في المخطط أعلاه، يمكن أن تأتي البيانات من مكانين:

  • طبقة الوصول إلى البيانات أو [dao] (DAO = كائن الوصول إلى البيانات) للبيانات المسجلة بالفعل في ملفات أو قواعد بيانات. قد يكون هذا هو الحال هنا بالنسبة لأسماء القوائم، وعدد المقاعد الشاغرة، والحد الأدنى للأصوات. ففي الواقع، تكون هذه المعلومات معروفة قبل الانتخابات نفسها.
  • طبقة واجهة المستخدم أو [ui] (UI = واجهة المستخدم) للبيانات التي يدخلها المستخدم أو التي تُعرض عليه. قد يكون هذا هو الحال هنا بالنسبة لأصوات القوائم التي لا تُعرف إلا في اللحظة الأخيرة وكذلك عرض نتائج الانتخابات.
  • بشكل عام، تتولى الطبقة [dao] الوصول إلى البيانات الدائمة (الملفات، قواعد البيانات) أو غير الدائمة (الشبكة، أجهزة الاستشعار، ...).
  • أما الطبقة [ui]، فهي تتولى التفاعلات مع المستخدم إن وجد.
  • أصبحت الطبقات الثلاث مستقلة بفضل استخدام واجهات Java.
  • هناك طرق مختلفة لدمج هذه الطبقات معًا في التطبيق. سنستخدم أداة تسمى "Spring". في المخطط، تظهر هذه الأداة بشكل عرضي عبر الطبقات الأخرى.

سنستأنف تطبيق [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] إلى بعض المعلومات لإنجاز عملها. لديها بالفعل القوائم من العملية (ب). كما تحتاج إلى عدد المقاعد الشاغرة وقيمة العتبة الانتخابية. وستطلب هذه المعلومات من الطبقة [dao] عبر المسار [3, 4, 5, 6]. [3] هي الطلب الأولي و [6] هي الرد على هذا الطلب.
  6. وبعد حصولها على جميع البيانات التي تحتاجها، تحسب الطبقة [metier] المقاعد التي حصلت عليها كل قائمة.
  7. يمكن للطبقة [metier] الآن الرد على الطلب المقدم من الطبقة [ui] في (د). هذا هو المسار [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(...);
  • السطر 1: يتم إنشاء مثيل للفئة [B1]
  • السطر 3: يتم طلب البيانات من هذه المثيل

إذا افترضنا أن فئة التنفيذ الجديدة [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(...);
  • السطر 1: يتم إنشاء مثيل [ib] الذي ينفذ واجهة [IB] عن طريق إنشاء مثيل للفئة [B1]
  • السطر 3: يتم طلب البيانات من مثيل [ib]

والآن، إذا استبدلنا التنفيذ [B1] للطبقة [B] بتنفيذ [B2]، وكان هذان التنفيذان يتوافقان مع نفس الواجهة [IB]، فإنه يجب تعديل السطر 1 فقط من الطبقة [A] دون غيره. وهذه ميزة كبيرة تبرر بحد ذاتها الاستخدام المنهجي للواجهات بين طبقتين.

يمكننا الذهاب إلى أبعد من ذلك وجعل الطبقة [A] مستقلة تمامًا عن الطبقة [B]. في الكود أعلاه، يمثل السطر 1 مشكلة لأنه يشير بشكل ثابت إلى الفئة [B1]. سيكون من المثالي أن تتمكن الطبقة [A] من الحصول على تنفيذ للواجهة [IB] دون الحاجة إلى تسمية فئة. سيكون ذلك متسقًا مع مخططنا أعلاه. نرى هنا أن الطبقة [A] تتعامل مع الواجهة [IB] ولا نرى سببًا يجعلها بحاجة إلى معرفة اسم الفئة التي تنفذ هذه الواجهة. هذا التفصيل غير مفيد للطبقة [A].

يتيح إطار عمل Spring (http://www.springframework.org) الحصول على هذه النتيجة. تتطور البنية السابقة على النحو التالي:

ستسمح الطبقة العرضية [Spring] لأي طبقة بالحصول، من خلال التكوين، على مرجع للطبقة الموجودة على يمينها دون الحاجة إلى معرفة اسم فئة تنفيذ تلك الطبقة. وسيكون هذا الاسم موجودًا في ملفات التكوين وليس في كود Java. يأخذ كود Java للطبقة [A] الشكل التالي:

1
2
3
IB ib; // تم تهيئته بواسطة Spring
..
ib.getData(...);
  • السطر 1: مثيل [ib] الذي ينفذ واجهة [IB] للطبقة [B]. يتم إنشاء هذا المثيل بواسطة Spring استنادًا إلى المعلومات الموجودة في ملف التكوين. سيتولى Spring إنشاء:
    • المثيل [b] الذي ينفذ الطبقة [B]
    • المثيل [a] الذي ينفذ الطبقة [A]. سيتم تهيئة هذا المثيل. سيتلقى الحقل [ib] أعلاه قيمة المرجع [b] للكائن الذي ينفذ الطبقة [B]
  • السطر 3: يتم طلب البيانات من المثيل [ib]

نرى الآن أن فئة التنفيذ [B1] للطبقة B لا تظهر في أي مكان في كود الطبقة [A]. عندما يتم استبدال التنفيذ [B1] بتنفيذ جديد [B2]، لن يتغير شيء في كود فئة [A]. سنقوم ببساطة بتغيير ملفات تكوين Spring لإنشاء مثيل [B2] بدلاً من [B1].

يوفر الثنائي Spring وواجهات Java تحسينًا حاسمًا في صيانة التطبيقات من خلال جعل طبقاتها منفصلة عن بعضها البعض. هذه هي الحل الذي سنستخدمه لتطبيق [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);
}
  • السطر 1: تسمى الواجهة [IElectionsDao]. وهي تحدد أربع طرق:
    • ثلاث طرق لقراءة البيانات الواردة من مصدر البيانات: [getSeuilElectoral, getNbSiegesAPourvoir, getListesElectorales]. ستسمح هذه الطرق الثلاث للطبقة [metier] بالحصول على البيانات التي تميز الانتخابات الحالية.
    • طريقة واحدة لكتابة البيانات في مصدر البيانات: [setListesElectorales]. ستسمح هذه الطريقة للطبقة [metier] بطلب تسجيل النتائج التي ستحسبها.

لنعد إلى بنية الطبقات الثلاث لتطبيقنا:

ما هي الواجهة التي يجب أن تقدمها الطبقة [metier] إلى الطبقة [ui]؟ دعونا ندرس التفاعلات المحتملة بين هاتين الطبقتين.

  1. سيكون دور الطبقة [ui] هو طلب أصوات المستخدم للقوائم المختلفة المتنافسة. ولذلك، يجب أن تعرف عدد القوائم. ويمكنها طلب هذه المعلومة من الطبقة [metier] التي يمكنها بدورها طلب جدول القوائم المتنافسة من الطبقة [dao]. وإذا كانت الطبقة [metier] تمتلك هذا الجدول، فمن الأفضل نقل هذا الجدول إلى الطبقة [ui]. وبذلك ستتوفر لدى هذه الطبقة أسماء القوائم وستتمكن من تحسين رسائلها الموجهة إلى المستخدم بطلبها، على سبيل المثال، "عدد الأصوات للقائمة أ".
  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);

}
  • السطر 1: تسمى الواجهة [IElectionsMetier]. وهي تحدد الطرق التالية:
    • السطر 3: طريقة [getListesElectorales] التي ستسمح للطبقة [ui] بالحصول على جدول القوائم المتنافسة؛
    • السطر 5: تسمح الطريقة [getNbSiegesAPourvoir] بالحصول على عدد المقاعد الشاغرة؛
    • السطر 7: الطريقة [getSeuilElectoral] تسمح بالحصول على العتبة الانتخابية؛
    • السطر 11: طريقة [calculerSieges] (السطر 36) التي ستسمح للطبقة [ui] بطلب حساب المقاعد بمجرد معرفة أعداد الأصوات للقوائم المختلفة. المعلمة هي جدول القوائم المتنافسة، بدون مقاعدها وبدون المتغير المنطقي "تم الاستبعاد". والنتيجة المعروضة هي نفس الجدول مع تهيئة الحقول [sièges, elimine] هذه المرة؛
    • السطر 9: طريقة [recordResultats] التي ستسمح للطبقة [ui] بطلب تسجيل النتائج.

ملاحظة: نظرًا لموقعها، تستعيد الطبقة [métier] بعض طرق الطبقة [DAO] لتقديمها إلى الطبقة [UI]. وبسبب هذا التكرار، قد نميل إلى تجميع كل شيء في طبقة واحدة تجمع بين الوظيفة والوصول إلى البيانات. يُطلق على هذه الطبقة الوحيدة أحيانًا اسم النموذج، وهو الحرف M في الاختصار MVC (نموذج - عرض - وحدة تحكم). MVC هو نمط تصميم (design pattern) شائع في تطبيقات الويب.

دعونا ندرس توقيع الطريقة [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 (آلة Java الافتراضية):

إذا استدعت الطبقة [ui] الطريقة calculerSieges من الطبقة [DAO]، فإنها تمتلك بالفعل مرجعًا للمعلمة [ListeElectorale[] 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]. لدينا الآن كائنان متطابقان (من حيث المحتوى) في طبقتين مختلفتين: 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;
  • الحقل في السطر 2 مفروض. يمكن الاحتفاظ به كما هو واستخدامه لأي فئة من النوع [Serializable].

4.3. فئة الاستثناء

لنعد إلى واجهة الطبقة [DAO]:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}

تعمل هذه الطرق مع قاعدة بيانات وقد تواجه أخطاء متنوعة، على سبيل المثال عدم توفر SGBD. عند كتابة طريقة، يجب دائمًا توقع حالات الخطأ. يتم الإبلاغ عن هذه الحالات عادةً باستثناء. لقد سبق أن تناولنا الفئة [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;

    // الحقول المحلية
    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) {
        // الأصل
        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;
    }

    // مُستردات ومُعيّنات
...
}
  • السطران 16-17: النوع [ElectionsException] يغلف:
    • رمز خطأ، السطر 16؛
    • قائمة برسائل الخطأ، السطر 17؛

تدعم الفئة خمسة منشئات:

  • السطر 20: ElectionsException()
  • السطر 24: ElectionsException(int code, Throwable e): المعلمة الثانية هي نوع [Throwable] وهي الفئة الأم للفئة [Exception]. يسمح هذا المنشئ بتغليف الاستثناء e برمز خطأ. يسمح النوع [Throwable] (وبالتالي النوع Exception) بتغليف استثناء واحد أو أكثر. الفكرة هي:
    • التقاط (catch) استثناء يحدث؛
    • إثرائها برسالة عن طريق تغليفها في استثناء جديد؛
    • إعادة تشغيل الاستثناء الجديد؛
try{
...
}catch (Exception1 e1){
   throw new Exception2(«un message»,e1);
}

يتم التغليف في السطر 34 بواسطة التعليمات [super(message,e)]. يمكن تكرار عملية التغليف هذه وإثراء الاستثناء الأولي برسائل مختلفة. ونقول عندئذٍ إن لدينا مكدسًا من الاستثناءات. تسمح الطريقة [private List<String> getErreursForException(Throwable th)] بالحصول على الرسائل المختلفة المرتبطة بالاستثناءات المغلفة:

  • (تابع)
    • (تابع)
      • يتم الحصول على الاستثناء المُغلف بواسطة الطريقة Throwable [Throwable].getCause()؛
      • الرسالة المرتبطة بالاستثناء هي الطريقة String [Throwable].getMessage()؛
  • السطران 28-29: يتم إنشاء الحقول [code, erreurs
  • السطر 32: public ElectionsException(int code, String message, Throwable e): هذا المنشئ مشابه للمنشئ السابق، إلا أنه يثري الاستثناء الذي سيقوم بتغليفه برمز ورسالة؛
  • السطر 40: public ElectionsException(int code, String message): منشئ بدون تغليف استثناء؛
  • السطر 50: public ElectionsException(int code, List<String> erreurs): منشئ بدون تغليف استثناء أو رسالة؛

يمكن استخدام الفئة [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);
}