الوراثة وتعدّد الأشكال

مشروع: هرمية الأشكال الهندسية

15 دقيقة الدرس 10 من 14

مشروع: هرمية الأشكال الهندسية

يجمع هذا الدرس الختامي كل ما تعلّمته في هذا الفصل — الفئات المجردة، والوراثة، وتجاوز التوابع، والتعددية، والإرسال الديناميكي — في برنامج صغير ولكنّه مكتمل. ستصمّم هرمية للشكل Shape، وتُنفّذ صنفَين فرعيَّين محدَّدَين، وستكتب أداة تعمل مع أي شكل من خلال قائمة متعددة الأنواع.

الهدف

بناء برنامج يستطيع:

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

هذا عرض كلاسيكي لسبب وجود الوراثة والتعددية: تكتب خوارزمية واحدة (totalArea) تعمل مع أشكال لم تخترعها بعد.

الخطوة الأولى — الفئة الأساسية المجردة

تُعرِّف الفئة المجردة العقد الذي يجب أن تلتزم به جميع الأشكال دون تقييد أي تنفيذ محدَّد. لا معنى لتنفيذ area() على مستوى Shape — إذ سيحسبها كل صنف فرعي بطريقة مختلفة — لذا نُعلنها abstract.

public abstract class Shape { // يجب على كل شكل محدد تنفيذ هذا التابع public abstract double area(); // تابع محدد مشترك بين جميع الأشكال public String describe() { return getClass().getSimpleName() + " with area = " + String.format("%.2f", area()); } }
لماذا getClass().getSimpleName()؟ لأن describe() تعيش في الفئة الأساسية المجردة لكنّها تُستدعى على كائن من صنف فرعي، فتُعيد getClass() النوع الفعلي وقت التشغيل (Circle أو Rectangle أو غيرهما). هذا هو الإرسال الديناميكي يعمل داخل الفئة الأساسية نفسها.

الخطوة الثانية — الصنف الفرعي Circle

public class Circle extends Shape { private final double radius; public Circle(double radius) { if (radius <= 0) { throw new IllegalArgumentException("Radius must be positive"); } this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } }
تحقّق في المُنشئ. رفض البيانات غير الصحيحة مبكّرًا (نصف قطر سالب) يعني أنّه لا يمكن أن يوجد كائن Circle غير صالح على الإطلاق. هذا يُبقي كائناتك دائمًا في حالة متّسقة وذات معنى.

الخطوة الثالثة — الصنف الفرعي Rectangle

public class Rectangle extends Shape { private final double width; private final double height; public Rectangle(double width, double height) { if (width <= 0 || height <= 0) { throw new IllegalArgumentException("Width and height must be positive"); } this.width = width; this.height = height; } @Override public double area() { return width * height; } }

الخطوة الرابعة — حساب المجموع بالتعددية

هنا تظهر الفائدة الحقيقية. يقبل تابع totalArea قائمة List<Shape> — لا يعرف ولا يهتم بما إذا كانت القائمة تحتوي على دوائر أو مستطيلات أو أشكال لم تُصنَع بعد. كل استدعاء لـ s.area() يُرسَل إلى التنفيذ الصحيح وقت التشغيل.

import java.util.List; public class ShapeCalculator { public static double totalArea(List<Shape> shapes) { double total = 0; for (Shape s : shapes) { total += s.area(); // إرسال ديناميكي في كل تكرار } return total; } }

الخطوة الخامسة — تجميع كل شيء

import java.util.List; public class Main { public static void main(String[] args) { List<Shape> shapes = List.of( new Circle(5), new Rectangle(4, 6), new Circle(3), new Rectangle(10, 2) ); // طباعة وصف كل شكل (استدعاء متعدد الأشكال) for (Shape s : shapes) { System.out.println(s.describe()); } // حساب المجموع الكلي (متعدد الأشكال) double total = ShapeCalculator.totalArea(shapes); System.out.printf("%nTotal area of all shapes: %.2f%n", total); } }

مثال على الإخراج:

Circle with area = 78.54 Rectangle with area = 24.00 Circle with area = 28.27 Rectangle with area = 20.00 Total area of all shapes: 150.80

لماذا يعمل هذا التصميم؟

  • مبدأ الفتح/الإغلاق: إضافة صنف Triangle لا تستلزم أي تغيير في ShapeCalculator أو Main (سوى إضافته للقائمة). الكود الموجود مغلق أمام التعديل ومفتوح أمام التوسعة.
  • المسؤولية الفردية: تعرف Circle كيف تحسب مساحة الدائرة. تعرف ShapeCalculator كيف تجمع المساحات. لا تداخل بينهما.
  • التعددية تُلغي سلاسل if/instanceof: بدون البرمجة كائنية التوجه ستكتب if (s instanceof Circle) ... else if (s instanceof Rectangle) ... — كتلة هشّة يجب تحديثها مع كل شكل جديد.
تجنّب التحقق من النوع بالتبديل. كود من قبيل if (shape instanceof Circle) { ... } else if (shape instanceof Rectangle) { ... } يُفسد الغرض من التعددية. إذا وجدت نفسك تكتب هذا النمط، انقل السلوك إلى تابع مُجاوَز في كل صنف فرعي عوضًا عن ذلك.

توسعة الهرمية

لإضافة مثلث Triangle تحتاج فقط إلى كتابة صنف واحد جديد — لا يتغيّر شيء في بقية البرنامج:

public class Triangle extends Shape { private final double base; private final double height; public Triangle(double base, double height) { this.base = base; this.height = height; } @Override public double area() { return 0.5 * base * height; } }

أضف new Triangle(6, 4) إلى القائمة في Main وسيعالجه totalArea دون تغيير سطر واحد آخر.

الخلاصة

لقد بنيت هرمية صغيرة لكنّها ذات شكل إنتاجي حقيقي: فئة أساسية مجردة تُعرِّف العقد، أصناف فرعية محددة تُنفِّذه، وخوارزمية متعددة الأشكال تعمل على أي شكل من خلال النوع الأساسي. هذا النمط — نوع مجرد، تنفيذات متعددة، خوارزمية واحدة — هو أساس عدد لا يُحصى من واجهات Java البرمجية الحقيقية بما فيها java.io.InputStream وjava.util.Collection وكل مكتبة مكونات واجهة مستخدم كُتبت على الإطلاق.