اذهب إلى المحتوى

واجهة برمجة التطبيقات والحزم والوحدات والتوثيق Javadoc في جافا


رضوى العربي

كل تَطوُّر يَحدُث بواجهات المُستخدِم، يُقابله تَعقيد أكبر ينبغي على المبرمج التَعامُل معه، ففي حين تستطيع كتابة واجهة برنامج طرفية (console user interface) بواسطة عدد قليل من البرامج الفرعية (subroutines) البسيطة التي تقرأ ما يَكْتُبه المُستخدِم، وتَطبَع خَرْج البرنامج إلى الطرفية، فإن تطوير واجهات المُستخدِم الرسومية (graphical user interface) العصرية هو أمر أكثر تعقيدًا بمراحل عدة، خاصة مع كل هذا الكم الهائل من النوافذ، والأزرار، وشرائط التمرير، والقوائم، وصناديق الإِدْخَال النصية، وغيره، والتي أينعم تَمنَح المُستخدِم تجربة سهلة ومريحة، ولكنها في ذات الوقت تُجبر المبرمج على التَعامُل مع كمية هائلة من الاحتمالات والتعقيدات المتزايدة، والتي تَكُون بهيئة عدد ضخم من البرامج الفرعية المُخصَّصة فقط لأغراض إدارة واجهة المُستخدِم (user interface) ناهيك عن البرامج الفرعية الآخرى المُخصَّصة لغَيْر ذاك الغرض.

صناديق الأدوات (toolboxes)

اعتاد مبرمجي حاسوب ماكنتوش الأصلي (Macintosh) على التعامل مع "صندوق أدوات ماكنتوش (Macintosh Toolbox)"، والذي يَتكوَّن من أكثر من ألف برنامج فرعي (subroutines) مختلف؛ حيث تَتَوفَّر برامج لتَّنْفيذ جميع العمليات التي يُتوقَّع من الحاسوب القيام بها، فمثلًا، تَتَوفَّر تلك البرامج المُتعلقة بواجهة المُستخدِم (user interface) مثل فَتْح النوافذ (windows) وإِغلاقها، ورَسْم كُلًا من الأشكال الهندسية (geometric) والنصوص على تلك النوافذ، وإضافة الأزرار إليها، والاستجابة إلى ضغطات الفأرة على تلك النوافذ، وإضافة القوائم (menus)، والاستجابة لما يَختاره المُستخدِم منها. إلى جانب برامج واجهة المُستخدِم، تَتَوفَّر أيضًا برامج لفَتْح الملفات، وقراءة البيانات منها، وكذلك للاتصال الشبكي، ولإرسال الخَرْج إلى الطابعة، ولمُعالجة الاتصال بين البرامج. في المُقابل، يُوفِّر مايكروسوفت ويندوز (Microsoft Windows) مجموعة آخرى من البرامج الفرعية (subroutines) للمبرمجين، والتي تَختلِف نوعًا ما عن تلك المُستخدَمة بماكنتوش (Macintosh). علاوة على ذلك، يُوفِّر لينكس (Linux) للمبرمجين أكثر من صندوق أدوات لبرمجة واجهات المُستخدِم الرسومية (GUI toolboxes) يُمكِنهم الاختيار بينها.

أيّ مشروع برمجي هو بالنهاية خليط من كُلًا من الابتكار وإعادة الاِستخدَام. يبدأ المبرمج بمجموعة الأدوات البسيطة المَبْنِيَّة باللغة نفسها، كالمتغيرات، وتَعْليمَات الإِسْناد (assignment statements)، وتَعْليمَات التَفْرِيع if، وحَلْقات التَكْرار (loops). إلى جانب ذلك، فإنه قد يَستعِين بصناديق أدوات (toolboxes)، مَكْتوبة من قِبَل مُطوِّرين آخرين، والتي تَتضمَّن مجموعة من البرامج (routines) لتَّنْفيذ مَهَامّ معينة يَحتاجها المبرمج. إذا كانت تلك الأدوات مُصمَّمة تَصْمِيمًا جيدًا، فإنها تَكُون أَشْبه بصندوق أسود (black boxes)، لا يحتاج المبرمج أكثر من مُجرَّد استدعائها، دون مَعرِفة أية تفاصيل أو خطوات تُنفِّذها تلك الأداة لإنجاز المُهِمّة المُسنَدة إليها. كُل ما سبق يصُنَّف ضِمْن ذلك الجزء الخاص بإعادة الاِستخدَام، أما الجزء الآخر المُتَعلِّق بالابتكار، فيَتكوَّن من توظيف كل تلك الأدوات السابق ذِكرَها ضِمْن مشروع معين بهدف حل مشكلة معينة كمُعالجة النصوص، أو الاحتفاظ بالحسابات البنكية، أو مُعالجة الصور المُلتقَطة بواسطة مسبار فضاء، أو ألعاب الكمبيوتر، أو تَصفُّح الإنترنت، ..إلخ. يُطلَق على ذلك برمجة التطبيقات (applications programming).

صندوق الأدوات البرمجي (software toolbox) هو أَشْبه ما يَكُون بصندوق أسود، لابُدّ أن يَكُون مَصحُوبًا بواجهة (interface)، والتي تَتَكوَّن من توصيف لجميع البرامج (routines) المُعرَّفة بداخله، أيّ ذِكْر مُعامِلات (parameters) كل برنامج، والغرض منه. تُكوِّن تلك التوصيفات ما يُعرَف باسم واجهة برمجة التطبيقات (Application Programming Interface)، وتُختصَر إلى API، فمثلًا، تَحتوِي واجهة برمجة تطبيقات ماكنتوش (Macintosh API) على تَوصِيف لجميع البرامج (routines) المُتاحة "بصندوق أدوات ماكنتوش (Macintosh Toolbox)". عادةً ما تُصدر الشركات المُصنعة للأجهزة العتادية (hardware device) -مثل بطاقات الشبكة (network cards) المسئولة عن توصيل الحاسوب بشبكة معينة- واجهة برمجة تطبيقات (API) خاصة بذلك الجهاز، تَحتوِي على قائمة بالبرامج (routines) التي يُمكِن للمبرمجين استدعائها؛ للاتصال مع الجهاز والتَحكُّم به. علاوة على ذلك، يُوفِّر العلماء المُساهمين بكتابة البرامج المسئولة عن حِسَاب بعض العمليات المُعقدة نوعًا ما -مثل حلّ المعادلات التفاضلية (differential equations)- واجهة برمجة تطبيقات (API)، تَسمَح للآخرين باستدعاء تلك البرامج (routines) دون الحاجة إلى فهم تفاصيل تلك العمليات.

