برمجة الكائنات في جافا: الفئات، الكائنات، التغليف، الوراثة والتجريد

تعتبر جافا باستمرار واحدة من اللغات الثلاث الأكثر شعبية في العالم. إن اعتمادها في مجالات مثل تطوير البرمجيات المؤسسية، وتطبيقات أندرويد الهاتف المحمول، وتطبيقات الويب على نطاق واسع لا يضاهى. إن نظامها القوي في النوع، والنظام البيئي الواسع، وقدرتها على “الكتابة مرة واحدة، التشغيل في أي مكان” تجعلها جذابة بشكل خاص لبناء أنظمة قوية وقابلة للتوسع. في هذه المقالة، سنستكشف كيف تمكن ميزات البرمجة الكائنية في جافا المطورين من استغلال هذه القدرات بشكل فعال، مما يسمح لهم ببناء تطبيقات قابلة للصيانة والتوسع من خلال تنظيم الكود بشكل صحيح وإعادة استخدامه.

ملاحظة حول تنظيم وتشغيل كود جافا

قبل أن نبدأ في كتابة أي كود، دعنا نقوم ببعض الإعدادات.

مثلما هو الحال مع الصياغة، تمتلك جافا قواعد صارمة حول تنظيم الكود.

أولاً، يجب أن تكون كل فئة عامة في ملف خاص بها، تحمل نفس اسم الفئة ولكن بامتداد .java. لذا، إذا أردت كتابة فئة Laptop، يجب أن يكون اسم الملف Laptop.javaحساس لحالة الأحرف. يمكن وضع الفئات غير العامة في نفس الملف، ولكن من الأفضل فصلهم. أعلم أننا نتقدم قليلاً—نتحدث عن تنظيم الفئات حتى قبل كتابتها—لكن الحصول على فكرة تقريبية عن مكان وضع الأشياء مسبقًا فكرة جيدة.

يجب أن تحتوي جميع مشاريع جافا على ملف Main.java مع فئة Main. هنا يتم اختبار فئاتك من خلال إنشاء كائنات منها.

لتشغيل شفرة جافا، سنستخدم IntelliJ IDEA، واجهة تطوير متكاملة شهيرة لجافا. بعد تثبيت IntelliJ:

  1. أنشئ مشروع جافا جديد (ملف > جديد > مشروع)
  2. انقر بزر الماوس الأيمن على المجلد src لإنشاء Main.java والصق المحتويات التالية:
