لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 04/27/22 في كل الموقع
-
السلام عليكم ارجو مراجعة دورة دورة تطوير التطبيقات باستخدام JavaScript من حيث المحتوى مقارنة بالدورات الاخرى. يجب التركيز على مواضيع react او ِangular مقارنة بالمواضيع الاخرى الموجودة التى يتم شرحها باختصار شديد ولا يوجد عليها تطبيقات كافية. كل موضوع متروح يمكن عمل كورس كامل عليه. كل عام وانتم بخير.1 نقطة
-
انا خلصت دورت تطوير واجهات المستخدم "واخذت الشهاده" وبدأت في تطوير التطبيقات باستخدام لغة JavaScript هل عند انتهائي منها سأكون محترف1 نقطة
-
ايجاد افكار للمشاريع هو الأمر الآخر الذي ستتعلمه أكثر مع كبر دائرة خبراتك. ولكن لا يمنع هذا من وجود بعض الأفكار التي يمكن تطبيق عليها اغلب الافكار. من مثل: لغرض تحسين التعامل مع كائن الوقت في جافاسكربت نقوم ببناء عداد زمني أو ساعة حائطية نوظف فيها بجانب التوقيت تحويلات CSS مثلا. لغرض تحسين التعامل مع شجرة الوثيقة نقوم انشاء تطبيق to do list وبما انك في بداية الدورة، فسيكون الآن كافيا متابعة ما يقدم في الدورة والتطبيق العملي مع المدرب.1 نقطة
-
1 نقطة
-
الاحتراف في مجال ما لا يكون وليد لحظة معينة او قابلا للتأطير في مرحلة معينة فهو نتاج مرات عديدة من البناء والتطبيق العملي والممارسة والخبرة، فنفس التطبيق الذي تقوم ببناءه بعد الانتهاء تماما من الدورة وبعد زمن معين من الممارسة والتطبيق العملي سيكونان في مستويان مختلفان من الاحترافية. بدل التركيز على مثل هاته الأشياء يقترح الانطلاق في عمل مشاريعك وتوظيف افكارك الخاصة. سيساعدك هذا في قولبة أسلوب خاص بك يمكن ان يطلق عليه عملا محترفا. وبشكل عام، تعالج الدورة كامل النقاط التي ستساعدك في بلوغ هذا الهدف.1 نقطة
-
1 نقطة
-
1 نقطة
-
من فضلكم لدي استفسار لدي رغبة في تعلم برمجة برامج الويندوز خاصة المتعلقة بتسيير المخزون مثل البرامج المستعملة في سوبر ماركيت التي يتم تخزين السلع فيها وبمجرد تمرير عليها الكود بار الخاص بالمنتج يطلع السعر. والفواتير ..الخ ومختلف البرامج...الخ . هل لغة بايثون قادرة على عمل مثل هذه البرامج؟ اذا كان نعم ماهي لغات التي يجب ان تتم معها لعمل برنامج. ام هناك لغات افضل واكثر دقة من بايثون لعمل مثل هذه البرامج مثل C++ وغيرها . ارجو من لديه خبرة يرد علي ورمضان كريم1 نقطة
-
1 نقطة
-
1 نقطة
-
انا ك مطور بايثون او back-end django لماذا احتاج الى php ,js,r وعلم الحاسوب و تطور واجهات الويب؟1 نقطة
-
عند اشتراكك بأي من الدورات المتوفرة ضمن الأكاديمية فإنك تحصل على دروس كل المسارات لتلك الدورة مدى الحياة، إضافة لحصولك على أول مسار فقط من كل الدورات الباقية، وذلك في حال أحببت الاطلاع عليها والتسجيل بإحداها للتخرج من الدورة لست مطالبًا سوى بمسارات الدورة الخاصة بك، المسارات المفتوحة الإضافية هي للاطلاع فقط، في حال كان لديك مشكلة أو استفسار يمكنك البحث والقراءة ضمن قاعدة المعرفة للحصول على معلومات عامة عن الدورات1 نقطة
-
1 نقطة
-
حاول إعادة تشغيل ملف تنصيب Composer كمسؤول يبدو أنك تواجه مشكلة في صلاحية الوصول لبعض الملفات ضمن جهازك1 نقطة
-
1 نقطة
-
الشهادة الرسمية تظهر لك عند وضع كود الشهادة والضغط على التحقق، وبهذه الطريقة يمكن لأي جهة التأكد من أن شهادتك حقيقية وممنوحة من قبل الأكاديمية، في حال كنت تواجه أي مشاكل أو لديك استفسارات بخصوص الشهادة تواصل مع مركز المساعدة واطرح استفسارك وسيتم مساعدتك1 نقطة
-
1 نقطة
-
سنوجّه اهتمامنا في هذا القسم إلى التعامل مع الواجهة الخلفية وآلية تطوير التطبيق بإضافة وظائف جديدة تنفذ هذه المرة من قبل الخادم. سنبني تلك الوظائف باستخدام NodeJS وهي بيئة تشغيل JavaScript مبنية على محرك JavaScript من تصميم Google يدعى Chrome V8. كُتبَت المادة العلمية للمنهاج باستخدام Node.js 10.18.0، وتأكد من أن النسخة المثبتة على جهازك هي على الأقل مطابقة للإصدار السابق، ويمكنك استخدام الأمر node -v للتحقق من الإصدار المثبت لديك. أشرنا سابقًا في مقال أساسيات جافاسكربت اللازمة للعمل مع React أن المتصفحات لا تدعم كامل الميزات الأحدث للغة JavaScript مباشرةً، لذلك من الضروري نقل الشيفرة التي تعمل على المتصفح إلى إصدار أقدم باستخدام babel مثلًا. لكن الأمر سيختلف تمامًا مع JavaScript التي تُنفّذ في الواجهة الخلفية، ذلك أن الإصدار الأحدث من Node.js سيدعم الغالبية العظمى من الميزات الجديدة للغة، فلا حاجة عندها للنقل. سنضيف شيفرة تتعامل مع تطبيق الملاحظات الذي تعرفنا عليه سابقًا في مقالات سابقة من هذه السلسلة لكن تنفيذها سيكون في الواجهة الخلفية. لكن علينا أولًا تعلم الأساسيات من خلال كتابة التطبيق التقليدي "Hello world". ملاحظة: لن تكون جميع التطبيقات والتمارين في هذا القسم تطبيقات React، ولن نستخدم create-react-app في تهيئة المشاريع التي تضم التطبيقات. لقد تعرفنا سابقًا في مقال إحضار البيانات من الخادم في تطبيقات React على مدير الحزم npm وهو أداة لإدارة حزم JavaScript تعود أصلًا إلى بيئة Node.js. انتقل إلى مجلد مناسب وأنشئ قالبًا لتطبيقنا مستخدمًا الأمر npm init. أجب عن الأسئلة التي تولدها الأداة، وستكون النتيجة إنشاء الملف package.json ضمن جذر المشروع يضم معلومات عنه. { "name": "backend", "version": "0.0.1", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Matti Luukkainen", "license": "MIT" } من هذه المعلومات بالطبع -ستكون قد أدخلتها عند إجابتك عن الأسئلة- اسم المشروع ونقطة انطلاق التطبيق والتي هي الملف index.js في تطبيقنا. لنجري بعض التعديلات على محتوى الكائن scripts في الملف: { // ... "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, // ... } لننشئ الإصدار الأول لتطبيقنا بإضافة ملف باسم index.js إلى المجلد الجذري الذي أنشأنا فيه المشروع. اكتب الأمر التالي في الملف: console.log('hello world') يمكن تشغيل التطبيق من node بكتابة التالي في سطر الأوامر: node index.js كما يمكن تشغيله كسكريبت (npm script): npm start ستعمل سكريبت npm لأننا عرفناها ضمن الكائن script في ملف package.json: { // ... "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, // ... } سيُنفِّذ npm المشروع عند استدعاء الملف index.js من سطر الأوامر. يعتبر تشغيل التطبيق كسكريبت npm أمرًا اختياريًا. يعرّف ملف package.json افتراضيًا سكريبت npm أخرى تدعى npm test. وطالما أن المشروع لا يضم حتى الآن مكتبة للاختبارات، سينفذ npm الأمر التالي: echo "Error: no test specified" && exit 1 خادم ويب بسيط لنحوّل تطبيقنا إلى خادم ويب: const http = require('http') const app = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('Hello World') }) const PORT = 3001 app.listen(PORT) console.log(`Server running on port ${PORT}`) ستطبع الطرفية الرسالة التالية بمجرد تشغيل التطبيق: Server running on port 3001 سنشغل تطبيقنا المتواضع بطلب عنوان الموقع http://localhost:3001 من المتصفح: سيعمل الخادم كما سبق بغض النظر عن بقية أقسام العنوان، حتى أن كتابة عنوان الموقع بالشكل http://localhost:3001/foo/bar يعطي النتيجة نفسها. ملاحظة: إن كان المنفذ 3001 محجوزًا من قبل تطبيق آخر، سينتج عن تشغيل الخادم رسالة الخطأ التالية: ➜ hello npm start > hello@1.0.0 start /Users/mluukkai/opetus/_2019fullstack-code/part3/hello > node index.js Server running on port 3001 events.js:167 throw er; // Unhandled 'error' event ^ Error: listen EADDRINUSE :::3001 at Server.setupListenHandle [as _listen2] (net.js:1330:14) at listenInCluster (net.js:1378:12) في هذه الحالة ستكون أمام خياران: إما أن تغلق التطبيق الذي يَشغُل المنفذ 3001، أو اختيار منفذ آخر. لنتأمل الآن السطر الأول من شيفرة تطبيق الخادم: const http = require('http') يُدرج التطبيق وحدة خادم الويب web server المدمجة ضمن Node. لقد تعلمنا إدراج الوحدات في شيفرة الواجهة الأمامية لكن بعبارة مختلفة قليلًا: import http from 'http' تُستعمل حاليًا وحدات ES6 ضمن شيفرة الواجهة الأمامية. وتذكّر أنّ تعريف الوحدات يكون باستعمال التعليمة export واستخدامها ضمن الشيفرة باستعمال التعليمة import. تَستعمل Node.js ما يسمى CommonJS (وحدات JavaScript المشتركة). ذلك أن بيئتها تطلبت وجود الوحدات قبل أن تدعمها JavaScript بوقت طويل. بدأت Node.js بالدعم التجريبي لوحدات ES6 مؤخّرًا فقط. لن نجد فرقًا تقريبًا بين وحدات ES6 ووحدات CommonJS، على الأقل ضمن حدود منهاجنا. ستبدو القطعة التالية من الشيفرة على النحو التالي: const app = http.createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }) response.end('Hello World') }) تستخدم الشيفرة التابع createServer الموجود ضمن الوحدة http لإنشاء خادم ويب جديد. تعرّف الشيفرة بعد ذلك معالج حدث ضمن الخادم، يُستدعى كلما ورد طلب HTTP إلى العنوان http://localhost:3001. يستجيب الخادم برمز الحالة (200) معيدًا ترويسة "نوع المحتوى" على أنها text/plain (نص أو فارغ)، وكذلك محتوى صفحة الويب التي ستُعرض، وهذا المحتوى هو العبارة "Hello World". تهيئ أسطر الشيفرة الأخيرة خادم http الذي أُسند إلى المتغيّر App لينصت إلى طلبات HTTP القادمة إلى المنفذ 3001: const PORT = 3001 app.listen(PORT) console.log(`Server running on port ${PORT}`) إنّ الغاية الأساسية من استخدام خادم الواجهة الخلفية في منهاجنا، هو تقديم بيانات خام بصيغة JSON إلى الواجهة الأمامية. ولهذا السبب سنعدّل الخادم (تطبيق الخادم) ليعيد قائمة من الملاحظات المكتوبة مسبقًا بصيغة JSON: const http = require('http') let notes = [ { id: 1, content: "HTML is easy", date: "2019-05-30T17:30:31.098Z", important: true }, { id: 2, content: "Browser can execute only Javascript", date: "2019-05-30T18:39:34.091Z", important: false }, { id: 3, content: "GET and POST are the most important methods of HTTP protocol", date: "2019-05-30T19:20:14.298Z", important: true } ] const app = http.createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'application/json' }) response.end(JSON.stringify(notes)) }) const PORT = 3001 app.listen(PORT) console.log(`Server running on port ${PORT}`) لنعد تشغيل الخادم ولنحدث المتصفح. ملاحظة: يمكنك إغلاق الخادم بالضغط على ctrl+c من طرفية سطر أوامر Node.js. تُعلِم القيمة (application/JSON) الموجودة في ترويسة "نوع المحتوى" متلقي البيانات أنها بصيغة JSON.تُحوَّل المصفوفة notes إلى JSON باستخدام التابع ()JSON.stringify. ستظهر المعلومات على المتصفح تمامًا كما ظهرت في مقال إحضار البيانات من الخادم في تطبيقات React عندما استخدمنا خادم JSON. مكتبة Express يمكن كما رأينا كتابة شيفرة الخادم مباشرة باستخدام الوحدة http المدمجة ضمن Node.js.لكن الأمر سيغدو مربكًا عندما يزداد حجم التطبيق. طوّرت العديد من المكتبات لتسهّل تطوير تطبيقات الواجهة الخلفية باستخدام Node، وذلك بتقديم واجهة أكثر ملائمة للعمل بالموازنة مع وحدة http المدمجة. تعتبر المكتبة express حتى الآن الأكثر شعبية لتحقيق المطلوب. لنضع express موضع التنفيذ بتعريفها كملف اعتمادية dependency وذلك بتنفيذ الأمر: npm install express --save يُضاف ملف الاعتمادية أيضًا إلى الملف package.json: { // ... "dependencies": { "express": "^4.17.1" } } تُثبت الشيفرة المصدرية لملف الاعتمادية ضمن المجلد node_modules الموجود في المجلد الجذري للمشروع. يمكنك إيجاد عدد كبير من ملفات الاعتمادية بالإضافة إلى express ضمن هذا المجلد. في الواقع سيضم المجلد السابق ملفات اعتمادية express وملفات اعتمادية متعلقة بملفات اعتمادية express وهكذا. ندعو هذا الترتيب بملفات الاعتمادية الانتقالية transitive dependencies للمشروع. ثُبِّتت في مشروعنا المكتبة express 4.17.1. لكن ما الذي تعنيه إشارة (^) أمام رقم الإصدار في ملف package.json؟ "express": "^4.17.1" يستخدم npm ما يسمى بآلية الإصدار الدلالية semantic versioning، وتعني الدلالة (^) أنه إذا حُدِّثت ملفات اعتمادية المشروع فإن إصدار express سيبقى 4.17.1. يمكن أن يتغير رقم الدفعة ضمن الإصدار (الرقم الأخير) أو رقم الإصدار الثانوي (الرقم الأوسط) لكن رقم الإصدار الرئيسي (الرقم الأول) يجب أن يبقى كما هو. نستخدم الأمر التالي لتحديث ملفات اعتمادية المشروع: npm update يمكننا تثبيت أحدث ملفات اعتمادية معرفة في ملف package.json إذا أردنا أن نعمل على مشروعنا في حاسوب آخر باستخدام الأمر: npm install وبشكل عام عند تحديث ملف اعتمادية، يدل عدم تغير رقم الإصدار الرئيسي أن الإصدار الأحدث سيبقى متوافقًا مع الإصدار الأقدم دون الحاجة لتغييرات في الشيفرة. بينما تغير رقم الإصدار الرئيسي سيدل أن الإصدار الأحدث قد يحوي تغييرات قد تمنع التطبيق من العمل. فقد لا يعمل تطبيقنا إن كان إصدار المكتبة express فيه 4.17.7 مثلًا وتم تحديثها إلى الإصدار 5.0.0 (الذي قد نراه مستقبلًا)، بينما سيعمل مع الإصدار 4.99.1. استخدام express في تطوير صفحات الويب لنعد إلى تطبيقنا ونجري بعض التعديلات عليه: const express = require('express') const app = express() let notes = [ ... ] app.get('/', (req, res) => { res.send('<h1>Hello World!</h1>') }) app.get('/api/notes', (req, res) => { res.json(notes) }) const PORT = 3001 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) سنعيد تشغيل التطبيق حتى نحصل على النسخة الجديدة منه. طبعًا لن نلاحظ تغييرًا كليًا فلقد أدرجنا express على شكل دالة ستنشئ تطبيق express يُخزّن ضمن المتغيّر App: const express = require('express') const app = express() وبعدها عرّفنا مسارين للتطبيق، يعرّف الأول معالج حدث يتعامل مع طلبات HTTP-GET الموجهة إلى المجلد الجذري للتطبيق (' / '): app.get('/', (request, response) => { response.send('<h1>Hello World!</h1>') }) حيث تقبل دالة المعالج معاملين الأول request ويحوي كل المعلومات عن طلب HTTP، ويستخدم الثاني response لتحديد آلية الاستجابة للطلب. يستجيب الخادم إلى الطلب في شيفرتنا باستخدام التابع send العائد للكائن response. حيث يرسل الخادم عند الاستجابة العبارة النصية <h1>Hello World!</h1>التي مُرّرت كمعامل للتابع send. وطالما أن القيمة المعادة نصية ستَسند express القيمة text/html إلى ترويسة "نوع المحتوى" ويعاد رمز الحالة 200. يمكننا التحقق من ذلك من خلال النافذة Network في طرفية تطوير المتصفح: بالنسبة للمسار الثاني فإنه يعرّف معالج حدث يتعامل مع طلبات HTTP-GET الموجهة إلى موقع وجود الملاحظات، نهاية المسار notes: app.get('/api/notes', (request, response) => { response.json(notes) }) يستجيب الخادم إلى الطلب باستخدام التابع json العائد للكائن response. حيث تُرسل مصفوفة الملاحظات التي مُرّرت للتابع بصيغة JSON النصية. وكذلك ستتكفل express بإسناد القيمة application/json إلى ترويسة "نوع المحتوى". سنلقي تاليًا نظرة سريعة على البيانات التي أرسلت بصيغة JSON. كان علينا سابقًا تحويل البيانات إلى نصوص مكتوبة بصيغة JSON باستخدام التابع JSON.stringify: response.end(JSON.stringify(notes)) لا حاجة لذلك عند استخدام express، فهي تقوم بذلك تلقائيًا. وتجدر الإشارة هنا أنه لا فائدة من كون JSON مجرد نص، بل يجب أن يكون كائن JavaScript مثل notes الذي أُسند إليه. ستشرح لك التجربة الموضحة في الشكل التالي هذه الفكرة: أُنجز الاختبار السابق باستخدام الوحدة التفاعلية node-repl. يمكنك تشغيل هذه الوحدة بكتابة node في سطر الأوامر. تفيدك هذه الوحدة بشكل خاص لتوضيح طريقة عمل الأوامر، وننصح بشدة أن تستخدمها أثناء كتابة الشيفرة. مكتبة nodemon رأينا سابقًا ضرورة إعادة تشغيل التطبيق عند تعديله حتى تظهر نتائج التعديلات. ونقوم بذلك عن طريق إغلاق التطبيق أولًا باستخدام ctrl+c ثم تشغيله من جديد. طبعًا فالأمر مربك بالموازنة مع طريقة عمل React التي تقوم بذلك تلقائيًا بمجرد تغيّر الشيفرة. إن حل هذه المشكلة يكمن في استخدام nodemon. سنثبت الآن nodemon كملف اعتمادية باستخدام الأمر: npm install --save-dev nodemon سيتغير أيضًا محتوى الملف package.json: { //... "dependencies": { "express": "^4.17.1", }, "devDependencies": { "nodemon": "^2.0.2" } } إن أخطأت في كتابة الأمر وظهر ملف اعتمادية nodemon تحت مسمى ملفات الاعتمادية "dependencies" بدلًا من ملفات اعتمادية التطوير"devDependencies"، أصلح الأمر يدويًا في ملف package.json. إن الإشارة لملف اعتمادية على أنه ملف اعتمادية تطوير هو للدلالة على الحاجة له أثناء تطوير التطبيق فقط، ليعيد على سبيل المثال تشغيل التطبيق كما تفعل nodemon. ولن تحتاجها لاحقًا عندما تشغل التطبيق على خادم الاستثمار الفعلي مثل Heroku. لتشغيل التطبيق مع nodemon اكتب الأمر التالي: node_modules/.bin/nodemon index.js سيسبب الآن أي تغيير في الشيفرة إعادة تشغيل الخادم تلقائيًا. وطبعًا لا فائدة من إعادة تشغيل الخادم إن لم نحدث المتصفح الذي يعرض الصفحة، لذلك علينا القيام بذلك يدويًا. فلا نمتلك حاليًا وسيلة لإعادة التحميل الدائم (hot reload) كما في React. يبدو الأمر السابق طويلًا، فلنعرف إذًا سكريبت npm خاصة بتشغيل التطبيق ضمن الملف package.json: { // .. "scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "echo \"Error: no test specified\" && exit 1" }, // .. } لا حاجة في هذه السكريبت لتحديد المسار التالي node_modules/.bin/nodemon للمكتبة nodemon، لأن npm يعرف أين سيبحث عنها. لنشغل الخادم بوضعية التطوير كما يلي: npm run dev على خلاف مخطوطتي start و test يجب إضافة التعليمة run إلى الأمر. العمل مع واجهة التطبيقات REST لنجعل تطبيقنا قادرًا على تأمين واجهة http متوافقة مع REST كما فعلنا مع خادم JSON. قدم روي فيلدينغ مفهوم نقل حالة العرض (REpresentational State Transfer) واختصارًا REST، عام 2000 في أطروحته للدكتوراه وهي عبارة عن أسلوب تصميمي لبناء تطبيقات ويب بمقاسات قابلة للتعديل. لن نغوص في تعريف REST، أو نهدر وقتنا في التفكير بالواجهات المتوافقة أو غير المتوافقة معها، بل سنعتمد مقاربة ضيقة تهتم فقط بكيفية فهم التوافق مع REST من منظور تطبيقات الويب، فمفهوم REST الأصلي ليس محدودًا بتطبيقات الويب. لقد أشرنا في القسم السابق أن REST تعتبر كل الأشياء الفردية -كالملاحظات في تطبيقنا- موردًا. ولكل مورد موقع محدد URL يمثل العنوان الفريد لهذا المورد. إن أحد الأعراف المتبعة في إنشاء عنوان فريد للمورد هو دمج نوع المورد مع المعرف الفريد له. فلو افترضنا أن الموقع الجذري للخدمة هو www.example.com/api، وأننا عرّفنا نوع المورد الذي يمثل الملاحظات على أنه note وأننا نحتاج إلى المورد note ذو المعرف 10، سيكون عنوان موقع المورد www.example.com/api/notes/10. وسيكون عنوان موقع مجموعة الملاحظات www.example.com/api/notes. يمكننا تنفيذ العديد من العمليات على الموارد، وتعرّف العملية التي نريد تنفيذها على مورد على أنها فعل HTTP: الموقع الفعل الوظيفة notes/10 GET إحضار مورد واحد notes GET إحضار كل موارد المجموعة notes POST إنشاء مورد جديد بناء على البيانات الموجودة في الطلب notes/10 DELETE حذف المورد المحدد notes/10 PUT استبدال كامل المورد المحدد بالبيانات الموجودة في الطلب notes/10 PATCH استبدال جزء من المورد المحدد بالبيانات الموجودة في الطلب 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; } وهكذا نحدد الخطوط العامة لما تعنيه REST كواجهة نموذجية، أي أنها تعرّف أسلوبُا ثابتُا ومنسقًا للواجهات، تجعل الأنظمة المختلفة قادرة على العمل المشترك. إنّ المفهوم الذي اعتمدناه حول REST يصنف كمستوًى ثانٍ من التوافق وفق نموذج توافق ريتشاردسون. مع هذا فنحن إذ عّرفنا واجهة REST، لم نعرفها لتتوافق تمامًا مع المعايير التي قدمها روي فيلدينغ في أطروحته، وهذا حال الغالبية العظمى من الواجهات التي تدعي توافقها الكامل مع REST. يشار إلى نموذجنا الذي يقدم واجهة أساسية (إنشاء-قراءة-تحديث-حذف) (CRUD: Create-Read-Update-Delete) في العديد من المراجع (Richardson, Ruby: RESTful Web Services) على أنه تصميم موجه للعمل مع الموارد resource oriented architecture بدلًا من كونه متوافقًا مع REST. إحضار مورد واحد لنوسع تطبيقنا بحيث يقدم واجهة REST تتعامل مع الملاحظات بشكل فردي. لكن علينا أولًا إنشاء مسار لإحضار المورد. سنعتمد نموذج (نوع/معرف) في الوصول إلى موقع المورد (notes/10 مثلًا). يمكن تعريف معاملات المسار في express كما يلي: app.get('/api/notes/:id', (request, response) => { const id = request.params.id const note = notes.find(note => note.id === id) response.json(note) }) سيتعامل المسار (...,'app.get('/api/notes/:id مع طلبات HTTP-GET التي تأتي بالصيغة api/notes/SOMETHING حيث يشير SOMETHING إلى نص افتراضي. يمكن الوصول إلى المعامل id للمسار من خلال الكائن request: const id = request.params.id نستخدم التابع find الخاص بالمصفوفات للبحث عن الملاحظة من خلال معرفها الفريد id والذي يجب أن يتطابق مع قيمة المعامل، ثم تعاد الملاحظة إلى مرسل الطلب. لكن لو جربنا الشيفرة المكتوبة واستخدمنا المتصفح للوصول إلى العنوان http://localhost:3001/api/notes/1، فلن يعمل التطبيق كما هو متوقع، وستظهر صفحة فارغة. لن يفاجئني هذا كمطور اعتاد على المشاكل، لذا فقد حان وقت التنقيح. لنزرع الأمر console.log كما اعتدنا: app.get('/api/notes/:id', (request, response) => { const id = request.params.id console.log(id) const note = notes.find(note => note.id === id) console.log(note) response.json(note) }) بمجرد انتقال المتصفح إلى العنوان السابق ستُظهر الطرفية الرسالة التالية: لقد مُرِّر المعامل id إلى التطبيق، لكن التابع find لم يجد ما يتطابق معه. وللتعمق في تقصي مصدر الخطأ زرعنا الأمر console.log ضمن الدالة التي تُمرَّر كمعامل للتابع find. وكان علينا لتنفيذ ذلك إعادة كتابة الدالة السهمية بشكلها الموسع واستخدام عبارة return في نهايتها: app.get('/api/notes/:id', (request, response) => { const id = request.params.id const note = notes.find(note => { console.log(note.id, typeof note.id, id, typeof id, note.id === id) return note.id === id }) console.log(note) response.json(note) }) سيطبع لنا الأمر console.log (معرف الملاحظة، نوعه، المتغير id، نوعه، هل هناك تطابق). وعندما نتوجه مجددًا نحو عنوان الملاحظة عبر المتصفح، ستُطبع عبارة مختلفة على الطرفية مع كل استدعاء للدالة: 1 'number' '1' 'string' false 2 'number' '1' 'string' false 3 'number' '1' 'string' false يبدو أن سبب المشكلة قد توضّح الآن. إن المتغيّر id يضم قيمة نصية هي "1"، بينما يحمل معرف الملاحظة قيم صحيحة. حيث يعتبر عامل المساواة الثلاثي === في JavaScript وبشكل افتراضي أن القيم من أنواع مختلفة غير متساوية. لنصحح الخطأ بتحويل قيمة المتغير id إلى عدد: app.get('/api/notes/:id', (request, response) => { const id = Number(request.params.id) const note = notes.find(note => note.id === id) response.json(note) }) لقد تم الأمر! لاتزال هناك مشكلة أخرى في التطبيق. فلو بحثنا عن ملاحظة معّرفها غير موجود أصلًا، سيجيب الخادم كالتالي: يعيد الخادم رمز الحالة 200، وهذا يعني أن الاستجابة قد تمت بنجاح. وطبعًا لا توجد هناك بيانات لإعادتها طالما أن قيمة ترويسة "طول المحتوى" هي 0. يمكن التحقق من ذلك أيضًا عبر المتصفح. إن السبب الكامن خلف هذا السلوك، هو أن المتغير note سيأخذ القيمة undefined إن لم نحصل على تطابق. وبالتالي لابد من التعامل مع هذه المشكلة على الخادم الذي يجب أن يعيد رمز الحالة not found 404 بدلًا من 200. لنعدل الشيفرة كالتالي: app.get('/api/notes/:id', (request, response) => { const id = Number(request.params.id) const note = notes.find(note => note.id === id) if (note) { response.json(note) } else { response.status(404).end() } }) عندما لا تكون هناك بيانات لإعادتها نستخدم التابع status لإعداد حالة التطبيق، والتابع end للاستجابة على الطلب دون إرسال أية بيانات. تظهر لك العبارة الشرطية if أن كل كائنات JavaScript محققة (تعيد القيمة المنطقية "صحيح" في عمليات الموازنة)، بينما يعتبر الكائن undefiend خاطئ. سيعمل التطبيق الآن، وسيرسل رمز الحالة الصحيح إن لم يعثر على الملاحظة المطلوبة. لن يعرض التطبيق شيئًا على الصفحة كغيره من التطبيقات عندما تحاول الوصول إلى مورد غير موجود. وهذا في الواقع أمر طبيعي، فلا حاجة لعرض أي شيء على المتصفح طالما أن REST واجهة معدة للاستخدام برمجيًا، وسيكون رمز الحالة الذي يعيده التطبيق هو كل ما نحتاجه. حذف الموارد لنضف مسارًا لحذف مورد محدد. يتم ذلك من خلال الطلب HTTP-DELETE إلى موقع المورد: app.delete('/api/notes/:id', (request, response) => { const id = Number(request.params.id) notes = notes.filter(note => note.id !== id) response.status(204).end() }) إن نجحت عملية الحذف، أي أن المورد وُجد وحُذف، سيجيب التطبيق على الطلب برمز الحالة no content 204 دون إعادة أية بيانات. لا يوجد رمز حالة محدد لإعادته عند محاولة حذف مورد غير موجود، لذلك ولتبسيط الأمر سنعيد الرمز 204 في الحالتين. اختبار التطبيقات باستخدام Postman كيف نتأكد أن عملية حذف المورد قد تمت فعلًا؟ من السهل إجراء طلب HTTP-GET من خلال المتصفح والتأكد. لكن كتابة شيفرة اختبار ليست الطريقة الأنسب دائمًا. ستجد العديد من الأدوات الجاهزة لتسهيل الاختبارات على الواجهة الخلفية. فمنها على سبيل المثال curl وهو برنامج سطر أوامر أشرنا إليه في القسم السابق، لكننا سنستخدم هنا Postman للقيام بهذه المهمة. إذًا لنثبت Postman: من السهل جدًا استخدام Postman في حالتنا هذه، إذ يكفي أن تختار موقعا وطلب HTTP مناسب (DELETE في حالتنا). يستجيب خادم الواجهة الخلفية بشكل صحيح كما يبدو. فلو أرسلنا إلى الموقع http://localhost:3001/api/notes طلب HTTP-GET سنجد أن الملاحظة التي معرّفها 2 غير موجودة ضمن قائمة الملاحظات، فقد نجحت عملية الحذف. ستعود قائمة الملاحظات إلى وضعها الأصلي عند إعادة تشغيل التطبيق، لأن الملاحظات قد حُفِظت في ذاكرة الجهاز فقط. عميل REST في بيئة التطوير Visual Studio Code إن كنت تستخدم Visual Studio Code في تطوير التطبيقات يمكنك تثبيت الإضافة REST client واستعمالها بدلًا من Postman حيث ننشئ مجلدًا يدعى requests عند جذر التطبيق، ثم نحفظ كل طلبات REST client في ملف ينتهي باللاحقة rest. ونضعه في المجلد السابق. لننشئ الآن الملف getallnotes.rest ونعرّف فيه كل طلبات HTTP التي تحضر الملاحظات: بالنقر على النص Send Requests، سينفذ REST client طلبات HTTP وستظهر استجابة الخادم في نافذة المحرر. استقبال البيانات سنوسع التطبيق بحيث نغدو قادرين على إضافة ملاحظة جديدة إلى الخادم. تنفذ العملية باستخدام طلب HTTP-POST إلى العنوان http://localhost:3001/api/notes، حيث تُرسل المعلومات عن الملاحظة الجديدة بصيغة JSON ضمن جسم الطلب. وحتى نصل إلى معلومات الملاحظة بسهولة سنحتاج إلى مفسّر JSON الذي تقدمه express ويستخدم عبر تنفيذ الأمر (()app.use(express.json. لنستخدم مفسّر JSON ولنضف معالج حدث أوّلي للتعامل مع طلبات HTTP-POST: const express = require('express') const app = express() app.use(express.json()) //... app.post('/api/notes', (request, response) => { const note = request.body console.log(note) response.json(note) }) يمكن لدالة معالج الحدث الوصول إلى البيانات من خلال الخاصية body للكائن request. لكن من غير استخدام مفسر JSON ستكون هذه الخاصية غير معرّفة undefined. فوظيفة المفسر استخلاص بيانات JSON من الطلب وتحويلها إلى كائن JavaScript يرتبط مع الخاصية body للكائن requestوذلك قبل أن تُستدعى الدالة التي تتعامل مع مسار الطلب. حتى هذه اللحظة لا يفعل التطبيق شيئًا سوى استقبال البيانات وطباعتها على الطرفية ثم إعادة إرسالها كاستجابة للطلبات. قبل أن نضيف بقية الشيفرة إلى التطبيق، لنتأكد باستخدام Postman أن الخادم قد استقبل البيانات بالفعل. لكن يجب الانتباه إلى تعريف البيانات التي أرسلناها ضمن جسم الطلب بالإضافة إلى موقع المورد: يطبع البرنامج البيانات التي أرسلناها ضمن الطلب على الطرفية: ملاحظة: ابق الطرفية التي تُشغّل التطبيق مرئية بالنسبة لك طيلة فترة العمل مع الواجهة الخلفية. ستساعدنا Nodemon على إعادة تشغيل التطبيق عند حدوث أية تغييرات في الشيفرة. إذا ما دققت في ما تعرضه الطرفية، ستلاحظ مباشرة وجود أخطاء في التطبيق: من المفيد جدًا التحقق باستمرار من الطرفية لتتأكد من أن كل شيء يسير كما هو متوقع ضمن الواجهة الخلفية وفي مختلف الحالات، كما فعلنا عندما أرسلنا البيانات باستخدام الطلب HTTP-POST. ومن الطبيعي استخدام الأمر console.log في مرحلة التطوير لمراقبة الوضع. من الأسباب المحتملة لظهور الأخطاء هو الضبط الخاطئ لترويسة "نوع المحتوى" في الطلبات. فقد ترى الخطأ التالي مع Postman إذا لم تعرّف نوع جسم الطلب بشكل صحيح: حيث عُرّف نوع المحتوى على أنه text/plain (نص أو فارغ): ويبدو أن الخادم مهيأ لاستقبال مشروع فارغ فقط: لن يتمكن الخادم من تفسير البيانات بشكل صحيح دون وجود القيمة الصحيحة في الترويسة. ولن يخمن حتى صيغة البيانات نظرًا للكمية الهائلة من أنواع المحتوى التي قد تكونها البيانات. إن كنت قد ثبتت VS REST client ستجد أن الطلب HTTP-POST سيرسل بمساعدة REST client كما يلي: حيث أنشأنا الملف create_note.rest وصغنا الطلب كما يوصي توثيق البرنامج. يتمتع REST client بميزة على Postman بأن التعامل يدويًا مع الطلبات متاح في المجلد الجذري للمشروع، ويمكن توزيعها إلى أعضاء فريق التطوير. بينما وإن كان Postman سيسمح بحفظ الطلبات، لكن ستغدو الأمور فوضوية وخاصة إذا استُخدم في تطوير عدة مشاريع غير مترابطة. ملاحظات جانبية مهمة قد تحتاج عند تنقيح التطبيقات إلى معرفة أنواع الترويسات التي ترافق طلبات HTTP. يعتبر استخدام التابع get العائد للكائن request، إحدى الطرق التي تسمح بذلك. حيث يستخدم التابع للحصول على قيمة ترويسة محددة، كما يمتلك الكائن request الخاصية headers التي تحوي كل الترويسات الخاصة بطلب محدد. قد تحدث الأخطاء عند استخدام VS REST client لو أضفت سطرًا فارغًا بين السطر الأعلى والسطر الذي يعرّف ترويسات HTTP. حيث يفسر البرنامج ذلك بأن كل الترويسات قد تُركَت فارغة، وبالتالي لن يعلم خادم الواجهة الخلفية أن البيانات التي استقبلها بصيغة JSON. يمكنك أن ترصد ترويسة "نوع المحتوى" المفقودة إذا ما طبعت كل ترويسات الطلب على الطرفية باستخدام الأمر (console.log(request.headers. لنعد الآن إلى تطبيقنا، طالما تأكدنا أن الخادم قد استقبل البيانات الواردة إليه بالشكل الصحيح، وقد حان الوقت لإتمام معالجة الطلب: app.post('/api/notes', (request, response) => { const maxId = notes.length > 0 ? Math.max(...notes.map(n => n.id)) : 0 const note = request.body note.id = maxId + 1 notes = notes.concat(note) response.json(note) }) نحتاج إلى id فريد للملاحظة الجديدة وللقيام بذلك علينا أن نجد أكبر قيمة id تحملها قائمة الملاحظات ونسند قيمتها للمتغير maxId. هكذا سيكون id الملاحظة الجديدة هو maxId+1. في واقع الأمر، حتى لو استخدمنا هذا الأسلوب حاليًّا، فلا ننصح به، وسنتعلم أسلوبًا أفضل قريبًا. الشيء الآخر أن تطبيقنا بشكله الحالي يعاني مشكلة مفادها أن طلب HTTP-POST يمكن أن يُستخدَم لإضافة كائن له خصائص غير محددة. لذلك سنحسن التطبيق بأن لا نسمح لقيمة الخاصية content أن تكون فارغة، وسنعطي الخاصيتين important وdate قيمًا افتراضية، وسنهمل بقية الخصائص: const generateId = () => { const maxId = notes.length > 0 ? Math.max(...notes.map(n => n.id)) : 0 return maxId + 1 } app.post('/api/notes', (request, response) => { const body = request.body if (!body.content) { return response.status(400).json({ error: 'content missing' }) } const note = { content: body.content, important: body.important || false, date: new Date(), id: generateId(), } notes = notes.concat(note) response.json(note) }) وضعنا الشيفرة التي تولد المعرف id للملاحظة الجديدة ضمن الدالة generateId. فعندما يستقبل الخادم بيانات قيمة الخاصية content لها غير موجودة، سيجيب على الطلب برمز الحالة 400 bad request: if (!body.content) { return response.status(400).json({ error: 'content missing' }) } لاحظ أن استخدام return حيوي جدًا لسير العملية، فلولاها ستُنفّذ الشيفرة تباعًا وستُحفظ الملاحظة ذات الصياغة الخاطئة. وبالطبع ستحفظ الملاحظة التي تحمل فيها الخاصية content قيمة، بما تحويه من بيانات. ونذكّر أيضًا أننا تحدثنا عن ضرورة توليد زمن إنشاء الملاحظة من قبل الخادم وليس جهاز الواجهة الأمامية، لذلك ولّده الخادم. إن لم تحمل الخاصية important قيمة فسنعطيها القيمة الإفتراضية false. لاحظ الطريقة الغريبة التي استخدمناها: important: body.important || false, وتفسر الشيفرة السابقة كالتالي: إن حملت الخاصية important للبيانات المستقبلة قيمة ستعتمد العبارة السابقة هذه القيمة وإلا ستعطيها القيمة false. أمَّا عندما تكون قيمة الخاصية important هي false عندها ستعيد العبارة التالية body.important || false القيمة false بناء على قيمة الطرف الأيمن من العبارة. ستجد شيفرة التطبيق بشكله الحالي في المستودع part3-1 على موقع GitHub. يحوي المسار الرئيسي للمستودع كما ستلاحظ شيفرات للنسخ التي سنطورها لاحقًا، لكن النسخة الحالية في المسار part3-1. إن نسخت المشروع، فتأكد من تنفيذ الأمر npm install قبل أن تشغل التطبيق باستخدام إحدى التعليمتين npm start أو npm run dev. ملاحظة أخرى قبل الشروع بحل التمارين، سيبدو الشكل الحالي لدالة توليد المعرفات IDs كالتالي: const generateId = () => { const maxId = notes.length > 0 ? Math.max(...notes.map(n => n.id)) : 0 return maxId + 1 } يحتوي جسم الدالة سطرًا يبدو غريبًا نوعًا ما: Math.max(...notes.map(n => n.id)) ينشئ التابع (notes.map(n=>n.id مصفوفة جديدة تضم كل قيم المعرفات id للملاحظات. ثم يعيد التابع Math.max أعلى قيمة من القيم التي مُرّرت إليه. وطالما أن نتيجة تطبيق الأمر (notes.map(n=>n.ids ستكون مصفوفة، فمن الممكن تمريرها مباشرة كمعامل للتابع Math.max. وتذكّر أنه يمكن فصل المصفوفة إلى عناصرها باستخدام عامل النشر (…). التمارين 3.1 - 3.6 ملاحظة: من المفضل أن تنفذ تمارين هذا القسم ضمن مستودع خاص على git، وأن تضع الشيفرة المصدرية في جذر المستودع، وإلا ستواجه المشاكل عندما تصل للتمرين 3.10. ملاحظة: لا ننفذ حاليًا مشروعًا للواجهة الأمامية باستخدام React، ولم ننشئ المشروع باستخدام create-react-app. لذلك هيئ المشروع باستعمال الأمر npm init كما شرحنا في هذا الفصل. توصية مهمة: ابق نظرك دائمًا على الطرفية التي تُشغِّل تطبيقك عندما تطور التطبيقات للواجهة الخلفية. 3.1 دليل الهاتف للواجهة الخلفية: الخطوة 1 اكتب تطبيقًا باستخدام Node يعيد قائمة من مدخلات دليل هاتف مكتوبة مسبقًا وموجودة في الموقع http://localhost:3001/api/persons: لاحظ أنه ليس للمحرف '/' في المسار api/persons أي معنى خاص بل هو محرف كباقي المحارف في النص. يجب أن يُشّغل التطبيق باستخدام الأمر npm start. كما ينبغي على التطبيق أن يعمل كنتيجة لتنفيذ الأمر npm run dev وبالتالي سيكون قادرًا على إعادة تشغيل الخادم عند حفظ التغيرات التي قد تحدث في ملف الشيفرة المصدرية. 3.2 دليل الهاتف للواجهة الخلفية: الخطوة 2 أنشئ صفحة ويب عنوانها http://localhost:3001/info بحيث تبدو مشابهةً للصفحة التالية: ستظهر الصفحة الوقت الذي استقبل فيه الخادم الطلب، وعدد مدخلات دليل الهاتف في لحظة معالجة الطلب. 3.3 دليل الهاتف للواجهة الخلفية: الخطوة 3 أضف إمكانية إظهار معلومات مُدخل واحد من مُدخلات دليل الهاتف. يجب أن يكون موقع الحصول على بيانات الشخص الذي معرفه 5 هو http://localhost:3001/api/persons/5. إن لم يكن المُدخَل ذو المعرف المحدد موجودًا، على الخادم أن يستجيب معيدًا رمز الحالة المناسب. 3.4 دليل الهاتف للواجهة الخلفية: الخطوة 4 أضف إمكانية حذف مدخل واحد من مُدخلات دليل الهاتف مستعملًا الطلب HTTP-DELETE إلى عنوان المدخل الذي سيُحذَف ثم اختبر نجاح العملية مستخدمًا Postman أو Visual Studio Code REST client. 3.5 دليل الهاتف للواجهة الخلفية: الخطوة 5 أضف إمكانية إضافة مدخلات إلى دليل الهاتف مستعملًا الطلب HTTP-POST إلى عنوان مجموعة المدخلات http://localhost:3001/api/persons. استعمل الدالة Math.random لتوليد رقم المعرف id للمُدخل الجديد. واحرص أن يكون مجال توليد الأرقام العشوائية كبيرًا ليقلّ احتمال تكرار المعرف نفسه لمدخلين. 3.6 دليل الهاتف للواجهة الخلفية: الخطوة 6 أضف معالج أخطاء للتعامل مع ما قد يحدث عند إنشاء مدخل جديد. حيث لا يُسمح بنجاح العملية إذا كان: اسم الشخص أو رقمه غير موجودين الاسم موجود مسبقًا في الدليل. استجب للحالتين السابقتين برمز حالة مناسب، وأعد كذلك معلومات توضح سبب الخطأ كالتالي: { error: 'name must be unique' } فكرة عن أنواع طلبات HTTP يتحدث معيار HTTP عن خاصيتين متعلقتين بأنواع الطلبات هما الأمان safety والخمول idempotence. فيجب أن يكون طلب HTTP-GET آمنًا: يعني الأمان أن تنفيذ الطلب لن يسبب تأثيرات جانبية على الخادم. ونعني بالتأثيرات الجانبية أن حالة قاعدة البيانات لن تتغير كنتيجة للطلب وأن الاستجابة ستكون على شكل بيانات موجودة مسبقًا على الخادم. لا يمكن أن نضمن أمان الطلب GET وما ذكر مجرد توصية في معيار HTTP. لكن بالتزام مبادئ التوافق مع REST في واجهة تطبيقنا، ستكون طريقة استخدام GET آمنة دائمًا. كما عرّف معيار HTTP نمطًا للطلبات هو HEAD وعلى هذا الأخير أن يكون آمنًا أيضًا. يعمل HEAD تمامًا عمل GET، إلا أنه لا يعيد سوى رمز الحالة وترويسات الاستجابة. لن يعيد الخادم جسمًا للاستجابة عندما تستخدم HEAD. وعلى كل طلبات HTTP أن تكون خاملة ماعدا POST: ويعني هذا أن التأثيرات الجانبية التي يمكن أن يسببها طلب، يجب أن تبقى كما هي، بغض النظر عن عدد المرات التي يرسل فيها الطلب إلى الخادم. فلو أرسلنا الطلب HTTP-PUT إلى العنوان api/notes/10/ حاملًا البيانات {content:"no sideeffects!", important:true} فإن الاستجابة ستكون نفسها بغض النظر عن عدد المرات التي يُرسَل فيها هذا الطلب. وكما أن الأمان في GET لا يتحقق دومًا، كذلك الخمول. فكلاهما مجرد توصيات في معيارHTTP لا يمكن ضمانها معتمدين على نوع الطلب فقط. لكن بالتزام مبادئ التوافق مع REST في واجهة تطبيقنا، سنستخدم GET، HEAD PUT، DELELTE بطريقة تحقق خاصية الخمول. أما الطلب POST فلا يعتبر آمنًا ولا خاملًا. فلو أرسلنا 5 طلبات HTTP-POST إلى الموقع /api/notes، بحيث يضم جسم الطلب البيانات {content: "many same", important: true}، ستكون النتيجة 5 ملاحظات جديدة لها نفس المحتوى. البرمجيات الوسيطة Middleware يصنف مفسّر JSON الذي تقدمه express بأنه أداة وسطية. فالبرمجيات الوسيطة (Middleware) هي دوال تستخدم لمعالجة كائنات الطلبات والاستجابات. فمفسر JSON الذي تعرفنا عليه سابقًا في هذا الفصل سيأخذ بيانات خام من جسم كائن الطلب، ثم يحولها إلى كائن JavaScript ويسندها إلى الكائن request كقيمة للخاصية body. يمكن في الواقع استخدام عدة أدوات وسطية في نفس الوقت. ستنفذ البرمجيات الوسيطة بالتتالي وفق ترتيب استخدامها من قبل express. لنضف أدوات وسطية خاصة بنا لطباعة معلومات حول كل طلب أرسل إلى الخادم. الأداة الوسطية التي سنستعملها دالة تقبل ثلاث معاملات: const requestLogger = (request, response, next) => { console.log('Method:', request.method) console.log('Path: ', request.path) console.log('Body: ', request.body) console.log('---') next() } سنجد في نهاية الدالة، دالة أخرى هي ()next مررت كمعامل لدالة الأداة الوسطية وتستدعى في نهايتها. تنقل الدالة next التحكم إلى الأداة الوسطية التالية. تستخدم الأداة الوسطية كالتالي: app.use(requestLogger) تستدعى دوال البرمجيات الوسيطة بالتتالي وفق ترتيب استخدامها من قبل express عن طريق التابع use. وانتبه إلى أن مفسر JSON سيستخدم قبل الأداة الوسطية requestLogger، وإلا فلن يهيأ جسم الكائن request عندما تُنفّذ الدالة requestLogger. يجب أيضًا تعريف دوال البرمجيات الوسيطة قبل المسارات إن أردنا تنفيذها قبل أن يُستدعى معالج الحدث الخاص بالمسار. لكن قد تكون هناك حالات معاكسة نعرّف فيها الأداة الوسطية بعد تعريف المسار، وخاصة إن أردنا لدالة الأداة الوسطية أن تُنفّذ إن لم تُعرّف أية مسارات لمعالجة طلب HTTP. لنعرف الأداة الوسطية التالية في شيفرتنا بعد تعريف المسار، لالتقاط الطلبات الموجهة إلى مسارات غير موجودة. ستعيد دالة الأداة الوسطية في هذه الحالة رسالة خطأ بصيغة JSON: const unknownEndpoint = (request, response) => { response.status(404).send({ error: 'unknown endpoint' }) } app.use(unknownEndpoint) ستجد شيفرة التطبيق بشكله الحالي في المستودع part3-2 على موقع GitHub. التمارين 3.7 - 3.8 3.7 دليل هاتف للواجهة الخلفية: الخطوة 7 أضف الأداة الوسطية morgan إلى تطبيقك للمراقبة. اضبط الأداة لطباعة الرسائل على الطرفية وفقًا لنمط التهيئة tiny. لا يقدم لك توثيق Morgan كل ما تريده، وعليك قضاء بعض الوقت لتتعلم تهيئة الأداة بشكل صحيح. وهذا بالطبع حال التوثيقات جميعها، فلا بد من فك رموز هذه التوثيقات. ثبت Morgan كأي مكتبة أخرى باستخدام الأمر npm install. واستخدمها تمامًا كما نستخدم أية أداة وسطية بتنفيذ الأمر app.use. 3.8 دليل هاتف للواجهة الخلفية: الخطوة 8 * هيئ Morgan حتى تعرض لك أيضًا بيانات الطلبات HTTP-POST: قد يكون طباعة بيانات الطلبات خطرًا طالما أنها تحتوي على معلومات حساسة قد تخرق بعض قوانين الخصوصية لبعض البلدان مثل الاتحاد الأوروبي، أو قد تخرق معيار الأعمال. لا تهتم لهذه الناحية في تطبيقنا، لكن عندما تنطلق في حياتك المهنية، تجنب طباعة معلومات حساسة. يحمل التمرين بعض التحديات، علمًا أن الحل لا يتطلب كتابة الكثير من الشيفرة. يمكنك إنجاز التمرين بطرائق عدة، منها استخدام التقنيات التالية: إنشاء مفاتيح جديدة (new token) JSON.stringify ترجمة -وبتصرف- للفصل Node.js, Express من سلسلة Deep Dive Into Modern Web Development1 نقطة
-
هذا القسم بالذات من أجمل الأقسام التي قرأتها في ترجمة هذا الكتاب , وقد شرح مفهوم الكائن بشكل واضح جداً و متحرياً التفاصيل و لعلي سأشير به لكل من يقرأ في مفهوم البرمجة كائنية التوجه.1 نقطة
-
تُستخدَم الأصناف (classes) لتمثيل مجموعة كائنات (objects) تَتَشارَك نفس البِنْية (structure) والسلوك (behavior). يُحدِّد الصَنْف بِنْية تلك الكائنات عن طريق تَخْصِيصه للمُتْغيِّرات الموجودة ضِمْن أي نسخة (instance) من الصَنْف، كما يُحدِّد سلوكها بتَعْريفه لتوابع نُسخ (instance methods) تُعبر عن سلوك (behavior) تلك الكائنات. هذه الفكرة قوية للغاية، ولكنها مع ذلك مُمكِنة بغالبية اللغات البرمجية التقليدية، فما هي إذًا الفكرة الجديدة التي تُقدِّمها البرمجة كائنية التوجه (object-oriented programming)، وتُفرِّقها عن البرمجة التقليدية؟ في الواقع، تُدعِّم البرمجة كائنية التوجه كُلًا من الوراثة (inheritance) والتعددية الشكلية (polymorphism)، والتي تسمح للأصناف (classes) بتمثيل التشابهات بين مجموعة كائنات تَتَشارَك بعضًا من بِنْيتها وسُلوكها، وليس كلها. توسيع (extend) الأصناف الموجودة لابُدّ لأي مبرمج أن يفهم المقصود بكُلًا من المفاهيم التالية: الصَنْف الفرعي (subclass)، والوراثة (inheritance)، والتعددية الشكلية (polymorphism). قد تَستغرِق بعض الوقت حتى تستطيع تَوْظِيف مواضيع مثل الوراثة (inheritance) تَوْظِيفًا فعليًا بعيدًا عن مُجرَّد تَمْديد (extending) بعض الأصناف الموجودة، وهو ما سنُناقشه بالجزء الأول من هذا القسم. ببداية تَعامُل المبرمجين مع الكائنات، عادةً ما يَقْتصر اِستخدَامهم للأصناف الفرعية على موقف واحد فقط: "التَعْديل على صَنْف موجود بالفعل أو الإضافة إليه بحيث يَتلائَم مع ما يُريدونه". يُعدّ ذلك أكثر شيوعًا من تصميم مجموعة كاملة من الأصناف الأعلى والأصناف الفرعية (subclasses). تستطيع أن تُمدِّد (extend) صَنْف فرعي من صَنْف آخر باستخدام الصيغة التالية: public class <subclass-name> extends <existing-class-name> { . . // التعديلات والإضافات . } لنَفْترِض أنك تُريد كتابة برنامج للعبة الورق "بلاك جاك (Blackjack)" تَستخدِم فيه الأصناف Card و Hand و Deck من القسم ٥.٤. أولًا، تَختلِف اليد (hand) بلعبة بلاك جاك نوعًا ما عن اليد في العموم؛ حيث تُحسَب قيمة اليد بلعبة بلاك جاك وِفقًا لقواعد اللعبة، والتي يُمكِن تَلخَّيصها كالتالي: تُحسَب قيمة اليد بجَمْع قيم ورق اللعب باليد، بحيث تَكُون قيمة ورقة اللعب العددية (numeric card)، مثل "ثلاثة" أو "عشرة"، مُساوِية لنفس قيمتها العددية، أما ورقة اللعب المُصورة، مثل "الرجل" أو "الملكة" أو "الملك"، فقيمتها تُساوِي عشرة. وأخيرًا، قيمة ورقة "الآص" قد تُساوِي ١ أو ١١. إنها في العموم تُساوِي ١١ إلا لو أدى ذلك إلى قيمة كلية أكبر من ٢١، مما يَعنِي أن ورقة "الآص" الثانية والثالثة والرابعة قيمها بالضرورة ستُساوِي ١. أحد الطرائق لمعالجة ما سبق هو توسيع (extend) الصنف Hand الموجود بإضافة تابع (method) يَحسِب قيمة اليد وفقًا للعبة بلاك جاك. اُنظر تعريف (definition) الصنف: public class BlackjackHand extends Hand { /** * Computes and returns the value of this hand in the game * of Blackjack. */ public int getBlackjackValue() { int val; // قيمة اليد boolean ace; int cards; // عدد ورق اللعب باليد val = 0; ace = false; cards = getCardCount(); // تابع معرف بالصنف الأعلى for ( int i = 0; i < cards; i++ ) { // أضف قيمة ورقة اللعب الحالية Card card; // ورقة اللعب الحالية int cardVal; // قيمة بلاك جاك لورقة اللعب الحالية card = getCard(i); cardVal = card.getValue(); // قيمة ورقة اللعب if (cardVal > 10) { cardVal = 10; } if (cardVal == 1) { ace = true; // هناك على الأقل ورقة آص } val = val + cardVal; } if ( ace == true && val + 10 <= 21 ) val = val + 10; return val; } // نهاية getBlackjackValue } // نهاية الصنف BlackjackHand لمّا كان الصَنْف BlackjackHand هو صَنْف فرعي (subclass) من الصَنْف الأعلى Hand، فإن أيّ كائن من الصَنْف الفرعي BlackjackHand سيَتَضمَّن، إلى جانب تابع النسخة getBlackjackValue() المُعرَّف ضِمْن صَنْفه الفرعي، جميع مُتْغيِّرات النسخ (instance variables) وتوابع النسخ (instance methods) المُعرَّفة بالصَنْف الأعلى Hand. فمثلًا، إذا كان bjh مُتْغيِّرًا من النوع BlackjackHand، يَصِِح اِستخدَام أي من الاستدعاءات التالية: bjh.getCardCount() و bjh.removeCard(0) و bjh.getBlackjackValue(). على الرغم من أن التابعين الأول والثاني مُعرَّفين بالصَنْف Hand، إلا أنهما قد وُرِثَا إلى الصَنْف BlackjackHand. تُورَث المُتْغيِّرات والتوابع من الصَنْف الأعلى Hand إلى الصَنْف الفرعي BlackjackHand، ولهذا تستطيع اِستخدَامها بتعريف الصَنْف الفرعي BlackjackHand كما لو كانت مُعرَّفة فعليًا بذلك الصَنْف، باستثناء تلكم المُعرَّفة باِستخدَام المُبدِّل private ضِمْن الصَنْف الأعلى؛ حيث يَمنَع ذلك المُبدِّل الوصول إليها حتى من الأصناف الفرعية (subclasses). مثلًا، بتعريف التابع getBlackjackValue() -بالأعلى-، تَمَكَّنت التَعْليمَة cards = getCardCount(); من استدعاء تابع النسخة getCardCount() المُعرَّف بالصَنْف Hand. يُساعد تَمْديد (extend) الأصناف على الاعتماد على أعمالك السابقة، وفي الواقع لقد كُتبَت الكثير من الأصناف القياسية (standard classes) خصيصًا لتَكُون قاعدةً وأساسًا لإنشاء أصناف فرعية. تُستخدَم مُبدِّلات الوصول (access modifiers) مثل public و private للتَحكُّم بالوصول إلى أعضاء (members) الصَنْف. عندما نَأخُذ الأصناف الفرعية (subclasses) بالحُسبان، يُصبِح من المناسب الحديث عن مُبدِّل وصول آخر تُوفِّره الجافا، هو protected. عند اِستخدَام ذلك المُبدِّل أثناء التَّصْريح عن تابع (method) أو مُتْغيِّر عضو (member variable) بصَنْف، يُصبِح اِستخدَام ذلك العضو مُمكنًا ضِمْن الأصناف الفرعية لذلك الصَنْف وليس بأي مكان آخر. هنالك استثناء: عندما تَستخدِم المُبدِّل protected للتّصريح عن عضو (member)، فإنه يَكُون قابلًا للوصول من أي صَنْف بنفس الحزمة (package). ذَكَرَنا من قَبْل أنه في حالة عدم تخصيص أي مُبدِّل وصول (access modifier) لعضو معين، فإنه يُصبِح قابلًا للوصول من جميع الأصناف الواقعة ضِمْن نفس الحزمة وليس بأي مكان آخر. يُعدّ المُبدَّل protected أكثر تحررًا من ذلك بقليل؛ فبالإضافة إلى الأصناف الواقعة ضِمْن نفس الحزمة، فإنه يجعل العضو أيضًا قابلًا للوصول من الأصناف الفرعية (subclasses) التي لا تقع ضِمْن نفس الحزمة. عندما تُصرِّح عن تابع أو مُتْغيِّر عضو باستخدام المُبدِّل protected، يُصبِح ذلك العضو جزءًا من تّنْفيذ (implementation) الصنف لا واجهته (interface) العامة، كما يُسمَح للأصناف الفرعية (subclasses) باِستخدَام ذلك الجزء من التّنْفيذ (implementation) أو تَعْدِيله. على سبيل المثال، يَتَضمَّن الصَنْف PairOfDice مُتْغيِّري النسخة die1 و die2 لتمثيل الأعداد الظاهرة على حجرى نَّرد. قد نُصرِّح عنهما باِستخدَام المُبدِّل private لكي يُصبِح تَعْديل قيمتهما من خارج الصنف مستحيلًا، ثُمَّ نُعرِّف توابع جَلْب (getter methods) للسماح بقراءة قيمتهما من خارج الصنف، لكن لو تَبيَّن لك إمكانية الحاجة لاِستخدَام الصنف PairOfDice لإنشاء أصناف فرعية (subclasses)، فقد يَكُون من الأفضل حينها السماح لتلك الأصناف الفرعية (subclasses) بتَعْديل قيم تلك الأعداد، أي تعديل قيم تلك المُتْغيِّرات. فمثلًا، يَرسم الصنف الفرعي GraphicalDice حجري النَّرد، وقد يَضطرّ إلى تعديل قيم تلك الأعداد بتوقيت غَيْر ذلك الذي يُرمَي فيه حجري النَّرد. بدلًا من اِستخدَام المُبدِّل public في تلك الحالة، قد نَستخدِم المُبدِّل protected للتَّصْريح عن كُلًا من die1 و die2، مما سيَسمَح للأصناف الفرعية فقط -دون العالم الخارجي- بتَعْديل قيمتهما. كبديل، قد تُفضِّل تعريف توابع ضَبْط (setter methods) لتلك المُتْغيِّرات ضِمْن الصنف الأعلى، بحيث يُصرَّح عنها باِستخدَام المُبدِّل protected، وذلك لكي تَتَمكَّن من التَأكُّد من أن القيمة المطلوب إِسْنادها للمُتْغيِّر تقع ضِمْن نطاق يتراوح بين ١ و ٦. الوراثة (inheritance) وسلالة أصناف (class hierarchy) يُمكِن لصَنْف معين أن يَرِث جزء أو كل بِنيته (structure)، وسلوكه (behavior) من صَنْف آخر، وهو ما يُعرَف باسم الوراثة (inheritance). يُقال عن الصنف الوَارِث أنه صنفًا فرعيًا (subclass) من الصنف المَورُوث. إذا كان الصنف B هو صنف فرعي من الصنف A، فإننا نستطيع أيضًا أن نقول أن الصنف A هو صنف أعلى (superclass) من الصنف B. قد يُضيف الصنف الفرعي إلى ما وَرِثه من بنية وسلوك، كما قد يُعدِّل أو يَستبدِل ما وَرِثه من سلوك. تَستخدِم بعض اللغات البرمجية الآخرى، مثل لغة C++، مصطلحات أخرى كالصنف المُشتقّ (derived class) والصنف الأساسي (base class) بدلًا من الصنف الفرعي (subclass) والصنف الأعلى (superclass). عادةً ما تُوضَح العلاقة بين الصنف الفرعي (subclass) والصنف الأعلى (superclass) برسم توضيحي، يقع فيه الصنف الفرعي (subclass) أسفل صنفه الأعلى (superclass) ويَكُون مُتصلًا به، تمامًا كما هو مُوضَح بيسار الصورة التالية: تُستخدَم الشيفرة التالية لإنشاء صَنْف اسمه B بحيث يكون صنفًا فرعيًا من صنف آخر اسمه A: class B extends A { . . // إضافات أو تعديلات على الموروث من الصنف A . } يُمكِنك أن تُصرِّح عن عدة أصناف على أساس كَوْنها أصناف فرعية (subclasses) من نفس الصنف الأعلى (superclass)، وتُعدّ الأصناف الفرعية في تلك الحالة "أصناف أخوة (sibling classes)" تَتَشارَك بعضًا من بِنيتها (structure) وسلوكها (behavior)، تَحْديدًا تلك المُوروثة من الصَنْف الأعلى المشترك، فمثلًا، الأصناف B و C و D بيمين الصورة السابقة هي أصناف أخوة (sibling classes). يُمكِن للوراثة (inheritance) أن تتمدَّد أيضًا عَبْر أجيال من الأصناف، فمثلًا، الصنف E -بالصورة السابقة- هو صنف فرعي من الصنف D، والذي هو بدوره صنف فرعي من الصنف A، ويُعدّ الصنف E عندها صنفًا فرعيًا من الصنف A حتى وإن لَمْ يَكُن صنفًا فرعيًا مباشرًا. تُشكِّل مجموعة الأصناف تلك سُلالة أصناف (class hierarchy) صغيرة. مثال والآن، لنَفْترِض أننا نريد كتابة برنامج ينبغي له التَعامُل مع المركبات المتحركة كالسيارات، والشاحنات، والدراجات النارية (قد يَستخدِمه قسم المركبات المتحركة لأغراض تَعقُّب التسجيلات). يُمكِننا أن نُعرِّف صَنْفًا اسمه Vehicle لتمثيل جميع أنواع المركبات، ولأن السيارات والشاحنات والدراجات النارية هي أنواع من المركبات، يُمكِننا تمثيلها باستخدام أصناف فرعية (subclasses) من الصنف Vehicle، كما هو مُوضَح بسُلالة الأصناف (class hierarchy) التالية: قد يَتَضمَّن الصنف Vehicle مُتْغيِّرات نُسخ (instance variables) مثل registrationNumber و owner، بالإضافة إلى توابع نُسخ (instance methods) مثل transferOwnership(). ستَملُك جميع المركبات تلك المُتْغيِّرات والتوابع. نستطيع الآن تعريف الأصناف الفرعية (subclasses) الثلاثة Car و Truck و Motorcycle المُشتقّة من الصَنْف Vehicle بحيث يَحمِل كُلًا منها مُتْغيِّرات وتوابع تَخُصّ ذلك النوع المُحدَّد من المركبات، فمثلًا، قد يُعرَّف الصنف Car مُتْغيِّر نسخة numberOfDoors، بينما قد يُعرَّف الصنف Truck مُتْغيِّر نسخة numberOfAxles، وقد يُضيف الصنف Motorcycle مُتْغيِّرًا منطقيًا اسمه hasSidecar. يُمكِننا التَّصْريح عن تلك الأصناف كالتالي (ببرنامج حقيقي، ستُعرَّف تلك الأصناف عادةً بملفات مُنفصلة وعلى أساس كَوْنها عامة): class Vehicle { int registrationNumber; Person owner; // (بفرض تعريف الصنف Person) void transferOwnership(Person newOwner) { . . . } . . . } class Car extends Vehicle { int numberOfDoors; . . . } class Truck extends Vehicle { int numberOfAxles; . . . } class Motorcycle extends Vehicle { boolean hasSidecar; . . . } لنَفْترِض أن myCar هو مُتْغيِّر من النوع Car صُرِّح عنه وهُيئ باِستخدَام التَعْليمَة التالية: Car myCar = new Car(); الآن، نستطيع الإشارة إلى myCar.numberOfDoors؛ لأن numberOfDoors مُتْغيِّر نسخة بالصنف Car. بالإضافة إلى ذلك، لمّا كان الصنف Car مُشتقًّا من الصنف Vehicle، فإن أي كائن من الصنف Car يَملُك بنية وسلوك الكائنات من الصنف Vehicle، مما يعني إمكانية الإشارة إلى كُلًا من myCar.registrationNumber و myCar.owner و myCar.transferOwnership(). بالعالم الحقيقي، تُعدّ كُلًا من السيارات والشاحنات والدراجات النارية مركبات، وهو ما أَصبَح مُتحقِّقًا بالبرنامج أيضًا، فأيّ كائن من النوع Car أو Truck أو Motorcycle يُعدّ كذلك كائنًا من النوع Vehicle تلقائيًا. يَقُودنا ذلك إلى الحقيقة الهامة التالية: إذا أمكَّن لمُتْغيِّر حَمْل مَرجِع إلى كائن من الصَنْف A، فحتمًا بإِمكانه حَمْل مَرجِع إلى الكائنات المُنتمية لأيّ من أصناف A الفرعية. عمليًا، يَعنِي ذلك إمكانية إِسْناد كائن من الصنف Car إلى مُتْغيِّر من النوع Vehicle، كالتالي: Vehicle myVehicle = myCar; أو كالتالي: Vehicle myVehicle = new Car(); بَعْد إجراء أيّ من التَعْليمَتين -بالأعلى-، سيَحمِل المُتْغيِّر myVehicle مَرجِعًا (reference) إلى كائن من النوع Vehicle، والذي هو في حقيقته نُسخة (instance) من الصَنْف الفرعي Car. يُدرك الكائن حقيقة انتماءه للصنف Car وليس فقط للصنف Vehicle، كما تُخزَّن تلك المعلومة -أيّ صنف الكائن الفعليّ- كجزء من الكائن نفسه، وتستطيع حتى اختبار ما إذا كان كائن معين ينتمي إلى أحد الأصناف عن طريق العَامِل instanceof، كالتالي: if (myVehicle instanceof Car) ... تَفْحَص التَعْليمَة -بالأعلى- ما إذا كان الكائن المُشار إليه باستخدام المُتْغيِّر myVehicle هو بالأساس من النوع Car. في المقابل، لا تَصِح تَعْليمَة الإِسْناد (assignment) التالية: myCar = myVehicle; // خطأ لأن myVehicle قد يُشير إلى أنواع آخرى غَيْر النوع Car. يُشبه ذلك المشكلة التي تَعرَّضنا لها بالقسم الفرعي ٢.٥.٦: لن يَسمَح الحاسوب بإِسْناد قيمة من النوع int إلى مُتْغيِّر من النوع short؛ لأن ليس كل int هو short بالضرورة. بالمثل، فإنه لن يَسمَح بإِسْناد قيمة من النوع Vehicle إلى مُتْغيِّر من النوع Car؛ لأن ليس كل كائن من النوع Vehicle هو كائن من النوع Car بالضرورة. بإِمكانك تَجاوُز تلك المشكلة عن طريق إجراء عملية التَحْوِيل بين الأنواع (type-casting)، فمثلًا، إذا كنت تَعلَم -بطريقة ما- أن myVehicle يُشير فعليًا إلى كائن من النوع Car، تستطيع اِستخدَام (Car)myVehicle لإجبار الحاسوب على التَعامُل مع myVehicle كما لو كان من النوع Car. لذا تستطيع كتابة التالي: myCar = (Car)myVehicle; تستطيع حتى الإشارة إلى ((Car)myVehicle).numberOfDoors. لاحِظ أن الأقواس ضرورية لأسباب تَتَعلَّق بالأولوية (precedence)؛ فالعَامِل . لديه أولوية أكبر من عملية إجراء التَحْوِيل بين الأنواع (type-cast) أيّ أن التعبير (Car)myVehicle.numberOfDoors سيُقرأ كما لو كان مَكْتُوبًا على الصورة (Car)(myVehicle.numberOfDoors) وستُجرَى عندها محاولة للتَحْوِيل من النوع int إلى Vehicle وهو أمر مستحيل. سنَفْحَص الآن مثالًا لكيفية اِستخدَام ما سبق ضِمْن برنامج. لنَفْترِض أننا نُريد طباعة البيانات المُتعلّقة بالكائن المُشار إليه باِستخدَام المُتْغيِّر myVehicle، فمثلًا، نَطبَع numberOfDoors إذا كان الكائن من النوع Car. لا نستطيع ببساطة الإشارة إليه باستخدام myVehicle.numberOfDoors؛ لأن الصَنْف Vehicle لا يَتَضمَّن أي numberOfDoors، وإنما نستطيع كتابة التالي: System.out.println("Vehicle Data:"); System.out.println("Registration number: " + myVehicle.registrationNumber); if (myVehicle instanceof Car) { System.out.println("Type of vehicle: Car"); Car c; c = (Car)myVehicle; // تحويل للنوع للوصول إلى numberOfDoors System.out.println("Number of doors: " + c.numberOfDoors); } else if (myVehicle instanceof Truck) { System.out.println("Type of vehicle: Truck"); Truck t; t = (Truck)myVehicle; // تحويل للنوع للوصول إلى numberOfAxles System.out.println("Number of axles: " + t.numberOfAxles); } else if (myVehicle instanceof Motorcycle) { System.out.println("Type of vehicle: Motorcycle"); Motorcycle m; m = (Motorcycle)myVehicle; // تحويل للنوع للوصول إلى hasSidecar System.out.println("Has a sidecar: " + m.hasSidecar); } فيما هو مُتعلِّق بالأنواع الكائنية (object type)، عندما يُجرِي الحاسوب عملية التَحْويل بين الأنواع (type cast)، فإنه يَفْحص أولًا ما إذا كانت تلك العملية صالحة أم لا، فمثلًا، إذا كان myVehicle يُشير إلى كائن من النوع Truck، فلا تَصِح العملية (Car)myVehicle، وعليه، سيُبلَّغ عن اعتراض (exception) من النوع ClassCastException، ولهذا تَستخدِم الشيفرة -بالأعلى- العَامِل instanceof لاختبار نوع المُتْغيِّر قَبْل إجراء عملية التحويل بين الأنواع (type cast)؛ لتتجنَّب حُدوث الاعتراض ClassCastException. لاحِظ أن هذا الفَحْص يَحدُث وقت التنفيذ (run time) وليس وقت التصريف (compile time)؛ لأن النوع الفعليّ للكائن الذي يُشير إليه المُتْغيِّر myVehicle لا يَكُون معروفًا وقت تصريف البرنامج. التعددية الشكلية (polymorphism) لنَفْحص برنامجًا يَتعامَل مع أشكال تُرسَم على الشاشة، والتي قد تَكُون مستطيلة (rectangles)، أو بيضاوية (ovals)، أو مستطيلة بأركان دائرية (roundrect)، ومُلوَّنة جميعًا بألوان مختلفة. سنَستخدِم الأصناف الثلاثة Rectangle و Oval و RoundRect لتمثيل الأشكال المختلفة بحيث تَرِث تلك الأصناف من صَنْف أعلى (superclass) مُشترك Shape، وذلك لتمثيل السمات المشتركة بين جميع الأشكال. فمثلًا، قد يَتَضمَّن الصنف Shape مُتْغيِّرات نُسخ (instance variables) لتمثيل كُلًا من لون الشكل، وموضعه، وحجمه، كما قد يَتَضمَّن توابع نُسخ (instance methods) لتَعْديل قيم تلك السمات. مثلًا، لتَعْديل لون أحد الأشكال، فإننا سنُعدِّل قيمة مُتْغيِّر نُسخة ثم نُعيد رسم الشكل بلونه الجديد: class Shape { Color color; // لابد من استيرادها من حزمة javafx.scene.paint void setColor(Color newColor) { // تابع لتغيير لون الشكل color = newColor; // غير قيمة متغير النسخة redraw(); // أعد رسم الشكل ليظهر باللون الجديد } void redraw() { // تابع لرسم الشكل ? ? ? // ما الذي ينبغي كتابته هنا؟ } . . . // المزيد من متغيرات النسخ والتوابع } // نهاية الصنف Shape يُرسَم كل نوع من تلك الأشكال بطريقة مختلفة، ولهذا ستُواجهنا مشكلة بخصوص التابع redraw(). نستطيع عمومًا استدعاء التابع setColor() لأي نوع من الأشكال، وبالتالي سيَحتَاج الحاسوب إلى تَّنْفيذ الاستدعاء redraw()، فكيف له إذًا أن يَعرِف الشكل الذي ينبغي عليه رَسْمه؟ نظريًا، يَطلُب الحاسوب من الشكل نفسه أن يقوم بعملية الرسم، فبالنهاية، يُفْترَض أن يَكُون كل كائن من النوع Shape على علم بما عليه القيام به لكي يَرسِم نفسه. عمليًا، يَعنِي ذلك أن كل صنف من الأصناف الثلاثة ينبغي له أن يُعرِّف نسخته الخاصة من التابع redraw()، كالتالي: class Rectangle extends Shape { void redraw() { . . . // تعليمات رسم مستطيل } . . . // المزيد من توابع ومتغيرات النسخ } class Oval extends Shape { void redraw() { . . . // تعليمات رسم شكل بيضاوي } . . . // المزيد من توابع ومتغيرات النسخ } class RoundRect extends Shape { void redraw() { . . . // تعليمات رسم مستطيل دائري الأركان } . . . // المزيد من توابع ومتغيرات النسخ } لنَفْترِض أن someShape هو مُتْغيِّر من النوع Shape، أيّ أنه يستطيع الإشارة إلى أيّ كائن ينتمي لأيّ من الأنواع Rectangle و Oval و RoundRect. أثناء تَّنْفيذ البرنامج، قد تَتغيَّر قيمة المُتْغيِّر someShape، مما يَعنِي أنه قد يُشير إلى كائنات من أنواع مختلفة بأوقات مختلفة! لذا، عندما تُنفَّذ التَعْليمَة التالية: someShape.redraw(); فإن نسخة التابع redraw المُستدعَاة فعليًا تَكُون تلك المُتناسبة مع النوع الفعليّ للكائن الذي يُشير إليه المُتْغيِّر someShape. بالنظر إلى نص الشيفرة فقط، قد لا تَتَمكَّن حتى من مَعرِفة الشكل الذي ستَرسِمه تلك التَعْليمَة حيث يعتمد ذلك بالأساس على القيمة التي تَصادَف أن احتواها someShape أثناء تَّنْفيذ البرنامج. علاوة على ذلك، لنَفْترِض أن تلك التَعْليمَة مُضمَّنة بحَلْقة تَكْرار (loop) وتُنفَّذ عدة مرات. إذا كانت قيمة someShape تتغيَّر بينما تُنفَّذ حَلْقة التَكْرار، فقد تَستدعِي نفس تلك التَعْليمَة someShape.redraw(); -أثناء تَّنْفيذها عدة مرات- توابعًا (methods) مختلفة، وتَرسِم أنواعًا مختلفة من الأشكال. يُقال عندها أن التابع redraw() هو مُتعدِّد الأشكال (polymorphic). في العموم، يُعدّ تابع معين مُتعدد الأشكال (polymorphic) إذا كان ما يُنفِّذه ذلك التابع مُعتمدًا على النوع الفعليّ (actual type) للكائن المُطبقّ عليه التابع أثناء وقت التَّنْفيذ (run time)، وتُعدّ التعددية الشكلية (polymorphism) واحدة من أهم السمات المميزة للبرمجة كائنية التوجه (object-oriented programming). تَتضِح الصورة أكثر إذا كان لديك مصفوفة أشكال (array of shapes). لنَفْترِض أن shapelist هو مُتْغيِّر من النوع Shape[] يُشير إلى مصفوفة قد أُنشأت وهُيأت عناصرها بمجموعة كائنات، والتي قد تَكُون من النوع Rectangle أو Oval أو RoundRect. نستطيع رسم جميع الأشكال بالمصفوفة بكتابة التالي: for (int i = 0; i < shapelist.length; i++ ) { Shape shape = shapelist[i]; shape.redraw(); } بينما تُنفَّذ الحلقة (loop) -بالأعلى-، فإن التَعْليمَة shape.redraw() قد تَرسِم مستطيلًا أو شكلًا بيضاويًا أو مستطيلًا دائري الأركان اعتمادًا على نوع الكائن الذي يُشير إليه عنصر المصفوفة برقم المَوضِع i. ربما تَتضِح الفكرة أكثر إذا بدّلنا المصطلحات قليلًا كالتالي: بالبرمجة كائنية التوجه (object-oriented programming)، تُعدّ تَعْليمَة استدعاء أي تابع بمثابة إرسال رسالة إلى كائن، بحيث يَرُد ذلك الكائن على تلك الرسالة بتَّنْفيذ التابع المناسب. مثلًا، التَعْليمَة someShape.redraw(); هي بمثابة رسالة إلى الكائن المُشار إليه باِستخدَام someShape، ولمّا كان هذا الكائن يَعرِف بالضرورة النوع الذي ينتمي إليه، فإنه يَعرِف الكيفية التي ينبغي أن يَرُد بها على تلك الرسالة. وفقًا لذلك، يُنفِّذ الحاسوب الاستدعاء someShape.redraw(); بنفس الطريقة دائمًا، أيّ يُرسِل رسالة يَعتمِد الرد عليها على المُستقبِل. تَكُون الكائنات (objects) عندها بمثابة كيانات نَشِطة تُرسِل الرسائل وتَستقبِلها، كما تَكُون التعددية الشكلية (polymorphism) -وفقًا لهذا التصور- جزءًا طبيعًيا، بل وضروريًا، فتَعنِي فقط أنه من الممكن لكائنات مختلفة الرد على نفس الرسالة بطرائق مختلفة. تَسمَح لك التعددية الشكلية (polymorphism) بكتابة شيفرة تُنفِّذ أشياء قد لا تَتَصوَّرها حتى أثناء الكتابة، وهو في الواقع أحد أكثر الأشياء الرائعة المُتعلِّقة بها. لنَفْترِض مثلًا أنه قد تَقرَّر إضافة مستطيلات مشطوفة الأركان (beveled rectangles) إلى أنواع الأشكال التي يُمكِن للبرنامج التَعامُل معها. لتَّنْفيذ (implement) المستطيلات مشطوفة الأركان، يُمكِننا أن نُضيف صنفًا فرعيًا (subclass) جديدًا، وليَكُن BeveledRect، مُشتقًّا من الصنف Shape، ثم نُعرِّف به نسخته من التابع redraw(). تلقائيًا، ستَجِدْ أن الشيفرة التي كنا قد كتبناها سابقًا، أيّ someShape.redraw()، قد أَصبَح بإِمكانها فجأة رسم المستطيلات مشطوفة الأركان على الرغم من أن صَنْف تلك المستطيلات لم يَكُن موجودًا من الأساس أثناء كتابة تلك التَعْليمَة. تُرسَل الرسالة redraw إلى الكائن someShape أثناء تَّنْفيذ التَعْليمَة someShape.redraw();. لنَفْحَص مرة آخرى التابع المسئول عن تَعْديل لون الشكل والمُعرَّف بالصنف Shape: void setColor(Color newColor) { color = newColor; // غير قيمة متغير النسخة redraw(); // أعد رسم الشكل ليظهر باللون الجديد } بالأعلى، تُرسَل الرسالة redraw ولكن إلى أي كائن؟ حسنًا، يُعدّ التابع setColor هو الآخر بمثابة رسالة كانت قد أُرْسِلَت إلى كائن معين. لذا، فإن الرسالة redraw تُرسَل إلى نفس ذلك الكائن الذي كان قد اِستقبَل الرسالة setColor. إذا كان هذا الكائن مستطيلًا، فإنه يَحتوِي على تابع redraw() لرسم المستطيلات وذاك التابع هو ما سيُنفَّذ. أما إذا كان هذا الكائن شكلًا بيضاويًا، فسيُنفَّذ التابع redraw() المُعرَّف بالصنف Oval. يَعنِي ذلك أنه ليس من الضروري لتَعْليمَة redraw(); بالتابع setColor() أن تَستدعِي التابع redraw() المُعرَّف بالصنف Shape، وإنما قد يُنفَّذ أي تابع redraw() طالما كان مُعرَّفًا بصنف فرعي مشتق من Shape. هذه هي فقط حالة آخرى من التعددية الشكلية (polymorphism). الأصناف المجردة (abstract classes) وقتما يَضطرّ كائن من الصنف Rectangle أو Oval أو RoundRect إلى رَسْم نفسه، سيُنفَّذ التابع redraw() المُعرَّف بالصنف المناسب، وهو ما يَقُودنا للسؤال التالي: ما دور التابع redraw() المُعرَّف بالصنف Shape؟ وكيف لنا أن نُعرِّفه؟ قد تتفاجئ من الإجابة، ففي الحقيقة ينبغي أن نَترُكه فارغًا لأن الصنف Shape لا يُمثِل سوى فكرة مُجردة (abstract) لشكل، وبالتأكيد ليس هناك طريقة لرسم شيء كذلك، وإنما يُمكِن فقط رَسْم الأشكال الحقيقية (concrete) كالمستطيلات والأشكال البيضاوية. إذًا، لماذا ينبغي أن نُعرِّف التابع redraw() بالصنف Shape من الأساس؟ حسنًا، نحن مُضطرون إلى ذلك وإلا لن يَصِح استدعائه بالتابع setColor() المُعرَّف بالصنف Shape كما لن يَصِح كتابة شيئًا مثل someShape.redraw();؛ حيث سيَعترِض المُصرِّف لكَوْن someShape مُتْغيِّرًا من النوع Shape في حين لا يَحتوِي ذلك الصَنْف على التابع redraw(). وعليه، لن يُستدعَى التابع redraw() المُعرَّف بالصنف Shape نهائيًا، وربما -إذا فكرت بالأمر- ستَجِدْ أنه ليس هناك أي سبب قد يَضطرّنا حتى إلى إنشاء كائن فعليّ من الصنف Shape. تستطيع بالتأكيد إنشاء مُتْغيِّرات من النوع Shape، ولكنها دومًا ستُشير إلى كائنات (objects) من الأصناف الفرعية (subclasses) للصَنْف Shape. في مثل تلك الحالات، يُعدّ الصنف Shape صنفًا مُجرَّدًا (abstract class). لا تُستخدَم الأصناف المُجرّدة (abstract class) لإنشاء كائنات، وإنما فقط تُمثِل قاعدة وأساسًا لإنشاء أصناف فرعية أيّ أنها موجودة فقط للتعبير عن بعض السمات والخاصيات المشتركة بجميع أصنافها الفرعية (subclasses). أما الصَنْف غَيْر المُجرّد فيُعدّ صنفًا حقيقيًا (concrete class). في العموم، تستطيع إنشاء كائنات تنتمي لأصناف حقيقية (concrete class) وليس لأصناف مُجرّدة (abstract class)، كما يستطيع مُتْغيِّر من نوع هو عبارة عن صنف مُجرّد (abstract class) أن يُشير فقط إلى كائنات تنتمي لأحد الأصناف الفرعية الحقيقية (concrete subclasses) المُشتقّة من ذلك الصنف المُجرّد (abstract class). بالمثل، يُعدّ التابع redraw() المُعرَّف بالصنف Shape تابعًا مجردًا (abstract method)؛ فهو لا يُستدعَى نهائيًا وإنما تُستدعَى توابع redraw() المُعرَّفة بالأصناف الفرعية للصنف Shape لرَسْم الأشكال بصورة فعليّة. اِضطرّرنا مع ذلك لتعريف التابع redraw() بالصَنْف Shape للتأكيد على فِهم جميع الأشكال للرسالة redraw، أي أنه موجود فقط لتَخْصِيص الواجهة (interface) المُشتركة لنُسخ التابع redraw() الفعليّة والحقيقية (concrete) والمُعرَّفة بالأصناف الفرعية، وبالتالي ليس هناك ما يَستدعِي احتواء التابع المُجرَّد redraw() بالصنف Shape على أيّ شيفرة. كما ذَكَرَنا سابقًا، يُعدّ كُلًا من الصنف Shape وتابعه redraw() بمثابة أفكارًا مجردةً (abstract) على نحو دلالي فقط حتى الآن، ولكن تستطيع إِعلام الحاسوب بهذه الحقيقة على نحو صياغي (syntactically) أيضًا عن طريق إضافة المُبدِّل abstract إلى تعريف (definition) كُلًا منهما. بخلاف أي تابع عادي، لا يُكتَب الجزء التنفيذي (implementation) للتوابع المُجرّدة (abstract method) وإنما تُستخدَم فاصلة منقوطة (semicolon). في المقابل، لابُدّ من كتابة الجزء التنفيذي (implementation) للتابع المُجرّد (abstract method) ضِمْن أي صَنْف فرعي حقيقي (concrete subclass) مُشتقّ من الصنف المُجرّد (abstract class). تَستعرِض الشيفرة التالية طريقة تعريف الصَنْف Shape كصنف مُجرّد (abstract class): public abstract class Shape { Color color; // لون الشكل void setColor(Color newColor) { // تابع لتغيير لون الشكل color = newColor; // غير قيمة متغير النسخة redraw(); // أعد رسم الشكل ليظهر باللون الجديد } abstract void redraw(); // تابع مجرد ينبغي أن يُعرَّف بالأصناف الفرعية الحقيقية . . . // المزيد من التوابع والنسخ } // نهاية الصنف Shape بمُجرّد اِستخدَامك للمُبدِّل abstract أثناء التَّصْريح عن الصَنْف، لا تستطيع إنشاء كائنات فعليّة من النوع Shape، وسيُبلِّغ الحاسوب عن حُدوث خطأ في بناء الجملة (syntax error) إذا حاولت القيام بذلك. ربما يُعدّ الصَنْف Vehicle -الذي ناقشناه بالأعلى- صنفًا مجردًا (abstract class) أيضًا، فليس هناك أي طريقة لامتلاك مركبة كتلك، وإنما لابُدّ للمركبة الفعليّة أن تَكُون سيارة أو شاحنة أو دراجة نارية أو حتى أي نوع حقيقي (concrete) آخر. ذَكَرنا بالقسم الفرعي ٥.٣.٢ أن أيّ صنف لا يُصرَّح عن كَوْنه صنف فرعي (subclass) من أيّ صنف آخر -أيّ بدون الجزء extends- فإنه تلقائيًا يُصبِح صنفًا فرعيًا من الصنف القياسي Object كالتالي: public class myClass { . . . يُعدّ التَّصْريح بالأعلى مُكافئًا للتالي تمامًا: public class myClass extends Object { . . . يُمكِننا القول إذًا أن الصَنْف Object يقع بقمة سُلالة أصناف (class hierarchy) ضخمة تَتَضمَّن جميع الأصناف الآخرى. من الناحية الدلالية، يُعدّ الصنف Object صنفًا مجردًا (abstract class) بل ربما أكثر صنف مجرد على الإطلاق، ومع ذلك، لم يُصرَّح عنه على هذا الأساس من الناحية الصياغية، أيّ تستطيع إنشاء كائنات من النوع Object ولكن لن تَجِدْ الكثير لتفعله بها. ولأن أيّ صَنْف هو بالضرورة صنف فرعي (subclass) من الصَنْف Object، فيُمكِن لمُتْغيِّر من النوع Object أن يُشير إلى أي كائن مهما كان نوعه. بالمثل، تستطيع مصفوفة من النوع Object[] أن تَحمِل كائنات من أي نوع. تَستخدِم الشيفرة المصدرية بالملف ShapeDraw.java الصَنْف المُجرّد Shape، بالإضافة إلى مصفوفة من النوع Shape[] لحَمْل قائمة من الأشكال. قد تَرغَب بإلقاء نظرة على ذلك الملف مع أنك لن تَتَمكَّن من فهمه بالكامل حاليًا؛ فتعريفات الأصناف الفرعية بالملف مختلفة نوعًا ما عن تلك التي رأيتها بالأعلى. مثلًا، يَستقبِل التابع draw() مُعامِلًا (parameter) من النوع GraphicsContext؛ لأن الرَسْم بالجافا يَتطلَّب سياقًا رُسوميًا. سنَتَعرَّض لأمثلة شبيهة بالفصول اللاحقة بعدما تَتَعرَف على برمجة واجهات المُستخدِم الرُسومية (GUI). يُمكِنك مع ذلك فَحْص تعريف الصنف Shape وأصنافه الفرعية (subclasses) بالإضافة إلى طريقة اِستخدَام المصفوفة لحَمْل قائمة الأشكال. تَستعرِض الصورة التالية لقطة للشاشة أثناء تَشْغِيل البرنامج: بعد تَشْغِيل البرنامج ShapeDraw، تستطيع أن تُضيف شكلًا جديدًا إلى الصورة عن طريق الضغط على أيّ من الأزرار الموجودة بالأسفل. يُمكِنك أيضًا اختيار لون ذلك الشكل من خلال قائمة الألوان الموجودة أسفل مساحة الرسم (drawing area). سيَظهَر الشكل الجديد بالركن الأيسر العلوي من مساحة الرسم (drawing area)، وبمُجرّد ظُهوره، تستطيع سَحْبه باستخدَام الفأرة. سيُحافِظ الشكل دومًا على ترتيبه بالنسبة لبقية الأشكال الموجودة على الشاشة حتى أثناء عملية السَحْب، ومع ذلك، تستطيع اختيار شكل معين ليُصبِح أمام جميع الأشكال الأخرى عن طريق الضغط باستمرار على المفتاح "shift" بينما تَضغَط على ذلك الشكل. في الحقيقة، يُستخدَم الصنف الفعليّ للشكل أثناء إضافته إلى الشاشة فقط. بعد ذلك، يُعالَج بالكامل على أساس كَوْنه شكلًا مجردًا (abstract). على سبيل المثال، يَتعامَل البرنامج (routine) المسئول عن عملية السَحْب مع مُتْغيِّرات من النوع Shape، ولا يُشير نهائيًا إلى أي من الأصناف الفرعية (subclasses). عندما يحتاج إلى رَسْم شكل، فإنه فقط يَستدعِي التابع draw أي أنه غَيْر مضطرّ لمَعرِفة طريقة رَسْم الشكل أو حتى مَعرِفة نوعه الفعليّ، وإنما تُوكَل عملية الرسم إلى الكائن ذاته. إذا أردت أن تُضيف نوعًا جديدًا من الأشكال إلى البرنامج، كل ما عليك القيام به هو الآتي: أولًا، ستُعرِّف صنفًا فرعيًا (subclass) جديدًا مُشتقًّا من Shape، ثم ستُضيف زرًا جديدًا وتُبرمجه بحيث يُضيف الشكل إلى الشاشة. ترجمة -بتصرّف- للقسم Section 5: Inheritance, Polymorphism, and Abstract Classes من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.1 نقطة
-
كما ذَكَرَنا سابقًا، الأسماء هي أحد أركان البرمجة الأساسية. تَتَضمَّن مسألة التَّصْريح (declaring) عن الأسماء واِستخدَامها الكثير من التفاصيل التي تَجنَّبنا غالبيتها إلى الآن، ولكن حان وقت الكشف عن كثير من تلك التفاصيل. حاول التركيز عمومًا على محتوى القسمين الفرعيين "التهيئة أثناء التصريح" و "الثوابت المسماة (named constants)"؛ لأننا سنُعيد الإشارة إليهما باستمرار. التهيئة (initialization) أثناء التصريح (declaration) عندما نُصرِّح عن مُتَغيِّر ما (variable declaration)، يُخصِّص الحاسوب مساحة من الذاكرة لذلك المُتَغيِّر، والتي ينبغي تَهيئتها (initialize) مبدئيًا، بحيث يَتَضمَّن ذلك المُتَغيِّر قيمة معينة قَبْل أي مُحاولة لاِستخدَامه ضِمْن تعبير (expression). عادةً ما ترى تَعْليمَة التَّصْريح (declaration) عن مُتَغيِّر محليّ معين (local variable) مَتبوعة بتَعْليمَة إِسْناد (assignment statement) لغرض تهيئة ذلك المُتَغيِّر مبدئيًا (initialization). على سبيل المثال: int count; // صرح عن المتغير count count = 0; // أسند القيمة 0 إلى المتغير count في الواقع، تستطيع تَضْمِين التهيئة المبدئية (initialization) للمُتَغيِّر بنفس تَعْليمَة التصريح (declaration statement)، أيّ يُمكِن اختصار التَعْليمَتين بالأعلى إلى التَعْليمَة التالية: int count = 0; // صرح عن المتغير count وأسند إليه القيمة 0 سيُنفِّذ الحاسوب ذلك على خطوتين: سيُصرِّح أولًا عن مُتَغيِّر اسمه count، ثم سيُسنِد القيمة صفر إلى ذلك المُتَغيِّر المُنشَئ للتو. ليس ضروريًا أن تَكُون قيمة المُتَغيِّر المبدئية ثابتة (constant)، وإِنما قد تَتَكوَّن من أي تعبير (expression). يُمكِنك أيضًا تهيئة عدة مُتَغيِّرات بتَعْليمَة تَّصْريح واحدة كالتالي: char firstInitial = 'D', secondInitial = 'E'; int x, y = 1; // صالح ولكن أسندت القيمة 1 إلى المتغير y فقط int N = 3, M = N+2; // صالح، هُيأت القيمة N قبل استخدامها لمّا كان المُتَغيِّر المُتحكِّم بحَلْقة معينة (loop control variable) غير مَعنِي بأي شيء يَقَع خارج تلك الحَلْقة عمومًا، كان من المنطقي التَّصْريح عنه ضِمْن ذلك الجزء من البرنامج الذي يَستخدِمُه فعليًا. ولهذا يَشيَع اِستخدَام التَعْليمَة المُختصرة بالأعلى ضِمْن حَلْقات التَكْرار for؛ حيث تُمكِّنك من التَّصْريح عن المُتَغيِّر المُتحكِّم بالحَلْقة، وتَهيئته مبدئيًا بمكان واحد ضِمْن الحَلْقة ذاتها. على سبيل المثال: for ( int i = 0; i < 10; i++ ) { System.out.println(i); } الشيفرة بالأعلى هي بالأساس اختصار لما يَلي: { int i; for ( i = 0; i < 10; i++ ) { System.out.println(i); } } أَضفنا زوجًا إضافيًا من الأقواس حول الحَلْقة -بالأعلى- للتأكيد على أن المُتَغيِّر i قد أصبح مُتَغيِّرًا محليًا (local) لتَعْليمَة الحَلْقة for، أيّ أنه لَمْ يَعُدْ موجودًا بعد انتهاء الحَلْقة. يُمكِنك أيضًا إجراء التهيئة المبدئية (initialize) للمُتَغيِّرات الأعضاء (member variable) أثناء تَّصْريحك عنها (declare)، كما هو الحال مع المُتَغيِّرات المحليّة (local variable). فمثلًا: public class Bank { private static double interestRate = 0.05; private static int maxWithdrawal = 200; . . // المزيد من المتغيرات والبرامج الفرعية . } عندما يُحمِّل مُفسِّر الجافا (Java interpreter) صنفًا معينًا، فإنه يُنشِئ أي مُتَغيِّر عضو ساكن (static member variable) ضِمْن ذلك الصنف، ويُهيِئ قيمته المبدئية (initialization). لمّا كانت تَعْليمَات التَّصْريح (declaration statements) هي النوع الوحيد من التَعْليمَات التي يُمكِن كتابتها خارج أيّ برنامج فرعي (subroutine)، وعليه فإن تَعْليمَات الإِسْناد (assignment statements) غير ممكنة الكتابة خارجها، كان لزامًا تهيئة المُتَغيِّرات الأعضاء الساكنة أثناء التَّصْريح عنها -إذا أردنا القيام بالتهيئة-، فهو في تلك الحالة ليس مُجرَّد اختصار لتَعْليمَة تَّصْريح (declaration) مَتبوعة بتَعْليمَة إِسناد (assignment statement) مثلما هو الحال مع المُتَغيِّرات المحليّة. لاحِظ عدم إِمكانية القيام بالتالي: public class Bank { private static double interestRate; // غير صالح! لا يمكن استخدام تعليمة إسناد خارج برنامج فرعي interestRate = 0.05; . . . لذلك، غالبًا ما يَحتوِي التَّصْريح عن مُتَغيِّر عضو (member variables) على قيمته المبدئية. وكما ذَكَرَنا سلفًا بالقسم الفرعي ٤.٢.٤، فإنه في حالة عدم تَخْصِيص قيمة مبدئية لمُتَغيِّر عضو معين (member variable)، تُسنَد إليه قيمة مبدئية افتراضية. على سبيل المثال، اِستخدَام التَعْليمَة static int count للتَّصْريح عن المُتَغيِّر العضو count هو مُكافِئ تمامًا للتَعْليمَة static int count = 0;. نستطيع أيضًا تهيئة مُتَغيِّرات المصفوفة (array variables) مبدئيًا. ولمّا كانت المصفوفة عمومًا مُكوَّنة من عدة عناصر وليس مُجرَّد قيمة وحيدة، تُستخدَم قائمة من القيم، مَفصولة بفاصلة (comma)، ومُحاطة بزوج من الأقواس؛ لتهيئة (initialize) المُتَغيِّرات من ذلك النوع، كالتالي: int[] smallPrimes = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 }; تُنشِئ التَعْليمَة بالأعلى مصفوفة أعداد صحيحة (array of int)، وتَملؤها بالقيم ضِمْن القائمة. يُحدِّد عدد العناصر بالقائمة طول المصفوفة المُعرَّفة، أيّ أن طولها (length) -في المثال بالأعلى- يُساوِي ١٠. تَقْتصِر صيغة (syntax) تهيئة المصفوفات -يُقصَد بذلك قائمة العناصر- على تَعْليمَات التَّصْريح (declaration statement)، أي بينما نُصرِّح عن مُتَغيِّر مصفوفة (array variable)، ولا يُمكِن اِستخدَامها ضِمْن تَعْليمَات الإِسْناد (assignment statements). تستطيع أيضًا إنشاء مصفوفة باِستخدَام العَامِل new، ثم تَستخدِمها لتهيئة مُتَغيِّر مصفوفة (array variable). لاحِظ أن تلك الصيغة صالحة ضِمْن تَعْليمَات الإِسْناد أيضًا على العَكْس من الصيغة السابقة. على سبيل المثال: String[] nameList = new String[100]; ستَحتوِي جميع عناصر المصفوفة على القيمة الافتراضية في المثال بالأعلى. التصريح عن المتغيرات باستخدام var منذ الإصدار العاشر، تُوفِّر الجافا صيغة (syntax) جديدة للتَّصْريح عن المُتَغيِّرات، باستخدَام كلمة var، بشَّرْط تَخْصيص قيمة مبدئية ضِمْن تَعْليمَة التَّصْريح. تمتلك المُتَغيِّرات المُصرَّح عنها بتلك الطريقة نوعًا محدَّدًا، كأي مُتَغيِّر عادي آخر، ولكن بدلًا من تَحْديد ذلك النوع تحديدًا صريحًا، يعتمد مُصرِّف الجافا (Java compiler) على نوع القيمة المبدئية المُخصَّصة لتَحْديد نوع ذلك المُتَغيِّر. تَقْتصِر تلك الصيغة، مع ذلك، على المُتَغيِّرات المحليّة (local variables)، أي تلك المُصرَّح عنها ضِمْن برنامج فرعي (انظر القسم الفرعي ٤.٢.٤). اُنظر تَعْليمَة التَّصْريح التالية: var interestRate = 0.05; تُستخدَم التَعْليمَة بالأعلى لتعريف (define) مُتَغيِّر محليّ اسمه interestRate بقيمة مبدئية تُساوِي ٠,٠٥، ولمّا كانت القيمة المبدئية المُخصَّصة من النوع double، يَكُون نوع ذلك المُتَغيِّر هو double. بالمثل، للتَّصْريح عن مُتَغيِّر محليّ اسمه nameList من النوع String[]، يُمكِنك كتابة التالي: var nameList = new String[100]; تستطيع أيضًا اِستخدَام كلمة var أثناء التَّصْريح عن المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) ضِمْن الحَلْقة for، كالتالي: for ( var i = 0; i < 10; i++ ) { System.out.println(i); } قد لا يبدو ذلك مفيدًا في الوقت الحالي، ولكنك ستُدرِك أهميته عندما نَتَعرَّض لما يُعرَف باسم الأنواع المُعمَّمة أو الأنواع المُحدَّدة بمعاملات نوع (parameterized types)، والتي تُعدّ أكثر تعقيدًا. سنتناول تلك الأنواع بكُلًا من القسم ٧.٢، والفصل العاشر. الثوابت المسماة (named constants) في بعض الأحيان، لا يَكُون هناك حاجة لتَعْديل قيمة المُتَغيِّر بَعْد تهيئتها مبدئيًا (initialize). على سبيل المثال، هُيَئ المُتَغيِّر interestRate بالأعلى، بحيث تَكُون قيمته المبدئية مُساوِية للقيمة ٠,٠٥. دَعْنَا نفْترِض أنه لا حاجة لتَعْديل تلك القيمة طوال فترة تَّنْفيذ البرنامج. في مثل تلك الحالات، قد يبدو المُتَغيِّر عديم الفائدة، ولهذا قد تَتَساءل، لماذا قد يُعرِّف (defining) المبرمج ذلك المُتَغيِّر من الأساس؟ في الواقع، يَلجأ المُبرمجين عادةً إلى تعريف تلك المُتَغيِّرات؛ بهدف إعطاء قيمة عددية معينة (٠,٠٥ في هذا المثال) اسمًا له مَغزى، فبخلاف ذلك، ستَكُون القيمة ٠,٠٥ بلا أي مَعنَى؛ فمن الأسهل عمومًا فِهم تَعْليمَة مثل principal += principal*interestRate; بالمُوازنة مع principal += principal*0.05. يُمكِن اِستخدَام المُبدِّل (modifier) final أثناء التَّصْريح عن مُتَغيِّر معين (variable declaration)؛ للتَأكُّد من اِستحالة تَعْديل القيمة المُخزَّنة بذلك المُتَغيِّر بمجرد تَهيئتها (initialize). إذا صَرَّحت، مثلًا، عن المُتَغيِّر العضو interestRate باستخدام التَعْليمَة التالية: public final static double interestRate = 0.05; فسيَستحِيل تَعْديل قيمة ذلك المُتَغيِّر interestRate بأيّ مكان آخر داخل البرنامج، فإذا حَاوَلت، مثلًا، اِستخدَام تَعْليمَة إِسْناد لتَعْديل قيمته، فإن الحاسوب سيُبلِّغ عن وجود خطأ في بناء الجملة (syntax error) أثناء تَصْرِيف (compile) البرنامج. من المنطقي عمومًا اِستخدَام المُبدِّل final مع مُتَغيِّر عام (public) يُمثِل قيمة "مُعدل الفائدة"؛ فبينما يُريد بنك معين الإعلان عن قيمة "مُعدل الفائدة"، فإنه، وبكل تأكيد، لا يُريد أن يَسمَح لأشخاص عشوائية بتَعْديل قيمة "مُعدل الفائدة". تَبرز أهمية المُبدِّل final عند اِستخدَامه مع المُتَغيِّرات الأعضاء، ولكنه مع ذلك، قابل للاِستخدَام مع كلا من المُتَغيِّرات المحليّة (local variables)، والمُعامِلات الصُّوريّة (formal parameters). يُطلَق مصطلح الثوابت المُسمَاة (named constants) على المُتَغيِّرات الأعضاء الساكنة (static member variable) المُصرَّح عنها باِستخدَام المُبدِّل final؛ لأن قيمتها تَظلّ تابثة طوال فترة تَّنْفيذ البرنامج. اِستخدَامك للثوابت المُسمَاة (named constants) قادر على تَحسِّين شيفرة البرامج بشكل كبير، وجَعْلها مَقروءة؛ فأنت بالنهاية تُعطِي أسماء ذات مَغزى للمقادير المُهمة بالبرنامج، ويُوصَى عمومًا بأن تَكُون تلك الأسماء مُكوَّنة بالكامل من حروف كبيرة (upper case letters)، بحيث يَفصِل محرف الشرطة السُفلية (underscore) بين الكلمات إذا ما لَزِم الأمر. على سبيل المثال، يُمكِن تعريف "مُعدَل الفائدة" كالتالي: public final static double INTEREST_RATE = 0.05; يَشيِع اِستخدَام نَمط التسمية بالأعلى عمومًا بأصناف الجافا القياسية (standard classes)، والتي تُعرِّف الكثير من الثوابت المُسمَاة. فمثلًا، تَعرَّضنا بالفعل للمُتَغيِّر Math.PI من النوع double المُعرَّف ضِمْن الصنف Math باِستخدَام المُبدِّلات public final static. بالمثل، يَحتوِي الصَنْف Color على مجموعة من الثوابت المُسمَاة (named constants) مثل: Color.RED و Color.YELLOW، والتي هي مُتَغيِّرات من النوع Color، مُعرَّفة باِستخدَام نفس مجموعة المُبدِّلات public final static. تُعدّ ثوابت أنواع التعداد (enumerated type constants)، والتي تَعرَّضنا لها خلال القسم الفرعي ٢.٣.٣، مثالًا آخرًا للثوابت المُسمَاة. يُمكِننا تعريف تعداد من النوع Alignment كالتالي: enum Alignment { LEFT, RIGHT, CENTER } يُعرِّف ذلك التعداد كُلًا من الثوابت Alignment.LEFT و Alignment.RIGHT و Alignment.CENTER. تقنيًا، يُعدّ Alignment صنفًا (class)، وتُعدّ تلك الثوابت (constants) الثلاثة أعضاء ساكنة عامة ونهائية (public final static) ضِمْن ذلك الصَنْف. في الواقع، يُشبِه تعريف نوع التعداد (enumerated type) بالأعلى تعريف ثلاثة ثوابت من النوع int كالتالي: public static final int ALIGNMENT_LEFT = 0; public static final int ALIGNMENT_RIGHT = 1; public static final int ALIGNMENT_CENTER = 2; وفي الحقيقة، كانت تلك الطريقة هي الأسلوب المُتبَع قبل ظهور أنواع التعداد (enumerated types)، وما تزال في الواقع مُستخدَمة بكثير من الحالات. فبِتَوفُّر ثوابت الأعداد الصحيحة (integer constants) بالأعلى، تستطيع الآن ببساطة تعريف مُتَغيِّر من النوع int، وإِسْناد واحدة من تلك القيم الثلاثة ALIGNMENT_LEFT و ALIGNMENT_RIGHT و ALIGNMENT_CENTER له؛ بهدف تمثيل أنواع مختلفة من التعداد Alignment. مع ذلك هناك مشكلة، وهي عدم إِطلاع الحاسوب على نيتك باِستخدَام ذلك المُتَغيِّر لتمثيل قيمة من النوع Alignment، لذا فإنه لن يَعترِض إذا حاولت إِسْناد قيمة هي ليست ضِمْن تلك القيم الثلاثة الصالحة. في المقابل، ومع ظهور أنواع التعداد (enumerated type)، فإنه إذا كان لديك مُتَغيِّر من نوع التعداد Alignment، فيُمكِنك فقط إِسْناد إحدى تلك القيم الثابتة المُدرجة بتعريف التعداد إليه، وأي محاولة لإِسْناد قيم آخرى غَيْر صالحة ستُعدّ بمثابة خطأ ببناء الجملة (syntax error) سيَكْتَشفها الحاسوب أثناء تَصْرِيف (compile) البرنامج. يُوفِّر ذلك نوعًا من الأمان الإضافي، وهو أحد أهم مميزات أنواع التعداد (enumerated types). يُسهِل استخدام الثوابت المُسمَاة (named constants) من تَعْديل قيمة الثابت المُسمَى. بالطبع، لا يُقصَد بذلك تَعْديلها أثناء تَّنْفيذ البرنامج، وإنما بين تَّنْفيذ معين والذي يليه، أيّ تَعْديل القيمة داخل الشيفرة المصدرية ذاتها ثم إعادة تَصرِيف (recompile) البرنامج. لنُعيد التفكير بمثال "معدل الفائدة"، غالبًا ما ستَكُون قيمة "مُعدَل الفائدة" مُستخدَمة أكثر من مرة عبر شيفرة البرنامج. لنفْترِض أن البنك قد عَدَّل قيمة "معدل الفائدة"، وعليه ينبغي تَعْديلها بالبرنامج. إذا كانت القيمة ٠,٠٥ مُستخدَمة بشكل مُجرَّد ضِمْن الشيفرة، فسيَضطرّ المبرمج لتَعقُّب جميع الأماكن المُستخدِمة لتلك القيمة بحيث يُعدِّلها إلى القيمة الجديدة. يَصعُب القيام بذلك خصوصًا مع احتمالية اِستخدَام القيمة ٠,٠٥ داخل البرنامج لأهداف مختلفة غير "مُعدَل الفائدة"، بالإضافة إلى احتمالية اِستخدَام القيمة ٠,٠٢٥ مثلًا لتمثيل نصف قيمة "مُعدَل الفائدة". في المقابل، إذا كنا قد صَرَّحنا عن الثابت المُسمَى INTEREST_RATE ضِمْن البرنامج، واستخدَمناه بصورة مُتَّسقِة عبر شيفرة البرنامج بالكامل، فسنحتاج إلى تعديل سطر وحيد فقط هو سَطْر التهيئة المبدئية لذلك الثابت. لنأخذ مثالًا آخر من القسم السابق، سنُعيد خلاله كتابة نسخة جديدة من البرنامج RandomMosaicWalk. ستَستخدِم تلك النسخة عدة ثوابت مُسمَاة (named constants) بهدف تمثيل كُلًا من عدد الصفوف، وعدد الأعمدة، وحجم كل مربع صغير بنافذة الصَنْف mosaic. سيُصرَّح عن تلك الثوابت (constants) الثلاثة كمُتَغيِّرات أعضاء ساكنة نهائية (final static member) كالتالي: final static int ROWS = 20; // عدد صفوف النافذة final static int COLUMNS = 30; // عدد أعمدة النافذة final static int SQUARE_SIZE = 15; // حجم كل مربع بالنافذة عُدِّل أيضًا باقي البرنامج بحرِص بحيث يتلائم مع الثوابت المُسمَاة (named constants) المُعرَّفة، فمثلًا، لفَتْح نافذة Mosaic بالنسخة الجديدة من البرنامج، يُمكِن استخدام التَعْليمَة التالية: Mosaic.open(ROWS, COLUMNS, SQUARE_SIZE, SQUARE_SIZE); ليس من السهل دومًا العُثور على جميع تلك الأماكن التي يُفْترَض بها اِستخدَام ثابت مُسمَى معين. لذا، انتبه! ففي حالة عدم اِستخدَامك لثابت مُسمَى معين بصورة مُتَّسقِة عبر شيفرة البرنامج بالكامل، فأنت تقريبًا قد أَفسدت الهدف الأساسي منه. لذلك يُنصَح عادة بتجربة البرنامج عدة مرات، بحيث تَستخدِم قيمة مختلفة للثابت المُسمَى (named constant) في كل مرة؛ لاِختبار كَوْنه يَعَمَل بصورة سليمة بجميع الحالات. اُنظر النسخة الجديدة من البرنامج كاملة RandomMosaicWalk2 بالأسفل. لاحظ كيفية اِستخدَام الثابتين ROWS و COLUMNS داخل البرنامج randomMove() عند تحريكه للتشويش (disturbance) من إحدى حواف النافذة mosaic إلى الحافة المضادة. لم تُضاف التعليقات (comments) لغرض تَوْفِير المساحة. public class RandomMosaicWalk2 { final static int ROWS = 20; // عدد الصفوف بالنافذة final static int COLUMNS = 30; // عدد الأعمدة بالنافذة final static int SQUARE_SIZE = 15; // حجم كل مربع بالنافذة static int currentRow; // رقم الصف المعرض للتشويش static int currentColumn; // رقم العمود المعرض للتشويش public static void main(String[] args) { Mosaic.open( ROWS, COLUMNS, SQUARE_SIZE, SQUARE_SIZE ); fillWithRandomColors(); currentRow = ROWS / 2; // ابدأ بمنتصف النافذة currentColumn = COLUMNS / 2; while (true) { changeToRandomColor(currentRow, currentColumn); randomMove(); Mosaic.delay(5); } } // نهاية main static void fillWithRandomColors() { for (int row=0; row < ROWS; row++) { for (int column=0; column < COLUMNS; column++) { changeToRandomColor(row, column); } } } // نهاية fillWithRandomColors static void changeToRandomColor(int rowNum, int colNum) { // اختر قيم عشوائية تتراوح بين 0 و 255 // لقيم الألوان الثلاثة (الأحمر، و الأزرق، والأخضر) // بنظام الألوان RGB int red = (int)(256*Math.random()); int green = (int)(256*Math.random()); int blue = (int)(256*Math.random()); Mosaic.setColor(rowNum,colNum,red,green,blue); } // نهاية changeToRandomColor static void randomMove() { // اضبط القيمة عشوائيًا بحيث تتراوح من 0 وحتى 3 int directionNum; directionNum = (int)(4*Math.random()); switch (directionNum) { case 0: // تحرك للأعلى currentRow--; if (currentRow < 0) currentRow = ROWS - 1; break; case 1: // تحرك لليمين currentColumn++; if (currentColumn >= COLUMNS) currentColumn = 0; break; case 2: // تحرك للأسفل currentRow++; if (currentRow >= ROWS) currentRow = 0; break; case 3: // تحرك لليسار currentColumn--; if (currentColumn < 0) currentColumn = COLUMNS - 1; break; } } // نهاية randomMove } // نهاية الصنف RandomMosaicWalk2 التسمية وقواعد النطاق (scope rules) عندما نُصرِّح عن مُتَغيِّر ما (variable declaration)، يُخصِّص الحاسوب مساحة من الذاكرة لذلك المُتَغيِّر. بحيث تستطيع اِستخدَام اسم ذلك المُتَغيِّر ضِمْن جزء معين على الأقل من شيفرة البرنامج؛ بهدف الإشارة إلى تلك المساحة من الذاكرة أو إلى تلك البيانات المُخزَّنة بها. يُطلَق على ذلك الجزء من الشيفرة، والذي يَكُون فيه اسم المُتَغيِّر صالحًا للاِستخدَام، اسم نطاق المُتَغيِّر (scope of variable). بالمثل، يُمكِن الإشارة إلى نطاق كُلًا من أسماء البرامج الفرعية (subroutine) وأسماء المُعامِلات الصُّوريّة (formal parameter). لنبدأ بالأعضاء الساكنة من البرامج الفرعية، والتي تُعدّ قواعد نطاقها (scope rule) بسيطة نوعًا ما. يَمتد نطاق (scope) أي برنامج فرعي ساكن إلى كامل الشيفرة المصدرية للصَنْف (class) المُعرَّف بداخله، أيّ يُمكِن استدعاء ذلك البرنامج الفرعي من أيّ مكان داخل الصنف، بما في ذلك تلك الأماكن الواقعة قَبْل تعريف (definition) البرنامج الفرعي. بل أنه حتى من المُمكن لبرنامج فرعي معين استدعاء ذاته، وهو ما يُعرَف باسم التَعاود أو الاستدعاء الذاتي (recursion)، وهو أحد المواضيع المتقدمة نسبيًا، وسنتناوله عمومًا بالقسم ٩.١. وأخيرًا، إذا لم يَكُن البرنامج الفرعي خاصًا (private)، فتستطيع حتى الوصول إليه من خارج الصَنْف المُعرَّف بداخله بشَّرْط اِستخدَام اسمه الكامل. لننتقل الآن إلى الأعضاء الساكنة من المُتَغيِّرات، والتي تَملُك قواعد نطاق (scope rule) مشابهة بالإضافة إلى تعقيد واحد إضافي هو كالآتي. تستطيع عمومًا تعريف مُتَغيِّر محليّ (local variable) أو مُعامِل صُّوريّ (formal parameter) يَحمِل نفس اسم إحدى المُتَغيِّرات الأعضاء (member variable) ضِمْن الصَنْف، وفي تلك الحالة، يُعدّ المُتَغيِّر العضو مخفيًا ضِمْن نطاق المُتَغيِّر المحليّ أو المُعامِل الذي يَحمِل نفس الاسم. فعلى سبيل المثال، إذا كان لدينا الصَنْف Game كالتالي: public class Game { static int count; // متغير عضو static void playGame() { int count; // متغير محلي . . // بعض التعليمات لتعريف playGame() . } . . // المزيد من المتغيرات والبرامج الفرعية . } // نهاية الصنف Game يُشير الاسم count بالتَعْليمات المُؤلِّفة لمَتْن (body) البرنامج الفرعي playGame() إلى المُتَغيِّر المحليّ (local variable). أما ببقية الصنف Game، فإنه سيُشيِر إلى المُتَغيِّر العضو (member variable)، بالطبع إذا لم يُخفَى باستخدام مُتَغيِّر محليّ آخر أو مُعامِلات تَحمِل نفس الاسم count. مع ذلك، ما يزال بإمكانك الإشارة إلى المُتَغيِّر العضو count بواسطة اسمه الكامل Game.count، والذي يُستخدَم، في العادة، خارج الصَنْف الذي عُرِّف به العضو، ولكن ليس هناك قاعدة تَمنع اِستخدَامه داخل الصنف. لهذا يُمكِن اِستخدَام الاسم الكامل Game.count داخل البرنامج الفرعي playGame() للإشارة إلى المُتَغيِّر العضو بدلًا من المُتَغيِّر المحليّ. يُمكِننا الآن تلخيص قاعدة النطاق (scope rule) كالآتي: يَشمَل نطاق مُتَغيِّر عضو ساكن الصنف المُعرَّف بداخله بالكامل، وعندما يُصبِح الاسم البسيط (simple name) للمُتَغيِّر العضو مَخفيًا نتيجة تعريف مُتَغيِّر محليّ أو مُعامِل صُّوريّ يَحمِل نفس الاسم، يُصبِح من الضروري اِستخدَام الاسم الكامل على الصورة . للإشارة إلى المُتَغيِّر العضو. تُشبِه قواعد نطاق الأعضاء غير الساكنة (non-static) عمومًا تلك الخاصة بالأعضاء الساكنة، باستثناء أن الأولى لا يُمكِن اِستخدَامها بالبرامج الفرعية الساكنة (static subroutines)، كما سنرى لاحقًا. أخيرًا، يَتَكوَّن نطاق المُعامِل الصُّوريّ (formal parameter) لبرنامج فرعي معين من الكُتلَة (block) المُؤلِّفة لمَتْن البرنامج الفرعي (subroutine body). في المقابل، يَمتَد نطاق المُتَغيِّر المحليّ (local variable) بدايةً من تَعْليمَة التَّصْريح (declaration statement) المسئولة عن تعريف ذلك المُتَغيِّر وحتى نهاية الكُتلَة (block) التي حَدَثَ خلالها ذلك التَّصْريح. كما أشرنا بالأعلى، تستطيع التَّصْريح عن المُتَغيِّر المُتحكِّم بحَلْقة (loop control variable) for ضمن التعليمة ذاتها على الصورة for (int i=0; i < 10; i++)، ويُعدّ نطاق مثل هذا التَّصْريح (declaration) حالة خاصة: فهو صالح فقط ضِمْن تَعْليمَة for، ولا يَمتَد إلى بقية الكُتلَة المتضمنة للتَعْليمَة. لا يُسمَح عمومًا بإعادة تعريف (redefine) اسم المُعامِل الصُّوريّ أو المُتَغيِّر المحليّ ضِمْن نطاقه (scope)، حتى إذا كان ذلك داخل كُتلَة مُتداخِلة (nested block)، كالتالي: void badSub(int y) { int x; while (y > 0) { int x; // خطأ، لأن x مُعرَّفة بالفعل . . . } } في الواقع، تَسمَح بعض اللغات بذلك، بحيث يُخفِي التَّصْريح عن x ضِمْن حَلْقة while التَّصْريح الأصلى، ولكن لا تَسمَح الجافا بذلك، حيث يُصبِح اسم المُتَغيِّر متاحًا للاِستخدَام مرة آخرى فقط بعد انتهاء تَّنْفيذ الكُتلَة (block) المُصرَّح عن المُتَغيِّر ضِمْنها. اُنظر على سبيل المثال: void goodSub(int y) { while (y > 10) { int x; . . . // ينتهي نطاق x هنا } while (y > 0) { int x; // صالح، فتَّصريح x السابق انتهت صلاحيته . . . } } هل تَتَسبَّب أسماء المُتَغيِّرات المحليّة (local variable) بإخفاء أسماء البرامج الفرعية (subroutine names)؟ لا يُمكِن حُدوث ذلك لسبب قد يبدو مفاجئًا. ببساطة، لمّا كانت أسماء البرامج الفرعية دومًا مَتبوعة بزوج من الأقواس (parenthesis)، والتي يُسْتحسَن التفكير بها على أساس أنها جزء من اسم البرنامج الفرعي، كأن تقول البرنامج الفرعي main() وليس البرنامج الفرعي main، فإن الحاسوب عمومًا يُمكِنه دائمًا مَعرِفة ما إذا كان اسم معين يُشير إلى مُتَغيِّر أم إلى برنامج فرعي، لذا ليس ضروريًا أن تكون أسماء المُتَغيِّرات والبرامج الفرعية مختلفة من الأساس، حيث يُمكِنك ببساطة تعريف مُتَغيِّر اسمه count وبرنامج فرعي بنفس الاسم count ضِمْن نفس الصَنْف. كذلك لمّا كان الحاسوب قادرًا على مَعرِفة ما إذا كان اسم معين يُشير إلى اسم صَنْف أم لا وِفقًا لقواعد الصيغة (syntax)، فإنه حتى يُمكِن إعادة اِستخدَام أسماء الأصناف (classes) بهدف تسمية كلا من المُتَغيِّرات والبرامج الفرعية. اسم الصنف هو بالنهاية نوع، لذا يُمكِن اِستخدَام ذلك الاسم للتََّصْريح عن المُتَغيِّرات والمُعامِلات الصُّوريّة (formal parameters)، بالإضافة إلى تَخْصيص نوع القيمة المُعادة (return type) من دالة (function) معينة. يَعنِي ذلك أنك تستطيع التََّصْريح عن الدالة التالية ضِمْن صَنْف اسمه Insanity: static Insanity Insanity( Insanity Insanity ) { ... } يَستخدِم التََّصْريح -بالأعلى- الاسم Insanity ٤ مرات، تُشير الأولى إلى النوع المُعاد (return type) من الدالة، بينما تُشير الثانية إلى اسم تلك الدالة، والثالثة إلى نوع مُعاملها الصُّوريّ (formal parameter)، والرابعة إلى اسم ذلك المُعامِل. لكن تَذَكَّر! لا يُعدّ كل ما هو مُتاح فكرة جيدة بالضرورة. ترجمة -بتصرّف- للقسم Section 8: The Truth About Declarations من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.1 نقطة
-
يُخْزَّن البرنامج الفرعي (subroutine) بذاكرة الحاسوب على هيئة سِلسِلة نصية طويلة مُكوَّنة من الرقمين صفر وواحد، ولذلك فإنه لا يَختلف كثيرًا عن البيانات (data)، مثل الأعداد الصحيحة والسَلاسِل النصية والمصفوفات، والتي تُخْزَّن أيضًا بنفس الطريقة. ربما اِعتدت التفكير بكُلًا من البرامج الفرعية والبيانات على أساس أنهما شيئان مختلفان تمامًا، ولكن في الواقع يَتعامَل الحاسوب مع البرنامج الفرعي على أساس أنه مجرد نوع آخر من البيانات. تَسمَح حتى بعض اللغات البرمجية بالتَعامُل مع البرامج الفرعية بنفس الطريقة التي يَتعامَل معها الحاسوب، وهو ما أُضيف إلى الجافا على هيئة تعبيرات لامدا (lambda expressions) منذ الإصدار ٨. أصبحت تعبيرات لامدا أكثر شيوعًا ببرامج الجافا، وهي مفيدة في العموم، ولكن تَبرُز أهميتها بصورة أكبر ببرمجة واجهات المُستخدِم الرُسومية (GUI)، كالتَعامُل مع "أدوات تَحكُّم واجهة المُستخدَم الرسومية JavaFX (JavaFX GUI toolkit)"، وهو ما سنتناوله بالفصل السادس، كما سنَتَعرَّض لها مجددًا بنهاية الفصل الخامس، لذا يُمكِنك تَخطِّي هذا القسم إذا أردت إلى ذلك الحين. الدوال الكيانية (first-class functions) يُمكِننا كتابة دالة لحِسَاب قيمة مربع العدد، بحيث تَستقبِل ذلك العدد كمُعامِل صُّوري أو وهمي (dummy parameter)، كالتالي: static double square( double x ) { return x*x; } كالعادة، لابُدّ من تَخْصيص اسم لتلك الدالة، وفي تلك الحالة كان square، وعليه تُصبِح تلك الدالة جزءًا دائمًا من البرنامج (program)، وهو ما قد لا يَكُون مُلائمًا؛ بالأخص إذا كنت تَنوِي اِستخدَام الدالة مرة واحدة فقط. لامدا (lambda) هو أحد حروف الأبجدية الإغريقية (Greek alphabet)، والذي اِستخدَمه عالم الرياضيات ألونزو تشرتش (Alonzo Church) أثناء دراسته للدوال القابلة للحِسَاب (computable functions). على سبيل المثال، إذا كنا نُريد حِسَاب قيمة مربع عدد، وليكن x، فقد تَظُنّ أن الترميز x2 يُمثِل "دالة" تَحسِب مربع x، لكنه، في الواقع، ليس سوى تعبير (expression) يُمثِل "نتيجة" حِسَاب مربع x. قَدَّمَ ألونزو تشرتش (Alonzo Church) ترميز لامدا (lambda notation) والذي يُمكِن اِستخدَامه لتعريف دالة (function) بدون تَخْصيص اسم لها. بالتحديد اِستخدَم الترميز lambda(x).x2 (في الواقع يُستخدَم الحرف الإغريقي لامدا ذاته وليس الكلمة نفسها) للإشارة إلى "دالة x معطاة بواسطة x2". يُعدّ هذا الترميز دالة مجرَّدة (function literal)، أيّ تُمثِل قيمة من النوع "دالة (function)" بنفس الطريقة التي تُعدّ بها القيمة 42 عددًا صحيحًا مُجرَّدًا (integer literal) يُمثِل قيمة من النوع int. ينبغي أن تَدْفَعك الدوال المُجرَّدة (function literals) إلى التَفكير بالدوال (function) على أنها مُجرَّد نوع آخر من القيم، وعندما تَصِل إلى تلك المرحلة، فإنك ستَكُون قادرًا على تَطْبِيق نفس تلك الأشياء التي اعتدت إجرائها على القيم الآخرى على الدوال أيضًا، كإِسْناد دالة إلى مُتَغيِّر، أو تمرير دالة كمُعامِل (parameter) إلى برنامج فرعي (subroutine)، أو إعادة دالة كقيمة من برنامج فرعي، أو حتى إنشاء مصفوفة دوال (array of functions). تَسمَح بعض لغات البرمجة بالقيام بكل تلك الأشياء، ويُقال عندها أنها تُدعِّم "الدوال الكيانيَّة (first-class functions)" أو أنها تُعامِل الدوال بعدّها كائنات كيانيَّة (first-class objects). تُوفِّر لغة الجافا كل تلك الأشياء من خلال تعبيرات لامدا (lambda expressions)، والتي تَختلِف صيغتها عن الترميز الذي اِستخدَمه ألونزو تشرتش (Alonzo Church)، فهي الواقع، لا تَستخدِم حتى كلمة لامدا (lambda) على الرغم من مُسماها. تُكتَب دالة حِسَاب مربع العدد بتعبير لامدا (lambda expression) بلغة الجافا، كالتالي: x -> x*x يُنشِئ العَامِل -> تعبير لامدا، بحيث تَقَع المُعامِلات الوهمية أو الصُّوريّة (dummy parameter) على يسار العَامِل، بينما يَقَع التعبير (expression) المسئول عن حِسَاب قيمة الدالة على يمينه. قد ترى تعبير لامدا بالأعلى مُمرَّرًا كمُعامِل فعليّ (actual parameter) إلى برنامج فرعي، أو مُسنَد إلى مُتَغيِّر، أو مُعاد من دالة. إذًا، هل الدوال بلغة الجافا كيانيَّة (first-class)؟ لا تَتَوفَّر إجابة واضحة على هذا السؤال، لأن لغة الجافا لا تُدعِّم بعض الأشياء الآخرى المُتوفِّرة ضِمْن لغات برمجية آخرى. فمثلًا، في حين تستطيع إِسْناد تعبير لامدا المُعرَّف بالأعلى إلى مُتَغيِّر اسمه sqr، فإنك لن تستطيع بَعْدها اِستخدَام ذلك المُتَغيِّر كما لو كان دالة فعليّة، أيّ لا تستطيع كتابة sqr(42). لغة الجافا عمومًا هي لغة صارمة في تَحْدِيد النوع (strongly typed)، ولذلك لكي تَتَمكَّن من إِنشاء المُتَغيِّر sqr، فعليك التَّصْريح عنه أولًا وهو ما يَتضمَّن تَخْصيص نوعه، فيا تُرى ما هو ذلك النوع الذي يَتناسب مع قيمة هي عبارة عن دالة؟ تُوفِّر الجافا ما يُعرَف باسم واجهة نوع الدالة (functional interface) لهذا الغرض، وسنناقشها بَعْد قليل. ملاحظة أخيرة: على الرغم من ارتباط مصطلح الدالة (function) بتعبيرات لامدا (lambda expressions) بدلًا من مصطلحات مثل برنامج فرعي (subroutine) أو تابع (method)، فإنها في الواقع لا تَقْتصِر على تمثيل الدوال (functions)، وإنما يُمكِنها تمثيل أي برنامج فرعي (subroutines)، وبصورة عشوائية. واجهات نوع الدالة (functional interfaces) كي تَتَمكَّن من اِستخدَام برنامج فرعي (subroutine) معين، فلابُدّ أن تَعرِف كُلًا من اسمه، وعدد المُعامِلات (parameters) التي يَستقبِلها، وأنواعها، بالإضافة إلى نوع القيمة المُعادة (return type) من ذلك البرنامج الفرعي. تُوفِّر لغة الجافا ما يُعرَف باسم واجهات نوع الدالة (functional interface)، وهي أَشْبَه بالأصناف (class)، حيث تُعرَّف بملف امتداده هو .java كأي صَنْف، وتَتضمَّن تلك المعلومات المذكورة بالأعلى عن برنامج فرعي وحيد. انظر المثال التالي: public interface FunctionR2R { double valueAt( double x ); } تُصرِّح الشيفرة بالأعلى عن واجهة نوع الدالة FunctionR2R، والموجودة بملف يَحمِل اسم FunctionR2R.java. تُصرِّح تلك الواجهة عن الدالة valueAt، والتي تَستقبِل مُعامِل وحيد من النوع double، وتُعيد قيمة من النوع double. تَفرِض قواعد الصيغة تَخْصيص اسم للمُعامِل -x بالأعلى- على الرغم من أنه في الواقع ليس ذا أهمية هنا، وهو ما قد يَكُون مزعجًا نوعًا ما. ها هو مثال آخر: public interface ArrayProcessor { void process( String[] array, int count ); } تَتضمَّن لغة الجافا الكثير من واجهات نوع الدالة القياسية (standard functional interfaces) بصورة افتراضية. تُعدّ واجهة نوع الدالة Runnable واحدة من أهم تلك الواجهات، وأبسطها، والمُعرَّفة كالتالي: public interface Runnable { public void run(); } سنَستخدِم واجهات نوع الدالة (functional interfaces) الثلاثة بالأعلى للأمثلة التالية بهذا القسم. تُوفِّر لغة الجافا أيضًا ما يُعرَف باسم الواجهات (Interfaces)، والتي هي في الواقع أعم وأشمل، وربما أكثر تعقيدًا من واجهات نوع الدالة (functional interfaces). سنتعلَّّم المزيد عنها بالقسم ٥.٧. سنُقصِر حديثنا بهذا القسم على واجهات نوع الدالة (functional interfaces)؛ لارتباطها بموضوعنا الرئيسي: تعبيرات لامدا (lambda expressions). يُعدّ اسم واجهة نوع الدالة نوعًا (type)، تمامًا كالأنواع String و double، وكأيّ نوع، فإنه يُمكِن اِستخدَامه للتَّصْريح عن المُتَغيِّرات، والمُعامِلات، وكذلك لتَخْصيص نوع القيمة المُعادة (return type) من الدوال، ويُمكِن إِسْناد تعبيرات لامدا (lambda expression) كقيم للمُتَغيِّرات من ذلك النوع. في الواقع، تَتضمَّن واجهة نوع الدالة (functional interface) قالبًا (template) لبرنامج فرعي وحيد، وينبغي لتعبير لامدا (lambda expression) المُسنَد أن يَكُون مُتطابِقًا مع ذلك القالب. تعبيرات لامدا (lambda Expressions) يُعدّ أي تعبير لامدا (lambda expression) تمثيلًا لبرنامج فرعي مجهول الاسم (anonymous subroutine)، أيّ لا يَمتلك اسمًا، ولكنه في المقابل، وكأيّ برنامج فرعي، لديه قائمة من المُعامِلات الصُّوريّة (formal parameter list)، بالإضافة إلى التعريف (definition) نفسه، ويُكتَب عمومًا بالصيغة (syntax) التالية: ( <parameter-list> ) -> { <statements> } كما هو الحال مع البرامج الفرعية العادية، يُمكِن لقائمة المُعامِلات -بالأعلى- أن تَكُون فارغة، أو قد تتكوَّن من تَّصْريح عن مُعامِل (parameter declaration) وحيد أو أكثر، بحيث يُفصَل بين كل تَّصْريح والذي يليه بفاصلة (comma)، وبحيث يَتكوَّن كل تَّصْريح من نوع المُعامِل متبوعًا باسمه. في الواقع، تُبسَّط عادة تلك الصيغة وفقًا لمجموعة من القواعد: أولًا: يُمكِن حَذْف أنواع المُعامِلات إذا كان استنباطها من السِّياق مُمكِنًا. على سبيل المثال، إذا كان هناك تعبير لامدا مُعرَّف على أنه من النوع FunctionR2R، فإن مُعامِل التعبير لابُدّ وأن يَكُون من النوع double، ولذلك فإنه، في تلك الحالة، ليس من الضروري تَخْصيص نوع المُعامِل ضِمْن تعبير اللامدا. ثانيًا، يُمكِن حَذْف الأقواس () حول قائمة المُعامِلات (parameter list) إذا كانت مُكوَّنة من مُعامِل وحيد غَيْر مُحدَّد النوع. ثالثًا، يُمكِن حَذْف الأقواس {} الموجودة على الجانب الأيمن من العَامِل -> إذا كانت تَحتوِي على تَعْليمَة استدعاء برنامج فرعي (subroutine call) وحيدة. أخيرًا، إذا كان الجانب الأيمن من العَامِل -> مكتوبًا بالصيغة { return <expression>; }، فيُمكِن تبسيطها إلى التعبير وحَذْف أي شيء آخر. بفَرْض أننا نريد كتابة دالة لحِسَاب مربع القيم من النوع double، فإن نوع تلك الدالة هو من نفس نوع واجهة نوع الدالة FunctionR2R المُعرَّفة بالأعلى. الآن، إذا كان sqr مُتَغيِّرًا من النوع FunctionR2R، فإننا نستطيع أن نُسنِد تعبير لامدا (lambda expression) كقيمة لذلك المُتَغيِّر، ويُعدّ عندها ذلك التعبير تمثيلًا للدالة المطلوبة. يُمكِن القيام بذلك بأي من الصِيَغ التالية: sqr = (double x) -> { return x*x; }; sqr = (x) -> { return x*x; }; sqr = x -> { return x*x; }; sqr = x -> x*x; sqr = (double fred) -> fred*fred; sqr = (z) -> z*z; تُعرِّف تعبيرات لامدا (lambda expressions) الستة بالأعلى نفس الدالة بالضبط، وأُضيفت آخر تَعْليمَتين (statements) خصيصًا؛ للتأكيد على أن أسماء المُعامِلات غير مهمة، فهى بالنهاية مجرد مُعامِلات وهمية أو صُّوريّة (dummy parameters). لمّا كان المُصرِّف (compiler) على علم بكون المُتَغيِّر sqr من النوع FunctionR2R، ولأن النوع FunctionR2R يتطلَّب مُعامِلًا من النوع double، فقد تَمَكَّنا من حَذْف نوع المُعامِل double. يُمكِن اِستخدَام تعبيرات لامدا فقط ضِمْن السِّياقات التي يستطيع فيها المُصرِّف (compiler) استنباط نوعها، ولذلك لابُدّ من كتابة نوع المُعامِل إذا كان حَذْفه سيَتسبَّب بغموض نوع التعبير. لا يُعدّ المُتَغيِّر sqr -كما هو مُعرَّف بالأعلى- دالة (function) تمامًا، وإنما هو قيمة من النوع FunctionR2R، وبحسب ما هو مُخصَّص بتعريف الواجهة (interface definition) FunctionR2R، فإن ذلك المُتَغيِّر لابُدّ وأن يَحتوِي على دالة تَحمِل اسم valueAt. يمكن استدعاء تلك الدالة من خلال اسمها الكامل sqr.valueAt، فمثلًا يُمكِن كتابة sqr.valueAt(42) أو sqr.valueAt(x) + sqr.valueAt(y). إذا كانت قائمة مُعامِلات تعبير لامدا مُكوَّنة من أكثر من مُعامِل واحد، فإن الأقواس () المُحيطة بقائمة المُعامِلات (parameters list) لم تَعُدْ اختيارية. اُنظر المثال التالي، والذي يَستخدِم واجهة نوع الدالة ArrayProcessor، كما يُبيِّن طريقة كتابة تعبير لامدا عندما يكون تعريفها مُتعدِّد الأسطر (multiline definition): ArrayProcessor concat; concat = (A,n) -> { // الأقواس هنا مطلوبة String str; str = ""; for (int i = 0; i < n; i++) str += A[i]; System.out.println(str); }; // الفاصلة المنقوطة هنا ليست جزءًا من تعبير لامدا // وإنما تشير إلى انتهاء تعليمة الإسناد String[] nums; nums = new String[4]; nums[0] = "One"; nums[1] = "Two"; nums[2] = "Three"; nums[3] = "Four"; for (int i = 1; i < nums.length; i++) { concat.process( nums, i ); } مما سينتج عنه الخَرْج التالي: One OneTwo OneTwoThree OneTwoThreeFour تُصبِح الأمور أكثر تشويقًا عند تمرير تعبير لامدا كمُعامِل فعليّ (actual parameter)، وهو في الواقع الاِستخدَام الأكثر شيوعًا لتعبيرات لامدا. افترض أن لدينا الدالة التالية مثلًا: /** * احسب قيمة التعبير f(start) + f(start+1) + ... + f(end) * حيث f عبارة عن دالة تُستقبل كمعامل * قيمة المعامل end لابد أن تكون أكبر من أو تساوي start */ static double sum( FunctionR2R f, int start, int end ) { double total = 0; for (int n = start; n <= end; n++) { total = total + f.valueAt( n ); } return total; } لمّا كانت f من النوع FunctionR2R، فإن قيمة f عند n تُكتَب على الصورة f.valueAt(n). يُمكِن تمرير تعبير لامدا (lambda expression) كقيمة للمُعامِل الأول عند استدعاء الدالة sum، كالتالي: System.out.print("The sum of n squared for n from 1 to 100 is "); System.out.println( sum( x -> x*x, 1, 100 ) ); System.out.print("The sum of 2 raised to the power n, for n from 1 to 10 is "); System.out.println( sum( num -> Math.pow(2,num), 1, 10 ) ); كمثال آخر، افترض أن لدينا برنامجًا فرعيًا (subroutine) ينبغي أن يُنفِّذ مُهِمّة (task) مُعطاة عدة مرات، يُمكِننا ببساطة تمرير تلك المُهِمّة كمُعامِل من النوع Runnable، كالتالي: static void doSeveralTimes( Runnable task, int repCount ) { for (int i = 0; i < repCount; i++) { task.run(); // نفذ المهمة } } نستطيع الآن طباعة السِلسِلة النصية "Hello World" عشر مرات باستدعاء التالي: doSeveralTimes( () -> System.out.println("Hello World"), 10 ); تَتَكوَّن قائمة مُعامِلات (parameter list) تعبيرات لامدا من النوع Runnable من زوج فارغ من الأقواس ()؛ وذلك لتتماشى مع البرنامج الفرعي المُصرَّح عنه ضِمْن واجهة نوع الدالة Runnable. اُنظر المثال التالي: doSeveralTimes( () -> { int count = 5 + (int)(21*Math.random()); for (int i = 1; i <= count; i++) { System.out.print(i + " "); } System.out.println(); }, 100); على الرغم من أن الشيفرة بالأعلى قد تبدو معقدة نوعًا ما، إلا أنها، في الواقع، مُكوَّنة من تَعْليمَة وحيدة هي تَعْليمَة استدعاء البرنامج الفرعي doSeveralTimes. يَتَكوَّن مُعامِل البرنامج الفرعي الأول من تعبير لامدا (lambda expression) قد امتد تعريفه إلى عدة أسطر، أما مُعامِله الثاني فهو القيمة ١٠٠، وتُنهِي الفاصلة المنقوطة (semicolon) بنهاية آخر سطر تَعْليمَة استدعاء البرنامج الفرعي (subroutine call). اطلعنا على عدة أمثلة تُسنَد فيها تعبيرات لامدا (lambda expression) إلى مُتَغيِّرات، وآخرى تُمرَّر فيها تعبيرات لامدا كمُعامِلات فعليّة (actual parameter). تَبَقَّى الآن أن نراها مُستخدَمة كقيمة مُعادة (return value) من دالة. اُنظر المثال التالي: static FunctionR2R makePowerFunction( int n ) { return x -> Math.pow(x,n); } وبالتالي، ستُعيد makePowerFunction(2) قيمة من النوع FunctionR2R تَحسِب مربع قيمة المُعامِل المُمرَّر، بينما ستُعيد makePowerFunction(10) قيمة من النوع FunctionR2R تَحسِب قيمة المُعامِل المُمرَّر مرفوعًا للأس ١٠. يُوضِح هذا المثال أيضًا أن تعبيرات لامدا (lambda expression) تستطيع اِستخدَام مُتَغيِّرات آخرى إلى جانب مُعامِلاتها، مثل n في المثال بالأعلى. (في الواقع هناك بعض القيود لتَحْدِيد متى يُمكِن القيام بذلك). مراجع التوابع (Method References) لنفْترِض أننا نريد كتابة تعبير لامدا (lambda expression) من النوع FunctionR2R، بحيث يُمثِل دالة تَحسِب الجذر التربيعي. نستطيع كتابته على الصورة x -> Math.sqrt(x)، ولكننا إذا دققنا النظر، سنَجِدْ أن تعبير لامدا، في تلك الحالة تحديدًا، ليس سوى مجرد مُغلِّف (wrapper) بسيط للدالة Math.sqrt الموجودة بالفعل. ولهذا تُوفِّر الجافا ما يُعرَف باسم مَراجِع التوابع (method reference)؛ لاِستخدَامها في الحالات المشابهة (تَذَكَر أن "التابع [method]" هو مجرد كلمة آخرى للإشارة إلى "البرنامج الفرعي [subroutine]" بلغة الجافا). نستطيع مثلًا اِستخدَام المَرجِع التابعي Math::sqrt، والذي يُعدّ اختصارًا لتعبير اللامدا المذكور سلفًا، أينما أَمْكَن اِستخدَام تعبير اللامدا المناظر، كتمريره مثلًا كمُعامِل للدالة sum المُعرَّفة بالأعلى، كالتالي: System.out.print("The sum of the square root of n for n from 1 to 100 is "); System.out.println( sum( Math::sqrt, 1, 100 ) ); فقط لو أَمْكَننا اِستخدَام الاسم Math.sqrt هنا بدلًا من الاستعانة بترميز (notation) جديد ::! في الواقع، الترميز Math.sqrt مُعرَّف بالفعل ضِمْن الصَنْف Math لكن للإشارة إلى مُتَغيِّر اسمه sqrt. يُمكِن عمومًا اِستخدَام مَراجِع التوابع (method reference) بدلًا من تعبيرات لامدا (lambda expression) التي يَقْتصِر دورها على اِستدعاء أحد التوابع الساكنة (static method) الموجودة مُسْبَّقًا. تُكتَب مَراجِع التوابع على الصورة التالية: <classname>::<method-name> لا يَقْتصِر هذا الترميز (notation) على التوابع الساكنة، أي تلك المُنتمية للأصناف (classes)، وإنما يَمْتَدّ إلى تلكم المُنتمية للكائنات (objects) كذلك. على سبيل المثال، إذا كان str متغير من النوع String، فإن str بطبيعة الحال سيَحتوِي على التابع str.length()، وفي هذه الحالة، يُمكِن اِستخدَام المَرجِع التابعي str::length كتعبير لامدا من واجهة نوع الدالة SupplyInt المُعرَّفة كالتالي: public interface SupplyInt { int get( ); } ترجمة -بتصرّف- للقسم Section 5: Lambda Expressions من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.1 نقطة