لغة الجافا مُدعَّمة بواجهة برمجة التطبيقات القياسية (standard API)، والتي تَعرَّضنا لأجزاء منها بالفعل، مثل البرنامج الفرعي الرياضي Math.sqrt()‎، والصَنْف String، وما يَحتوِيه من برامج (routines)، بالإضافة إلى برامج طباعة الخَرْج System.out.print()‎. تَتضمَّن أيضًا تلك الواجهة برامج (routines) لبرمجة واجهات المُستخدِم الرسومية (graphical user interfaces)، وللاتصالات الشبكية (network communication)، ولقراءة الملفات وكتابتها، وغيرها. يَظُنّ البعض أن تلك البرامج (routines) هي جزء من لغة الجافا ذاتها، ولكنها في الواقع مُجرَّد برامج فرعية (subroutines) قد كُتبت وأُتيحت للاِستخدَام ببرامج (programs) الجافا.

ينبغي لأيّ واجهة برمجة تطبيقات جافا (Java API) العَمَل على جميع المنصات، وعندها يُعدّ برنامج (program) الجافا المَكْتوب وفقًا لتلك الواجهة مُستقلًا عن أيّ منصة (platform-independent)، أيّ أنه من المُمكِن تَشْغِيل نفس البرنامج (program) على منصات (platform) مُتعدِّدة، مثل ويندوز (Windows)، وماك (Mac OS)، ولينكس (Linux) وغيرها. ومع ذلك، لاحِظ أن الواجهة (interface) ذاتها هي التي تُعدّ مُستقلة عن المنصات (platform-independent)، أما تَّنْفيذ (implementation) تلك الواجهة فإنه قد يختلف من منصة لآخرى. فيما يتعلق بواجهة برمجة التطبيقات القياسية (standard API)، يَتضمَّن نظام الجافا (Java system) بأي حاسوب عمومًا تَّنْفيذًا (implementations) لجميع البرامج الموجودة بتلك الواجهة. عندما يَستدعِي برنامج جافا (Java program) واحدة من تلك البرامج القياسية (standard routines) ضِمْن الواجهة القياسية، فإن مُفسِّر الجافا (Java interpreter) -عند تَّنْفيذه للبرنامج (program)- يَسحَب تَّنْفيذ البرنامج المُستدعَى (routine implementation) المُتناسب مع المنصة (platform) الحالية، ثم يُنفِّذه. يَعنِي ذلك أنه بمُجرَّد تَعلُّمك لواجهة برمجة تطبيقات (API) واحدة، سيُصبِح بإمكانك استهداف مجموعة واسعة من المنصات، وهو ما يُعدّ ميزة قوية جدًا.

حزم الجافا القياسية (standard packages)

تُكتَب البرامج (routines) بواجهة برمجة التطبيقات القياسية (standard API) داخل أصناف (classes) كما هو الحال مع أي برنامج فرعي (subroutine). بالإضافة إلى ذلك، فإنه من المُمكِن تَجْميع الأصناف ضِمْن حزم (packages)، والتي تَعرَّضنا لها باختصار بالقسم الفرعي ٢.٦.٧؛ بهدف تنظيمها على نطاق أوسع (large-scale). يُمكِن أيضًا تَضْمِين الحزم (packages) داخل حزم آخرى لتحقيق مُستوى أعلى من التَجْميع. في الواقع، واجهة برمجة تطبيقات جافا القياسية (standard Java API) مُنفَّذة (implement) بالكامل ضِمْن عدة حزم، فمثلًا، تَحتوِي حزمة java على عدة حزم آخرى غَيْر مُتَعلِّقة بواجهة المُستخدِم الرسومية (non-GUI)، بالإضافة إلى احتوائها على أصناف AWT المُتَعَلِّقة بتلك الواجهات الرسومية. حزمة javax هي مثال آخر، وتَحتوِي على أصناف كثيرة، من ضِمْنها تلك الأصناف التي تَستخدِمها واجهة المُستخدِم الرسومية (GUI)‏ Swing. كذلك حزمة javafx والتي تَحتوِي على واجهة برمجة تطبيقات جافا إف إكس (JavaFX API) التي يَستخدِمها الكتاب لبرمجة واجهات المُستخدِم الرسومية (GUI)‏.

قد تَحتوِي أي حزمة (package) على أصناف أو حزم آخرى، وتُسمَى تلك الأخيرة باسم الحزم الفرعية (sub-package). تَتضمَّن كُلًا من الحزمتين java و javafx على حزم فرعية (sub-packages). فمثلًا، تَحتوِي الحزمة الفرعية util على تشكيلة من الأصناف، بما في ذلك الصَنْف Scanner الذي ناقشناه بالقسم الفرعي ٢.٤.٦، وهي في الواقع مُتضمَّنة داخل حزمة java، ولذا يَكُون اسمها الكامل هو java.util. مثال آخر هو الحزمة الفرعية java.io، والتي تُسهِل من عمليات الخَرْج والدَخْل (input/output). كذلك الحزمة الفرعية java.net، والتي تتعامل مع الاتصالات الشبكية. أخيرًا الحزمة الفرعية الأكثر بساطة java.lang، والتي تَحتوِي على الأصناف الأساسية مثل String و Math و Integer و Double.

اُنظر الصورة التالية والتي تَتضمَّن تمثيلًا رسوميًا (graphical representation) لمُستويات التَدَاخُل (nesting levels) بحزمة java، بما في ذلك حزمها الفرعية (sub-packages)، والأصناف الموجودة ضِمْن تلك الحزم الفرعية، بالإضافة إلى البرامج الفرعية (subroutines) الموجودة ضِمْن تلك الأصناف. لاحِظ أن هذا التمثيل الرسومي غَيْر كامل، فهو يَعرِض فقط عددًا قليلًا جدًا من العناصر الكثيرة الموجودة بكل عنصر.

001Package_Class_Subroutine.png]