public class Main { public static void main(String[] args) { // إنشاء واختبار الكائنات هنا } }

عندما نتحدث عن الفصول، نكتب الشيفرة في ملفات أخرى غير ملف Main.java . ولكن إذا كنا نتحدث عن إنشاء واختبار الكائنات، ننتقل إلى Main.java.

لتشغيل البرنامج، يمكنك النقر على زر التشغيل الأخضر بجوار الطريقة الرئيسية:

سيتم عرض الناتج في نافذة أداة التشغيل في الأسفل.

إذا كنت جديدًا تمامًا على لغة الجافا، يرجى الاطلاع على دورتنا التعليمية للجافا، التي تغطي أساسيات أنواع البيانات في الجافا وسير التحكم قبل المتابعة.

وإلا، لنقم بالانتقال مباشرةً.

الفصول والكائنات في الجافا

إذا، ما هي الفئات بالضبط؟

الفئات هي بنى برمجية في جافا تُستخدم لتمثيل مفاهيم العالم الحقيقي. على سبيل المثال، فكر في فئة MenuItem (أنشئ ملفًا لكتابة هذه الفئة في بيئة تطوير البرمجيات الخاصة بك):

public class MenuItem { public String name; public double price; }

تُعطي الصف النموذج أو القالب لتمثيل مختلف عناصر القائمة في مطعم. من خلال تغيير السمتين للصف، name, و price، يمكننا إنشاء عناصر القائمة بلا حصر مثل برغر أو سلطة.

لإنشاء فئة في جافا، تبدأ بسطر يصف مستوى الوصول إلى الفئة (خاص، عام، أو محمي) يتبعه اسم الفئة. مباشرة بعد الأقواس، تحدد سمات فئتك.

ولكن كيف ننشئ كائنات تنتمي إلى هذه الفئة؟ يتيح لنا جافا القيام بذلك من خلال طرق البناء:

public class MenuItem { public String name; public double price; // طريقة البناء public MenuItem(String name, double price) { this.name = name; this.price = price; } }

طريقة البناء هي طريقة خاصة يتم استدعاؤها عندما ننشئ كائنًا جديدًا من فئة. تقوم بتهيئة سمات الكائن بالقيم التي نقدمها. في المثال أعلاه، تأخذ طريقة البناء معلمتي الاسم والسعر وتعينهما لحقول الكائن باستخدام الكلمة المفتاحية ‘this’ للإشارة إلى كائن مستقبلي.

يختلف بناء الجملة للبناء عن أساليب الفئة الأخرى لأنه لا يتطلب منك تحديد نوع العائد. أيضًا، يجب أن يحمل البناء نفس اسم الفئة، ويجب أن يحتوي على نفس عدد السمات التي قمت بتعريفها بعد تعريف الفئة. أعلاه، يقوم البناء بإنشاء سمتين لأننا قمنا بتعريف سمتين بعد تعريف الفئة: name و price.

بعد كتابة الفئة وبنائها، يمكنك إنشاء مثيلات (كائنات) منها في الطريقة الرئيسية:

public class Main { public static void main(String[] args) { // أنشئ الكائنات هنا MenuItem burger = new MenuItem("Burger", 3.5); MenuItem salad = new MenuItem("Salad", 2.5); System.out.println(burger.name + ", " + burger.price); } }

النتيجة:

Burger, 3.5

أعلاه، نقوم بإنشاء اثنين من MenuItem كائنات في متغيرين هما burger و salad. كما هو مطلوب في لغة الجافا، يجب تعريف نوع المتغير، والذي هو MenuItem. ثم، لإنشاء مثيل من فئتنا، نكتب الكلمة المفتاحية new تليها استدعاء طريقة المُنشئ.

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

public class MenuItem { public String name; public double price; // الكونستراكتور public MenuItem(String name, double price) { this.name = name; this.price = price; } // الطريقة لحساب السعر بعد الضريبة public double getPriceAfterTax() { double taxRate = 0.08; // نسبة الضريبة 8% return price + (price * taxRate); } }

الآن يمكننا حساب السعر الإجمالي، بما في ذلك الضريبة:

public class Main { public static void main(String[] args) { MenuItem burger = new MenuItem("Burger", 3.5); System.out.println("Price after tax: $" + burger.getPriceAfterTax()); } }

الناتج:

Price after tax: $3.78

التغليف

غرض الصنف هو توفير نموذج لإنشاء الكائنات. سيتم استخدام هذه الكائنات بواسطة سكربتات أو برامج أخرى. على سبيل المثال، يمكن استخدام كائناتنا من نوع MenuItem من قبل واجهة مستخدم تعرض اسمها، وسعرها، وصورتها على الشاشة.

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

// داخل Main.java MenuItem applePie = new MenuItem("Apple Pie", -5.99); // سعر سالب! MenuItem sandwich = new MenuItem("Sandwich", 1000000); // باهظ الثمن System.out.println("Apple pie price: $" + applePie.price); System.out.println("Sandwich price: $" + sandwich.price);

لذلك، الأمر الأول بعد كتابة صنف هو حماية سماته من خلال تقييد كيفية إنشائها والوصول إليها. في البداية، نريد السماح فقط بالقيم الإيجابية لـالسعر وتحديد قيمة قصوى لتجنب عرض العناصر المكلفة بشكل لا معقول عن طريق الخطأ.

تتيح لنا جافا تحقيق ذلك باستخدام طرق الوضع (Setter Methods):

public class MenuItem { private String name; private double price; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { this.name = name; setPrice(price); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

دعونا نفحص ما هو جديد في كتلة الشيفرة أعلاه:

1. جعلنا الخصائص خاصة عن طريق إضافة private الكلمة المفتاحية. هذا يعني أنه يمكن الوصول إليها فقط داخل فئة MenuItem. تبدأ التغليف بهذه الخطوة الحاسمة.

2. أضفنا ثابتًا جديدًا MAX_PRICE وهو:

  • خاص (يمكن الوصول إليه فقط داخل الفئة)
  • ثابت (مشترك بين جميع الحالات)
  • نهائي (لا يمكن تغييره بعد التهيئة)
  • تم تعيينه إلى 100.0 دولار كحد أقصى معقول للسعر

3. أضفنا setPrice() طريقة تقوم بـ:

