مرحبا مجددا! في الدرس الأخير، تعرفنا على الفصول والمنشئين، وتعلمنا كيفية إنشاء فصولنا الخاصة. اليوم سوف نتعرف بشكل أفضل على أساليب Java، وهي جزء أساسي من الفصول الدراسية. الأساليب في Java عبارة عن مجموعة من الأوامر التي تسمح لك بتنفيذ عملية معينة في برنامج ما. بمعنى آخر، الطريقة هي وظيفة؛ شيء يستطيع صفك القيام به. في لغات البرمجة الأخرى، غالبًا ما تسمى الطرق "الوظائف"، ولكن في Java، تكون كلمة "طريقة" أكثر شيوعًا. :) إذا كنت تتذكر، قمنا في الدرس الأخير بإنشاء طرق بسيطة لفصل القطط ، حتى تتمكن قططنا من قول المواء والقفز:
public class Cat {
String name;
int age;
public void sayMeow() {
System.out.println("Meow!");
}
public void jump() {
System.out.println("Pounce!");
}
public static void main(String[] args) {
Cat smudge = new Cat();
smudge.age = 3;
smudge.name = "Smudge";
smudge.sayMeow();
smudge.jump();
}
}
sayMeow() و Jump() هما طريقتان لفصلنا. وينتج عن تشغيل هذه الطرق إخراج وحدة التحكم التالية:
Meow!
Pounce!
أساليبنا بسيطة للغاية: فهي ببساطة تقوم بإخراج النص إلى وحدة التحكم. لكن في Java، للطرق مهمة مهمة: فهي تنفذ إجراءات على بيانات الكائن. يقومون بتغيير بيانات الكائن وتحويلها وعرضها والقيام بأشياء أخرى بها. أساليبنا الحالية لا تفعل أي شيء مع بيانات كائن Cat . دعونا نلقي نظرة على مثال توضيحي أكثر:
public class Truck {
int length;
int width;
int height;
int weight;
public int getVolume() {
int volume = length * width * height;
return volume;
}
}
على سبيل المثال، لدينا هنا فئة تمثل شاحنة . تتميز الشاحنة بالطول والعرض والارتفاع والوزن (وهو ما سنحتاج إليه لاحقًا). في طريقة getVolume() ، نقوم بإجراء العمليات الحسابية، وتحويل بيانات كائننا إلى رقم يمثل حجمه (نضرب الطول والعرض والارتفاع). وسيكون هذا الرقم نتيجة لهذه الطريقة. لاحظ أن تعريف الطريقة مكتوب كـ public int getVolume . وهذا يعني أن هذه الطريقة يجب أن تُرجع int . لقد حسبنا القيمة المرجعة للطريقة، والآن يجب علينا إعادتها إلى البرنامج الذي يسمى طريقتنا. لإرجاع نتيجة إحدى الطرق في Java، نستخدم الكلمة الأساسية return. حجم العودة
معلمات طريقة جافا
يمكننا تمرير قيم تسمى "الوسائط" إلى الطريقة عند استدعائها. يتضمن إعلان الطريقة قائمة من المتغيرات التي تخبرنا بنوع وترتيب المتغيرات التي يمكن أن تقبلها الطريقة. تسمى هذه القائمة "معلمات الطريقة". لا تحدد طريقة getVolume() الخاصة بفئة Truck حاليًا أي معلمات، لذلك دعونا نحاول توسيع مثال الشاحنة الخاص بنا. قم بإنشاء فئة جديدة تسمى BridgeOfficer . هذا هو ضابط الشرطة المناوب على الجسر، والذي يقوم بفحص جميع الشاحنات المارة لمعرفة ما إذا كانت حمولتها تتجاوز الوزن المسموح به.public class BridgeOfficer {
int maxWeight;
public BridgeOfficer(int normalWeight) {
this.maxWeight = normalWeight;
}
public boolean checkTruck(Truck truck) {
if (truck.weight > maxWeight) {
return false;
} else {
return true;
}
}
}
يقبل الأسلوب checkTruck وسيطة واحدة، وهي كائن Truck ، ويحدد ما إذا كان الضابط سيسمح للشاحنة بالوصول إلى الجسر أم لا. المنطق داخل الطريقة بسيط بما فيه الكفاية: إذا تجاوز وزن الشاحنة الحد الأقصى المسموح به، فستُرجع الطريقة false . سيتعين عليها العثور على طريق آخر :( إذا كان الوزن أقل من أو يساوي الحد الأقصى، فيمكنه المرور، وترجع الطريقة صحيحًا . إذا لم تفهم تمامًا العبارات "return" أو "ترجع الطريقة a value" ومع ذلك، فلنأخذ استراحة من البرمجة ونفكر فيها باستخدام مثال بسيط من الحياة الواقعية. :) لنفترض أنك مرضت وبقيت في المنزل من العمل لبضعة أيام. تذهب إلى قسم المحاسبة بمذكرة طبيبك، لأنه من المفترض أن تكون الإجازة المرضية مدفوعة الأجر. إذا قارنا هذا الوضع بالطرق، فإن المحاسب لديه طريقة paySickLeave() . تقوم بتمرير مذكرة الطبيب كحجة لهذه الطريقة (بدونها لن تعمل الطريقة ولن تحصل على أجر!). ومن ثم يتم إجراء الحسابات اللازمة داخل الطريقة باستخدام ملاحظتك (يستخدمها المحاسب لحساب المبلغ الذي يجب أن تدفعه لك الشركة)، ويتم إرجاع نتيجة عملك (مبلغ من المال) إليك. برنامجنا يعمل بطريقة مماثلة. فهو يستدعي طريقة، ويمرر البيانات إليها، ويتلقى النتيجة في النهاية. إليك الطريقة الرئيسية () لبرنامج BridgeOfficer الخاص بنا :
public static void main(String[] args) {
Truck first = new Truck();
first.weight = 10000;
Truck second = new Truck();
second.weight = 20000;
BridgeOfficer officer = new BridgeOfficer(15000);
System.out.println("Truck 1! Can I go, officer?");
boolean canFirstTruckGo = officer.checkTruck(first);
System.out.println(canFirstTruckGo);
System.out.println();
System.out.println("Truck 2! And can I?");
boolean canSecondTruckGo = officer.checkTruck(second);
System.out.println(canSecondTruckGo);
}
قمنا بإنشاء شاحنتين بحمولات 10000 و 20000. والجسر الذي يعمل فيه الضابط يبلغ الحد الأقصى لوزنه 15000. يستدعي البرنامج أسلوب الضابط.checkTruck(الأول) . تحسب الطريقة كل شيء ثم تعيد القيمة true ، والتي يقوم البرنامج بعد ذلك بحفظها في المتغير المنطقي canFirstTruckGo . الآن يمكنك أن تفعل ما تريد به (تمامًا كما تفعل بالمال الذي قدمه لك المحاسب). وفي نهاية اليوم، الرمز
boolean canFirstTruckGo = officer.checkTruck(first);
يتلخص في
boolean canFirstTruckGo = true;
إليك نقطة مهمة جدًا: عبارة الإرجاع لا تُرجع القيمة المرجعة للطريقة فحسب، بل تقوم أيضًا بإيقاف تشغيل الطريقة! لن يتم تنفيذ أي كود يأتي بعد بيان الإرجاع !
public boolean checkTruck(Truck truck) {
if (truck.weight > maxWeight) {
return false;
System.out.println("Turn around, you're overweight!");
} else {
return true;
System.out.println("Everything looks good, go ahead!");
}
}
لن يتم عرض تعليقات الضابط، لأن الطريقة قد أعادت نتيجة بالفعل وتم إنهاؤها! يعود البرنامج إلى المكان الذي تم استدعاء الطريقة فيه. لا يتعين عليك الانتباه لهذا: فمترجم Java ذكي بما يكفي لإنشاء خطأ عند محاولة كتابة التعليمات البرمجية بعد عبارة الإرجاع .
المنتقمون: حرب المعلمات
هناك مواقف نحتاج فيها إلى عدة طرق لاستدعاء إحدى الطرق. لماذا لا ننشئ ذكاءً اصطناعيًا خاصًا بنا؟ أمازون لديها Alexa، و Apple لديها Siri، فلماذا لا يكون لدينا واحد؟ :) في فيلم الرجل الحديدي، ابتكر توني ستارك ذكاءه الاصطناعي المذهل، جارفيس. دعونا نشيد بهذه الشخصية الرائعة ونطلق اسم الذكاء الاصطناعي الخاص بنا على شرفه. :) أول شيء يتعين علينا القيام به هو تعليم جارفيس إلقاء التحية على الأشخاص الذين يدخلون الغرفة (سيكون غريبًا أن يتبين أن هذا الفكر المذهل غير مهذب).public class Jarvis {
public void sayHi(String name) {
System.out.println("Good evening, " + name + ". How are you?");
}
public static void main(String[] args) {
Jarvis jarvis = new Jarvis();
jarvis.sayHi("Tony Stark");
}
}
إخراج وحدة التحكم:
Good evening, Tony Stark. How are you?
جيد جدًا! أصبح جارفيس الآن قادرًا على الترحيب بالضيوف. بالطبع، في أغلب الأحيان سيكون سيده، توني ستارك. ولكن ماذا لو لم يأتي وحده! تقبل طريقة sayHi() الخاصة بنا وسيطة واحدة فقط. وبالتالي يمكنه فقط الترحيب بشخص واحد يدخل الغرفة، وسيتجاهل الآخر. ليس مهذبا جدا، ألا توافقين على ذلك؟ :/
طريقة جافا التحميل الزائد
في هذه الحالة، يمكننا حل المشكلة ببساطة عن طريق كتابة طريقتين بنفس الاسم، ولكن بمعلمات مختلفة:public class Jarvis {
public void sayHi(String firstGuest) {
System.out.println("Good evening, " + firstGuest + ". How are you?");
}
public void sayHi(String firstGuest, String secondGuest) {
System.out.println("Good evening, " + firstGuest + " and " + secondGuest + ". How are you?");
}
}
وهذا ما يسمى طريقة التحميل الزائد. يتيح التحميل الزائد للطرق لبرنامجنا أن يكون أكثر مرونة ويستوعب طرق العمل المختلفة. دعونا نراجع كيف يعمل:
public class Jarvis {
public void sayHi(String firstGuest) {
System.out.println("Good evening, " + firstGuest + ". How are you?");
}
public void sayHi(String firstGuest, String secondGuest) {
System.out.println("Good evening, " + firstGuest + " and " + secondGuest + ". How are you?");
}
public static void main(String[] args) {
Jarvis jarvis = new Jarvis();
jarvis.sayHi("Tony Stark");
jarvis.sayHi("Tony Stark", "Captain America");
}
}
إخراج وحدة التحكم:
Good evening, Tony Stark. How are you?
Good evening, Tony Stark and Captain America. How are you?
ممتاز، كلا الإصدارين يعملان. :) لكننا لم نحل المشكلة! ماذا لو كان هناك ثلاثة ضيوف؟ يمكننا بالطبع زيادة تحميل طريقة sayHi() مرة أخرى، بحيث تقبل ثلاثة أسماء ضيوف. ولكن من الممكن أن يكون هناك 4 أو 5. وصولاً إلى اللانهاية. أليس هناك طريقة أفضل لتعليم جارفيس كيفية التعامل مع أي عدد من الأسماء، دون التحميل الزائد على طريقة sayHi() مليون مرة؟ :/ بالطبع هناك! إذا لم يكن الأمر كذلك، هل تعتقد أن Java ستكون لغة البرمجة الأكثر شعبية في العالم؟ ;)
public void sayHi(String...names) {
for (String name: names) {
System.out.println("Good evening, " + name + ". How are you?");
}
}
عند استخدام ( String...names ) كمعلمة، فهذا يشير إلى أنه سيتم تمرير مجموعة من السلاسل إلى الطريقة. ليس من الضروري أن نحدد مسبقًا عدد الأشخاص الذين سيكونون موجودين، لذا أصبحت طريقتنا الآن أكثر مرونة:
public class Jarvis {
public void sayHi(String...names) {
for (String name: names) {
System.out.println("Good evening, " + name + ". How are you?");
}
}
public static void main(String[] args) {
Jarvis jarvis = new Jarvis();
jarvis.sayHi("Tony Stark", "Captain America", "Black Widow", "Hulk");
}
}
إخراج وحدة التحكم:
Good evening, Tony Stark. How are you?
Good evening, Captain America. How are you?
Good evening, Black Widow. How are you?
Good evening, Hulk. How are you?
قد تكون بعض التعليمات البرمجية هنا غير مألوفة لك، لكن لا تقلق بشأن ذلك. إنها بسيطة في جوهرها: الطريقة تأخذ كل اسم على حدة وتحيي كل ضيف! بالإضافة إلى ذلك، فإنه سيعمل مع أي عدد من السلاسل التي تم تمريرها! اثنان، عشرة، وحتى ألف - ستعمل الطريقة بشكل صحيح مع أي عدد من الضيوف. طريقة أكثر ملاءمة من التحميل الزائد على الطريقة لجميع الاحتمالات، ألا تعتقد ذلك؟ :) إليك نقطة أخرى مهمة: ترتيب الحجج مهم! لنفترض أن طريقتنا تأخذ سلسلة ورقمًا:
public class Person {
public static void sayYourAge(String greeting, int age) {
System.out.println(greeting + " " + age);
}
public static void main(String[] args) {
sayYourAge("My age is ", 33);
sayYourAge(33, "My age is "); // Error!
}
}
إذا كانت طريقة sayYourAge الخاصة بفئة الأشخاص تأخذ سلسلة ورقمًا كمدخلات، فيجب على البرنامج تمريرها بهذا الترتيب المحدد! إذا مررناها بترتيب مختلف، فسينتج عن المترجم خطأ ولن يتمكن الشخص من تحديد عمره. بالمناسبة، المنشئات، التي تناولناها في الدرس الأخير، هي أيضًا طرق! يمكنك أيضًا تحميلها بشكل زائد (أي إنشاء عدة مُنشئات بمجموعات مختلفة من المعلمات) ويكون ترتيب الوسائط التي تم تمريرها مهمًا جدًا بالنسبة لهم أيضًا. إنها أساليب حقيقية! :)
مرة أخرى فيما يتعلق بالمعلمات
نعم، آسف، لم ننتهي منهم بعد. :) الموضوع الذي سندرسه الآن مهم جداً. هناك احتمال بنسبة 90% أن يتم سؤالك عن هذا الأمر في كل مقابلة مستقبلية! دعونا نتحدث عن تمرير الحجج إلى الأساليب. خذ مثالاً بسيطًا:public class TimeMachine {
public void goToFuture(int currentYear) {
currentYear = currentYear+10;
}
public void goToPast(int currentYear) {
currentYear = currentYear-10;
}
public static void main(String[] args) {
TimeMachine timeMachine = new TimeMachine();
int currentYear = 2018;
System.out.println("What year is it?");
System.out.println(currentYear);
timeMachine.goToPast(currentYear);
System.out.println("How about now?");
System.out.println(currentYear);
}
}
آلة الزمن لها طريقتان. كلاهما يأخذ الرقم الذي يمثل السنة الحالية كمدخل، ويزيدان أو ينقصان قيمته (اعتمادًا على ما إذا كنا نريد الذهاب إلى الماضي أو المستقبل). ولكن، كما ترون من إخراج وحدة التحكم، فإن الطريقة لا تعمل! إخراج وحدة التحكم:
What year is it?
2018
How about now?
2018
لقد قمنا بتمرير المتغير currentYear إلى طريقة goToPast() ، لكن قيمته لم تتغير. كنا في عام 2018، وبقينا هنا. لكن لماذا؟ :/ لأن الأوليات في Java يتم تمريرها إلى الأساليب حسب القيمة. ماذا يعني ذالك؟ عندما نستدعي الأسلوب goToPast() ونمرر المتغير int currentYear (=2018) إليها، فإن الطريقة لا تحصل على المتغير currentYear نفسه، بل تحصل على نسخة منه. بالطبع، قيمة هذه النسخة هي أيضًا 2018، لكن أي تغييرات في النسخة لا تؤثر على متغير currentYear الأصلي بأي شكل من الأشكال! لنجعل الكود الخاص بنا أكثر وضوحًا ونشاهد ما يحدث مع currentYear:
public class TimeMachine {
public void goToFuture(int currentYear) {
currentYear = currentYear+10;
}
public void goToPast(int currentYear) {
System.out.println("The goToPast method has started running!");
System.out.println("currentYear inside the goToPast method (at the beginning) = " + currentYear);
currentYear = currentYear-10;
System.out.println("currentYear inside the goToPast method (at the end) = " + currentYear);
}
public static void main(String[] args) {
TimeMachine timeMachine = new TimeMachine();
int currentYear = 2018;
System.out.println("What was the year when the program started?");
System.out.println(currentYear);
timeMachine.goToPast(currentYear);
System.out.println("And what year is it now?");
System.out.println(currentYear);
}
}
إخراج وحدة التحكم:
What was the year when the program started?
2018
The goToPast method has started running!
currentYear inside the goToPast method (at the beginning) = 2018
currentYear inside the goToPast method (at the end) = 2008
And what year is it now?
2018
يوضح هذا بوضوح أن المتغير الذي تم تمريره إلى طريقة goToPast() ليس سوى نسخة من currentYear . وتغيير النسخة لا يؤثر على القيمة "الأصلية". "التمرير بالرجوع" يعني العكس تمامًا. دعونا نتدرب على القطط! أعني، دعونا نرى كيف يبدو التمرير حسب المرجع باستخدام مثال القطة. :)
public class Cat {
int age;
public Cat(int age) {
this.age = age;
}
}
الآن وبمساعدة آلة الزمن الخاصة بنا، سنرسل سمادج ، أول قطة تسافر عبر الزمن في العالم، إلى الماضي والمستقبل! دعونا نعدل فئة TimeMachine بحيث تعمل مع كائنات Cat ؛
public class TimeMachine {
public void goToFuture(Cat cat) {
cat.age += 10;
}
public void goToPast(Cat cat) {
cat.age -= 10;
}
}
الآن لا تقوم الطرق بتغيير الرقم الذي تم تمريره فقط. بدلاً من ذلك، يقومون بتغيير حقل عمر Cat المحدد . ستتذكر أن هذا لم ينجح بالنسبة لنا مع الأرقام الأولية، لأن الرقم الأصلي لم يتغير. دعنا نرى ما سيحدث!
public static void main(String[] args) {
TimeMachine timeMachine = new TimeMachine();
Cat smudge = new Cat(5);
System.out.println("How old was Smudge when the program started?");
System.out.println(smudge.age);
timeMachine.goToFuture(smudge);
System.out.println("How about now?");
System.out.println(smudge.age);
System.out.println("Holy smokes! Smudge has aged 10 years! Back up quickly!");
timeMachine.goToPast(smudge);
System.out.println("Did it work? Have we returned the cat to its original age?");
System.out.println(smudge.age);
}
إخراج وحدة التحكم:
How old was Smudge when the program started running?
5
How about now?
15
Holy smokes! Smudge has aged 10 years! Back up quickly!
Did it work? Have we returned the cat to its original age?
5
رائع! الآن فعلت الطريقة شيئًا مختلفًا: لقد تقدمت قطتنا في العمر بشكل كبير، لكنها عادت شابة مرة أخرى! :) دعونا نحاول معرفة السبب. على عكس المثال مع الأوليات، عندما يتم تمرير الكائنات إلى طريقة ما، يتم تمريرها حسب المرجع. تم تمرير مرجع إلى كائن التلطخ الأصلي إلى طريقة ChangeAge() . لذلك، عندما نقوم بتغيير smudge.age داخل الطريقة، فإننا نشير إلى نفس منطقة الذاكرة التي تم تخزين كائننا فيها. إنها إشارة إلى نفس Smudge الذي أنشأناه في البداية. وهذا ما يسمى "التمرير عن طريق المرجع"! ومع ذلك، ليس كل شيء مع المراجع بهذه السهولة. :) دعونا نحاول تغيير مثالنا:
public class TimeMachine {
public void goToFuture(Cat cat) {
cat = new Cat(cat.age);
cat.age += 10;
}
public void goToPast(Cat cat) {
cat = new Cat(cat.age);
cat.age -= 10;
}
public static void main(String[] args) {
TimeMachine timeMachine = new TimeMachine();
Cat smudge = new Cat(5);
System.out.println("How old was Smudge when the program started?");
System.out.println(smudge.age);
timeMachine.goToFuture(smudge);
System.out.println ("Smudge went to the future! Has his age changed?");
System.out.println(smudge.age);
System.out.println ("And if you try going back?");
timeMachine.goToPast(smudge);
System.out.println(smudge.age);
}
}
إخراج وحدة التحكم:
How old was Smudge when the program started running?
5
Smudge went to the future! Has his age changed?
5
And if you try going back?
5
لا يعمل مرة أخرى! О_О دعونا معرفة ما حدث. :) يتعلق الأمر بكل ما يتعلق بأساليب goToPast / goToFuture وكيفية عمل المراجع. الآن، انتباهكم، من فضلك! هذا هو أهم شيء يجب فهمه حول كيفية عمل المراجع والأساليب. الحقيقة هي أنه عندما نطلق على طريقة goToFuture(Cat cat) ، فهي نسخة من المرجع إلى كائن cat الذي تم تمريره، وليس المرجع نفسه. وبالتالي، عندما نقوم بتمرير كائن إلى طريقة ما، هناك مرجعان للكائن. وهذا مهم جدًا لفهم ما يحدث. وهذا هو بالضبط سبب عدم تغير عمر القطة في مثالنا الأخير. في المثال السابق، عند تغيير العمر، أخذنا ببساطة المرجع الذي تم تمريره إلى طريقة goToFuture() ، واستخدمناه للعثور على الكائن في الذاكرة وتغيير عمره ( cat.age += 10 ). لكن الآن، داخل طريقة goToFuture() ، نقوم بإنشاء كائن جديد ( cat = new Cat(cat.age) )، ويتم تعيين نفس النسخة المرجعية التي تم تمريرها إلى الطريقة لهذا الكائن. نتيجة ل:
- المرجع الأول ( Cat smudge = New Cat (5) ) يشير إلى القطة الأصلية (بعمر 5 سنوات)
- بعد ذلك، عندما مررنا المتغير cat بطريقة goToPast() وقمنا بتعيين كائن جديد له، تم نسخ المرجع.
cat.age += 10;
وبالطبع، في الطريقة main() يمكننا أن نرى على وحدة التحكم أن عمر القطة smudge.age لم يتغير. بعد كل شيء، smudge هو متغير مرجعي لا يزال يشير إلى الكائن الأصلي القديم الذي يبلغ عمره 5 سنوات، ولم نفعل أي شيء بهذا الكائن. تم إجراء جميع التغييرات العمرية على الكائن الجديد. لذلك، اتضح أن الكائنات يتم تمريرها إلى الأساليب حسب المرجع. لا يتم إنشاء نسخ من الكائنات تلقائيًا أبدًا. إذا قمت بتمرير كائن قطة إلى طريقة ما وقمت بتغيير عمره، فسوف تقوم بتغيير عمره. ولكن يتم نسخ المتغيرات المرجعية عند تعيين القيم و/أو طرق الاتصال! دعونا نكرر هنا ما قلناه عن تمرير العناصر الأولية: "عندما نستدعي الأسلوب ChangeInt() ونمرر المتغير int x (=15) ، فإن الطريقة لا تحصل على المتغير x نفسه، بل تحصل على نسخة منه. لذلك، أي تغييرات يتم إجراؤها على النسخة لا تؤثر على المتغير x الأصلي بأي شكل من الأشكال." عند نسخ المراجع، كل شيء يعمل بنفس الطريقة تمامًا! قمت بتمرير كائن القط إلى الطريقة. إذا فعلت شيئًا ما للقطة نفسها (أي مع وجود الكائن في الذاكرة)، فسيتم تطبيق جميع تغييراتك بنجاح، نظرًا لأنه كان لدينا كائن واحد فقط ولا يزال لدينا كائن واحد فقط. لكن، إذا قمت بإنشاء كائن جديد داخل الطريقة وقمت بتعيينه إلى المتغير المرجعي الذي تم تمريره إلى الطريقة كوسيطة، فسوف تقوم فقط بتعيين الكائن الجديد إلى نسخة من المتغير المرجعي. من تلك اللحظة فصاعدا، سيكون لدينا كائنين ومتغيرين مرجعيين. هذا كل شيء! لم يكن ذلك سهلاً. ربما كان عليك قراءة الدرس عدة مرات. لكن الشيء المهم هو أنك أتقنت هذا الموضوع فائق الأهمية. سينتهي بك الأمر إلى الجدال أكثر من مرة حول كيفية تمرير الوسائط في Java (حتى بين المطورين ذوي الخبرة). لكن الآن أنت تعرف بالضبط كيف يعمل. استمر! :) لتعزيز ما تعلمته، نقترح عليك مشاهدة درس فيديو من دورة Java الخاصة بنا
المزيد من القراءة: |
---|
GO TO FULL VERSION