بالمثل، تَحتوِي حزمة javafx على الحزمة الفرعية javafx.scene، والتي تَحتوِي بدورها على كُلًا من الحزمتين الفرعيتين javafx.scene.control و javafx.scene.paint. تَحتوِي أولاهما على أصناف (classes) لتمثيل مُكوِّنات واجهة المُستخدِم الرسومية (GUI components)، كالأزرار (buttons)، وصناديق الإِدْخال (input boxes)، بينما تَحتوِي الآخرى على الصَنْف Color، وأصناف آخرى لأغراض مَلْئ الأشكال (filling) وتَحْدِيد حوافها (stroking).

تَتضمَّن واجهة برمجة تطبيقات جافا القياسية (standard Java API) آلافًا من الأصناف مُجمَّعة ضِمْن مئات من الحزم. الكثير من تلك الأصناف هو، في الواقع، مُتخصِّص للغاية أو غَيْر مَعْروف، لذا لا داعي للإلمام بكامل واجهة برمجة تطبيقات جافا (Java API)، ولا حتى غالبيتها، فحتى خبراء المبرمجين ليسوا على دِرَايَة بكامل الواجهة. ستُواجه عشرات الأصناف (classes) أثناء دراستك لهذا الكتاب، وستَجِدْ أنهم كافيين تمامًا لكتابة تشكيلة واسعة من البرامج (programs). مع ذلك، يُمكِنك تَصفُّح توثيق واجهة برمجة التطبيقات (API) للإصدار ٨ من الجافا بالإضافة إلى توثيق واجهة برمجة تطبيقات جافا إف إكس (JavaFX) للإطلاع على ما هو مُتاح عمومًا.

يُناقِش القسم الفرعي "الوحدات (modules)" بالأسفل بعضًا من التَغْيِيرات التي طرأت بالإصدار ٩ من الجافا، كما ستَجِدْ روابط تَوْثيق (documentation) الإصدار ١١ من الجافا. لكن لاحِظ أن تَوْثيق الإصدار ٨ يُعدّ أكثر سهولة في الاِستخدَام فيما يَتعلَّق بدراسة هذا الكتاب.

استخدام الأصناف ضمن الحزم

تَتضمَّن الحزمة javafx.scene.paint الصَنْف Color، لذا فإن الاسم الكامل للصنف هو javafx.scene.paint.Color. ذلك الصَنْف، وكأي صنف، هو بالنهاية نوع، أيّ أنك تستطيع اِستخدَامه للتَّصْريح (declare) عن كلًا من المُتَغيِّرات، والمُعامِلات (parameters)، وكذلك لتَخْصيص نوع القيمة المعادة (return type) من دالة (function). إذا أردت اِستخدَام ذلك الصَنْف ضِمْن أحد البرامج (program) التي تقوم بكتابتها، فإن أحد الطرائق للقيام بذلك هو استخدام الاسم الكامل للصَنْف كاسم للنوع. مثلًا، إذا كنت تريد التَّصْريح عن مُتَغيِّر اسمه rectColor من النوع Color، تستطيع كتابة التالي:

javafx.scene.paint.Color  rectColor;

تُمثِل الشيفرة بالأعلى مُجرَّد تَّصْريح (declaration) عن مُتَغيِّر عادي على الصياغة ;‎. مع ذلك، فإن اِستخدَام الاسم الكامل للصَنْف هو حتمًا أمر مُتعب، وفي الواقع، نادرًا ما ستَجِدْ تلك الأسماء الكاملة مُستخدَمة بأيّ برنامج (program)؛ حيث تَسمَح الجافا بتَجَنُّب اِستخدَام الاسم الكامل للصَنْف، وفي المقابل، ستحتاج إلى اِستيراد (importing) ذلك الصَنْف أولًا. يُمكِنك استيراد الصَنْف بإضافة السَطْر التالي إلى بداية ملف الشيفرة المصدرية (source code):

import javafx.scene.paint.Color;

بذلك، تستطيع ببقية ذلك الملف كتابة الاسم البسيط (simple name) للصَنْف، أيّ Color، بدلًا من الاسم الكامل javafx.scene.paint.Color. لابُدّ أن يُكتَب سَطْر الاستيراد import ببداية الملف (بعد تَعْليمَة package في حالة وجودها)، وبحيث لا يَقَع سَطْر الاستيراد import ضِمْن أيّ صَنْف (class) مُعرَّف داخل الملف. يُسمِى البعض سَطْر الاستيراد import -بالأعلى- أحيانًا باسم التَعْليمَة (statement)، ولكن من الأنسب تَسميته بالمُوجِّه (directive)‏ import؛ لأنه ليس تَعْليمَة بالمعنى المُعتاد. والآن، سَمَح لك المُوجِّه import javafx.scene.paint.Color بكتابة التالي للتَّصْريح عن مُتَغيِّر:

Color  rectColor;

يَقْتصِر دور المُوجِّه (directive)‏ import على السماح باِستخدَام الأسماء البسيطة للأصناف بدلًا من أسمائها الكاملة .، أي أنه لا يَستوِرد أي شيء فعليّ، فأنت ما زلت تستطيع الوصول إلى الصَنْف (class) بدون اِستخدَام ذلك المُوجِّه، فقط ستحتاج إلى تعيين اسم الصَنْف كاملًا. تَتَوفَّر طريقة مُختصرة لاستيراد (importing) جميع الأصناف الموجودة ضِمْن حزمة معينة. على سبيل المثال، اِستخدِم مُوجِّه import التالي بهدف استيراد جميع الأصناف (classes) الموجودة بحزمة java.util:

import java.util.*;

لاحِظ أنه في حين يَتطابَق محرف البدل (wildcard) * مع جميع الأصناف الموجودة ضِمْن حزمة معينة، فإنه لا يَتطابَق مع حزمها الفرعية (sub-packages)، أيّ أنك لا تستطيع استيراد جميع الحزم الفرعية (sub-packages) الموجودة ضِمْن حزمة javafx بمُجرَّد كتابة import javafx.*‎.