  • أخذ معلمة السعر
  • يتحقق من أن السعر ليس سالبًا
  • يتحقق من أن السعر لا يتجاوز MAX_PRICE
  • يُلقي استثناء IllegalArgumentException مع رسائل وصفية في حالة فشل التحقق
  • يُعين السعر فقط إذا نجحت جميع عمليات التحقق

4. قمنا بتعديل البناء لاستخدام setPrice() بدلاً من تعيين السعر مباشرة. يضمن هذا حدوث التحقق من السعر أثناء إنشاء الكائن.

لقد قمنا للتو بتنفيذ أحد الركائز الأساسية لتصميم البرمجيات الجيد القائم على الكائنات — التغليف. يفرض هذا النموذج إخفاء البيانات والوصول المحدود إلى سمات الكائن، مما يضمن حماية تفاصيل التنفيذ الداخلية من التدخل الخارجي ولا يمكن تعديلها إلا من خلال واجهات محددة جيدًا.

دعونا نوضح النقطة من خلال تطبيق التغليف على سمة الاسم. تخيل أن لدينا مقهى يقدم فقط اللاتيه، والكابتشينو، والإسبريسو، والأمريكانو، والموكا.

لذا، يمكن أن تكون أسماء عناصر القائمة الخاصة بنا فقط واحدة من العناصر في هذه القائمة. ها هي الطريقة التي يمكننا بها تطبيق ذلك في الكود:

// بقية الفئة هنا ... private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private String name; public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); }

يقوم الكود أعلاه بتنفيذ التحقق من الاسم لعناصر القائمة في مقهى. دعونا نقسمها:

1. أولاً، يعرّف مصفوفة ثابتة خاصة VALID_NAMES تحتوي على أسماء المشروبات المسموح بها فقط: لاتيه، كابتشينو، اسبريسو، أمريكانو، وموكا. يكون هذه المصفوفة:

  • خاص: يمكن الوصول إليه فقط داخل الفئة
  • ثابت: يتم مشاركته عبر جميع الحالات
  • النهائي: لا يمكن تعديله بعد التهيئة

2. يعلن حقل String اسم خاص لتخزين اسم المشروب

3. تقوم طريقة setName() بتنفيذ منطق التحقق:

