لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 11/27/22 في كل الموقع
-
تعرّفنا فيما سبق من دروس هذه السلسلة على أساسيات لغة بايثون، من مُتغيّرات وحلقات تكرار إلى الدوال، وقد حان الوقتُ للدخول إلى أساسيات البرمجة كائنية التوجّه Object Oriented Programming وهي ببساطة طريقة أخرى للبرمجة بحيث تكون أجزاء الشّيفرة مجموعة داخل دوال تُسمّى التوابع methods والدوال تكون داخل صنف معيّن Class. عند إنشاء كائن object من هذا الصنف فإنّنا نستطيع أن نُنفّذ عليه مُختلف العمليات الموجودة داخل التوابع والتي بدورها توجد داخل الصنف. هناك تسميّات عربية أخرى لهذا النّوع من البرمجة، لذا لا تقلق إذا صادَفتَ أحد هذه التّسميات في أماكن أخرى، فكلّها تُشير إلى نفس المعنى: برمجة غرضيّة التوجه برمجة شيئية المنحى برمجة كائنيّة المنحى بنية الصنف Class الصنف ببساطة يحتوي على أجزاء مُختلفة من الشيفرة تماما مثل الدالة، الفرق هنا هو أنّ الصنف يحتوي على دوال كذلك، وهذه الدوال تُسمى التّوابع، ويحتوي كذلك على مُتغيّرات وتنقسم هذه الأخيرة إلى نوعين، مُتغيّر الصنف، والمُتغيّر العادي، الفرق بينهما هو أنّك تستطيع الوصول إلى مُتغيّر الصنف في أي مكان داخل الصنف (سواء داخل التوابع أو خارجها). إنشاء صنف لإنشاء صنف في لغة بايثون كلّ ما عليك فعله هو كتابة كلمة class وبعدها اسم الصنف ثمّ إلحاق نقطتين، بعدها اكتب الشيفرة بإزاحة أربع مسافات: >>> class My_class: ... pass أنشأنا أعلاه صنفًا بسيطا باسم My_class وهو لا يفعل أي شيء يُذكر (كلمة pass تُخبر بايثون بالمرور دون تنفيذ أي شيء). إذا كتبت اسم الصنف على مفسّر بايثون فستجد مُخرجا كالتّالي: >>> My_class <class __main__.My_class at 0x7fd0efc41460> لاحظ بأنّ الكلمة الأولى من المُخرج هي class أي أنّنا أصبحنا نمتلك صنفًا جديدًا، ما يتبع at هو المكان في الذاكرة الذي وُضع فيه الصنف ويتغيّر بين الحين والآخر. إنشاء كائن من صنف بعد أن أنشأنا الصنف سنتمكّن الآن من إنشاء كائن من هذا الصنف، والكائن مُجرّد اسم تماما كالمُتغيّر: my_object = My_class() الآن الكائن my_object هو من صنف My_class. تعريف المتغيرات داخل صنف يُمكننا أن نُعرّف مُتغيّرات في الصنف تماما كما نُعرّف المُتغيّرات بشكل عادي. class My_class: my_variable = 'This is my variable' للوصول إلى المُتغيّر ننشئ أولا كائنا من الصنف وبعدها نكتب اسم الكائن ثمّ نقطة ثمّ اسم المُتغيّر: my_object = My_class() print my_object.my_variable المُخرج: This is my variable يُمكن كذلك الحصول على النّتيجة ذاتها في سطر واحد: print My_class().my_variable إنشاء التوابع التوابع هي دوال خاصّة بالصنف، ويُمكننا إنشاء التابع بنفس الطّريقة التي نُنشئ بها الدالة، الإختلاف هنا هو أنّ جميع التوابع يجب أن تُعرّف مع مُعامل باسم self وذلك للإشارة إلى أنّ الدالة/التابع تابع للصنف، لننشئ تابعا داخل صنف الآن. class My_class: my_variable = 'This is my variable' def my_method(self): print 'This is my method' الآن إذا أنشأنا كائنا فإنّنا سنتمكّن من الوصول إلى التابع، وتذكّر بأنّ التابع تلحقه الأقواس: my_object = My_class() my_object.my_method() المُخرج: This is my method يُمكن كذلك الحصول على النّتيجة ذاتها في سطر واحد: My_class().my_method() كما تُلاحظ فقد نُفّذت الشيفرة الموجودة داخل التّابع my_method ويُمكننا كذلك أن نجعل التّابع يقبل المُعاملات، لكن تذكّر الحفاظ على الكلمة self كمُتغيّر أول. class My_class: my_variable = 'This is my variable' def my_method(self, my_parameter): print 'This is my method ; {} is my parameter'.format(my_parameter) يُمكنك استدعاء التّابع كالتّالي: my_object = My_class() my_object.my_method('Parameter1') my_object.my_method('Parameter2') المُخرج: This is my method ; Parameter1 is my parameter This is my method ; Parameter2 is my parameter في البرنامج السّابق، أنشأنا أولا صنفًا باسم My_class وقُمنا بتعريف مُتغيّر، ثمّ بتعريف تابع باسم my_method يقبل مُعاملين self و my_parameter، بالنّسبة لاستدعاء التّابع، فنحتاج فقط إلى تمرير المُعاملات الموجودة بعد المُعامل selfولا نحتاج إلى تعيين قيمة لهذا المُعامل. مُلاحظة: يُمكنك إعادة تسميّة المُعامل الأول كما تشاء، أي أنّ البرنامج التّالي سيعمل دون مشاكل. class My_class: def my_method(this, my_parameter): print '{} is my parameter'.format(my_parameter) ولكن رغم ذلك فالمُتعارف عليه بين مُبرمجي لغة بايثون هو استعمال self، وفي كثير من اللغات الأخرى تُستعمل this عوضا عن self، أما في برامِجك فمن المُفضّل الإبقاء على هذه التّسميّة المُتعارف عنها، وذلك لتكون شيفراته سهلة القراءة. الوصول إلى متغيرات الصنف داخل التوابع تأمّل الصنف التّالي: class Person: lastname = 'Dyouri' job = 'Writer, Developer' def say_hello(self): name = 'Abdelhadi' print 'Hello, My name is {}'.format(name) البرنامج أعلاه بسيط جدا، أولا نعرّف صنف باسم Person وبعدها نقوم بتعيين قيمتين للمُتغيّرين name و lastname، وبعدها عرّفنا تابعا باسم say_hello يطبع جملة Hello, My name is Abdelhadi. كلّ شيء جيد، لكن ماذا لو أردنا أن نصل إلى المُتغيّرات الأخرى الموجودة خارج التّابع، فلا يُمكننا مثلا أن نقوم بالأمر كالتّالي: class Person: lastname = 'Dyouri' job = 'Writer, Developer' def say_hello(self): name = 'Abdelhadi' print 'Hello, My name is {}'.format(name) print lastname print job ستحصل على الخطأ التّالي: global name 'lastname' is not defined لتفادي هذا الخطأ سنستعمل كلمة self قبل المُتغيّر. class Person: lastname = 'Dyouri' job = 'Writer, Developer' def say_hello(self): name = 'Abdelhadi' print 'Hello, My name is {}'.format(name) print 'My Last name is {} '.format(self.lastname) print 'I am a {}'.format(self.job) استدعاء التّابع: me = Person() me.say_hello() المُخرج: Hello, My name is Abdelhadi My Last name is Dyouri I am a Writer, Developer لاحظ بأنّنا قُمنا بالوصول إلى مُتغيّر lastname عن طريق استدعائه بـ self.lastname وكذا الحال مع المُتغيّر job، وهذه الطّريقة مُشابهة لاستخدام كلمة global الفرق هنا أنّ هذه الأخيرة تُمكن من الوصول إلى المُتغيّر في كامل البرنامج، أمّا كلمة self فتُشير إلى المُتغيّر المُعرّف في الصنف الحاليّة فقط. لتفهم أكثر كيفيّة عمل الكلمة self فقط تخيّل بأنّها تحمل نفس اسم الصنف، مثلا: class Person: lastname = 'Dyouri' job = 'Writer, Developer' def say_hello(self): name = 'Abdelhadi' print 'Hello, My name is {}'.format(name) print 'My Last name is {} '.format(Abd.lastname) print 'I am a {}'.format(Abd.job) لاحظ بأنّنا غيّرنا كلمة self إلى اسم الصنف واستمرّ عمل البرنامج دون مشاكل. وبنفس الطّريقة يُمكنك أن تستدعي تابعا داخل تابع آخر في نفس الصنف: class Person: def say_name(self): print 'Abdelhadi' def say_hello(self): print 'Hello My name is:' self.say_name() المُخرج: Hello My name is: Abdelhadi ما حدث هو أنّ التّابع say_hello قام بطباعة جملة :Hello My name is ثمّ قام باستدعاء التّابع say_name الذي قام بدوره بطباعة الاسم Abdelhadi. لماذا تستعمل البرمجة الكائنية، ومتى يجب علي استخدامها قد تُلاحظ بأنّ ما يُمكنك فعله بالبرمجة الكائنيّة يُمكن القيام به بالدوال والمُتغيّرات فقط. هذا صحيح، وهو أمر واضح، البرمجة الكائنيّة تُستعمل أساسا إذا كان البرنامج الذي تبنيه مُعقّدا مع العديد من الوظائف المُتعلّقة ببعضها (كمكتبة برمجيّة)، مثلا لنقل بأنّك تُطوّر برنامجا مسؤولا عن جلب بيانات من موقع مُعيّن، وبعدها التّعديل عليها، ثمّ إخراج مستند PDF يحتوي على هذه البيانات بشكل يُسهّل قراءتها، هنا ستحتاج إلى صنف لجلب البيانات والتّعديل عليها، وصنف أخرى لتحويل البيانات إلى نصّ مقروء واضح ثمّ إلى ملفّ PDF. إذا نشرت برنامجك مع صديقك وأراد أن يعمل على الجزء الثاني لإضافة وظيفة تُمكن المُستخدم من طباعة المُستند فلا يُعقل أن يضطر للمرور على كل ما يتعلّق بجلب البيانات فقط لأنّه يريد أن يضيف خاصيّة لا علاقة لها بجلب البيانات. استعمال البرمجة الكائنيّة في هذا المشروع سيسمح لك بالتّركيز على الجزء الأول، وسيسمح لصديقك بالتّركيز على تطوير الجزء الثاني. خلاصة الأمر هي أنّك لست مضطرا لاستعمال البرمجة الكائنيّة إلا إذا كان برنامجك طويلا يحتوي على وظائف تتعلّق ببعضها البعض (وظائف من نفس الصنف)، ونسبة استخدام الآخرين لشيفرتك عالية. تمارين التمرين 1 أنشئ صنفًا باسمك، وقم بتعريف مُتغيّرين lastname (الاسم العائلي) و age (العمر)، ثم أنشئ كائنا باسم me (أنا) وقم بطباعة اسمك العائلي وعمرك. التمرين 2 أنشئ صنفًا باسم Car (سيارة) وقم بتعريف مُتغيّرات لصفات السّيارة، مثلا brand لاسم الشّركة، release_date لتاريخ الإعلان عن السّيارة. التمرين 3 أضف توابع إلى الصنف Car التي أنشأتها في التّمرين الثّاني، يُمكن أن تكون التوابع عبارة عن عمليّات تقوم بها السّيارة مثلا move للحركة، stop للتوقّف، slow_down لتخفيض السّرعة، وقم بطباعة جمل تفيد بأنّ العمليّة قد نجحت. المفروض أن يتمكّن الآخرون من إنشاء كائنات خاصّة بهم بحيث تُستخدم بهذه الطّريقة: bmw = Car() bmw.move() bmw.slow_down() bmw.stop() خاتمة تعرّفنا في هذا الدّرس على بعض من أهم أساسيات البرمجة الكائنيّة التوجه في لغة بايثون وهذا الموضوع أطول من أن أشرحه في درس واحد لذلك سيكون الدّرس التّالي تكملة لما تعلّمناه في هذا الدّرس حول تعلم بايثون وسيغطي مفاهيم أخرى حول البرمجة كائنية التوجّه.1 نقطة
-
1 نقطة
-
هياكل البيانات data structures -أو تدعى بنى المعطيات أحيانًا- مصطلح يتكرر كثيرًا في علوم الحاسوب خصوصًا والبرمجة عمومًا ويعد من المصطلحات المعقدة البسيطة أو السهلة الممتنعة كما أن الكثير يخلط بينه وبين أنواع البيانات أو لا يكاد يميز بينهما، ولابد على أي داخل لمجال علوم الحاسوب ومن يريد تعلم البرمجة أن يفهم هذا المصطلح جيدًا لأنه الأساس الذي سيستند عليه في بناء بقية المفاهيم الأخرى اللاحقة. بناء على ذلك، جاء هذا المقال ليكون تعريفًا بهياكل البيانات وأنواعها بأسلوب بسيط سهل مدعوم بالصور التوضيحية كل هيكل بيانات بالإضافة إلى ذكر ميزات وعيوب بعض الأنواع، كما يتناول هذا المقال التعريف بأهمية هياكل البيانات وأهم تطبيقاتها والفرق بينها وبين أنواع البيانات، لذا لنبدأ! ما هي هياكل البيانات؟ تُعَدّ هياكل البيانات data structures من أهم المفاهيم التأسيسية في مجال علوم الحاسوب، فهي ببساطة مجموعة من الوسائل والطرق المستعملة في ترتيب البيانات في ذاكرة الحاسوب بهدف التعامل معها بكفاءة وفعالية وتسهيل إجراء العمليات عليها، وتوجد هناك أنواع أساسية ومتطورة من هياكل البيانات وجميعها مصمَّمة لتنظيم البيانات لاستخدامها في هدف محدَّد. أهمية هياكل البيانات data structures أحب بداية ضرب مثال يوضح هياكل البيانات وأهميتها وهو أننا نستعمل هذا المفهوم في حياتنا اليومية بصورة أو بأخرى، لنفترض أن البيانات تمثل ملابسنا، فهل نرمي الملابس في خزانة واحدة كلها؟ لا بالطبع لأن إخراج بنطال معين آنذاك سيستغرق وقتًا من البحث للعثور عليه وإخراج بل نضع الألبسة بطريقة معينة في عدة خزانات وسحابات وبطرق مختلفة بحيث نصل إلى بنطال أو قميص محدد بمجرد طلبه فورًا آنيًا دون تأخير. الأمر نفسه مع ترتيب البهارات وأوعية المطبخ والكتب وكل شيء حولنا تقريبًا. هذا بالضبط ما يحصل عند كتابة كلمة في محرك البحث لتظهر لك اقتراحات عد قد تطابق تمامًا ما تنوي كتابته في غضون أجزاء من الثانية، فهل تساءلت كيف تمكن محرك البحث من البحث في كم البيانات الضخم الموجودة في الإنترنت ووصل إلى النتيجة؟ لا يمكن ذلك بآلية الرمي العشوائي في مكان واحد التي ذكرناها في ترتيب الملابس تلك بل بتنظيم البيانات ضمن هياكل بيانات تشبه الأوعية والخزانات تسهل الوصول إليها بأسرع ما يمكن. نظرًا لتطور التطبيقات وازدياد تعقيدها وازدياد كمية البيانات الضخم يومًا بعد يوم والذي تتطلب عملية معالجتها معالجات هائلة السرعة، فكان لا بد من تنظيم البيانات وإدارتها بكفاءة في هياكل البيانات تمثل أوعية تخزين وتنظيم لها، إذ يمكن أن يساعد استخدام هياكل البيانات المناسبة في توفير قدر كبير من الوقت أثناء إجراء عمليات عليها مثل تخزين البيانات أو استرجاعها أو معالجتها، وبالتالي وصول أسرع إلى الذاكرة وتوفير في استهلاك العتاد من عمليات معالجة وطاقة للحاسوب. من الجدير بالذكر أيضًا أنّ هياكل البيانات ضرورية لتصميم خوارزميات فعالة والتي سنراها في تطبيقات هياكل البيانات ضمن هذا المقال. الفرق بين أنواع البيانات وهياكل البيانات يبدو لك أنّ أنواع البيانات Data Types وهياكل البيانات Data Structures وجهان لعملة واحدة لأنّ كلاهما يتعامل مع طبيعة البيانات وتنظيمها، ولكن في الحقيقة الأول يشرح نوع وطبيعة البيانات في حين يمثِّل الآخر التجميعات التي تُخزَّن تلك البيانات فيها، وفيما يلي الفروق الأساسية بين نوع البيانات وهيكل البيانات. يمثِّل نوع البيانات ماهية البيانات التي سيجري التعامل معها مثل أن تكون البيانات أعداد أو سلاسل نصية أو رموز …إلخ. في حين يُعَدّ هيكل البيانات تجميعةً collection تحوي تلك البيانات وهناك عدة أنواع من الهياكل تناسب مختلف أنواع البيانات. يحدد نوع البيانات ما يترتب عليه من عمليات تطبق على تلك البيانات مثل العمليات الرياضية على الأعداد وعمليات المعالجة على النصوص وهكذا أما هياكل البيانات فتحدد كيفية تخزين البيانات والوصول إليها والبحث فيها وغيرها من العمليات كما تدرس كيفية تحسين الأداء في تلك العمليات. يأخذ نوع البيانات شكل التنفيذ المجرد abstract implementation الذي يُعرَّف من خلال لغات البرمجة نفسها، في حين يُنفَّذ هيكل البيانات تنفيذًا حقيقيًا concrete implementation بحيث يُنشأ ليكون متوافقًا مع التصميم الذي يحتاجه المبرمج بالحجم الذي يريده وبنوع البيانات التي سيحتويها ضمن تطبيقه. يسمى نوع البيانات بأنه عديم القيمة dataless لأنه لا يخزِّن قيمة البيانات وإنما يمثِّل فقط نوع البيانات التي يمكن تخزينها، في حين يستطيع هيكل البيانات الاحتفاظ بأنواع مختلفة من البيانات مع قيمتها في كائن واحد. تحدد أنواع البيانات مع المتغيرات مثلًا في لغات البرمجة عند تعريفها، في حين تحتاج إلى كتابة بعض الخوارزميات لإسناد قيم البيانات إلى المتغيرات عند استخدام هيكل بيانات مثل عمليتَي الإضافة Push والجلب Pop للبيانات. لا يُكترث للزمن عند استخدام نوع البيانات، في حين يؤخذ بالحسبان عند استخدام هيكل البيانات (يطلق عليه تعقيد زمني BigO). تطبيقات هياكل البيانات يكاد لا يخلو أي تطبيق أو برنامج أو خوارزمية برمجية من وجود هيكل بيانات أو بنية معطيات تساعده في تحقيق الهدف المرجو منه، ومن هذه التطبيقات ما يلي: تنظيم البيانات في ذاكرة الحاسوب. تمثيل المعلومات في قواعد البيانات. خوارزميات معالجة البيانات مثل معالجة النصوص. خوارزميات تحليل البيانات مثل data minar. خوارزميات البحث في البيانات مثل محرك البحث. خورزميات توليد البيانات مثل مولِّد الأعداد العشوائية. خوارزميات ضغط البيانات وتفك ضغطها مثل المعتمدَة في برنامج zip. خوارزميات تشفير البيانات وتفك تشفيرها مثل المعتمدَة في نظام الأمان. البرامج التي تدير الملفات والمجلدات مثل مدير الملفات. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن أنواع هياكل البيانات في لغات البرمجة تنقسم هياكل البيانات إلى هياكل بيانات أولية Primitive وهياكل بيانات غير أولية Non Primitive أو هياكل بيانات معقدة وكل منها يضم عدة أقسام سنشرحها تباعًا. هياكل البيانات الأولية Primitive هي هياكل البيانات الأساسية والبسيطة والتي تُستخدَم لتخزين قيمة من جزء واحد فقط ذات نوع محدَّد، ويندرج تحت هذا النوع ما يلي: integer: من أجل الأعداد الصحيحة مثل العدد 15. character: من أجل محرف واحد فقط. float: من أجل الأعداد العشرية. real: من أجل الأعداد الحقيقية. boolean: من أجل القيم البوليانية والذي يأخذ إحدى القيمتين؛ إما محققة true أو غير محققة false. ملاحظة: قد تتساءل عن هياكل تخزين النصوص، وهي في الحقيقة تدخل ضمن الهياكل الغير أولية أن النصوص مجموعة من الحروف والرموز التي تدعى محارف characters لذا تخزن عادة ضمن مصفوفة وأحيانًا تضيف لغات البرمجة نوعًا خاصًا لها يدعى string سلسلة نصية أو لا يضيف وتكون بالشكل char[] وما تراه [] يدل على هيكل مصفوفة. يوجد شيء خاص يدعى مؤشر pointer والمشهور في بعض اللغات البرمجية مثل سي C وسي بلس بلس ++C، إذ يُعَدّ مكانًا في الذاكرة ويخزِّن عنوان المتغير الذي يشير إليه والشي الخاص فيه أنه يملك نوع بيانات والذي يجب أن يطابق نوع البيانات الذي يشير إليها. ملاحظة: ممكن تسمية هياكل البيانات التي تخزن الأعداد العشرية باسم double (عدد عشري مضاعف الدقة) أو float (عدد عشري) عوضًا عن real بصورة عامة ويكمن الاختلاف حسب اللغة البرمجية في عدد الخانات العشرية الممكن تخزينها، أي عدد الخانات بعد الفاصلة العشرية، بالإضافة إلى الحجم في الذاكرة وأخيرًا طريقة التعريف declaration، ففي لغة سي شارب #C يكون تعريف المتغير من نمط float كما يلي: float x = 1.5f; أما تعريفه عندما يكون من نمط double، فيكون كما يلي: double x = 1.5; ملاحظة: يطلق أحيانًا على هياكل البيانات الأولية أنواع البيانات Data Type. هياكل البيانات غير الأولية هي هياكل البيانات التي تُستخدَم من أجل التخزين المعقّد والتي يمكنها أن تحمل أكثر من قيمة في بنيتها، وتنقسم إلى هياكل بيانات خطية Linear وهياكل بيانات غير خطية Non Linear. هياكل البيانات الخطية تكون هياكل البيانات خطيّةً إذا كانت عناصرها تشكل تسلسلًا فيما بينها بحيث يرتبط كل عنصر فيها بعنصر سابق وعنصر لاحق ضمن مستوى واحد وفي مسار واحد، ومن هذه الهياكل ما يلي: المصفوفة Array القائمة المترابطة Linked List المكدس Stack الرتل Queue المصفوفة تُعَدّ المصفوفة الخطية array أو المصفوفة ذات البعد الواحد أبسط أنواع الهياكل الخطية، ويمكن تشبيهها بقائمة من عدد محدود من العناصر التي تمتلك النوع ذاته، ويكون بعدها -أو طولها- هو عدد العناصر التي تملكها، كما تُخزّن في الذاكرة بحجم ثابت وبمواقع متجاورة. كل عنصر من عناصر هذه المصفوفة الخطية يملك فهرسًا للوصول إليه، وعادةً ما يبدأ الفهرس بالعدد 0 أي يكون فهرس العنصر الأخير هو n-1 في مصفوفة بعدها n، وفي بعض لغات البرمجة يكون فهرس البداية هو العدد 1 مثل لغة باسكال، وفيما يلي صورة توضِّح هذه البنية: ملاحظات: يُنظَر إلى السلسلة النصية string على أنها مصفوفة أحادية من المحارف، أي كلمة Hello هي سلسلة نصية وهي مصفوفة طولها 5 مكوّنة من خمسة عناصر بحيث يمثِّل كل عنصر محرفًا من المحارف كما أشرنا سابقًا. المصفوفة ثنائية البعد هي أيضًا مجموعة من المتغيرات المفهرسة التي تحتوي على النوع نفسه من البيانات ولكن تخزين البيانات فيها يكون على صورة جدول له أعمدة وأسطر، بحيث يمكن الوصول إلى العنصر في هذه المصفوفة من خلال فهرسين أحدهما يحدِّد السطر والآخر يحدِّد العمود. من الجدير بالذكر أنّ بُعد المصفوفة ثابت في بعض لغات البرمجة وبالتالي يجب معرفة عدد العناصر قبل تعريفها فإذا أردت تغيير البُعد وإضافة عناصر جديدة، فستحتاج إلى مصفوفة جديدة ببُعد جديد لتنقل إليها العناصر السابقة وبعدها تضيف العناصر الجديدة إليها، كما أنّ إضافة عنصر ما في مكان ما مملوء مسبقًا أي في وسط المصفوفة مثلًا سيستهلك الكثير من العمليات، إذ ستحتاج إلى إزاحة العناصر اللاحقة ليصبح له مكانًا فارغًا، ويُعَدّ ذلك من عيوب استخدام المصفوفات. هنالك لغات أخرى لا تشترط تحديد عدد عناصر المصفوفة ولكنها تستهلك آنذاك عمليات كثيرة وتكون مكلفة لعمليات المعالجة أما تلك التي تحدد عدد العناصر مسبقًا فذلك عائد إلى توفير عمليات المعالجة. ملاحظة: قد تجد كلمة static ساكن وكلمة dynamic متحرك لتصنيف الأنواع الغير خطية ولكن شرحهما متقدم جدًا يحتاج إلى مقال بأكمله وستحتاج إليه عند التعمق كثيرًا وليس في البداية. القائمة المترابطة تُعَدّ القائمة المترابطة linked list مجموعةً من العقد التي تخزَّن في الذاكرة بمواقع غير متجاورة، وكل عقدة من عقد القائمة مرتبطة بالعقدة المجاورة لها بمؤشر عدا العقدة الأخيرة التي يكون المؤشر فيها عبارة عن Null. تسمح هذه البنية بإدراج عناصر جديدة أو حذف عناصر موجودة سابقًا بسهولة وهذا ما يميزها عن المصفوفات؛ أما سرعة الوصول، فتُعَدّ المصفوفات أسرع لأنه يكفي ذكر الفهرس الرابع على عكس القائمة المترابطة التي يجب البدء فيها من الرأس ثم المرور على العناصر بالتتالي إلى حين الوصول إلى العنصر الرابع. من الجدير بالذكر أنّ القائمة المترابطة تحتاج إلى مساحة إضافية في الذاكرة من أجل المؤشر الذي يرتبط بالعنصر، وهذا عيب آخر من عيوب القوائم المترابطة. يوجد نوع متطور من القوائم المترابطة وهو القوائم المترابطة المزدوجة Doubly Linked List، إذ ترتبط كل عقدة بمؤشرَين أحدهما يربطها بالعقدة السابقة ويسمى عادةً prev والآخر يربطها بالعقدة التالية ويسمى عادةً next، وبالتالي تحتاج هذه القائمة إلى مساحة إضافية في الذاكرة لامتلاكها على مؤشر إضافي، وبالمثل يكون المؤشر prev في عقدة الرأس Null ومؤشر next في العقدة الأخيرة هو Null. يوجد نوع أخير من القوائم المترابطة وهو القوائم المترابطة الدائرية Circular Linked Lists والتي تُعَدّ تطويرًا عن المفردة بحيث يشير مؤشر next الخاص بعقدة النهاية إلى عقدة الرأس. كما توجد القوائم الدائرية المزدوجة بحيث يشير مؤشر prev الخاص بعقدة الرأس إلى عقدة النهاية ويشير مؤشر next الخاص بعقدة النهاية إلى عقدة الرأس، وبالتالي لا تحتوي القوائم المترابطة الدائرية على مؤشرات Null. المكدس يُعَدّ المكدِّس stack هيكل بيانات خطية يتبع ترتيبًا محددًا في تنفيذ عمليات الحذف والإضافة، والترتيب يكون LIFO أي الذي يدخل آخرًا يخرج أولًا Last In First Out، والذي يميز المكدس هنا هو دخول العناصر وخروجها من قمة المكدِّس أي من جهة واحدة. تدخل -أو تُضاف- العناصر إلى المكدِّس عن طريق عملية وحيدة وخاصة وهي دفع Push وبالمثل فإنها تخرج منه -أو تُحذَف- عن طريق عملية وحيدة وخاصة أيضًا وتدعى إخراج Pop، وتوجد أيضًا عمليتان خاصتان بالمكدِّس وهما Top -أو Peek- التي تعيد القيمة الموجودة في قمة المكدِّس دون حذفها، والعملية الأخرى هي عملية IsEmpty التي تُعيد القيمة true إذا كان المكدِّس فارغًا. عند إضافة عنصر إلى مكدِّس ممتلئ لا يستوعب المزيد من العناصر فستحصل حالة طفحان المكدِّس overflow؛ أما عند إجراء عملية الحذف من مكدِّس فارغ، فسنواجه حالة طفحان أو تجاوز الحد الأدنى underflow (قعر المكدس). الرتل يُعَدّ الرتل queue أحد هياكل البيانات الخطية شبيه بالمكدس لامتلاكه عمليات خاصة للحذف والإضافة ولكنه يختلف عنه في مكان الحذف والإضافة كما سنرى. يدعى الرتل أو الطابور بـ FIFO أي الذي يدخل أولًا يخرج أولًا First In First Out بحيث يكون الدخول -أو الإضافة- من الجهة الخلفية rear والخروج -أو الحذف- من الجهة الأخرى أي الأمامية front (كما يحصل عند الوقوف ضمن الطوابير تمامًا لشراء شيء ما)، وتحدث الإضافة عن طريق عملية ENQUEUE أما عملية الحذف فتكون عن طريق عملية DEQUEUE. يتميز الرتل أيضًا بامتلاكه عمليات خاصة وهي العملية IsFull التي تُعيد true إذا كان الرتل ممتلئًا، والعملية IsEmpty التي تُعيد true إذا كان الرتل فارغًا، والعملية Front التي تُعيد العنصر الأمامي من الرتل، بالإضافة إلى العملية Rear التي تُعيد العنصر الخلفي من الرتل. هياكل البيانات غير الخطية لا تحتوي هياكل البيانات غير الخطية -أو المتشعبة- على أيّ تسلسل محدد يربط جميع عناصرها، إذ يمكن أن يكون لكل عنصر أكثر من مسار ليرتبط بالعناصر الأخرى، كما أنّ العناصر الموجودة ضمن هذه الهياكل لا يمكن اجتيازها أو المرور عليها في جولة واحدة أو باستخدام حلقة برمجية واحدة. وأهم ما يميّز هذا النوع على الرغم من صعوبة التنفيذ موازنةً بالهياكل الخطية التي يزداد فيها تعقيد الوقت مع ازدياد حجم البيانات أنه يُعد أكثر كفاءة في استخدام الذاكرة وأكثر سرعة في تطبيق العمليات مثل عمليات البحث، ومن هذه الهياكل: الشجرة الرسم البياني الشجرة تُعَدّ الشجرة tree هيكل بيانات متعدد المستويات وتُعرَّف على أنها مجموعة من العقد التي تحتوي فيما بينها على علاقة هرمية بحيث تسمى العقدة العليا بالعقدة الجذر، كما تحتوي كل عقدة على أب وحيد، في حين يمكن أن يكون لها أكثر من ابن أو تابع. تسمى العقد التي تتفرع من عقدة معينة بأبناء children تلك العقدة والتي بدورها تدعى بالعقدة الأب parent، في حين تسمى العقد التي لا تمتلك أبناء بالأوراق leaves. تمتلك الشجرة عدة أنواع وهي: الشجرة الثنائية binary tree شجرة البحث الثنائية binary search tree شجرة AVL شجرة R-B شجرة البادئات الشجرة الثنائية الشجرة الثنائية binary tree هي شجرة بيانات تمتلك كل عقدة فيها -ما عدا الأوراق- على عقدَتي ابن فقط وهما الابن الأيمن والابن الأيسر. شجرة البحث الثنائية شجرة البحث الثنائية binary search tree أو BST اختصارًا هي شجرة ثنائية تحقق خاصيتان أساسيتان وهما أنّ العقد الواقعة في الفرع اليميني تكون أكبر من العقدة الأب والعقد الواقعة في الفرع اليساري تكون أصغر من العقدة الأب، مع ضمان وجود ابنَين لكل عقدة وعدم تكرار العقد. شجرة AVL تُعَدّ اختصارًا لـ Adelson-Velskii Tree إذ يُعَدّ أديسون Adelson وفيلسكي Velskii مخترعَيها للحفاظ على توازن شجرة البحث الثنائي وذلك لمنع تدهورها إلى قائمة مرتبطة عندما تحتوي الشجرة بأكملها على الشجرة الفرعية اليسرى فقط أو على الشجرة اليمنى فقط مما سينعكس سلبًا على أداء الشجرة، إذ يمكن استخدام سلسلة من عمليات التدوير بحيث تُحدَّد في كل عملية عقدة جذر إلى حين الوصول إلى شجرة بعقدة جذر معينة بحيث تكون متوازنة أي ارتفاع الشجرة اليسرى مساويًا لارتفاع الشجرة اليمنى، ويمكن القول هنا أنّ شجرة AVL هي شجرة BST تحقق شرط التوازن، علمًا أنّ ارتفاع الشجرة هو أكبر عمق موجود لها. شجرة R-B تعني الشجرة الحمراء والسوداء Red-Black tree وهي شجرة بحث ثنائية لها خصائص تميزها بحيث تحتوي كل عقدة فيها على بت تخزين يشير إلى لون العقدة والتي يمكن أن تكون حمراء أو سوداء فقط، كما أنّ عقدة الجذر والعقد الأوراق سوداء دائمًا، وإذا كانت العقدة حمراء فيجب أن يكون أبناءها سود، وأخيرًا يجب أن تحتوي جميع المسارات من عقدة إلى أحفادها العدد نفسه من العقد السوداء، فإذا تحقق ما سبق، فستكون الشجرة شجرة بحث ثنائية متوازنة. شجرة البادئات تُعَدّ شجرة البادئات Prefix tree نوعًا من أشجار البحث وتعرف أيضًا بالشجرة الرقمية أو tri كما تُعرَف بشجرة القاموس وتُستخدَم في البحث عن الكلمات بصورة عامة بما أنها مكوَّنة من أحرف الهجاء، وأهم ما يميزها أنّ جذرها لا يحتوي على أيّ محرف. الكومة تُعَدّ الكومة Heap بنية معطيات شجرية تمتلك خاصة الكومة وهي وجود أسلوب ترتيب متَّبع بين العقد الآباء والعقد الأبناء مثل أن يكون كل أب أكبر من جميع أبنائه وتسمى حينها بالكومة العظمى Max-Heap أو أن يكون كل أب أصغر من جميع أبنائه وتسمى حينها بالكومة الصغرى Min-Heap، وتُستخدَم الكومة بكثرة في خوارزميات الترتيب، كما تتميز الكومة بأنّ جميع مستوياتها ممتلئة بالكامل عدا المستوى الأخير، وفيما يلي صورة توضِّح كومة صغرى. الرسم البياني يختلف الرسم البياني graph أو المبيان عن الشجرة في عدم احتوائه على جذر ومن الممكن أن تتصل العقد مع بعضها باتجاه واحد directed graph أو بالاتجاهين معًا Bi-directional أو بدون اتجاه undirected، كما أنّ طبيعة العلاقات بين العقد في هذا النوع ليست ذات طبيعة هرمية، كما تسمى العقد بالرؤوس vertices والروابط التي بينها تسمى بالحواف أو الأضلاع edges ويكون عدد كل من الرؤوس والأضلاع محدودًا. يكون الزوج (1,2) في الرسم البياني الموجَّه والذي يدل على وجود اتجاه من الرأس 1 إلى الرأس 2 مختلفًا عن الزوج (2,1)، كما تُرمَز مجموعة الرؤوس في هذا النوع بالرمز V ومجموعة الأضلاع بالرمز E ويُستخدَم هذا النوع من البنى في تمثيل الشبكات الواقعية مثل شبكة الهاتف المحمول أو شبكات التواصل الاجتماعي مثل الفيسبوك على سبيل المثال. التقطيع Hashing يُعَدّ التقطيع Hashing أو التجزئة تحسينًا لهياكل البيانات السابقة في بعض التطبيقات التي تحتاج إلى ترتيب بياناتها بواسطة أعداد كبيرة وفريدة موجودة ضمن هذه البيانات مثل ترتيب سجلات المرضى ضمن المستشفيات بناءً على أرقام هواتفهم والتي تُعَدّ مفاتيحًا فريدةً unique لهذه السجلات، ويكون ذلك من خلال الاستعانة بدالة تدعى دالة التقطيع hashing function والتي تحوِّل هذا المفتاح الفريد إلى عدد صغير وصحيح بحيث يكون فهرسًا لجدول جديد يدعى جدول التقطيع hashing table. من الممكن أن يكون فهرس جدول التقطيع هو ذاته رقم هاتف المريض بحيث يكون طوله هو أكبر رقم هاتف موجود ضمن السجلات زائد واحد بما أنّ الفهرسة تبدأ من الصفر، وبالتالي نحصل على سرعة وصول عالية لأيّ سجل من خلال رقم الهاتف الخاص به، ولكن سيتسبب ذلك بتواجد فجوات gaps بين العناصر لعدم الحاجة لوجود جميع أرقام الهواتف المحتملة وبالتالي زيادة حجم التخزين في الذاكرة، لذا تُستخدَم دالة التقطيع لحل هذه المشكلة، وعندها يكون جدول التقطيع مصفوفةً يمثِّل كل عنصر فيها مؤشرًا على السجل الذي يحتوي على رقم الهاتف -في مثالنا- والذي تكون نتيجة تطبيق دالة التقطيع عليه هي فهرس هذا العنصر. من الضروري التأكد من أنّ دالة التقطيع لا تعطي النتيجة ذاتها لأكثر من مفتاح، فإذا كانت دالة التقطيع مثلًا هي تأخذ فقط الرقم الأخير من رقم الهاتف، فسيكون لرقمَي الهاتف 45451 و 56561 على سبيل المثال النتيجة نفسها أي العدد 1 وبالتالي سيحدث تصادم في جدول التقطيع الذي فهرسه العدد 1، وأحد حلول هذه المشكلة أن يكون كل عنصر من جدول التقطيع مؤشرًا على قائمة مترابطة تحتوي على السجلات التي نتيجة تطبيق دالة التقطيع على أرقام هواتفها هي ذاتها. ليكن لدينا الأعداد التالية 12 – 17 – 29 – 6 – 30 – 31 – 4 – 8، فإذا كان فهرس جدول التقطيع هو ذاته العدد المعطى، فسنحتاج إلى جدول بحجم 32، ولكن فعليًا نحتاج إلى 8 أماكن للتخزين وبالتالي سنحصل على فجوات وهدر في الذاكرة، لذا سنلجأ إلى دالة التقطيع والتي ستعطي آحاد العدد المُعطى، وبالتالي ستكون نتيجتها 2 – 7 – 9 – 6 – 0 – 1 – 4 – 8 عند تطبيقها على الأعداد السابقة على الترتيب، بحيث يُخزَّن العدد 12 في العنصر الذي فهرسه هو العدد 2 وهكذا، وبالتالي سنحتاج إلى جدول تقطيع بحجم 8 بدلًا من 32 كما يلي: 12 17 29 6 30 31 4 8 2 7 9 6 0 1 4 8 يمكن تقليص حجم هذا الجدول ليصبح 4 بتحسين دالة التقطيع، بحيث يكون الفهرس هو باقي قسمة كل عدد من الأعداد المعطاة على العدد 4، ولكن ستكون نتيجة الأعداد 17 و 19 هي الفهرس 1، أي سيحدث تصادم، ويمكن حل هذه المشكلة بجعل كل عنصر من عناصر جدول التقطيع مؤشرًا على قائمة مترابطة من الأعداد التي باقي قسمتها على العدد 4 هو فهرس هذا العنصر، أي كما في الشكل التالي: يمكن زيادة حجم جدول التقطيع في المستقبل، كما أنّ هذه التقنية هي الأكثر استخدامًا في جوجل google. الخاتمة تعرَّفنا في هذا المقال على هياكل البيانات بكافة أنواعها مع إعطاء أمثلة توضيحية تساعدك على تخيل هذه البنى، بالإضافة إلى التطرق إلى ذكر أهميتها والفرق بينها وبين أنواع البيانات مع ذكر أهم تطبيقاتها. اقرأ أيضًا مقدمة إلى مفهوم البيانات الضخمة Big Data هياكل البيانات: القوائم المترابطة Linked lists والأشجار Trees في لغة سي C هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت تصميم قواعد البيانات table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }1 نقطة
-
يمكنك اعطاء العنصر header في الصفحة الرئيسية خاصية id وتحديد قيمة لها, على سبيل المثال <div id="main-header"> وفي ملف css يمكنك اعطاء التنسيقات من خلال تحديد العنصر باستخدام ال id كالتالي #main-header{ border-bottom: 1px solid red; } القوة التحديدية لل id اقوى من الصنف او class وبالتالي لن تواجه مشكلة في تضارب التنسيقات1 نقطة
-
إن هذه الخاصية تكون موجودة عندما نكون في وضع ال debug، و دائماً يفضل إيقاف تشغيل هذا الوضع عند القيام برفع الكود على استضافة ما. في لارافل يمكنك إيقاف هذا الوضع عن طريق تغيير أحد متغيرات البيئة و هو APP_DEBUG لتضع قيمة false له، أي كما يلي: APP_DEBUG=false1 نقطة
-
يمكنك إنشاء كلاس class جديد في header في الصفحة الرئيسية فقط ، مثلاً ليكون باسم header-border-bottom . <div class="header header-border-bottom"> وإعطائه التنسيقات المناسبة في CSS .header-border-bottom{ border-bottom: 2px solid black; } ويمكنك إضافة التنسيق في ملف التنسيقات دون إنشاء ملف جديد .1 نقطة
-
إذا نظرت في رسالة الخطاء في console سوف تجد أن نوع الخطاء من نوع بناء الجملة Syntax Error,لذلك أنصحك بالبحث عن أنواع الاخطاء البرمجية لكي يصبح حل المشاكل البرمجية أسهل بالنسبة لك. هناك بعض المكتبات التي تساعدك في حل مثل هذة الاخطاء علي سبيل المثال مكتبة ESLint وهذه نبذة عن المكتبة وما تستطيع فعلة. هي مكتبة هدفها مساعدة مبرمجين الجافاسكريبت على كتابة أكواد جافاسكريبت خالية من الأخطاء فهي تفرض على المطور بعض القواعد التي يجب عليه احترامها للحصول في النهاية على شفرة برمجية خالية من الأخطاء. يمكنك استكشاف المكتبة من هنا. وهذه بعض المكتبات الأخري ولكل مكتبة مميزاتها: JSLint JSHint StandardJS1 نقطة
-
يوجد برامج لتحويل المخططات الي 3d مثل -revit -3d max -rhino -sketchup اما بالنسبه لبرامج الرندره فلكل برنامج له رندرته ولكن يوجد مواقع مخصصه للرندره تعطي احسن كوالتي مثل -v-ray -corona -lumion -blender1 نقطة
-
يوجد العديد من البرامج المرادفة لبرنامج lumion ولكل منها ميزاته وخصائصه، من بعض الأمثلة التي يمكنك استخدامها: SketchUp Blender Cinema 4D V-Ray Enscape Twinmotion D5 Render Artlantis في حال كنت تبحث عن برنامج مجاني، يمكنك استخدام Blender كونه مفتوح المصدر.1 نقطة
-
سنناقش بهذا المقال مجموعةً من البرامج التي تَستخدِم الشبكات والخيوط. تتشارك تلك التطبيقات بمشكلة توفير الاتصال الشبكي بين مجموعةٍ من البرامج المُشغَّلة على حواسيبٍ مختلفة. تُعدّ الألعاب ثنائية اللاعبين أو متعددة اللاعبين عبر الشبكة واحدةً من الأمثلة النموذجية على هذا النوع من التطبيقات، ولكن تضطّر تطبيقاتٌ أخرى أكثر جدية لمواجهة نفس المشكلة أيضًا. سندرس بالقسم الأول من هذا المقال إطار عملٍ framework يُمكِن اِستخدامه ببرامج مختلفة تندرج تحت هذا النوع، ثم سنناقش بالجزء المتبقي من هذا المقال ثلاثة تطبيقات مبنيةً على هذا الإطار. في الواقع، قد تكون تلك التطبيقات هي الأكثر تعقيدًا بهذه السلسلة، ولذلك فهمها ليس ضروريًا لفهم أساسيات الشبكات. في الواقع، يعود الفضل لهذا المقال إلى الطالبين Alexander Kittelberger و Kieran Koehnlein؛ فقد أرادا أن يَكتُبا برنامج لعبة بوكر عبر الشبكة مشروعًا نهائيًا بالصف الذي كان الكاتب يُدرسّه. لقد ساعدهما الكاتب بالجزء المُتعلّق بالشبكات بكتابة إطار عملٍ بسيط يدعم الاتصال بين اللاعبين. يتعرَّض التطبيق إلى الكثير من الأفكار الهامة، ولذلك تقررت إضافة إصدارٍ أعم وأكثر تطورًا من إطار العمل إلى السلسلة. يمثّل المثال الأخير بهذا المقال برنامج لعبة بوكر عبر الشبكة. إطار عمل Netgame تشترك جميع الألعاب المختلفة عبر الشبكة بشيءٍ واحد فيما يتعلّق بالشبكات على الأقل؛ حيث لا بُدّ من وجود طريقةٍ لإيصال الأفعال التي يفعلها لاعبٌ معينٌ إلى اللاعبين الآخرين عبر الشبكة. لذلك، سيكون من الأفضل لو أتحنا تلك الإمكانيات بقاعدةٍ مشتركة قابلةٍ لإعادة الاستخدام بواسطة الكثير من الألعاب المختلفة. على سبيل المثال، تحتوي حزمة netgame.common، التي طورها الكاتب على الكثير من الأصناف. لم نتعامل كثيرًا مع الحزم packages بهذه السلسلة بخلاف استخدام الأصناف المبنية مسبقًا built-in. كنا قد شرحنا ماهية الحزم بمقال بيئات البرمجة programming environment في جافا، ولكننا لم نَستخدِم بجميع الأمثلة البرمجية حتى الآن سوى الحزمة الافتراضية default package. تُستخدَم الحزم عمليًا بجميع المشروعات البرمجية بهدف تقسيم الشيفرة إلى مجموعةٍ من الأصناف المرتبطة، ولذلك كان من البديهي تعريف إطار العمل القابل لإعادة الاستخدام بحزمةٍ يُمكِن إضافتها مثل مجموعة متكاملة unit إلى أي مشروع. تُسهِّل بيئات التطوير المتكاملة Integrated development environments مثل Eclipse استخدام الحزم packages: فمن أجل استخدام حزمة netgame ضمن مشروع باِستخدَام إحدى بيئات التطوير المتكاملة، كل ما عليك فعله هو نَسْخ مجلد netgame بالكامل إلى المشروع؛ ونظرًا لاستخدام حزمة netgame مكتبة JavaFX، ينبغي ضبط المشروع ببيئة Eclipse ليدعم استخدام مكتبة JavaFX. إذا كنت تَعمَل بسطر الأوامر، ينبغي أن يحتوي مجلد العمل working directory على المجلد netgame بهيئة مجلدٍ فرعي. إذا لم تكن تَستخدِم إصدارًا قديمًا من JDK يتضمَّن مكتبة JavaFX مسبقًا، فينبغى أن تُضيف خيار JavaFX إلى أوامر javac و java. لنفترض أننا عرَّفنا الأمرين jfxc و jfx ليكافئا الأمرين javac و java بعد إضافة خيارات مكتبة JavaFX إليهما. والآن، إذا أردنا أن نُصرِّف كل ملفات جافا المُعرَّفة بحزمة netgame.common مثلًا، يُمكِننا استخدام الأمر التالي في نظامي Mac OS و Linux: jfxc netgame/common/*.java أما بالنسبة لنظام Windows، يُمكِننا استخدام الشرطة المائلة للخلف بدلًا من الشرطة المائلة للأمام كما يلي: jfxc netgame\common\*.java بالنسبة لإصدارات JDK التي تتضمَّن بالفعل مكتبة JavaFX، يمكن استخدام javac وليس jfxc. ستحتاج إلى أوامرٍ مشابهة لتتمكَّن من تصريف الشيفرة المصدرية لبعض الأمثلة ضمن هذا المقال، وستجدها معرَّفةً ضمن حزمٍ فرعية subpackages أخرى من حزمة netgame. لتتمكَّن من تشغيل البرنامج main المُعَّرف بحزمةٍ معينة، ينبغي أن تكون بمجلدٍ يتضمَّن تلك الحزمة بهيئة مجلدٍ فرعي، كما ينبغي استخدام الاسم الكامل للصنف الذي تريد تشغيله. إذا أردت مثلًا تشغيل الصنف ChatRoomWindow -سيُناقش لاحقًا ضمن هذا المقال- المُعرَّف بحزمة netgame.chat، شغِّل الأمر التالي: jfx netgame.chat.ChatRoomWindow تُعدّ التطبيقات التي سنناقشها ضمن هذا المقال أمثلةً على البرمجة الموزَّعة؛ فهي تتضمَّن عدة حواسيب تتواصل عبر الشبكة. تَستخدِم تلك التطبيقات كما هو الحال في مثال قسم الحوسبة الموزعة من مقال المقال السابق خادمًا server مركزيًا أو برنامجًا رئيسيًا master مُوصَّلًا بمجموعةٍ من العملاء clients؛ حيث تمر جميع الرسائل عبر الخادم، أي لا يستطيع عميلٌ معينٌ إرسال رسالةٍ إلى عميلٍ آخر مباشرةً. سنشير إلى الخادم ضمن هذا المقال باسم "الموزّع hub" بمعنى موزع الاتصالات. هناك عدة أشياء ينبغي أن تفهمها جيدًا: لا بُدّ أن يكون الموزع مُشغَّلًا قبل بدء تشغيل أيٍّ من العملاء. يتصل العملاء بالموزع ليتمكَّنوا من إرسال رسائلهم إليه، ويُعالِج الموزع جميع رسائل العملاء واحدةً تلو الأخرى بنفس ترتيب استقبالها، وبناءً على تلك المعالجة، يُمكِنه أن يرسل عدة رسائلٍ إلى عميلٍ واحدٍ أو أكثر. يَملُك كل عميل مُعرِّف هوية ID خاصٍ به، ويُمثِل ذلك إطار عمل framework عام يُمكِن اِستخدامه بمختلف أنواع التطبيقات، بحيث يُعرِّف كل تطبيق ما يخصُّه من رسائلٍ ومعالجات. لنتعمَّق الآن بتفاصيل البرنامج. كانت الرسائل في قسم الحوسبة الموزعة من المقال السابق تُرسَل جيئةً وذهابًا بين الخادم والعميل وفقًا لمتتاليةٍ مُحدَّدةٍ ومُعرَّفةٍ مُسبقًا، حيث كان الاتصال بين الخادم والعميل بمثابة اتصالٍ بين خيطٍ واحدٍ مُشغَّلٍ ببرنامج الخادم وخيطٍ آخرٍ مُشغّلٍ ببرنامج العميل. بالنسبة لإطار عمل netgame، نريد أن نجعل الاتصال غير متزامن asynchronous؛ بمعنى أننا لا نريد انتظار وصول الرسائل بناءً على متتاليةٍ متوقَّعة مسبقًا، ولجعل هذا ممكنًا، يَستخدِم العميل وفقًا لإطار عمل netgame خيطين للاتصال: الأول لإرسال الرسائل إلى الموزع، والآخر لاستقبال الرسائل منه. وبالمثل، يَستخدِم الموزع وفقًا لإطار عمل netgame خيطين للاتصال مع كل عميل. يكون الموزع عمومًا متصلًا مع عدة عملاء، ويستطيع استقبال الرسائل من أي عميل بأي وقت. ينبغي أن يُعالِج الموزع الرسائل بطريقةٍ ما، ويَستخدِم لإجراء تلك المعالجة خيط اتصالٍ واحد فقط لمعالجة جميع الرسائل. عندما يَستقبِل ذلك الخيط رسالةً معينةً من عميلٍ ما، فإنه يُدْخِلها إلى رتلٍ queue يحتوي على الرسائل المُستقبَلة؛ حيث يتوفَّر رتل واحد فقط تُخزِّن به رسائل جميع العملاء. في المقابل، يُنفِّذ خيط معالجة الرسائل حلقة تكرار loop يقرأ خلالها رسالةً واحدةً من الرتل ويُعالجها، ثم يقرأ رسالةً أخرى ويُعالجها وهكذا. لاحِظ أن الرتل هو كائن من النوع LinkedBlockingQueue. يتضمَّن الموزع خيطًا آخرًا غير مُوضَّحٍ بالصورة السابقة؛ حيث يُنشِئ ذلك الخيط مقبسًا من النوع ServerSocket، ويَستخدِمه للاستماع لطلبات الاتصال الآتية من العملاء. يُسلّم الموزع كل طلب اتصالٍ يَستقبِله إلى كائنٍ آخر من النوع المُتداخِل ConnectionToClient؛ حيث يُعالِج ذلك الكائن الاتصال مع العميل. يَملُك كل عميلٍ متصلٍ رقم مُعرِّف هويةٍ خاصٍ به، حيث تُسنَد مُعرِّفات الهوية 1 و2 و3 .. إلى العملاء عند اتصالهم؛ ونظرًا لأن بإمكانهم غلق الاتصال، قد لا تكون أرقام مُعرِّفات الهوية الخاصة بالعملاء المتصلين متتاليةً ضمن لحظةٍ معينة. يُستخدَم متغيرٌ من النوع TreeMap<Integer,ConnectionToClient> لربط أرقام مُعرِّفات الهوية الخاصة بالعملاء المتصلين مع الكائنات المسؤولة عن معالجة اتصالاتهم. تمثِّل الرسائل المُرسَلة والمُستقبَلة كائنات، ولذلك اِستخدَمنا مجاري دْخَل وخَرْج I/O streams من النوع ObjectInputStream و ObjectOutputStream لقراءة الكائنات وكتابتها. (انظر قسم إدخال وإخراج الكائنات المسلسلة من مقال قنوات الدخل والخرج وعمليتي القراءة والكتابة في جافا). لقد غلَّفنا مجرى الخرج الخاص بالمقبس باستخدام الصنف ObjectOutputStream لنَسمَح بنقل الكائنات عبر المقبس؛ في حين غلَّفنا مجرى الدخل الخاص بالمقبس باستخدام الصنف ObjectInputStream لنَسمَح باستقبال الكائنات. ملاحظة: لا بُدّ أن تُنفِّذ الكائنات المُستخدَمة مع هذا النوع من المجاري الواجهة java.io.Serializable. ستَجِد الصنف Hub مُعرَّفًا بالملف Hub.java في حزمة netgame.common. يجب تخصيص رقم المنفذ الذي سيستمع إليه مقبس الخادم بهيئة معامل يُمرَّر إلى الباني constructor. يُعرِّف الصنف Hub التابع التالي: protected void messageReceived(int playerID, Object message) عندما تصِل رسالةٌ من عميلٍ معينٍ إلى مقدمة رتل الرسائل، يقرأها خيط معالجة الرسائل من الرتل، ويَستدعِي ذلك التابع. بتلك اللحظة فقط، تبدأ المعالجة الفعلية لرسالة العميل. يُمثِّل المعامل الأول playerID رقم مُعرِّف الهوية الخاص بالعميل المُرسِل للرسالة؛ بينما يُمثِّل المعامل الثاني الرسالة نفسها. يُمرِّر التابع نسخةً من تلك الرسالة إلى جميع العملاء المتصلين، ويُمثِّل ذلك المعالجة الافتراضية التي يُجريها الموزع على الرسائل المُستقبَلة؛ ولكي يُنفِّذ ذلك، يُغلِّف الموزع أولًا كُلًا من رقم مُعرِّف الهوية playerID ومحتويات الرسالة message بكائنٍ من النوع ForwardedMessage (مُعرَّفٌ بالملف ForwardedMessage.java بحزمة netgame.common). قد تكون المعالجة الافتراضية مناسبة تمامًا لاحتياجات تطبيقٍ بسيطٍ، مثل برنامج غرفة المحادثة الذي سنناقشه لاحقًا، ولكن سنضطّر إلى تعريف صنفٍ فرعي subclass من الصنف Hub بالنسبة لغالبية التطبيقات، وإعادة تعريف التابع messageReceived() به لإجراء معالجةٍ أكثر تعقيدًا. في الواقع، يحتوي الصنف Hub على عدة توابعٍ أخرى، والتي قد تحتاج إلى إعادة تعريفها أيضًا. نستعرِض بعضًا منها فيما يلي: protected void playerConnected(int playerID): يُستدعَى ذلك التابع في كل مرةٍ يتصل خلالها لاعبٌ بالموزع؛ حيث يُمثِّل المعامل playerID رقم مُعرِّف الهوية الخاص باللاعب الجديد. لا يفعَل هذا التابع شيئًا بالصنف Hub. (لقد أرسَلَ الموزع رسالةً من النوع StatusMessage بالفعل إلى كل عميل ليخبره بوجود لاعبٍ جديد؛ أما الغرض من التابع playerConnected() فهو إجراء أي عملياتٍ أخرى إضافية قد ترغب الأصناف الفرعية المُشتقَة من الصنف Hub بتنفيذها). من الممكن الوصول إلى قائمة مُعرِّفات الهوية لجميع اللاعبين المتصلين حاليًا باستدعاء التابع getPlayerList. protected void playerDisconnected(int playerID): يُستدعَى ذلك التابع في كل مرةٍ يُغلِق خلالها لاعبٌ معينٌ اتصاله مع الموزع (بعد أن يُرسِل الموزع رسالةً من النوع StatusMessage إلى العملاء). يُمثِّل المعامل رقم مُعرِّف الهوية الخاص باللاعب الذي أغلق الاتصال. لا يفعَل هذا التابع شيئًا بالصنف Hub. يُعرِّف الصنف Hub بعض التوابع العامة public المفيدة منها: sendToAll(message): يُرسِل ذلك التابع الرسالة message المُمرَّرة لكل عميلٍ متصل بالموزع حاليًا. لا بُدّ أن تكون الرسالة كائنًا غير فارغ يُنفِّذ الواجهة Serializable. sendToOne(recipientID,message): يُرسِل ذلك التابع الرسالة message المُمرَّرة إلى مُستخدِمٍ واحدٍ فقط؛ حيث يُمثِّل المعامل الأول recipientID رقم مُعرِّف الهوية الخاص بالعميل الذي ينبغي أن يَستقبِل تلك الرسالة، ويُعيد التابع قيمة من النوع boolean تُساوِي false في حالة عدم وجود عميلٍ مقابلٍ لقيمة recipientID. shutDownServerSocket(): يُغلِق هذا التابع مقبس الخادم الخاص بالموزع؛ لكي لا يتمكَّن أي عميلٍ آخر من الاتصال. يُمكِننا اِستخدَامه بالألعاب الثنائية (لاعبين فقط) بعد أن يتصِل اللاعب الثاني مثلًا. setAutoreset(autoreset): يضبُط قيمة خاصية autoreset؛ فإذا كانت قيمتها تساوي true، يُعَاد ضبط المجاري -من النوع ObjectOutputStreams- المُستخدَمة لنقل الرسائل إلى العملاء أوتوماتيكيًا قبل نقل تلك الرسائل. القيمة الافتراضية لتلك الخاصية هي false. يكون لإعادة ضبط مجرًى من النوع ObjectOutputStream معنًى إذا كان هناك كائنٌ قد كُتِبَ بالمجرى بالفعل، ثم عُدّل، ثم كُتِبَ إلى المجرى مرةً أخرى. إذا لم نُعِد ضبط المجرى قبل كتابة الكائن المُعدَّل به، تُرسَل القيمة القديمة غير المُعدَّلة إلى المجرى بدلًا من القيمة الجديدة. في تلك الحالة، يُفضَّل اِستخدَام كائناتٍ ثابتة غير قابلة للتعديل immutable مع الاتصال، ولا تكون عندها إعادة الضبط ضرورية. ينبغي أن تقرأ الشيفرة المصدرية للملف Hub.java للإطلاع على طريقة تنفيذ كل ما سبق وللمزيد من المعلومات في العموم. مع قليلٍ من الجهد والدراسة، ستكون قادرًا على فهم كل شيء موجود ضمن ذلك الملف، ومع ذلك تحتاج فقط إلى فهم الواجهة العامة public والمحمية protected بالصنف Hub والأصناف الأخرى المُعرَّفة بإطار عمل netgame حتى تتمكَّن من كتابة بعض التطبيقات المبنية عليها. لننتقل الآن إلى جانب العميل، ستَجِد تعريف الصنف المُمثِل للعميل بالملف Client.java بحزمة netgame.common؛ حيث يحتوي الصنف على باني كائن constructor يَستقبِل كُلًا من اسم أو عنوان بروتوكول الإنترنت، ورقم المنفذ الخاصين بالموزع الذي سيتصل به العميل. يُسبِّب هذا الباني تعطيلًا إلى أن يُنشَأ الاتصال. الصنف Client هو صنفٌ مجرَّد abstract؛ أي لا بُدّ لأي تطبيق netgame أن يُعرِّف صنفًا فرعيًا مُشتقًا من الصنف Client، وأن يُوفِّر تعريفًا للتابع المُجرّد abstract التالي: abstract protected void messageReceived(Object message); يُستدعَى التابع السابق بكل مرةٍ يَستقبِل خلالها العميل رسالةً من الموزع. قد يُعيد الصنف الفرعي المُشتق تعريف override التوابع المحمية الآتية: playerConnected. playerDisconnected. serverShutdown. connectionClosedByError. انظر الشيفرة المصدرية لمزيدٍ من المعلومات. علاوةً على ذلك، يحتوي الصنف Client على متغير نسخة instance variable، اسمه connectedPlayerIDs من نوع المصفوفة int[]. تُمثِّل تلك المصفوفة قائمة أرقام مُعرِّفات الهوية الخاصة بجميع العملاء المتصلين حاليًا بالموزع. يُعرِّف الصنف Client مجموعةً من التوابع العامة، وسنستعرِض بعضًا من أهمها فيما يلي: send(message): ينقل هذا التابع رسالةً إلى الموزع. قد يكون المعامل message أي كائنٍ غير فارغ بشرط أن يُنفِّذ الواجهة Serializable. getID(): يسترجع هذا التابع رقم مُعرِّف الهوية الذي أسنده الموزع إلى العميل. disconnect(): يغلِق اتصال العميل مع الموزع. لاحظ أن العميل لن يتمكَّن من إرسال أي رسائلٍ أخرى بعد غلق الاتصال. إذا حاول العميل فعل ذلك، سيُبلِّغ التابع send() عن استثناءٍ من النوع IllegalStateException. صُممّ الصنفان Hub و Client عمومًا ليُوفِّرا إطار عملٍ عام يُمكِن استخدامه أساسًا لكثيرٍ من الألعاب الشبكية المختلفة والبرامج المُوزَّعة أيضًا. جميع التفاصيل منخفضة المستوى المتعلقة بالاتصالات الشبكية والخيوط المتعدّدة مخفيةٌ ضمن الأقسام الخاصة private المُعرَّفة بتلك الأصناف، وتتمكَّن بذلك التطبيقات المبنية على تلك الأصناف من العمل وفقًا لمصطلحاتٍ عالية المستوى مثل اللاعبين والرسائل. صُممت تلك الأصناف على عدة مراحل بناءً على الخبرة المُكتسبَة من مجموعةٍ من التطبيقات الحقيقية، ولهذا يُفضَّل إلقاء نظرةٍ على الشيفرة المصدرية لرؤية طريقة استخدام الصنفين Hub و Client للخيوط والمقابس ومجاري الدْخَل والخرج. سنمرّ سريعًا بالجزء المُتبقِي من هذا المقال على ثلاثة تطبيقات مبنيةٍ على إطار عمل netgame وبدون مناقشة التفاصيل. يُمكِنك الإطلاع على الشيفرة المصدرية للتطبيقات الثلاثة كاملةً بحزمة netgame. تطبيق غرفة محادثة بسيط تطبيقنا الشبكي الأول هو "غرفة محادثة" تُمكِّن المُستخدمين من الاتصال بخادمٍ معين، ومن ثم إرسال الرسائل؛ حيث يستطيع المُستخدمون المتواجدون بنفس الغرفة رؤية تلك الرسائل. يتشابه هذا التطبيق مع البرنامج GUIChat من قسم برنامج محادثة عبر الشبكة غير متزامن من مقال الخيوط Threads والشبكات في جافا باستثناء أنه من الممكن لأي عددٍ من المُستخدِمين التواجد بالمحادثة. لا يُعدّ هذا التطبيق لعبة، ولكنه يظهر الوظائف الأساسية التي يُوفِّرها إطار عمل netgame. يتكوَّن تطبيق غرفة المحادثة من برنامجين: الأول هو ChatRoomServer.java، وهو في الواقع برنامجٌ بسيطٌ للغاية، فكل ما يفعله هو إنشاء موزع Hub يَستمِع إلى طلبات الاتصال القادمة من عملاء netgame: public static void main(String[] args) { try { new Hub(PORT); } catch (IOException e) { System.out.println("Can't create listening socket. Shutting down."); } } يُعرَّف رقم المنفذ PORT على أنه ثابتٌ بالبرنامج، ويمكن أن يكون أي رقمٍ عشوائي شرط أن يَستخدِم الخادم والعملاء نفس الرقم. يَستخدِم البرنامج ChatRoom الصنف Hub نفسه لا صنفًا فرعيًا منه. أما البرنامج الثاني من تطبيق غرفة المحادثة فهو البرنامج ChatRoomWindow.java، الذي ينبغي أن يُشغِّله المُستخدمون الذين يريدون المشاركة بغرفة المحادثة. يجب أن يعرِف المُستخدِم اسم أو عنوان بروتوكول الانترنت الخاص بالحاسوب الذي يَعمَل عليه الموزع. (لأغراض اختبار البرنامج، يُمكِنك تشغيل برنامج العميل بنفس الحاسوب الذي يَعمَل عليه الموزع باستخدام localhost اسمًا لحاسوب الموزع). يَعرِض ChatRoomWindow عند تشغيله صندوق نافذة ليطلب من المُستخدِم إدخال تلك المعلومات، ثم يَفتَح نافذةً تُمثِّل واجهة المُستخدِم لغرفة المحادثة؛ حيث تحتوي تلك النافذة على مساحةٍ نصيةٍ كبيرة لعرض الرسائل التي يُرسِلها المُستخدمون إلى الغرفة؛ كما تحتوي على صندوق إدخال نصي حيث يَستطيع المُستخدِم إدخال الرسائل. عندما يُدْخِل المُستخدِم رسالة، تظهر الرسالة بالمساحة النصية الموجودة بنافذة كل مُستخدِم مُتصِّلٍ بالموزع، ولذلك يرى المستخدمين جميع الرسائل المُرسَلة بواسطة أي مُستخدِم. لنفحص الآن طريقة برمجة ذلك. لابُدّ أن تُعرِّف تطبيقات netgame صنفًا فرعيًا مشتقًا من الصنف المجرّد Client. بالنسبة لتطبيق غرفة المحادثة، عرَّفنا العملاء بواسطة الصنف المتداخل ChatClient داخل البرنامج ChatRoomWindow. يُعرِّف البرنامج متغير النسخة connection من النوع ChatClient، والذي يُمثِّل اتصال البرنامج مع الموزع. عندما يُدْخِل المُستخدِم رسالة، تُرسَل الرسالة إلى الموزع باستدعاء التابع التالي: connection.send(message); عندما يَستقبِل الموزع رسالة، فإنه يُغلِّفها ضمن كائنٍ من النوع ForwardedMessage مع إضافة رقم مُعرِّف الهوية الخاص بالعميل المُرسِل للرسالة. بعد ذلك، يُرسِل الموزع نسخةً من هذا الكائن إلى كل عميلٍ متصلٍ بالموزع، بما في ذلك العميل الذي أرسل الرسالة. من الجهة الأخرى، عندما يَستقبِل العميل رسالةً من الموزع، يُستدعَى التابع messageReceived() المُعرَّف بالكائن المنتمي إلى الصنف ChatClient؛ ويُعيد الصنف ChatClient تعريف ذلك التابع لكي يجعله يضيف الرسالة إلى المساحة النصية ببرنامج ChatClientWindow. اختصارًا لما سبق، تُرسَل أي رسالةٍ يُدْخِلها أي مُستخدِم إلى الموزع بينما يُرسِل الموزع نُسخًا من أي رسالةٍ يَستقبِلها إلى كل عميل، ولذلك يتسلَّم جميع العملاء نفس مجرى الرسائل من الموزع. بالإضافة إلى ما سبق، يُنبَّه كل عميلٍ عندما يَتصِل لاعبٌ جديدٌ بالموزع أو عندما يُغلِق لاعبٌ معينٌ اتصاله مع الموزع، وكذلك عندما يَفقِد هو نفسه اتصاله مع الموزع. يعيد البرنامج ChatClient تعريف التوابع التي تُستدعَى عند وقوع تلك الأحداث ليتمكَّن من إضافة رسائلٍ مناسبة إلى المساحة النصية. نستعرِض فيما يلي تعريف صنف العميل الخاص بتطبيق غرفة المحادثة: // 1 private class ChatClient extends Client { // 2 ChatClient(String host) throws IOException { super(host, PORT); } //3 protected void messageReceived(Object message) { if (message instanceof ForwardedMessage) { // لا يوجد أنواع رسائلٍ أخرى متوقعة ForwardedMessage bm = (ForwardedMessage)message; addToTranscript("#" + bm.senderID + " SAYS: " + bm.message); } } // 4 protected void connectionClosedByError(String message) { addToTranscript( "Sorry, communication has shut down due to an error:\n " + message ); Platform.runLater( () -> { sendButton.setDisable(true); messageInput.setEditable(false); messageInput.setDisable(true); messageInput.setText(""); }); connected = false; connection = null; } // 5 protected void playerConnected(int newPlayerID) { addToTranscript( "Someone new has joined the chat room, with ID number " + newPlayerID ); } // 6 protected void playerDisconnected(int departingPlayerID) { addToTranscript( "The person with ID number " + departingPlayerID + " has left the chat room"); } } // end nested class ChatClient حيث أن: [1] يتصِل برنامج ChatClient مع الموزع ويُستخدَم لإرسال الرسائل واستقبالها من وإلى الموزع. تكون الرسائل المُستقبَلة من الموزع من النوع ForwardedMessage وتحتوي على رقم مُعرِّف الهوية الخاص بالمُرسِل وعلى السلسلة النصية التي أرسلها المُستخدِم. [2] يُنشِئ اتصالًا مع خادم غرفة المحادثة بالحاسوب المُخصَّص. [3] يُنفَّذ هذا التابع عند استقبال رسالةٍ من الخادم. ينبغي أن تكون الرسالة من النوع ForwardedMessage، وتُمثِّل شيئًا أرسله أحد المُستخدِمين المتواجدين بغرفة المحادثة. تُضَاف الرسالة ببساطة إلى الشاشة مع رقم مُعرِّف الهوية الخاص بالمُرسِل. [4] يُستدعَى هذا التابع عندما يُغلَق الاتصال مع عميلٍ ما نتيجةً لحدوث خطأ ما (يحدث ذلك عند غلق الخادم). [5] يُظهِر رسالةً على الشاشة عندما ينضم شخصٌ ما إلى غرفة المحادثة. [6] يُظهر رسالةً على الشاشة عندما يغادر شخصٌ ما غرفة المحادثة. يُمكِنك الإطلاع على ملفات الشيفرة المصدرية الخاصة بتطبيق غرفة المحادثة من حزمة netgame.chat. لعبة إكس-أو عبر الشبكة تطبيقنا الثاني سيكون لعبةً بسيطةً للغاية: لعبة إكس-أو الشهيرة؛ حيث يضع لاعبان بتلك اللعبة علاماتٍ بلوحةٍ مكوَّنة من 3 صفوف و3 أعمدة. يلعب أحدهما الرمز X بينما يلعب الآخر O، ويكون الهدف هو الحصول على 3 رموز X أو 3 رموز O بصفٍ واحد. تتكوَّن حالة لعبة إكس-أو بأي لحظة من مجموعةٍ من المعلومات، مثل مكونات اللوحة الحالية؛ واللاعب الذي حان دوره؛ وفي حال انتهت اللعبة، فمن الفائز ومن الخاسر. إذا لم نكن نُطوّر برنامجًا شبكيًا، كان بإمكاننا اِستخدَام متغيرات نسخة لتمثيل حالة اللعبة بحيث يستعين بها البرنامج لتحديد طريقة رسم اللوحة وطريقة الاستجابة لأفعال المُستخدِم مثل نقرات الفأرة، ولكن نظرًا لأننا نُطوِّر برنامجًا شبكيًا من اللعبة، سنَستخدِم ثلاثة كائنات؛ بحيث ينتمي اثنان منهما إلى صنف العميل الذي يُوفِّر واجهة المُستخدِمين للعبة بالإضافة إلى الكائن المُمثِل للموزع والمسؤول عن إدارة الاتصالات مع العملاء. لا تتواجد تلك الكائنات بنفس الحاسوب، وبالتالي لا يُمكِنهم بالتأكيد اِستخدَام نفس متغيرات الحالة؛ ومع ذلك هناك حالةٌ واحدةٌ مُحدَّدة للعبة بأي لحظة، وينبغي أن يكون اللاعبان على درايةٍ بتلك الحالة. يُمكِننا حل تلك المشكلة بتخزين الحالة "الرسمية" للعبة بالموزع، وإرسال نسخةٍ منها إلى كل لاعب كلما تغيرت. لا يستطيع اللاعبان تعديل الحالة مباشرةً، فعندما يُنفِّذ لاعبٌ فِعلًا معينًا مثل وضع قطعةٍ على اللوحة، يُرسَل ذلك الفعِل إلى الموزع بهيئة رسالة. بعد ذلك، يُعدِّل الموزع حالة اللعبة لكي تَعكس نتيجة ذلك الفعل، ثم يُرسِل الحالة الجديدة لكلا اللاعبين، وعندها تُحدَّث نافذتهما لتعكس الحالة الجديدة. يُمكِننا بتلك الطريقة ضمَان ظهور اللعبة بنفس الحالة دائمًا عند كلا اللاعبين. بدلًا من إرسال نسخةٍ كاملةٍ من الحالة بكل مرةٍ تُعدَّل فيها، قد نُرسِل ذلك التعديل فقط. ولكن، سيتطلَّب ذلك استخدام طريقةٍ ما لتشفير التعديلات وتحويلها إلى رسائلٍ يُمكِن إرسالها عبر الشبكة. نظرًا لأن الحالة بهذا البرنامج بسيطةٌ للغاية، فإنه من الأسهل إرسالها كاملة. ستَجِد البرنامج إكس-أو مُعرَّفًا بعدة أصناف بحزمة netgame.tictactoe؛ حيث يُمثِّل الصنف TicTacToeGameState حالة اللعبة، ويتضمَّن التابع التالي: public void applyMessage(int senderID, Object message) يُعدِّل ذلك التابع حالة اللعبة لتَعكِس تأثير الرسالة المُرسَلة من إحدى اللاعبين؛ حيث تُمثِّل تلك الرسالة فعِلًا معينًا أقدم عليه اللاعب، مثل النقر على اللوحة. لا يَعرِف الصنف Hub أي شيءٍ عن تطبيق إكس-أو، ولأن الموزع بهذا التطبيق ينبغي أن يحتفظ بحالة اللعبة، كان من الضروري تعريف صنفٍ فرعي مشتقٍ من الصنف Hub. عرَّفنا الصنف TicTacToeGameHub، وهو صنفٌ بسيطٌ للغاية يُعيد تعريف التابع messageReceived() ليجعله يَستجيب لرسائل اللاعبين بتطبيقها على حالة اللعبة ثم إرسال نسخةٍ من الحالة الجديدة لكلا اللاعبين. يُعيد ذلك الصنف تعريف التابعين playerConnected() و playerDisconnected() أيضًا لكي يُنفِّذا الفعِل المناسب؛ لأن المباراة تُلعَب فقط عندما يكون هناك لاعبين متصلين. انظر الشيفرة المصدرية كاملة: package netgame.tictactoe; import java.io.IOException; import netgame.common.Hub; // 1 public class TicTacToeGameHub extends Hub { private TicTacToeGameState state; // يسجّل حالة اللعبة // 2 public TicTacToeGameHub(int port) throws IOException { super(port); state = new TicTacToeGameState(); setAutoreset(true); } // 3 protected void messageReceived(int playerID, Object message) { state.applyMessage(playerID, message); sendToAll(state); } // 4 protected void playerConnected(int playerID) { if (getPlayerList().length == 2) { shutdownServerSocket(); state.startFirstGame(); sendToAll(state); } } // 5 protected void playerDisconnected(int playerID) { state.playerDisconnected = true; sendToAll(state); } } حيث أن: [1]: يُمثِّل الموزع hub باللعبة إكس-أو. هناك موزعٌ واحدٌ فقط باللعبة، ويتصِل كلا اللاعبين بنفس الموزع، الذي تتواجد بد المعلومات الرسمية عن حالة اللعبة؛ وعند حدوث تغييرات بتلك الحالة، يُرسِل الموزع الحالة الجديدة لكلا اللاعبين ليتأكّد من ظهور نفس الحالة لكليهما. [2]: يُنشِئ موزعًا يَستمِع إلى رقم المنفذ المُخصَّص. يَستدعِي التابع setAutoreset(true) لكي يتأكّد من إعادة ضبط مجرى الخرج الخاص بكل عميل تلقائيًا قبل إرسال أي رسالة. يُعدّ ذلك ضروريًا لأن نفس الكائن المُمثِل للحالة يُرسَل مرارً وتكرارً مع بعض التعديلات. يُمثِّل port رقم المنفذ الذي سيستمِع إليه الموزع. قد يُبلِّغ التابع عن استثناءِ من النوع IOException في حالة عدم التمكُّن من فتح اتصالٍ برقم المنفذ المُخصَّص. [3]: يُنفَّذ هذا التابع عند وصول رسالةٍ من عميل؛ حيث تُطبَّق الرسالة عندئذٍ على حالة اللعبة باستدعاء التابع state.applyMessage(). تُنقَل الحالة بعد ذلك إلى جميع اللاعبين المتصلين إذا كانت قد تغيرت. [4]: يُستدعَى هذا التابع عند اتصال لاعب؛ وإذا كان ذلك اللاعب هو اللاعب الثاني، يُغلَق مقبس الاستماع الخاص بالخادم (يُسمح فقط وجود لاعبين). بعد ذلك، تبدأ اللعبة الأولى وتُنقَل الحالة الجديدة إلى كلا اللاعبين. [5]: يُستدعَى هذا التابع عندما يغلق لاعبٌ اتصاله. يُنهِي ذلك اللعبة ويتسبَّب بإغلاقها عند اللاعب الآخر أيضًا. يحدث ذلك بضبط قيمة state.playerDisconnected إلى true ثم إرسال الحالة الجديدة إلى اللاعب الآخر إذا كان موجودًا لتبليغه بأن اللعبة قد انتهت. يُمثَّل الصنف TicTacToeWindow واجهة اللاعب إلى اللعبة. كما حدث في تطبيق غرفة المحادثة، يُعرِّف ذلك الصنف صنفًا فرعيًا متداخلًا مُشتقًا من الصنف Client لتمثيل اتصال العميل مع الموزع. عندما تتغيّر حالة اللعبة، تُرسَل رسالةٌ إلى كل عميل، ويُستدعَى التابع messageReceived() الخاص بالعميل ليُعالِج تلك الرسالة. يستدعي ذلك التابع بدوره التابع newState() المُعرَّف بالصنف TicTacToeWindow لتحديث النافذة. اِستخدَمنا Platform.runLater() لكي نَستدعيه بخيط تطبيق JavaFX : protected void messageReceived(Object message) { if (message instanceof TicTacToeGameState) { Platform.runLater( () -> newState( (TicTacToeGameState)message ) ); } } والآن، لنُشغِّل تطبيق إكس-أو، ينبغي أن يُشغِّل اللاعبان البرنامج Main.java الموجود بحزمة netgame.tictactoe، حيث يعرِض البرنامج نافذةً للمُستخدِم تُمكِّنه من اختيار بدء لعبة جديدة أو الانضمام إلى لعبةٍ موجودة. إذا بدأ المُستخدِم لعبةً جديدة، يُنشِئ البرنامج موزعًا من النوع TicTacToeHub لإدارة اللعبة، ويَعرِض نافذةً جديدةً من النوع TicTacToeWindow، وتكون متصلةً بالموزع على الفور. تبدأ اللعبة بمجرد اتصال لاعبٍ آخر بذلك الموزع. في المقابل، إذا اختار المُستخدِم الانضمام إلى لعبةٍ موجودة، لا يُنشِئ البرنامج موزعًا جديدًا، وإنما يُنشِئ نافذةً من النوع TicTacToeWindow، وتحاول تلك النافذة الاتصال بالموزع الذي أنشأه اللاعب الأول. ولذلك، لا بُدّ أن يَعرِف اللاعب الثاني اسم الحاسوب الذي يَعمَل عليه برنامج اللاعب الأول. إذا أردت اختبار البرنامج، يُمكِنك تشغيل كل شيء بنفس الحاسوب واستخدام "localhost" اسمًا للحاسوب. يُعدّ هذا البرنامج هو أول برنامج يَستخدِم نافذتين مختلفتين نراه. لاحِظ أن الصنف TicTacToeWindow مُعرَّف على أنه صنفٌ فرعي من الصنف Stage الذي يُستخدَم لتمثيل النوافذ بمكتبة JavaFX. تبدأ برامج JavaFX "بمرحلة رئيسية primary stage" يُنشئِها النظام ويُمرِّرها معاملًا إلى التابع start()، ولكن يستطيع التطبيق إلى جانب ذلك إنشاء أي نوافذ إضافية. لعبة بوكر Poker عبر الشبكة ننتقل الآن إلى التطبيق الذي يُعدّ المُلهم لإطار عمل netgame، وهو تطبيق لعبة بوكر. بالتحديد، نفَّذنا نسخةً من النسخة التقليدية "سحب خمسة بطاقات five card draw" من تلك اللعبة وبلاعبين. هذا التطبيق معقدٌ نوعًا ما، ولن نناقشه تفصيليًا هنا، ولكننا سنوضِّح تصميمه العام. يُمكِنك الإطلاع على الشيفرة المصدرية كاملة بحزمة netgame.fivecarddraw. ينبغي أن تكون على درايةٍ بتلك النسخة من اللعبة لتتمكَّن من فهم البرنامج بالكامل. تتشابه لعبة Poker مع لعبة إكس-أو في العموم، حيث يتوفَّر الصنف Main الذي يُشغِّله كلا اللاعبين. يبدأ اللاعب الأول لعبةً جديدة بينما ينضم اللاعب الثاني إلى اللعبة الموجودة. يُمثِّل الصنف PokerGameState حالة اللعبة؛ بينما يدير الصنف الفرعي PokerHub اللعبة. لعبة Poker أكثر تعقيدًا من لعبة إكس-أو، وهو ما ينعكس على حالة اللعبة، فهي معقدةٌ بالموازنة مع حالة لعبة إكس-أو، وبالتالي فإننا لا نرغب بنشر النسخة الجديدة من حالة اللعبة كاملةً إلى اللاعبين في كل مرة نُجرِي فيها تعديلًا صغيرًا على الحالة. علاوة على ذلك، لا معنى لأن يَعرِف اللاعبان حالة اللعبة بالكامل بما في ذلك بطاقات الخصم وبطاقات اللعب التي يَسحَب منها اللاعبان. لن تكون برامج العملاء مضطّرةً لعرض حالة اللعبة بالكامل للاعبين، ولكنهما قد يَستبدلا تلك البرامج ببرامجٍ أخرى ويتمكَّنا من الغش بسهولة. ولذلك، سيكون الموزع من النوع PokerHub فقط على درايةٍ بكامل حالة اللعبة بهذا التطبيق. سيُمثِّل كائنٌ من الصنف PokerGameState حالة اللعبة من وجهة نظر لاعبٍ واحدٍ فقط. عندما تتغير حالة اللعبة، سيُنشِئ الموزع كائنين مختلفين من النوع PokerGameState يُمثِّلان حالة اللعبة من وجهة نظر كل لاعب، ثم سيُرسِل الكائن المناسب لكل لاعب. يُمكِنك الإطلاع على الشيفرة المصدرية لمزيدٍ من التفاصيل. تُعدّ موازنة يد لاعبين لمعرفة أيهما أكبر الجزء الأصعب بلعبة البوكر، وقد عالجناها بهذا التطبيق بالصنف PokerRank. ربما تجده مفيدًا بألعاب بوكر أخرى. ترجمة -بتصرّف- للقسم Section 5: Network Programming Example: A Networked Game Framework من فصل Chapter 12: Threads and Multiprocessing من كتاب Introduction to Programming Using Java. اقرأ أيضًا المقال السابق: الخيوط Threads والشبكات في جافا تواصل تطبيقات جافا عبر الشبكة تطبيق عملي: بناء لعبة ورق في جافا1 نقطة
-
تُعدّ القائمة المترابطة linked list نوعًا خاصًا من بنى البيانات data structure، حيث تتكوَّن من مجموعة كائناتٍ مربوطةٍ مع بعضها بعضًا باستخدام مؤشرات pointers. استخدمنا في المقال السابق قائمةً مترابطةً لتخزين قائمةٍ مرتّبةٍ من السلاسل النصية من النوع String، كما نفَّذنا عمليات الإضافة والحذف والبحث على تلك القائمة. ومع ذلك، كان بإمكاننا بسهولة تخزين قائمة السلاسل النصية بمصفوفةٍ أو كائنٍ من النوع ArrayList بدلًا من استخدام قائمةٍ مترابطة، وكنا سنتمكَّن من إجراء نفس العمليات على القائمة. على الرغم من أن تنفيذ تلك العمليات سيكون مختلفًا بالتأكيد، إلا أن الواجهات interfaces المُستخدمة والسلوك المنطقي لتلك العمليات سيكون نفسه. يُشير مصطلح نوع بيانات مجرد abstract data type -أو اختصارًا ADT- إلى مجموعة قيمٍ محتملة ومجموعة من العمليات المسموح بتطبيقها على تلك القيم دون أي تخصيصٍ لطريقة تمثيل تلك القيم أو طريقة تنفيذ تلك العمليات، حيث يُمكِننا مثلًا تعريف قائمةٍ مُرتَّبةٍ من السلاسل النصية باستخدام نوع بياناتٍ مجرد، على أنها أي متتاليةٍ من السلاسل النصية من النوع String شرط أن تكون مُرتَّبةً ترتيبًا تصاعديًا، وبحيث يُمكِننا إجراء عملياتٍ عليها، مثل إضافة سلسلةٍ نصيةٍ جديدة، أو حذف سلسلةٍ نصية، أو البحث عن سلسلةٍ نصيةٍ ضمن القائمة. يُمكِنك بالتأكيد تنفيذ نفس نوع البيانات المُجرَّد ADT باستخدام طرقٍ مختلفة، فمثلًا قد تُنفِّذ قائمة السلاسل النصية المُرتَّبة باستخدام قائمة مترابطة أو مصفوفة array. في الواقع، تعتمد البرامج المُستخدِمة لهذا النوع من البيانات على التعريف المجرد للنوع، وبالتالي يُمكِنها استخدام أيٍ من تنفيذاتها implementation المُتوفِّرة، كما يُمكِنها التبديل بينها بسهولة؛ وهذا يعني أنه من الممكن تبديل التنفيذ الخاص بنوع بياناتٍ مجردٍ معين دون التأثير على البرنامج، ويُسهِّل ذلك من عملية تنقيح أخطاء debug البرامج. تُعدّ أنواع البيانات المجردة ADTs عمومًا واحدةً من الأدوات الهامة بهندسة البرمجيات software engineering. سنناقش في هذا المقال نوعين من أنواع البيانات المجردة، هما المكدس stack والرتل queue، حيث تُستخدَم عادةً القوائم المترابطة لتنفيذ كليهما، ولكنها بالتأكيد ليست التنفيذ الأوحد. يُمكِنك النظر للجزء المتبقي من هذا القسم على أساس كونه مناقشةً عن الأكداس والأرتال من ناحية؛ وعلى أساس كونه دراسة حالة case study لأنواع البيانات المجردة من الناحية الأخرى. 9.3.1: المكدسات Stacks يتكوَّن المكدس stack من متتاليةٍ من العناصر التي يُمكِننا النظر إليها على أنها مجموعةٌ من العناصر المتراكمة فوق بعضها بعضًا، حيث يُمكننا الوصول مباشرةً وفي أي لحظة إلى العنصر الموجود بالأعلى، كما يُمكِننا حذفه من المكدس باستخدام عمليةٍ تُعرَف باسم السحب pop، ويُمكِننا في المقابل حذف عنصرٍ آخر أسفل المكدس فقط بعد سحب pop جميع العناصر الواقعة أعلى ذلك العنصر من المكدس. من الممكن أيضًا إضافة عنصرٍ جديدٍ أعلى المكدس باستخدام عمليةٍ تُعرَف باسم الدفع push. يُمكِن لعناصر المكدس أن تكون من أي نوع، فإذا كانت العناصر قيمٌ من النوع int مثلًا، فيمكن تنفيذ عمليتي السحب pop والدفع push كما في توابع النسخ instance methods التالية: void push(int newItem): لإضافة عنصرٍ جديد newItem أعلى المكدس. int pop(): لحذف العنصر الموجود أعلى المكدس وإعادته. لاحِظ أنه من الخطأ أن تحاول سحب pop عنصرٍ من مكدسٍ فارغ، ولذلك عليك أن تختبر أولًَا فيما إذا كان المكدس فارغًا أم لا، وسنحتاج بالتالي إلى عمليةٍ أخرى لإجراء ذلك الاختبار، والتي يُمكِننا تنفيذها باستخدام تابع النسخة التالي: boolean isEmpty(): يعيد القيمة true إذا كان المكدس فارغًا. نكون الآن قد عرَّفنا مكدسًا من الأعداد الصحيحة على أنه نوع بيانات مجرد abstract data type - ADT، ويُمكِننا تنفيذه بعدة طرقٍ شرط أن يَكُون سلوكه العام مكافئًا للتصور المُجرَّد للمكدس. لدى تنفيذ المكدس باستخدام القوائم المترابطة linked list، ستكون عقدة رأس القائمة head أعلى المكدس. تُعدّ إضافة العقد إلى مقدمة قائمةٍ مترابطة أو حذفها من المقدمة أسهل بكثير بالموازنة مع الإضافة أو الحذف من منتصف قائمة مترابطة. يَستخدِم الصنف بالشيفرة التالية قائمةً مترابطةً لتنفيذ النوع المجرد ADT مكدس أعداد صحيحة. لاحِظ تعريف الصنف المُوضّح بالأسفل لصنفٍ متداخلٍ nested ساكن static لتمثيل عقد القائمة المترابطة، وهو ما يُعدّ جزءًا من التنفيذ الخاص private implementation للنوع المجرد. public class StackOfInts { private static class Node { int item; Node next; } // مؤشر إلى العقدة الموجودة أعلى المكدس // إذا كان top يُساوِي القيمة الفارغة، فإن المكدس يكون فارغًا private Node top; // أضف N إلى أعلى المكدس public void push( int N ) { Node newTop; // العقدة التي ستحمل العنصر الجديد newTop = new Node(); newTop.item = N; // خزِّن N بالعقدة الجديدة newTop.next = top; // تشير العقدة الجديدة إلى العقدة التي كانت تحتل موضع أعلى المكدس مسبقًا top = newTop; // تُصبح العقدة الجديدة أعلى المكدس } // 1 public int pop() { if ( top == null ) throw new IllegalStateException("Can't pop from an empty stack."); int topItem = top.item; // العنصر المسحوب من المكدس top = top.next; // العنصر الموجود أعلى المكدس حاليًا return topItem; } // 2 public boolean isEmpty() { return (top == null); } } // end class StackOfInts [1] احذف العنصر الموجود في أعلى المكدس، ثم أعده. إذا كان المكدس فارغًا، بلِّغ عن حدوث اعتراض من النوع IllegalStateException. [2] أعد القيمة المنطقية true إذا كان المكدس فارغًا؛ أما إذا كان يحتوي على عنصرٍ واحد أو أكثر، أعد القيمة المنطقية false. عليك التأكُّد من فهم طريقة عمل عمليتي السحب pop والدفع push بالقائمة المترابطة، وقد يُساعدك رسم بعض الصور على ذلك. تُمثّل القائمة المترابطة بالأعلى جزءًا من التنفيذ الخاص للصنف StackOfInts، أي لا يحتاج البرنامج الذي يَستخدِم ذلك الصنف إلى معرفة حقيقة كونه يستخدم قائمةً مترابطة. يُمكننا الآن أيضًا تنفيذ المكدس باستخدام مصفوفةٍ بدلًا من قائمةٍ مترابطة، حيث سنحتاج إلى عدّاد counter لمعرفة عدد الخانات المُستخدَمة فعليًا بالمصفوفة، لأن عدد عناصر المكدس يختلف من وقتٍ لآخر. بفرض أن اسم العداد هو top، فستكون عناصر المكدس مُخزَّنةً بمواضع المصفوفة 0، و1، … حتى top-1، ويتواجد العنصر بالموضِع 0 أسفل المكدس بينما يتواجد العنصر بالموضِع top-1 أعلى المكدس. من السهل دفع push عنصرٍ إلى المكدس على النحو التالي: ضع العنصر بالموضِع top وزِد قيمة top بمقدار الواحد. وإذا لم نكن نريد وضع حدٍ أقصى لعدد العناصر التي يستطيع المكدس أن يحملها، يُمكِننا استخدام مصفوفةٍ ديناميكية dynamic array، التي كنا قد تعرَّضنا لها بالقسم الفرعي 7.2.4. يعرض التصور النموذجي للمصفوفة المكدس مقلوبًا رأسًا على عقب؛ أي سيظهر أسفل المكدس بأعلى المصفوفة، وهذا في الواقع غير مهم؛ فالمصفوفة هي مجردُ تنفيذٍ للفكرة المجرَّدة للمكدس، وطالما تجري العمليات على المكدس كما ينبغي لها، فليس هناك أي داعٍ للقلق. تعرض الشيفرة التالية تنفيذًا آخرًا للصنف StackOfInts باستخدام مصفوفةٍ ديناميكية. import java.util.Arrays; // Arrays.copyOf() من أجل تابع public class StackOfInts { // (إصدار بديل، استعمال مصفوفة) private int[] items = new int[10]; // مصفوفة لحمل عناصر المكدس private int top = 0; // عدد العناصر الموجودة بالمكدس حاليًا // أضف N إلى أعلى المكدس public void push( int N ) { if (top == items.length) { // إذا كانت المصفوفة ممتلئة، أنشِئ واحدة جديدة أكبر حجمًا // وانسخ العناصر الموجودة بالمكدس حاليًا إليها items = Arrays.copyOf( items, 2*items.length ); } items[top] = N; // ضع N بالموضِع المتاح التالي top++; // أزد عدد العناصر بمقدار الواحد } // 1 public int pop() { if ( top == 0 ) throw new IllegalStateException("Can't pop from an empty stack."); int topItem = items[top - 1]; // العنصر الموجود أعلى المكدس top--; // انقص عدد العناصر الموجودة بالمكدس بمقدار الواحد return topItem; } // 2 public boolean isEmpty() { return (top == 0); } } // end class StackOfInts [1] احذف العنصر الموجود أعلى المكدس، ثم أعده مثل قيمةٍ للدالة، وإذا كان المكدس فارغًا، بلِّغ عن حدوث اعتراض من النوع IllegalStateException. [2] أعد القيمة المنطقية true إذا كان المكدس فارغًا؛ أما إذا كان يحتوي على عنصرٍ واحدٍ أو أكثر، أعد القيمة المنطقية false. نُعيد التأكيد على أن تنفيذ المكدس على هيئة مصفوفة شأنٌ خاصٌ بالصنف؛ وهذا يعني أنه من الممكن استبدال نسختي الصنف StackOfInts المُعرفتين بالأعلى بعضهما ببعض دون أي مشكلة، لأن واجهتهما العامة public interface متطابقة. يُمكِننا الآن تحليل زمن تشغيل run time العمليات على المكدس (ألقِ نظرةً على القسم 8.5)، حيث يمكننا قياس حجم المشكلة من خلال عدد العناصر الموجودة بالمكدس؛ فبالنسبة للتنفيذ الذي يَستخدِم قائمةً مترابطة، فإن زمن تشغيل الحالة الأسوأ worst case لعمليتي الدفع push والسحب pop يُساوِي Θ(1)، وهذا يعني أن زمن التشغيل أقل من قيمةٍ ثابتةٍ معينة لا تعتمد على عدد عناصر المكدس. يُمكِنك رؤية ذلك بوضوحٍ من خلال الشيفرة ذاتها؛ فتنفيذ العمليتين مُكوَّنٌ من عدة تعليمات إسناد assignment قليلة وبسيطة، وعدد عناصر المكدس ليس له أي دور. أما بالنسبة للتنفيذ المُعتمِد على مصفوفة، فهناك حالةٌ خاصة تَحدُث أحيانًا أثناء عملية الدفع push، تحديدًا عندما تكون المصفوفة ممتلئة، حيث يُنشِئ الصنف في تلك الحالة مصفوفةً جديدةً ويَنسَخ جميع عناصر المكدس إلى تلك المصفوفة الجديدة. يستغرق ذلك وقتًا يتناسب طرديًا مع عدد عناصر المكدس. لذلك، على الرغم من أن زمن تشغيل عملية الدفع push يُساوِي عادةً Θ(1)، فإنه في الحالة الأسوأ worst case يُساوِي Θ(n)، حيث تمثّل n عدد العناصر الموجودة بالمكدس. نظرًا لندرة حدوث الحالة الأسوأ، يُمكِننا أن نقول أن زمن تشغيل الحالة المتوسطة average case هو Θ(1). 9.3.2: الأرتال Queues على نحوٍ مشابه من المكدس، يتكوَّن الرتل queue من متتاليةٍ من العناصر كما يوجد عددٌ من القيود بخصوص الكيفية التي نُضيف بها العناصر إلى الرتل أو نحذفها منه؛ وبخلاف المكدس، فإن الرتل لديه طرفين هما الطرف الأمامي والخلفي، حيث تُضاف العناصر دائمًا إلى الطرف الخلفي من الرتل، بينما تُحذَف من طرفه الأمامي. سنُطلِق على عملية إضافة العناصر إلى الرتل اسم إدراج enqueue، بينما سنُطلِق على عملية حذف العناصر منه اسم "سحب dequeue*، مع الملاحظة بأن هذه التسميات ليست قياسيةً بخلاف عمليتي *الدفع push* والسحب pop. عند إضافة عنصر إلى الطرف الخلفي من الرتل، فإنه يبقى بالرتل إلى أن تُحذَف جميع العناصر التي تسبقه، ويُعدّ ذلك أمرًا بديهيًا؛ فالرتل queue بالنهاية يُشبه خطًا أو صفًا من العملاء المنتظرين لخدمةٍ معينة، حيث تُقدَم الخدمة للعملاء بنفس ترتيب وصولهم إلى الصف. يُمكِن لعناصر الرتل أن تكون من أي نوع، فمثلًا قد يكون لدينا رتلًا من النوع العددي الصحيح int، كما يُمكِن تنفيذ عمليتي الإدراج enqueue والسحب dequeue كأنها توابع نسخ instance methods داخل تعريف الصنف QueueOfInts. علاوةً على ذلك، سنحتاج أيضًا إلى تابع نسخة آخر لاختبار فيما إذا كان الرتل queue فارغًا أم لا: void enqueue(int N): يضيف عنصرًا جديدًا N إلى الطرف الخلفي من الرتل. int dequeue(): يَحذِف عنصرًا من الطرف الأمامي من الرتل ويعيده. boolean isEmpty(): يُعيد القيمة true إذا كان الرتل فارغًا. يمكننا تنفيذ الرتل باستخدام قائمةٍ مترابطة أو مصفوفة، ومن الممكن أن يكون استخدام مصفوفةٍ لتنفيذ الرتل أعقد قليلًا من استخدامها لتنفيذ المكدس stack، ولهذا لن نتناوله هنا، وسنكتفي بالتنفيذ المبني على قائمةٍ مترابطة. يُمثِل العنصر الأول بقائمةٍ مترابطة الطرف الأمامي من الرتل، بينما يُمثِل العنصر الأخير بالقائمة الطرف الخلفي من الرتل، وتشبه عملية سحب dequeue عنصرٍ من الطرف الأمامي للرتل عملية سحب pop عنصرٍ من المكدس. في المقابل، تتضمَّن عملية إدراج enqueue عنصرٍ جديدٍ إلى الرتل ضبط المؤشر pointer الموجود بآخر عقدةٍ ضمن القائمة؛ لكي يُشير إلى عقدةٍ جديدةٍ تحتوي على العنصر المطلوب إضافته. يُمكِننا إجراء ذلك بتنفيذ الأمر tail.next = newNode;، حيث تمثّل tail مؤشرًا لآخر عقدةٍ ضمن القائمة المترابطة. بفرض أن head مؤشرٌ لأول عقدةٍ ضمن قائمةٍ مترابطة، يُمكننا استخدام الشيفرة التالية للحصول على مؤشرٍ لآخر عقدةٍ ضمن تلك القائمة: Node tail; // سيشير إلى العنصر الأخير ضمن القائمة tail = head; // ابدأ من العقدة الأولى while (tail.next != null) { tail = tail.next; // تحرَك إلى العقدة التالية } // 1 [1] بوصولنا إلى تلك النقطة من البرنامج، فإن tail.next يكون فارغًا مما يَعنِي أن tail يُشير إلى آخر عقدةٍ بالقائمة. ومع ذلك، سيكون من غير المناسب فعل ذلك في كل مرةٍ نرغب فيها بإضافة عنصرٍ جديدٍ إلى الرتل. لزيادة كفاءة الشيفرة بالأعلى، سنُعرِّف متغير نسخة instance variable يحتوي على مؤشرٍ لآخر عقدةٍ ضمن القائمة المترابطة، وهذا سيُعقِّد الصنف QueueOfInts بعض الشيء؛ وبالتالي علينا الانتباه إلى ضرورة تحديث قيمة ذلك المتغير أينما أضفنا عقدةً جديدةً إلى نهاية القائمة. يُمكِننا إذًا إعادة كتابة الصنف QueueOfInts على النحو التالي: public class QueueOfInts { /** * كائنٌ من النوع Node * يحمل أحد العناصر الموجودة ضمن القائمة المترابطة الممثلة للرتل */ private static class Node { int item; Node next; } private Node head = null; // يشير إلى العنصر الأول ضمن القائمة private Node tail = null; // يُشير إلى العنصر الأخير ضمن القائمة // أضف N إلى الطرف الخلفي من الرتل public void enqueue( int N ) { Node newTail = new Node(); // العقدة التي تحمل العنصر الجديد newTail.item = N; if (head == null) { // 1 head = newTail; tail = newTail; } else { // تُصبِح العقدة الجديدة هي ذيل القائمة بينما لا يتأثر رأسها tail.next = newTail; tail = newTail; } } // 2 public int dequeue() { if ( head == null) throw new IllegalStateException("Can't dequeue from an empty queue."); int firstItem = head.item; head = head.next; // أصبح العنصر الثاني مسبقًا العنصر الأول بالرتل if (head == null) { // 3 tail = null; } return firstItem; } // أعد القيمة المنطقية `true` إذا كان الرتل فارغًا boolean isEmpty() { return (head == null); } } // end class QueueOfInts [1] بما أن الرتل كان فارغًا، ستصبح العقدة الجديدة العقدة الوحيدة الموجودة بالقائمة؛ ونظرًا لكونها العقدة الأولى والأخيرة، سيشير head وtail إليها. [2] احذِف العنصر الموجود بمقدمة الرتل وأعده مثل قيمةٍ للتابع. في حال كان الرتل فارغًا، بلِّغ عن اعتراضٍ من النوع IllegalStateException. [3] أصبح الرتل فارغًا، وبما أن العقدة التي حذفناها كانت بمثابة عقدة الرأس head والذيل tail، فلم يَعُدّ هناك ذيلٌ للقائمة في الوقت الحالي. لفهم الدور الذي يلعبه المؤشر tail بالأعلى، يمكنك التفكير وفقًا لقاعدة الأصناف اللامتغايرة class invariant (أو الصنف اللامتغاير)، التي تعرَّضنا لها بالقسم الفرعي 8.2.3 والتي تنص على: "إذا لم يكن الرتل queue فارغًا، فإن tail يُشير إلى آخر عقدةٍ بالرتل". لا بُدّ أن يكون اللامتغاير صحيحًا ببداية ونهاية كل عملية استدعاءٍ للتابع. إذا طبَقنا تلك القاعدة على التابع enqueue في حالة القوائم غير الفارغة، يُخبرنا اللامتغاير بأننا نستطيع إضافة عقدةٍ جديدةٍ إلى نهاية القائمة بتنفيذ تعليمة مثل tail.next = newNode، كما يُخبرنا بكيفية ضبط قيمة tail قبل العودة من التابع، وجعله يُشير إلى العقدة الجديدة التي أُضيفَت للتو إلى الرتل. يستخدم الحاسوب الأرتال queues عندما يرغب بمعالجة عنصرٍ واحدٍ فقط بكل مرة في حين تنتظر عناصرٌ أخرى دورها للمعالجة. نستعرض فيما يلي بعض الأمثلة على ذلك: برامج جافا المُستخدِمة لعدِّة خيوط threads، حيث يحتفظ الحاسوب بالخيوط التي تطمع بزمنٍ للمعالجة من قِبَل وحدة المعالجة المركزية CPU داخل رتل. عند إنشاء خيطٍ جديد، سيُضيفه الحاسوب إلى الطرف الخلفي من ذلك الرتل. تسير الأمور على النحو التالي: يَحذِف الحاسوب خيطًا من الطرف الأمامي للرتل ثم يُعطِيه بعضًا من زمن المعالجة، وإذا لم ينتهي بعدها، يُرسِله الحاسوب مُجددًا إلى الطرف الخلفي من الرتل لينتظر دوره مرةً أخرى. تُخزَّن الأحداث events، مثل نقرات الفأرة وضغطات لوحة المفاتيح داخل رتلٍ اسمه "رتل الأحداث event queue"، ويَحذِف برنامجٌ معينٌ الأحداث الموجودة بذلك الرتل ويُعالِجها واحدًا تلو الآخر. يُمكِن وقوع أحداثٍ كثيرة أخرى بينما يُعالِج البرنامج حدثًا معينًا، ولكن نظرًا لأنها تُخزَّن تلقائيًا داخل ذلك الرتل، فإن البرنامج يُعالِجها دائمًا بنفس ترتيب حدوثها. يستقبل خادم الويب web server طلباتٍ من المتصفحات للوصول إلى بعض الصفحات. بطبيعة الحال، تَصِل طلبات جديدة بينما يُعالِج خادم الويب طلبًا سابقًا؛ لذلك تُوضَع الطلبات الجديدة برتلٍ وتنتظر إلى أن يحين دورها للمعالجة، حيث يَضمَن الرتل معالجة الطلبات بنفس ترتيب وصولها. تُنفِّذ الأرتال سياسة الداخل أولًا، يخرج أولًا FIFO؛ بينما تُنفِّذ الأكداس stacks سياسة الداخل آخرًا، يخرج أولًا LIFO. على نحوٍ مشابهٍ من الأرتال، يُمكِن اِستخدَام الأكداس لحمل العناصر التي تنتظر أن يحين دورها للمعالجة، على الرغم من أنها قد لا تكون عادلة ببعض الأحيان. لتوضيح الفرق بين المكدس stack والرتل queue، سنناقش البرنامج التوضيحي DepthBreadth.java. حاول تشغيل البرنامج، فهو يَعرِض شبكة مربعاتٍ مُلوَّنةٍ جميعها باللون الأبيض على نحوٍ مبدئي، وعندما يَنقُر المُستخدِم على مربعٍ أبيض، سيَضِع البرنامج علامةً على ذلك المربع، ثم يُحوِّله إلى اللون الأحمر. بعد ذلك، سيبدأ البرنامج بوضع علاماتٍ على أي مربعٍ مُتصِلٍ أفقيًا أو رأسيًا مع أيٍ من المربعات التي سَبَقَ للبرنامج وضع علامةٍ عليها. بالنهاية، ستُطبَق تلك العملية على جميع مربعات الشبكة. لنتمكَّن من فهم طريقة عمل البرنامج، سنحاول أن نضع أنفسنا مكان البرنامج: عندما يَنقُر المستخدم على مربع، لنتخيل أننا سنحصل على بطاقةٍ مكتوب عليها موضع ذلك المربع أي رقمي الصف والعمود. سنَضَع بعدها تلك البطاقة في كومة pile، والتي ستَتَضمَّن الآن بطاقةً واحدةً فقط. سنُكرِّر بعد ذلك ما يلي: إذا كانت الكومة فارغةً، نكون قد انتهينا؛ أما إذا لم تكن فارغة، فسنسحب إحدى البطاقات التي تُقابِل إحدى مربعات الشبكة. سنَفْحَص جميع المربعات المجاورة أفقيًا ورأسيًا لذلك المربع؛ فإذا لم نَكن قد مررنا بأيٍ من المربعات المجاورة من قبل، سنُدوِن موضعه على بطاقةٍ جديدة ونضعها بالكومة، ونكون قد انتهينا عندما لا يكون هناك أي بطاقات أخرى بالكومة تنتظر المعالجة. بهذا البرنامج: عندما يكون هناك مربعٌ بالكومة بانتظار المعالجة، يكون ملوَّنًا باللون الأحمر؛ وبالتالي تُمثِل المربعات الملونة بالأحمر تلك المربعات التي مررنا عبرها ولم نعالجها بعد. يتبدل لونه إلى اللون الرمادي بعد أن نأخذ المربع من الكومة ونعالجه، وبمجرد حدوث ذلك، فإن البرنامج يتخطاه دائمًا ولن يحاول معالجته مرة أخرى؛ لأن جميع المربعات المجاورة له أخذت بالحسبان بالفعل. بالنهاية، سيُعالِج البرنامج جميع المربعات، أي سيتحول لونها جميعًا إلى اللون الرمادي، وسينتهي البرنامج. يمكنك تشغيل البرنامج باستخدام إحدى الخيارات الثلاثة التالية: المكدس stack أو الرتل queue أو عشوائيًا، وسيتبّع البرنامج نفس النهج العام أيًا كان الخيار، حيث أن الفرق الوحيد بينها هو أسلوب إدارة كومة البطاقات؛ فبالنسبة للمكدس، ستُضاف البطاقات وتُحذَف بأعلى الكومة؛ أما بالنسبة للرتل، ستُضاف البطاقات إلى أسفل الكومة وتُحذَف من أعلى الكومة؛ وبالنسبة للحالة العشوائية، سيختار البرنامج إحدى البطاقات الموجودة بالكومة عشوائيًا. سيختلف بالتأكيد ترتيب معالجة مربعات الشبكة تمامًا باختلاف نوع الكومة، وهو ما يظهر بوضوحٍ من خلال الصور الثلاثة التالية المأخوذة من البرنامج، حيث سيبدأ البرنامج باختيار مربعٍ بالقرب من منتصف الشبكة مهما كان نوع الكومة المُستخدَمة. يَستخدِم البرنامج على اليسار مكدسًا، أما البرنامج بالمنتصف فيَستخدِم رتلًا، أما البرنامج على اليمين فيَستخدِم الاختيار العشوائي random selection. تختلف الأنماط الناتجة اختلافًا كبيرً، فعندما يَستخدِم البرنامج مكدسًا stack، فإنه يُحاوِل أن يستكشف أقصى قدرٍ ممكنٍ قبل البدء بإجراء تتبعٍ خلفي backtracking لفحص المربعات التي واجهها مُسبقًا؛ وعندما يستخدم البرنامج رتلًا queue، فإنه يعالج المربعات بنفس ترتيب بُعدها عن النقطة المبدئية تقريبًا؛ أما عندما يستخدم البرنامج الاختيار العشوائي random selection، فإننا نحصل على كائنٍ blob غير منتظم، ولكنه يَكون متصلًا بالتأكيد؛ حيث يمكن للبرنامج أن يواجه مربعًا معينًا فقط في حالة كان ذلك المربع مجاورًا لمربعٍ سَبَقَ أن واجهه البرنامج. يُمكنك تجريب البرنامج لترى طريقة عمله، وحاول أيضًا فهم طريقة استخدام المكدس والرتل ضمن ذلك البرنامج،وابدأ بمربعٍ واقعٍ بإحدى الزوايا المربعة. بينما ما يزال البرنامج قيد المعالجة، تستطيع أن تنقر على مربعاتٍ بيضاء أخرى، وستُضاف عندها إلى قائمة المربعات التي واجهها البرنامج. في تلك الحالة، إذا كان البرنامج يَستخدِم مكدسًا، ستلاحظِ أن البرنامج يُعالِج المربع الذي نقرت عليه فورًا بينما ستضطّر بقية المربعات الحمراء التي كانت بالفعل تنتظر دورها إلى الانتظار أكثر؛ وإذا كان البرنامج يستخدم رتلًا، فإنه لن يُعالِج المربع الذي نقرت عليه إلا بعد الانتهاء من معالجة جميع المربعات التي كانت موجودة بالفعل ضمن الكومة. يُمكِنك الإطلاع على الشيفرة المصدرية للبرنامج من الملف DepthBreadth.java. يبدو الرتل أكثر بداهةً لأنه يحدث بصورةٍ أكبر بالحياة الواقعية؛ ولكن في بعض الأحيان، يكون المكدس مناسبًا أكثر أو حتى أساسيًا. فماذا سيحدث مثلًا عندما يستدعي برنامج routine برنامجًا فرعيًا subroutine؟ يُعلَّق البرنامج الأول أثناء تنفيذ البرنامج الفرعي، ولا يكتمل تنفيذه إلى بعد انتهاء البرنامج الفرعي. لنفترض الآن أن ذلك البرنامج الفرعي استدعى بدوره برنامجًا فرعيًا ثانيًا، وأن ذلك البرنامج الفرعي الثاني قد استدعى بدوره برنامجًا ثالثًا، وهكذا، لذلك لا بُدّ من تعليق تنفيذ كل برنامجٍ فرعي بينما تُنفَّذ البرامج الفرعية اللاحقة؛ وهذا يعني أن الحاسوب لا بُدّ أن يحتفظ بحالة جميع البرامج الفرعية المُعلَّقة، وهو ما يفعله باستخدام مكدس. عند استدعاء برنامجٍ فرعي، يُنشَأ سجلٌ نشطٌ activation record له؛ حيث يحتوي على المعلومات المُتعلِّقة بتنفيذ البرنامج الفرعي، مثل متغيراته المحلية local variables ومعاملاته parameters، ويُوضَع ذلك السجل بالمكدس، ويُحذَف فقط عندما ينتهي البرنامج الفرعي من عمله ويعيد قيمة. إذا استدعى البرنامج الفرعي برنامجًا فرعيًا آخرًا، يُنشَئ سجلًا نشطًا جديدًا للبرنامج الفرعي الآخر ويُدفَع push إلى المكدس أعلى السجل الخاص بالبرنامج الفرعي الأول. يستمر المكدس بالنمو مع كل استدعاءٍ لبرنامجٍ فرعي، ويتقلص حجمه عند انتهاء تلك البرامج الفرعية من عملها. قد يحتوي المكدس على عدة سجلات لنفس البرنامج الفرعي في حالة البرامج الفرعية التعاودية recursive subroutine؛ أي تلك التي تعاود استدعاء ذاتها تعاوديًا. في الواقع، هذه هي ببساطة الطريقة التي يتمكَّن بها الحاسوب من تذكُّر مجموعة من الاستدعاءات التعاودية بنفس الوقت. 9.3.3: تعبيرات الإلحاق Postfix Expressions يمكن استخدام المكدس لتحصيل قيم تعبيرات الإلحاق postfix expressions. يُطلَق اسم "تعبير تدوين داخلي infix expression" على أي تعبيرٍ حسابيٍ عادي، مثل "2+(15-12)17"، حيث يُوضَع العامل operator في هذا النوع من التعبيرات بين معاملين operands، مثل "2 + 2"؛ أما بالنسبة لتعبيرات الإلحاق، يأتي العامل بعد معامليه مثل "2 2 +". يُمكِن كتابة التعبير "2+(15-12)17" بصياغة تعبيرات الإلحاق على النحو التالي "2 15 12 - 17 * +"، حيث يُطبَق العامل "-" بهذا التعبير على المعاملين اللذين يَسبِقانه أي "15" و"12". يُطبّق العامل "*" بالمثل على المعاملين اللذين يسبقانه أي "15 12 -" و"17". أخيرًا، يُطبّق "+" على "2" و"15 12 - 17 *". في الواقع، هذه هي نفس العمليات الحسابية التي يُنفِّذها تعبير التدوين الداخلي الأصلي. لنفترض الآن أننا نريد معالجة التعبير "2 15 12 - 17 * +" من اليسار إلى اليمين، وحساب قيمته. سيكون العنصر الأول الذي نواجهه هو 2، ولكن ما الذي يُمكِننا أن نفعله به؟ لا نعرف بهذه النقطة من البرنامج أي عاملٍ ينبغي تطبيقه على العدد 2، كما أننا لا نعرف قيمة المعامل الآخر. ينبغي إذًا أن نتذكر العدد 2 حيث سنحتاج إلى مُعالِجته لاحقًا بدفعه push إلى المكدس. سننتقل بعدها إلى العنصر التالي، أي 15، والذي سندفعه أيضًا إلى المكدس أعلى العدد 2، ثم ندفع العدد 12. الآن، سنواجه العامل "-"، والذي ينبغي أن نطبّقه على المعاملين اللذين يسبقانه بالتعبير، وبما أننا خزَّنا قيمة هذين المعاملين بالمكدس، يُمكِننا معالجة العامل "-" بسحب pull عددين من المكدس أي 12 و15، ثم نحسب قيمة التعبير "15 - 12" لنحصل على الإجابة 3. لا بُدّ من أن نتذكر تلك الإجابة لأننا سنحتاج إلى مُعالِجتها لاحقًا، ولهذا سندفعها إلى المكدس أعلى العدد 2 الذي ما يزال منتظرًا. سنفحص الآن العنصر التالي بالتعبير، أي العدد 17، والذي سندفعه إلى المكدس أعلى العدد 3. ولنتمكَّن من معالجة العنصر التالي أي "*"، يجب أن نسحب pop عددين من المكدس أي 17 و3، حيث يُمثّل العدد 3 قيمة "15 12 -". سنحسب الآن حاصل ضرب العددين، وسنحصل على الناتج 51، الذي سندفعه إلى المكدس. سنجد بعد ذلك أن العنصر التالي بالتعبير هو العامل "+"، والذي يُمكِننا مُعالجته بسحب pop العددين 51 و2 من المكدس، ثم حساب مجموعهما، ودفع الناتج 53 إلى المكدس. أخيرًا، وصلنا إلى نهاية التعبير، ويكون العدد الموجود بالمكدس هو القيمة الإجمالية للتعبير؛ أي أن كل ما علينا فعله هو أن نسحبه من المكدس، ونكون قد انتهينا. على الرغم من أنك قد ترى أن تعبيرات التدوين الداخلي infix expressions أكثر سهولةً، لكن تتمتع تعبيرات الإلحاق postfix expression ببعض المميزات؛ فهي لا تتطلَّب أي أقواسٍ أو قواعد أولوية precedence rules، حيث يعتمد الترتيب الذي تُطبّق على أساسه العوامل operators على ترتيب حدوثها ضمن التعبير، ويسمح ذلك بكتابة خوارزمياتٍ algorithms بسيطةٍ ومباشرة لتحصيل قيم تعبيرات الإلحاق. ألقِ نظرةً على ما يلي: // ابدأ بمكدس فارغ Start with an empty stack // لكل عنصر ضمن التعبير، نفذ ما يلي for each item in the expression: // إذا كان العنصر عددًا if the item is a number: // ادفع العدد إلى داخل المكدس Push the number onto the stack // إذا كان العنصر عاملًا else if the item is an operator: // اسحب معاملين من المكدس Pop the operands from the stack // Can generate an error // طبّق العامل على المعاملين Apply the operator to the operands // ادفع الناتج إلى المكدس Push the result onto the stack else // هناك خطأ بالتعبير There is an error in the expression // اسحب عددًا من المكدس Pop a number from the stack // Can generate an error // إذا كان المكدس فارغًا if the stack is not empty: // هناك خطأ بالتعبير There is an error in the expression else: // القيمة الأخيرة المسحوبة من المكدس هي قيمة التعبير The last number that was popped is the value of the expression علاوةً على ذلك، يمكن الكشف عن الأخطاء الموجودة ضمن أي تعبيرٍ بسهولة. لنفترض مثلًا أنه لدينا التعبير التالي "2 3 + "، حيث يُمكِننا بسهولة أن نرى أنه لا توجد معاملاٌت operands كافيةٌ للعامل ""، وستكشف الخوارزمية الموضحة أعلاه عن ذلك الخطأ عندما تحاول سحب pop معاملٍ ثانٍ للعامل "*" من المكدس الذي سيكون فارغًا. قد تحدث مشكلةٌ أخرى عندما لا يكون هناك عددٌ كافٍ من العوامل operators لجميع الأعداد ضمن التعبير مثل "2 3 4 +". ستكشف الخوارزمية عن ذلك الخطأ عندما يبقى العدد 2 بالمكدس بعد انتهاء الخوارزمية. يستخدم البرنامج التوضيحي PostfixEval.java تلك الخوارزمية، حيث يسمح للمُستخدم بكتابة تعبيرات إلحاقٍ posftix expression مكوّنةٍ من أعدادٍ حقيقيةٍ موجبة إلى جانب العوامل التالية "+" و"-" و"*" و"/" و"^"، حيث يُمثِل العامل "^" الأس؛ أي يُقيّّم التعبير "2 3 ^" على النحو التالي 23؛ كما يطبع البرنامج رسالةً نصيةً أثناء معالجته لكل عنصرٍ ضمن التعبير، ويستخدِم الصنف StackOfDouble المُعرَّف بالملف StackOfDouble.java، والذي يتطابق مع الصنف الأول StackOfInts المُبيَّن أعلاه باستثناء أنه يُخزِّن قيمًا من النوع double بدلًا من النوع int. الجانب الوحيد الشيّق في هذا البرنامج هو التابع method المُنفِّذ لخوارزمية تحصيل قيمة تعبيرات الإلحاق postfix evaluation. تعرض الشيفرة التالية تنفيذًا لتلك الخوارزمية، والذي هو في الواقع مجرد تحويلٍ مباشرٍ للشيفرة الوهمية المُوضحة بالأعلى إلى لغة جافا. private static void readAndEvaluate() { StackOfDouble stack; // المكدس المستخدم لتحصيل قيمة التعبير stack = new StackOfDouble(); // أنشِئ مكدسًا فارغًا System.out.println(); while (TextIO.peek() != '\n') { if ( Character.isDigit(TextIO.peek()) ) { // العنصر التالي المُدْخَل عبارة عن عدد // اِقرأه وخزنه بالمكدس double num = TextIO.getDouble(); stack.push(num); System.out.println(" Pushed constant " + num); } else { // نظرًا لأن العنصر التالي ليس عددًا، فإنه ينبغي أن يكون عاملًا // اقرأ العامل ونفذ العملية char op; // العامل الذي ينبغي أن تكون قيمته + أو - أو / أو * double x,y; // المعاملين اللذين سنسحبهما من المكدس double answer; // ناتج العملية op = TextIO.getChar(); if (op != '+' && op != '-' && op != '*' && op != '/' && op != '^') { // لا يُمثِل المحرف أي من العمليات المتاحة System.out.println("\nIllegal operator found in input: " + op); return; } if (stack.isEmpty()) { System.out.println(" Stack is empty while trying to evaluate " + op); System.out.println("\nNot enough numbers in expression!"); return; } y = stack.pop(); if (stack.isEmpty()) { System.out.println(" Stack is empty while trying to evaluate " + op); System.out.println("\nNot enough numbers in expression!"); return; } x = stack.pop(); switch (op) { case '+': answer = x + y; break; case '-': answer = x - y; break; case '*': answer = x * y; break; case '/': answer = x / y; break; default: answer = Math.pow(x,y); // (op must be '^'.) } stack.push(answer); System.out.println(" Evaluated " + op + " and pushed " + answer); } TextIO.skipBlanks(); } // end while // 1 if (stack.isEmpty()) { // يستحيل إذا كان المدخل غير فارغ فعليًا System.out.println("No expression provided."); return; } double value = stack.pop(); // قيمة التعبير System.out.println(" Popped " + value + " at end of expression."); if (stack.isEmpty() == false) { System.out.println(" Stack is not empty."); System.out.println("\nNot enough operators for all the numbers!"); return; } System.out.println("\nValue = " + value); } // end readAndEvaluate() [1] إذا وصلنا إلى تلك النقطة من البرنامج، سنكون قد قرأنا المُدْخلات بصورة سليمة. إذا كان التعبير صالحًا، فإن قيمة التعبير ستكون الشيء الوحيد الموجود بالمكدس. يلجأ الحاسوب عادةً إلى الاعتماد على تعبيرات الإلحاق postfix expressions. في الواقع، تُعدّ آلة جافا الافتراضية Java virtual machine آلة مكدس stack machine، بمعنى أنها تَستخدِم أسلوبًا يعتمد على المكدس لتحصيل قيم التعبيرات expressions. يُمكِن تمديد الخوارزمية بسهولةٍ بحيث تتمكَّن من معالجة المتغيرات variables والثوابت constants. عندما تواجه الخوارزمية متغيرًا ضمن تعبير، فإن قيمة ذلك المتغير ينبغي أن تُدفَع إلى المكدس، ويُمكن أيضًا تطبيقها على العوامل operators التي تَملُك أكثر أو أقل من مجرد معاملين اثنين operands، حيث يُمكِننا ببساطةٍ سحب pop العدد المطلوب من المعاملات من المكدس، ثم دفع الناتج إليه؛ فيُستخدَم على سبيل المثال عامل الطرح الأحادي "-" ضمن تعبيرٍ مثل "-x" له معاملٌ واحد. سنستمر بمناقشة التعبيرات وتحصيل قيمة التعبيرات expression evaluation بالقسمين التاليين. ترجمة -بتصرّف- للقسم Section 3: Stacks, Queues, and ADTs من فصل Chapter 9: Linked Data Structures and Recursion من كتاب Introduction to Programming Using Java. اقرأ أيضًا مقدمة إلى الاستثناءت exceptions ومعالجتها في جافا التوكيد assertion والتوصيف annotation في لغة جافا مقدمة إلى صحة البرامج ومتانتها في جافا التعاود recursion والمكدس stack في جافاسكربت1 نقطة
-
import cv2 as cv image = cv.imread("C:/temp/1.png") gray_image= cv.cvtColor(image,cv2.COLOR_BGR2GRAY) sift = cv.SIFT() kp = sift.detect(gray_image,None) image=cv.drawKeypoints(gray_image,kp) cv.imshow(image) cv.waitKey() cv.destroyAllWindows() والنتيجة: بدايةً علينا أن نقوم ببناء جسم SIFT، ثم نقوم بتمرير البيانات المختلفة إليه (اختياري). أولاً نقوم بقراءة الصورة وتحويلها للصيغة الرمادية: image = cv.imread("C:/temp/1.png") gray_image= cv.cvtColor(image,cv2.COLOR_BGR2GRAY) ثم نقوم بإنشاء جسم خوارزمية SIFT وذلك من خلال التابع cv2.SIFT: sift = cv.SIFT() ثم نستخدم التابع sift.detect لإيجاد النقاط الرئيسية في الصورة. يمكنك تمرير قناع إذا أردت البحث عنها فقط في جزء محدد. كل نقطة رئيسية لها تركيب خاص وهي تحوي على العديد من الخصائص مثل الإحداثيات x و y، وحجم الجوار ذو المعنى، والزاوية التي تحدد الاتجاه، والاستجابة التي تحدد قوة النقاط الرئيسية. kp = sift.detect(gray_image,None) أيضاً استخدمنا التابع: image=cv.drawKeypoints(gray_image,kp) الذي يقوم برسم دوائر صغيرة في مواقع النقاط الرئيسية. وإذا مررت cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS له، فسوف يرسم دائرة بحجم النقطة الأساسية وسيعرض أيضاً الاتجاه. أي: img=cv2.drawKeypoints(gray,kp,flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) النتيجة ستكون: الآن لحساب الوصف يمكننا القيام بذلك بطريقتين: 1. في حالة أوجدنا النقاط الرئيسية (كما فعلنا) ،استخدم الدالة sift.compute التي تحسب الوصف من خلال النقاط الرئيسية. kp,des = sift.compute(gray_image,kp) 2. إذا لم تقوم بإيجاد النقاط الرئيسية، عندها يمكنك إيجاد النقاط الرئيسية+الوصف من خلال الدالة sift.detectAndCompute: sift = cv2.SIFT() kp, des = sift.detectAndCompute(gray_image,None) هنا kp تمثل مصفوفة بالنقاط الرئيسية، و des تمثل مصفوفة من الشكل (عدد النقاط * 28) .1 نقطة
-
الحوسبة خفيّة الخوادم (Serverless computing) في طريقها لإحداث ثورة في طرق تطوير البرمجيّات المتعارف عليها. ستساعدك المنصّات المفتوحة المصدر التي نقدّمها هنا في التعرّف على هذا المجال. كثُر الحديث في الآونة الأخيرة عن خفاء الخوادم (Serverless)، فلنوضّح المعنى المقصود بهذا المصطلح، والمصطلحات المرتبطة به، مثل الحوسبة خفيّة الخوادم (Serverless computing) والمنصّات خفيّة الخوادم (Serverless platform). يُستخدَم مصطلح خفاء الخوادم عادةً على أنّه مرادف لتقديم الوظائف بصيغة خدمات (Functions-as-a-Service, FaaS)؛ إلّا أنّ المصطلح لا يعني غيّاب الخادم، عكس ما قد يوحي به الاسم. في الواقع، توجد خوادم عدّة لأنّ مزوّدي الخدمات السحابيّة للعموم يوفّرون خوادم تنشُر، وتشغّل ، وتدير تطبيقك. تعدّ الحوسبة خفيّة الخوادم قسمًا جديدًا يمثّل تحوّلًا في الطريقة التي يبني بها المطوّرون ويوزّعون الأنظمة البرمجية. يمكن أن يؤدّي عزلُ البنية التحتيّة للتطبيقات عن الشفرة البرمجيّة إلى تسهيل عمليّة التطوير مع الحصول على فوائد أخرى من حيث التكلفة والفاعليّة. يرى عدد من المتخصّصين أنّ الحوسبة خفيّة الخوادم وFaaS ستلعبان دورًا أساسيًّا في تحديد أبعاد الحقبة القادمة من تقنيّة المعلومات في المؤسسّات، جنبًا لجنب مع خدمات السحابة الأصيلة Cloud-native والخدمات السحابيّة الهجينة (Hybrid cloud). توفّر المنصّات خفيّة الاسم واجهات تطبيقات برمجيّة (API) تتيح للمستخدمين تشغيل دوالّ برمجيّة (تُسمّى أيضًا إجراءات (Actions)) وتُرجِع نتيجة تشغيل كلّ دالة. توفّر المنصّات خفيّة الخادم كذلك نهايات HTTPS لتمكين المطوّر من الحصول على نتائج الدالّة. يمكن استخدام هذه النهايات (Endpoints) لتكون مُدخلًا Input لدوالّ أخرى؛ وبالتالي توفير متتاليّة (أو سلسلة) من الدوالّ المترابطة. تعمل أغلب المنصّات خفيّة الاسم بحيث ينشُر المستخدم (أو يُنشئ) الدوالّ قبل تنفيذها. يتوفّر لدى المنصّة بعد ذلك كلّ ما يلزم لتنفيذ الشفرة البرمجيّة حالما يُطلب منها ذلك. يُمكن طلب تنفيذ الدالّة خفيّة الخادم يدويًّا بتنفيذ أمر، أو يمكن التسبّب في تنفيذها عبر حدث مرجعي مُعدّ لتنشيط الدالّة للإجابة على أحداث مثل إشعار Cron، وتحميل ملفّ، وأحداث أخرى كثيرة. سبع منصّات مفتوحة المصدر للتعرّف على الحوسبة خفيّة الخوادم Apache OpenWhisk: منصّة مفتوحة المصدر تمكّنك من تنفيذ شفرة برمجيّة استجابةً للأحداث مهما كان عددها. كُتبت المنصّة بلغة Scala، ويمكنها معالجة مُدخلات انطلاقًا من طلبات HTTP ثم تشغيل شفرات برمجيّة مكتوبة بلغة جافاسكريبت أو سويفت Swift. Fission: إطار عمل للحوسبة خفيّة الخوادم يمكّن المطوّرين من إنشاء دوالّ باستخدام Kubernetes. يتيح إطار العمل هذا للمبرمجين كتابة دوالّ تدوم لمدّة قصيرة بأيّة لغة برمجة، وربطها بأي حدث مثل طلبات HTTP. IronFunctions: إطار عمل للحوسبة خفيّة الخوادم يوفّر منصّة للخدمات الصغيرة (Microservices) المترابطة، عن طريق دمج الخدمات الموجودة والاستفادة من Docker. يكتُب المطوّرون الدوالّ بلغة Go. FnProject: منصّة خفيّة الخوادم مُوجَّهة أساسًا للحاويّات يمكن تشغيلها في أي مكان على السحابة أو في البنية التحتيّة الداخليّة. استخدام المنصّة سهل، كما أنّها عاليّة الأداء، و تدعم لغات البرمجة جميعًا، ويمكن توسعتها. OpenLambda: مشروع حوسبة خفيّة الخوادم مُرخص برخصة Apache، ومكتوب بلغة Go، ويعتمد على حاويّات لينكس. يهدف OpenLambda في المقام الأول إلى التمكين من استغلال المقاربات الجديدة للحوسبة خفيّة الخوادم. Kubeless: إطار عمل يعتمد على Kubernetes ويسمح للمطوّر بنشر شفرات برمجيّة قصيرة دون التفكير في البنية التحتيّة المستخدمة. يعتمد Kubeless على موارد Kubernetes لتوفير قابليّة التوسّع الذاتيّة، وتوجيه واجهات التطبيقات البرمجية، والمراقبة، والتشخيص وغيرها. OpenFaas: إطار عمل لبناء دوالّ خفيّة الخوادم باستخدام Docker وKubernates؛ يوفّر دعمًا مدمجًا للقيّاسات والإحصاءات. يُمكن تحزيم أي عمليّة Process على هيئة دالّة، ممّا يسمح باستغلال مجموعة من أحداث الويب دون الحاجة لكتابة شفرات مُكرَّرة. يعدّ Kubernates المنصّة الأكثر انتشارًا لإدارة أحمال العمل في المنصّات خفيّة الخوادم، وحاويّات التطبيقات ذات الخدمات الصغيرة. يستخدم Kubernates نموذج نشر معدًّا بدقّة لمعالجة أحمال العمل بسرعة أكبر وسهولة أكثر. يمكّن Knative Serving من بناء خدمات وتطبيقات خفيّة الخوادم ونشرها على Kubernates، مع استخدام Istio للتوسّع ودعم سيناريوهات عمل متقدّمة، مثل: النشر السريع لحاويات خفيّة الخوادم التوسع والتقلّص التلقائيّيْن (Scaling up and down) التوجيه وبرمجة الشبكات بالنسبة لعانصر Istio أخذ لقطات (Snapshots) من الشفرة المنشورة والإعدادات في نقاط زمنيّة محدّدة. يركّز Knative على المهامّ الاعتيّاديّة من بناءٍ للتطبيقات وتشغيلها على منصّات السحابة الأصليّة (Cloud-native)؛ بهدف تنسيق عمليّات البناء من الشفرة البرمجيّة إلى الحاويّة، وربط الخدمات بأحداث النظام، وتوجيه حركة البيانات وإدارتها أثناء النشر، والتوسّع التلقائي حسب حمل العمل. أمّا Istio، فهو منصّة مفتوحة للاتّصال بالخدمات الصغيرة وتأمينها (وهو في الواقع مستوى تحكّم بنسيج الخدمة Service mesh في الوسيط Envoy) صُمِّم ليتناسب مع تفاعل أشخاص مختلفين مع إطار العمل بما في ذلك المطوّرون، عمّال الصيّانة ومزوّدو الخدمات. يمكن – على سبيل المثال – نشر جافاسكريبت خفيّة الخادم باستخدام Knative Serving على منصّة Minishift محليّة باتّباع الشفرة التاليّة: ## Dockerfile FROM bucharestgold/centos7-s2i-nodejs:10.x WORKDIR /opt/app-root/src COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 3000 CMD ["npm", "start"] ## package.json { "name": "greeter", "version": "0.0.1", "private": true, "scripts": { "start": "node app.js" }, "dependencies": { "express": "~4.16.0" } } ## app.js var express = require("express"); var app = express(); var msg = (process.env.MESSAGE_PREFIX || "") + "NodeJs::Knative on OpenShift"; app.get("/", function(req, res, next) { res.status(200).send(msg); }); app.listen(8080, function() { console.log("App started in port 8080"); }); ## service.yaml apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata: name: greeter spec: configuration: revisionTemplate: spec: container: image: dev.local/greeter:0.0.1-SNAPSHOT أنشئ تطبيق Node.js وانشره على منصّة Kubernates المحليّة. ستحتاج لتثبيت متطلّبات المنصّة (Knative، و Istio، و Knative Serving على Kubernetes (أو Minishift)). اربط المنصّة بخدمة Docker بالأمر التالي: (minishift docker-env) && eval(minishift oc-env) أنشئ نسخة من حاوية خفيّة الخادم باستخدام Jib عبر الأمر التالي: ./mvnw -DskipTests clean compile jib:dockerBuild انشر خدمة خفيّة الاسم مثل Minishift على عنقود Kubernates بالأمر التالي: kubectl apply -f service.yaml خاتمة يوضّح المثال أعلاه أين وكيف يمكن البدء في تطوير تطبيقات خفيّة الخادم باستخدام منصّات سحابة أصليّة مثل Kubernates، و Knative Serving وIstio. ترجمة - بتصرّف - للمقال 7 open source platforms to get started with serverless computing لصاحبه Daniel Oh.1 نقطة