لمّا كان اِستخدَام محرف البدل (wildcard) * بتَعْليمَة import يُتيح عددًا كبيرًا من أسماء الأصناف التي على الأرجح لن تُستخدَم، بل والتي ربما لا يَعَلم المبرمج عنها شيئًا، فإن بعض المبرمجين يُفضِّلون استيراد كل صَنْف سيَستخدِمونه فعليًا استيرادًا صريحًا وبصورة مُنفصلة. يَنصَح الكاتب باِستخدَام محرف البدل (wildcard) فقط مع الحزم (packages) الأكثر صلة بالتطبيق؛ بهدف استيراد جميع الأصناف (classes) الموجودة بها، أما في حالة اِستخدَام صَنْف واحد فقط أو اثنين من حزمة معينة، فلربما عندها من الأفضل اِستخدَام استيرادات (imports) فردية.

على سبيل المثال، قد يَحتوِي برنامج يتعامل بصورة أساسية مع الشبكات على المُوجِّه import java.net.*;‎، بينما قد يَحتوِي برنامج آخر يقرأ الملفات ويكتبها على المُوجِّه import java.io.*;‎. لكن لاحِظ أنه في حالة بدأت باستيراد الكثير من الحزم بتلك الطريقة، فلابُدّ من الانتباه لأمر هام. قد تَحتوِي حزمتان مختلفتان على صَنْفين (classes) يَحمِل كلاهما نفس الاسم، فمثلًا تَحتوِي كُلًا من الحزمتين java.awt و java.util على صَنْف اسمه List. الآن، إذا استوردت كُلًا من java.awt.*‎ و java.util.*‎، فسيُصبِح الاسم البسيط للصَنْف List مُبهمًا، وبالتالي، إذا حاولت التَّصْريح (declare) عن مُتَغيِّر من النوع List، ستَحصُل على رسالة خطأ من المُصرِّف (compiler) بشأن وجود اسم صَنْف مُبْهَم. مع ذلك، ما زلت تستطيع اِستخدَام كِلا الصنفين بالبرنامج (program)، إما باِستخدَام الاسم الكامل للصَنْف مثل java.awt.List و java.util.List، أو باِستخدَام المُوجِّه import لاستيراد الأصناف المُفردة التي تحتاجها بدلًا من استيراد الحزم (packages) بالكامل.

تُعدّ الحزمة java.lang أحد أهم الحزم الأساسية؛ لاحتوائها على بعض الأصناف الرئيسية، ولذلك تُستورَد جميع الأصناف الموجودة بتلك الحزمة أتوماتيكيًا بأيّ برنامج (program)، أيّ كما لو كان كل برنامج يبدأ بالتَعْليمَة import java.lang.*;‎. وهو في الواقع ما قد مَكَّنا سابقًا من اِستخدَام اسم الصَنْف String بدلًا من java.lang.String، واِستخدَام Math.sqrt()‎ بدلًا من java.lang.Math.sqrt()‎. ومع ذلك، يُمكِنك أيضًا اِستخدَام الصياغة الأطول من أسماء تلك الأصناف.

تُستخدَم تَعْليمَة package لإنشاء حزم (packages) جديدة، والتي قد تَتضمَّن مجموعة من الأصناف (classes). كل ما عليك القيام به هو كتابة تلك التَعْليمَة ببداية ملفات الشيفرة المصدرية (source code) التي تَحتوِي على تعريف الأصناف المطلوب تَضْمِينها بتلك الحزمة. مثلًا، إذا أردت إنشاء حزمة اسمها utilities، اِستخدِم السطر التالي:

package utilities;

لابُدّ أن تُكْتَب تَعْليمَة package ببداية الملف، أيّ حتى قَبْل أيّ مُوجِّه import قد يَكُون موجودًا بذلك الملف. بالإضافة إلى ذلك، لابُدّ أن يوجد ذلك الملف بمجلد يَحمِل نفس اسم الحزمة، أي utilities بهذا المثال. وبالمثل، لابُدّ أن توجد ملفات الأصناف ضِمْن الحزم الفرعية (sub-package) بمجلدات فرعية. على سبيل المثال، ينبغي لملفات الأصناف الموجودة بحزمة اسمها utilities.net أن تكون موجودة بمجلد اسمه net الموجود بدوره بمجلد آخر اسمه utilities. يستطيع أي صَنْف داخل حزمة (package) معينة الوصول أتوماتيكيًا إلى جميع الأصناف (classes) الآخرى الموجودة بنفس الحزمة. بتعبير آخر، لا يحتاج صَنْف معين أن يَستورِد (import) الأصناف من نفس الحزمة (package) المُعرَّف بها.

عادة ما يَلجأ المبرمجون لإنشاء حزم جديدة وذلك إِما بالمشروعات التي تُعرِّف عددًا كبيرًا من الأصناف؛ بهدف تنظيمها، أو عند إِنشائهم لصناديق أدوات (toolboxes)؛ لتوفير واجهة برمجة تطبيقات (APIs) لبعض الوظائف والميزات غير المُتوفِّرة بواجهة برمجة تطبيقات جافا القياسية (standard Java API). في الواقع، يَحظَى المبرمجون "من صَانِعي الأدوات" عادة بهَيْبة واحترام أكبر من مُبرمجي التطبيقات الذين يَستخدِمون تلك الأدوات.

لاحظ أن غالبية الأصناف المَكْتوبة لهذا الكتاب غَيْر مُضمَّنة بأي حزمة، مع عدة استثناءات قليلة مثل الصَنْف TextIO بالحزمة textio. لأغراض هذا الكتاب، فإنك ستحتاج أن تدرك ماهية الحزم (packages)؛ حتى تَتَمكَّن من استيراد الصَنْف TextIO، وكذلك الأصناف الموجودة ضِمْن الحزم القياسية (standard packages). لاحِظ أن الحزم القياسية هي دائمًا مُتوفِّرة بجميع البرامج (programs) التي تَكتُبها، قد تَتَساءل، أين توجد تلك الأصناف القياسية (standard classes) بصورة فعليّة؟ يَعتمِد ذلك على إصدار الجافا المُستخدَم إلى حد كبير. مثلًا، بالإصدار ٨ من الجافا، فإنها تَكُون مُخزَّنة بملفات جافا أَرْشيفيّة (jar files/Java archive) تقع بالمجلد الفرعي lib الموجود بمجلد التثبيت الخاص ببيئة تَّنْفيذ الجافا (Java Runtime Environment). إن ملفات جافا الأَرْشيفيّة (jar file/Java archive) هي ببساطة ملفات بامتداد ‎.jar قد تَحتوِي على عدة أصناف. توجد غالبية الأصناف المُستخدَمة بالإصدار ٨ من الجافا بملف جافا أرشيفي (jar file) اسمه rt.jar. طرأت بعض التَغْيِيرات بالإصدار ٩ من الجافا، وهو ما سنُناقشه بالقسم الفرعي التالي.