  • تأخذ معلمة اسم من نوع String
  • تحولها إلى حروف صغيرة لجعل المقارنة غير حساسة لحالة الأحرف
  • تكرر من خلال مصفوفة VALID_NAMES
  • إذا تم العثور على تطابق، يعين الاسم ويعيد
  • إذا لم يتم العثور على تطابق، يقوم برمي استثناء IllegalArgumentException مع رسالة وصفية تعرض جميع الخيارات الصحيحة

ها هي الفئة الكاملة حتى الآن:

public class MenuItem { private String name; private double price; private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { setName(name); setPrice(price); } public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

بعد حماية طريقة إنشاء السمات، نريد أيضًا حماية كيفية الوصول إليها. يتم ذلك باستخدام طرق الحصول:

public class MenuItem { // بقية الشيفرة هنا ... public String getName() { return name; } public double getPrice() { return price; } }

توفر طرق الحصول وصولًا مراقبًا إلى السمات الخاصة بفئة. إنها تحل مشكلة الوصول المباشر إلى السمة التي يمكن أن تؤدي إلى تعديلات غير مرغوب فيها وتكسير التجزئة.

على سبيل المثال، بدون getters، قد نصل إلى السمات مباشرة:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.name; // الوصول المباشر إلى السمة item.name = "INVALID"; // يمكن تعديلها مباشرة، متجاوزة الصلاحية

باستخدام getters، نفرض الوصول السليم:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.getName(); // وصول متحكم من خلال الـ getter // item.name = "INVALID"; // غير مسموح - يجب استخدام setName() الذي يقوم بالتحقق

هذا التغليف:

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

التوريث

بدأت فصلنا يبدو جيدًا ولكن هناك العديد من المشاكل معه. على سبيل المثال، لمطعم كبير يخدم العديد من أنواع الأطباق والمشروبات، الفصل ليس مرنًا بما يكفي.

إذا أردنا إضافة أنواع مختلفة من الطعام، سنواجه العديد من التحديات. يمكن إعداد بعض الأطباق للتناول في المنزل، بينما تحتاج البعض الآخر إلى تناول فوري. قد تكون لديها العناصر المختلفة من القائمة أسعارًا وخصومات متفاوتة. قد تحتاج الأطباق إلى تتبع درجة الحرارة أو تخزين خاص. يمكن أن تكون المشروبات ساخنة أو باردة مع مكونات قابلة للتخصيص. قد تحتاج العناصر إلى معلومات حساسية وخيارات حجم. النظام الحالي لا يتعامل مع هذه المتطلبات المتنوعة.

الوراثة توفر حلاً أنيقًا لجميع هذه المشاكل. تسمح لنا بإنشاء إصدارات متخصصة من عناصر القائمة عن طريق تحديد فئة MenuItem الأساسية مع السمات المشتركة ثم إنشاء فئات فرعية ترث هذه الأساسيات مع إضافة ميزات فريدة.

على سبيل المثال، يمكن أن يكون لدينا فئة Drink للمشروبات مع خيارات الحرارة، وفئة Food للعناصر التي تحتاج إلى استهلاك فوري، وفئة Dessert للعناصر التي تحتاج إلى طريقة تخزين خاصة – جميعها ترث وظائف عنصر القائمة الأساسية.

توسيع الفئات

لنقم بتنفيذ تلك الأفكار بدءًا من Drink:

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { this.name = name; this.price = price; this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

لتعريف فئة فرعية ترث من فئة أساسية، نستخدم الكلمة المفتاحيةextends بعد اسم الفئة الفرعية تتبعه اسم الفئة الأساسية. بعد تعريف الفئة، نعرف أي سمات جديدة لهذه الفئة الفرعية وننفذ مُنشئها.

ولكن لاحظ كيف يتعين علينا تكرار تهيئة name و price بالإضافة إلى isCold. هذا ليس مثاليًا لأن الفئة الأم قد تحتوي على مئات السمات. بالإضافة إلى ذلك، سيرمي الكود أعلاه خطأ عند تجميعه لأن هذه ليست الطريقة الصحيحة لتهيئة سمات الفئة الأم. الطريقة الصحيحة ستكون باستخدام الكلمة الرئيسية super:

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

الكلمة super تُستخدم لاستدعاء مُنشئ الفئة الأم. في هذه الحالة، super(name, price) يستدعي MenuItem مُنشئها لتهيئة تلك السمات، مما يجنب تكرار الشيفرة. نحن بحاجة فقط إلى تهيئة السمة الجديدة isCold الخاصة بفئة Drink.

الكلمة المفتاحية مرنة جدًا لأنه يمكنك استخدامها للإشارة إلى الفئة الأم في أي جزء من الفئة الفرعية. على سبيل المثال، لاستدعاء طريقة من الفئة الأم، تستخدم super.methodName() بينما super.attributeName للاستخدام مع الخصائص.

تجاوز الطريقة

الآن، لنفترض أننا نريد إضافة طريقة جديدة إلى فئاتنا لحساب السعر الإجمالي بعد الضريبة. نظرًا لأن العناصر المختلفة في القائمة قد تحتوي على معدلات ضريبة مختلفة (على سبيل المثال، الطعام المُعد مقابل المشروبات المعبأة)، يمكننا استخدام تجاوز الطريقة لتنفيذ حسابات ضريبة محددة في كل فئة فرعية مع الحفاظ على اسم طريقة شائع في الفئة الأم.

إليك كيف يبدو هذا:

public class MenuItem { // بقية فئة MenuItem public double calculateTotalPrice() { // معدل الضريبة الافتراضي هو 10% return price * 1.10; } } public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { // الطعام عليه ضريبة 15% return super.getPrice() * 1.15; } } public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } @Override public double calculateTotalPrice() { // المشروبات عليها ضريبة 8% return super.getPrice() * 1.08; } }

في هذا المثال، يسمح تجاوز الطريقة لكل فئة فرعية بتوفير تنفيذها الخاص لـ calculateTotalPrice():

تعريف القاعدة الافتراضي للضريبة بنسبة ١٠٪ هو الفئة MenuItem.

عندما الطعام و الشراب تمتد الفئات عنصر القائمة, فإنها تعدل هذه الطريقة لتنفيذ معدلات الضريبة الخاصة بها:

  • تمتلك عناصر الطعام معدل ضريبة أعلى بنسبة 15٪
  • تتمتع المشروبات بمعدل ضريبة أقل يبلغ 8%

تستخدم @Override للتعبير بوضوح عن أن هذه الطرق تقوم بتجاوز طريقة الفئة الأصلية. يساعد ذلك في اكتشاف الأخطاء إذا لم تتطابق توقيع الطريقة مع الفئة الأصلية.

لا يزال بإمكان كل فئة فرعية الوصول إلى سعر الفئة الأصلية باستخدام super.getPrice()، مما يُظهر كيف يمكن أن تستفيد الطرق المتجاوزة من وظائف الفئة الأصلية بينما تضيف سلوكها الخاص.

باختصار، تعد تجاوز الطريقة جزءًا أساسيًا من التوريث يسمح للفئات الفرعية بتوفير تنفيذها للطرق المحددة في الفئة الأب، مما يتيح سلوكًا أكثر تحديدًا مع الاحتفاظ بنفس توقيع الطريقة.

فئات مجردة

تعمل تسلسل الفئة لدينا، ولكن هناك مشكلة: هل ينبغي لأي شخص أن يكون قادرًا على إنشاء عنصر MenuItem عادي؟ على كل حال، في مطعمنا، كل عنصر قائمة إما طعام أو مشروب – ليس هناك شيء مثل “عنصر قائمة عام”.

يمكننا منع ذلك من خلال جعل MenuItem فئة مجردة. توفر الفئة المجردة فقط مخططًا أساسيًا – يمكن استخدامها فقط كفئة والد للإرث، ولا يمكن تهيئتها مباشرة.

لجعل MenuItem مجردًا، نضيف الكلمة المفتاحية abstract بعد محدد الوصول الخاص به:

public abstract class MenuItem { private String name; private double price; public MenuItem(String name, double price) { setName(name); setPrice(price); } // الدوال الموجودة تبقى كما هي // جعل هذه الدالة مجردة - يجب على كل فصيلة فرعية تنفيذها public abstract double calculateTotalPrice(); }

يمكن أن تحتوي الفئات المجردة أيضًا على طرق مجردة مثل calculateTotalPrice() أعلاه. تعمل هذه الطرق المجردة كعقود تجبر الفئات الفرعية على تقديم تنفيذاتها. بعبارة أخرى، يجب تنفيذ أي طريقة مجردة في فئة مجردة بواسطة الفئات الفرعية.

لذا، دعنا نعيد كتابةالطعام والشراب مع هذه التغييرات في الاعتبار:

public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { return getPrice() * 1.15; // ضريبة 15% } } public class Drink extends MenuItem { private boolean hasCaffeine; public Drink(String name, double price, boolean hasCaffeine) { super(name, price); this.hasCaffeine = hasCaffeine; } @Override public double calculateTotalPrice() { return getPrice() * 1.10; // ضريبة 10% } }

من خلال تنفيذ نظام القائمة هذا، رأينا كيف تعمل التجريد والوراثة معًا لإنشاء كود مرن وقابل للصيانة يمكن أن يتكيف بسهولة مع متطلبات الأعمال المختلفة.

الاستنتاج

اليوم، لقد ألقينا نظرة سريعة على قدرات الجافا كلغة برمجة موجهة نحو الكائنات. لقد غطينا أساسيات الفصائل، الكائنات، وبعض الركائز الرئيسية لبرمجة الكائنات: التغليف، والوراثة، والتجريد من خلال نظام قوائم الطعام في مطعم.

لجعل هذا النظام جاهزًا للإنتاج، لا تزال تحتاج إلى تعلم العديد من الأمور، مثل الواجهات (جزء من التجريد)، التعددية، وأنماط تصميم برمجة الكائنات. لمزيد من المعلومات حول هذه المفاهيم، راجع مقدمة في برمجة الكائنات في الجافا الدورة.

إذا كنت ترغب في اختبار معرفتك بلغة جافا، حاول الإجابة عن بعض الأسئلة في مقالة أسئلة مقابلة جافا لدينا.

Source:
https://www.datacamp.com/tutorial/oop-in-java