لاحِظ أن أيّ صَنْف بالنهاية لابُدّ وأن يقع ضِمْن حزمة (package)، حتى في حالة عدم تَحْدِيد الحزمة التي يُفْترَض أن يقع بها الصنف صراحةً، وفي تلك الحالات، يُوضَع الصَنْف تلقائيًا فيما يُعرَف باسم الحزمة الافتراضية (default package)، والتي ليس لها اسم. تقع تقريبًا كل الأمثلة التي ستراها بهذا الكتاب بالحزمة الافتراضية.

الوحدات (modules)

تُدعِّم الجافا منذ الإصدار ٩ ما يُعرَف باسم الوحدات (modules)، وهو ما تَسَبَّب بحُدوث بعض التَغْيِيرات على بنيتها ذات النطاقات الواسعة (large-scale structure). تُوفِّر الوحدات (modules) عمومًا مستوًى آخرًا من التَجْميع؛ حيث تَتضمَّن الوحدات مجموعة من الحزم، التي تَحتوِي على أصناف، والتي بدورها تَحتوِي على مُتَغيِّرات وتوابع (methods). ليس ضروريًا أن تقع الحزمة ضِمْن وحدة (module) حتى تُصبِح قابلة للاِستخدَام، ومع ذلك، فإن جميع الأصناف القياسية (standard classes) بكُلًا من جافا (Java) وجافا إف إكس (JavaFX) قد ضُمّنت داخل وحدات (modules).

وَفَّرت الجافا الوحدات (modules) لعدة أسباب: أولًا، لتَحسِّين التَحكُّم بالوصول (access control)، وهو في الواقع أحد أهم الأسباب الرئيسية. قَبْل الوحدات، كان بإِمكانك اِستخدَام الأصناف العامة، أيّ تلك المُصرَّح عنها باِستخدَام المُبدِّل public، بأيّ مكان وداخل أيّ صَنْف ضِمْن أيّ حزمة، وبالمثل مُتَغيِّراته وتوابعه المُصرَّح عنها باِستخدَام المُبدِّل public. في المقابل، يَعنِي اِستخدَام المُبدِّل العام public مع صَنْف، مُعرَّف ضِمْن وحدة (module)، كَوْن ذلك الصَنْف عامًا public ضِمْن الوحدة (module) المُعرَّف بداخلها فقط. ومع ذلك، ما يزال بإمكان الوحدات تصدير (export) أي حزمة بشكل صريح، وبناءً عليه، ستُصبِح الأصناف العامة، أيّ تلك المُعرَّفة باِستخدَام المُبدِّل public، ضمن تلك الحزمة قابلة للوصول (accessible) مرة آخرى من أي مكان، بما في ذلك الوحدات (modules) الآخرى، وكذلك الأصناف التي ليست جزءًا من أي وحدة. يمكن حتى تَخْصيص الوحدات (modules) المطلوب تصدير الحزمة إليها، وهو ما يُوفِّر تَحكُّمًا بالوصول (access control) أكثر دقة. يَعنِي كل ما سبق أنه يُمكِننا الآن إنشاء حزم هي خاصة (private) بالأساس، أيّ أنها غَيْر مَرئية (invisible) من خارج الوحدة، ولكنها في نفس الوقت تُوفِّر خدمات (services) للحزم الآخرى داخل نفس الوحدة. يُمكِننا بذلك عدّ الوحدة نوعًا آخرًا من الصندوق الأسود (black box)، تَكُون فيه الحزم غَيْر المُصدَّرة (non-exported) جزءًا من تَّنْفيذه (implementation) الخفي. تُعدّ التركيبية (modularity) على هذا المُستوى الواسع من النطاق مهمة بحقّ، وبالأخص للتطبيقات ذات النطاقات الواسعة (large-scale applications).

الدافع الآخر لتَوْفِير الوحدات (modules) هو الحجم الكلي لبيئة تَّنْفيذ الجافا القياسية (Java Runtime Environment)، وتُختصر إلى JRE. تَتضمَّن تلك البيئة جميع الأصناف القياسية (standard classes) على الرغم من أن التطبيقات (application) تَستخدِم عادةً جزءًا صغيرًا منها. تَسمَح التركيبية (Modularization) بإنشاء بيئات تَّنْفيذ جافا مُخصَّصة (custom JREs) أصغر؛ بحيث تحتوي فقط على الوحدات التي يحتاجها التطبيق. تَتضمَّن عُدة تطوير الجافا (Java Development Kit)، وتُختصر إلى JDK، الأمر jlink المُستخدَم لإنشاء بيئات تَّنْفيذ مُخصَّصة (custom runtimes)، والتي عادةً ما تَتضمَّن وحدات (modules) التطبيق ذاته بالإضافة إلى الوحدات القياسية (standard modules) المطلوبة لتَشْغِيل ذلك التطبيق (application). بعد ذلك، تُوزَّع (distribute) بيئات التَّنْفيذ تلك كتطبيق قائم بذاته (standalone)، والذي يُمكِن تَشْغِيله حتى بتلك الحواسيب التي لم تُثبَّت عليها عُدة تطوير الجافا (JDK). ولكن لاحِظ أنك ستحتاج إلى إنشاء إصدارات مختلفة من بيئة التَّنْفيذ المُخصَّصة (custom runtime) للمنصات المختلفة مثل الويندوز (Windows) والماك (Mac OS) ولينكس (Linux)، كما هو الحال مع عُدة تطوير الجافا (JDK) ذاتها. بالإضافة إلى ذلك، لن تُطبَق تحديثات الأمان (security updates) الصادرة لعُدة تطوير الجافا (JDK) أتوماتيكيًا على بيئات التَّنْفيذ المُخصَّصة (custom runtime)، ولذلك تقع مسئولية تحديثها على مُطوِّر التطبيق. عمومًا يُعدّ ذلك ميزة مفيدة جدًا للتطبيقات الكبيرة.

منذ الإصدار ٩ من الجافا، تُخزَّن ملفات أصناف الوحدات القياسية (standard modules) المُصرَّفة داخل الملف modules، والموجود بالمجلد الفرعي lib الموجود بدوره بالمجلد الرئيسي لعُدة تطوير الجافا (JDK). صيغة ذلك الملف هي jimage، يُمكِنك التعامل معها باِستخدَام أداة سطر الأوامر jimage. في الواقع، عندما تُستخدَم أداة jlink لإنشاء بيئة تَّنْفيذ مُخصَّصة (custom runtime)، فإن جزءًا مما تقوم به هو إِنشاء ملف modules مُخصَّص يَحتوِي فقط على الوحدات (modules) المطلوبة لبيئة التَّنْفيذ (runtime) تلك. بفَحْص مجلد الإصدار ١٢ من عُدة تطوير الجافا (JDK) بحاسوب لينكس (Linux) الخاص بالكاتب، تَبيَّن أن الملف modules يَتضمَّن حوالي ٣٠١٩٩ صَنْف، ضِمْن ١٠٠٠ حزمة، ضِمْن ٧٠ وحدة، كما وصل حجمه إلى حوالي ١٣٠ ميجا بايت. يَحتوِي المجلد الرئيسي لعُدة تطوير الجافا (JDK) أيضًا على المجلد الفرعي jmods، والذي يَتضمَّن تلك الوحدات (modules) ولكن بصيغة آخرى، وعمومًا هو ليس مطلوبًا لتَصْرِيف البرامج وتَشْغِيلها، وغالبًا يَقْتصِر اِستخدَامه على الأداة jlink على حَدْ عِلم الكاتب.

تَتضمَّن عُدة تطوير الجافا (JDK) مجموعة من الوحدات، من بينها الوحدتين java.base و java.desktop. تَحتوِي أولاهما على الحزم الأساسية مثل java.lang و java.util، بينما تَتضمَّن الثانية حزم (packages) خاصة بـ"أدوات تَحكُّم واجهة المُستخدَم الرسومية Swing ‏(Swing GUI toolkit)". في المقابل، تَتضمَّن منصة جافا إف إكس (JavaFX) كُلًا من الوحدات javafx.base و javafx.control و javafx.graphics بالإضافة إلى وحدات آخرى غير شائعة الاِستخدَام عمومًا. يُقسَّم تَوْثيق واجهة برمجة التطبيقات (API) للإصدارات التركيبية (modular) من الجافا إلى وحدات (modules)، مُقسَّمة بدورها إلى حزم (packages)، وأخيرًا إلى أصناف (classes)، وهو ما قد يَجعَل التَوْثيق (documentation) أصعب قليلًا في التَصفُّح بالموازنة مع الإصدارات الأقدم. مع ذلك تَتوفَّر مِيزَة بحث فعالة بالموقع الالكتروني للتَوْثيق. يُمكِنك تَصفُّح توثيق الإصدار ١١ من الجافا بالإضافة إلى توثيق الإصدار ١١ من جافا إف إكس (JavaFX).

ليس ضروريًا أن يَقَع الصَنْف ضِمْن وحدة (module)، وفي تلك الحالة، فإنه ما يزال يستطيع اِستخدَام الحزم من الوحدات الاخرى، بشَّرْط أن تَكُون تلك الحزم قد صُدّرت (exported) بالوحدات المُعرَّفة داخلها. يستطيع المبرمج عمومًا اِستخدَام الأصناف الموجودة بعُدة تطوير الجافا (JDK) دون الحاجة للتفكير نهائيًا بالوحدات أو حتى مَعرِفة وجودها. يَنطبِق ذلك على برامج سطر الأوامر (command-line programs) بهذا الكتاب، أما برامج واجهة المُستخدِم الرسومية (GUI programs) التي تَستخدِم منصة جافا إف إكس (JavaFX)، فالأمور تختلف قليلًا منذ الإصدار ١١ من الجافا؛ وذلك لأن تلك المنصة قد حُذفَت من عُدة تطوير الجافا (JDK)، وأصبحت تُوزَّع بصورة مستقلة كمجموعة من الوحدات. لذلك، عند تَصْرِيف برنامج جافا إف إكس (JavaFX)، أو تَشْغِيله، ستحتاج، كما رأينا بالقسم ٢.٦، إلى تَخْصيص مسار الوحدة (module path)، والذي لابُدّ أن يَحتوِي على وحدات تلك المنصة، كما ستحتاج إلى تمرير قيمة للخيار ‎--add-modules. مُرِّرت ALL-MODULE-PATH كقيمة للخيار ‎--add-modules بالقسم ٢.٦؛ للسماح للبرنامج بالوصول إلى أي وحدات (modules) موجودة بمسار الوحدة (module path) المُمرَّر. بدلًا من ذلك، قد تُمرِّر قائمة بأسماء الوحدات المُستخدَمة فعليًا بالبرنامج فقط.

تنبيه: لا يُغطِى هذا الكتاب موضوع الوحدات (modules) بصورة أكبر من مُجرَّد اِستخدَامها مع منصة جافا إف إكس (JavaFX)، بالإضافة إلى المعلومات الأساسية بهذا القسم.

التوثيق بأداة Javadoc

ينبغي عمومًا كتابة تَوْثيق (documentation) جيد لأيّ واجهة برمجة تطبيقات (API)؛ وذلك حتى يَتَمكَّن المبرمجين من اِستخدَامها بصورة فعالة. يَشيِع اِستخدَام نظام Javadoc لتَوْثيق غالبية واجهات برمجة تطبيقات جافا (Java APIs)، فمثلًا، يُستخدَم هذا النظام لتجهيز تَوْثيق حزم الجافا القياسية (standard packages)، كما يَنشُر غالبية المبرمجين تقريبًا تَوْثيقًا باِستخدَام نفس ذلك النظام لأي صندوق أدوات (toolbox) يُطوروه بالجافا.

يُجهَز تَوْثيق Javadoc بالاعتماد على مجموعة من التعليقات (comments) الخاصة، والتي تُكتَب بملفات الشيفرة المصدرية. كما تَعَلم، تُكتَب أحد أنواع تعليقات الجافا ضِمْن الترميزين ‎/*‎ و ‎*/‎. بالمثل، تُكتَب تعليقات Javadoc بنفس الصياغة لكنها تبدأ بالترميز ‎/**‎‎ بدلًا من الترميز ‎/*‎. لقد تَعرَّضت بالفعل لتعليقات مَكْتوبة بتلك الصياغة بكثير من الأمثلة بهذا الكتاب.

ينبغي أن تُوْضَع تعليقات Javadoc قَبْل البرنامج الفرعي المَعنِي بالتعليق مباشرة. لاحظ ضرورة اتباع تلك القاعدة عمومًا وبَغْض النظر عن العنصر المَعنِي بالتَوْثيق. تُستخدَم تعليقات Javadoc عمومًا مع البرامج الفرعية (subroutines)، والمُتَغيِّرات الأعضاء (member variables)، والأصناف (classes)، وفي جميع تلك الحالات، لابُدّ دائمًا أن يَسبِق تعليق Javadoc العنصر المَعنِي بالتعليق مباشرة.

عندما يُصرِّف (compile) الحاسوب ملفات الشيفرة المصدرية، فإنه يتجاهل تعليقات Javadoc مثلما يتجاهل أي تعليق عادي آخر. تَتوفَّر الأداة javadoc، والتي تَقْرأ ملفات الشيفرة المصدرية، لتَستخرِج منها تعليقات Javadoc، ثم تُنشِئ مجموعة من صفحات الانترنت التي تَتضمَّن تلك التعليقات بصياغة متناسقة ومترابطة. لاحظ أن تلك الأداة تَستخرِج افتراضيًا المعلومات المُتعلِّقة بكُلًا من الأصناف العامة والبرامج الفرعية العامة والمُتَغيِّرات الأعضاء العامة فقط، أي تلك المُصرَّح عنها باِستخدَام المُبدِّل public، ومع ذلك فهي تَسمَح بإنشاء تَوْثيق للعناصر غير العامة (non-public) من خلال خيار خاص. إذا لم تَعثُر الأداة javadoc على أية تعليقات Javadoc لعنصر معين، فإنها تُنشِئ واحدًا افتراضيًا يَتكوَّن من مُجرد معلومات بسيطة عن ذلك العنصر، مثل اسم المُتَغيِّر العضو ونوعه في حالة كان العنصر مُتَغيِّر عضو، أو كُلًا من اسم البرنامج الفرعي، ونوع القيمة المُعادة منه وقائمة مُعامِلاته في حالة كان العنصر برنامجًا فرعيًا. تعدّ تلك المعلومات مُجرد معلومات صياغية (syntactic)، أما لإضافة معلومات دلالية (semantics) واقعية، فلابُدّ من كتابة تعليق Javadoc.

كمثال، يُمكِنك فَحْص "توثيق Javadoc للصنف TextIO". اُنشأت صفحة التَوْثيق تلك باِستخدَام أداة javadoc مع ملف الشيفرة المصدري TextIO.java. إذا حَمَّلت نسخة الكتاب المتاحة عبر الإنترنت، ستَجِدْ ذلك التوثيق بالمجلد TextIO_Javadoc.

يُعدّ استخدام الترميز * ببداية كل سطر ضِمْن تعليقات Javadoc أمرًا اختياريًا؛ حيث تَحذفه أداة javadoc على أية حال. بالإضافة إلى النص العادي، قد تَحتوِي تعليقات Javadoc على ترميزات (codes) خاصة، مثل أوامر HTML الترميزية (HTML mark-up commands)‏. تُستخدَم لغة HTML عمومًا لإنشاء صفحات الانترنت، ولمّا كانت تعليقات Javadoc مَعنيَّة بالظهور على تلك الصفحات، فإن أداة javadoc تَنسَخ أي أوامر HTML بالتعليقات إلى صفحات الانترنت التي تُنشئها. لن يتناول الكتاب أيّ شرح تفصيلي عن HTML، لكن، كمُجرَّد مثال، تستطيع مثلًا إضافة <p> للإشارة إلى بداية فقرة جديدة. في حالة غياب أوامر HTML، تتجاهل الأداة الأسطر الفارغة والفراغات (spaces) الإضافية الموجودة بالتعليق. انتبه أيضًا للمحرفين & و ‎<‎؛ لأن لهما معنًى خاصًا بلغة HTML، ولذا لا ينبغي اِستخدَامهما بتعليقات Javadoc لمعنى غير تلك المَعانِي، ولكن يُمكِن كتابتهما باِستخدَام ‎&amp;‎ و ‎&lt‎;‎ على التوالي.

يُوفِّر نظام التوثيق Javadoc ما يُعرَف باسم الوُسوم التوثيقية (doc tags). تُكتَب تلك الوسوم ضِمْن تعليقات Javadoc، بحيث تُعالجها أداة javadoc بعدّها نوعًا من الأوامر الخاصة. يبدأ أيّ وَسم تَوْثيقي (doc tag) بالمحرف @ مَتبوعًا باسمه. سنتناول ٤ وسوم (tags) فقط، هي كالتالي: الوُسوم ‎@author و ‎@param و ‎@return و ‎@throws. يُستخدَم الوسم ‎@author فقط مع الأصناف، وينبغي أن يُتبَع باسم المؤلف. تُستخدَم الوسوم الثلاثة الاخرى بتعليقات Javadoc التي تستهدف البرامج الفرعية بهدف تَوْفِير معلومات عن مُعاملاتها، وقيمها المُعادة، والاعتراضات (exceptions) التي قد تبلِّغ عنها. لابُدّ أن تُوْضَع تلك الأوسمة بنهاية التعليق، أي بَعْد وصف البرنامج الفرعي نفسه. تُكتَب بالصيغ (syntax) التالية:

@param  <parameter-name>   <description-of-parameter>

@return  <description-of-return-value>

@throws  <exception-class-name>   <description-of-exception>

يُمكِن أن تمتد الأوصاف و و إلى عدة أسطر؛ حيث ينتهي أي وصف إما ببداية الوَسْم التوثيقي (doc tag) التالي أو بنهاية تعليق Javadoc بالكامل. يُمكِنك اِستخدَام الوَسْم ‎@param لوصف كل مُعامِل (parameter) يَستقبِله البرنامج الفرعي، وكذلك الوَسْم ‎@throws بقدر ما تريد تَوْثيقه من أنواع الاعتراضات (exception) المختلفة. وأخيرًا الوَسْم ‎@return والذي يُفْترَض كتابته للبرامج الفرعية التي تُعيد قيمة فعلية لا void. لا يُشترَط عمومًا كتابة تلك الوسوم (tags) بترتيب معين.

اُنظر المثال التالي والذي يَستخدِم وُسوم التَوْثيق (doc tag) الثلاثة:

/**
 * يحسب هذا البرنامج الفرعي مساحة المستطيل بفرض استقباله
 * لكلا من طول وعرض المستطيل. 
 * ينبغي أن يكون كلا من طول وعرض المستطيل الممررين قيمة موجبة
 * @param width طول أحد جوانب المستطيل 
 * @param height طول الجانب الآخر من المستطيل
 * @return مساحة المستطيل
 * @throws IllegalArgumentException إذا كان عرض أو طول المستطيل قيمة سالبة
 */
public static double areaOfRectangle( double height, double width ) {
    if ( width < 0  ||  height < 0 )
       throw new IllegalArgumentException("Sides must have positive length.");
    double area;
    area = width * height;
    return area; 
}

حَاوِل أن تَستخدِم تعليقات Javadoc بالشيفرة الخاصة بك، حتى لو لَمْ تَكُن تَنوِي إنشاء صفحة انترنت للتوثيق (documentation)؛ لأنها تُعدّ الصيغة القياسية لكتابة التوثيقات، لذا ستَكُون مألوفة لغالبية مُبرمجي الجافا.

أما إذا أردت إنشاء صفحة انترنت للتَوْثيق، فستحتاج إلى اِستخدَام أداة javadoc، وهي متاحة كأمر بعُدة تطوير الجافا (JDK) التي ناقشناها بالقسم ٢.٦. تستطيع عمومًا اِستخدَام أداة javadoc بواجهة سطر الأوامر (command line interface) بنفس الطريقة التي تَستخدِم بها أوامر مثل javac و java. لاحِظ أنه يُمكِن تطبيق توثيق Javadoc أيضًا داخل بيئات التطوير المتكاملة (integrated development environments)، تُعرَف اختصارًا باسم IDE، التي تحدثنا عنها بالقسم ٢.٦. لن نَتَعرَّض لأيّ من تلك التفاصيل هنا، حيث يُمكِنك ببساطة مراجعة توثيق بيئة البرمجة (programming environment) الخاصة بك.

الاستيراد الساكن (static import)

كنقطة أخيرة بهذا القسم، سنتناول امتدادًا للمُوجِّه (directive)‏ import. رأيت بالفعل كيف مَكَّنَك المُوجِّه import من الإشارة إلى صَنْف مثل java.util.Scanner باِستخدَام الاسم البسيط للصَنْف Scanner. لكنك ما زلت مُضطرًا لاِستخدَام الأسماء المُركَّبة للإشارة إلى المُتَغيِّرات الأعضاء الساكنة (static member variables) مثل System.out، والتوابع الساكنة (static methods) مثل Math.sqrt.

في الواقع، تَتَوفَّر صياغة آخرى من المُوجِّه import، تَستورِد الأعضاء الساكنة (static members) الموجودة بصَنْف معين بنفس الطريقة التي يَستورَد بها المُوجِّه import الأصناف (classes) من حزمة (package). تُسمَى تلك الصياغة من المُوجِّه باسم الاستيراد الساكن (static import). فمثلًا، لاستيراد اسم عضو ساكن (static member) واحد من صَنْف، نَكتُب الصيغة (syntax) التالية :

import static <package-name>.<class-name>.<static-member-name>;

بالمثل، قد تُكتَب الصيغة التالية لاستيراد جميع الأعضاء الساكنة العامة (public static members) ضِمْن صَنْف معين:

import static <package-name>.<class-name>.*;

على سبيل المثال، إذا كَتَبَت المُوجِّه import التالي قَبْل تعريف صَنْف (class definition) معين:

import static java.lang.System.out;

فإنك تستطيع بَعْدها -ضِمْن ذلك الصَنْف- اِستخدَام الاسم البسيط out بدلًا من الاسم المُركَّب System.out، وكذلك كتابة out.println بدلًا من System.out.println.

إذا كنت ستَستخدِم الصَنْف Math بكثرة ضِمْن صَنْف معين، فلربما قد تُفضِّل اِستِباق تعريف ذلك الصنف بالمُوجِّه import التالي:

import static java.lang.Math.*;

وهو ما سيَسمَح لك بكتابة sqrt بدلًا من Math.sqrt، وكتابة log بدلًا من Math.log، وأيضًا PI بدلًا من Math.PI، وغيره. يُمكِنك أيضًا استيراد الدالة getlnInt من الصنف TextIO باِستخدَام:

import static textio.TextIO.getlnInt;

تنبيه: يتَطلَّب مُوجِّه الاستيراد الساكن (static import directive) تَخْصيص اسم الحزمة حتى مع الأصناف المُعرَّفة بالحزمة القياسية java.lang، مما يَعنِي أنك لن تَكُون قادرًا على تَّنْفيذ أيّ استيراد ساكن (static import) من صَنْف مُعرَّف بالحزمة الافتراضية (default package)؛ لأن ليس لها اسم.

ترجمة -بتصرّف- للقسم Section 6: APIs, Packages, Modules, and Javadoc من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.


تفاعل الأعضاء

أفضل التعليقات

يعطيك العافية, مقال مفيد و مبسط.

أود الإشارة لخطأ بسيط في فقرة "التوثيق بأداة Javadoc" و بالتحديد في المثال الذي يستخدم الذي وسوم التوثيق (doc tag). باراميتر الدالة يجب أن يكون إسمه height و ليس length ليعمل باقي الكود الموضوع في الدالة.

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ 23 ساعات قال Mhamad:

يعطيك العافية, مقال مفيد و مبسط.

أود الإشارة لخطأ بسيط في فقرة "التوثيق بأداة Javadoc" و بالتحديد في المثال الذي يستخدم الذي وسوم التوثيق (doc tag). باراميتر الدالة يجب أن يكون إسمه height و ليس length ليعمل باقي الكود الموضوع في الدالة.

صدقت، جرى التصحيح. كان المثال مكتوبًا بتلك الصورة في المقال الأصلي، ربما لم ينتبه الكاتب لهذا الخطأ وسأحاول تبليغه لتصحيحه في المقال الأصلي أيضًا. شكرًا جزيلًا لك لانتباهك لهذه التفاصيل.
@Mhamad

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...