مبادئ البرمجة كائنية التوجه في جافا: دليل شامل للمبتدئين في مفاهيم OOP

دقائق القراءة: 21

البرمجة كائنية التوجه توفر طريقة مستدامة لكتابة أكواد معقدة. إنها تتيح لك تجميع البرامج كسلسلة من الرقع.

— بول جراهام

أساسيات البرمجة كائنية التوجه (OOP)

البرمجة كائنية التوجه (Object-Oriented Programming) هي نموذج برمجي يتم فيه تمثيل كل شيء كـ كائن (object). تتراسل الكائنات فيما بينها، ويقرر كل كائن ما سيفعله بالرسالة المستلمة. تركز البرمجة كائنية التوجه على حالات وسلوكيات كل كائن.

ما هي الكائنات؟

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

يمكننا في الواقع تمثيل كلب حقيقي في برنامج ككائن برمجي عن طريق تحديد حالاته وسلوكياته. الكائنات البرمجية هي التمثيل الفعلي لكائنات العالم الحقيقي. يتم تخصيص الذاكرة في ذاكرة الوصول العشوائي (RAM) عند إنشاء كائن منطقي. يُشار إلى الكائن أيضًا على أنه نسخة (instance) من فئة (class). إنشاء نسخة من فئة يعني نفس الشيء مثل إنشاء كائن.

الشيء المهم الذي يجب تذكره عند إنشاء كائن هو: يجب أن يكون نوع المرجع (reference type) هو نفس نوع الكائن أو نوعًا أعلى منه (super type). سنتطرق إلى ما هو نوع المرجع لاحقًا في هذا المقال.

ما هي الفئات (Classes)؟

الفئة هي قالب أو مخطط يتم من خلاله إنشاء الكائنات. تخيل الفئة كقالب لتقطيع البسكويت، والكائنات كالبسكويت الناتج.

صورة توضيحية لقالب بسكويت وبسكويت، تمثل العلاقة بين الفئة والكائن في البرمجة كائنية التوجه.

الشكل 1: يوضح العلاقة بين الفئة والكائن من خلال قالب البسكويت والبسكويت.

تحدد الفئات الحالات كمتغيرات للنسخة (instance variables) والسلوكيات كطرق للنسخة (instance methods). تُعرف متغيرات النسخة أيضًا بمتغيرات الأعضاء (member variables). لا تستهلك الفئات أي مساحة في الذاكرة.

لإعطائك فكرة عن الفئات والكائنات، دعنا ننشئ فئة Cat تمثل حالات وسلوكيات القطط في العالم الحقيقي.

 public class Cat {
    /* Instance variables: states of Cat */
    String name;
    int age;
    String color;
    String breed;

    /* Instance methods: behaviors of Cat */
    void sleep () {
        System.out.println( "Sleeping" );
    }

    void play () {
        System.out.println( "Playing" );
    }

    void feed () {
        System.out.println( "Eating" );
    }
 }

الآن، قمنا بتعريف قالب للقط بنجاح. لنفترض أن لدينا قطتين اسمهما ثور (Thor) ورامبو (Rambo).

قط روسي أزرق اسمه ثور نائم، يوضح كائنًا من فئة القطط.

الشكل 2: ثور نائم.

قط ماين كون اسمه رامبو يلعب، يوضح كائنًا آخر من فئة القطط.

الشكل 3: رامبو يلعب.

كيف يمكننا تعريفهم في برنامجنا؟ أولاً، نحتاج إلى إنشاء كائنين من فئة Cat.

 public class Main {
    public static void main (String[] args) {
        Cat thor = new Cat();
        Cat rambo = new Cat();
    }
 }

بعد ذلك، سنحدد حالاتهم وسلوكياتهم.

 public class Main {
    public static void main (String[] args) {
        /* Creating objects */
        Cat thor = new Cat();
        Cat rambo = new Cat();

        /* Defining Thor cat */
        thor.name = "Thor";
        thor.age = 3;
        thor.breed = "Russian Blue";
        thor.color = "Brown";
        thor.sleep();

        /* Defining Rambo cat */
        rambo.name = "Rambo";
        rambo.age = 4;
        rambo.breed = "Maine Coon";
        rambo.color = "Brown";
        rambo.play();
    }
 }

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

مبادئ البرمجة كائنية التوجه

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

  • التغليف (Encapsulation)
  • الوراثة (Inheritance)
  • التجريد (Abstraction)
  • تعدد الأشكال (Polymorphism)

الآن دعنا ننظر إلى كل مبدأ بمزيد من التفصيل.

التغليف (Encapsulation)

التغليف هو عملية تجميع الكود والبيانات معًا في وحدة واحدة. إنه يشبه تمامًا الكبسولة التي تحتوي على مزيج من عدة أدوية، وهو تقنية تساعد في الحفاظ على متغيرات النسخة محمية. يمكن تحقيق ذلك باستخدام معدّلات الوصول (access modifiers) من نوع private التي لا يمكن الوصول إليها من أي شيء خارج الفئة. من أجل الوصول إلى الحالات الخاصة (private states) بأمان، يجب علينا توفير طرق عامة (public) للحصول على البيانات (getter methods) وتعيينها (setter methods). (في جافا، يجب أن تتبع هذه الطرق معايير تسمية JavaBeans).

لنفترض أن هناك متجر تسجيلات يبيع ألبومات موسيقية لفنانين مختلفين، وهناك أمين مخزن يديرها.

مخطط فئة يوضح العلاقة بين فئة Album وفئة StockKeeper بدون تغليف، حيث يمكن لأمين المخزن الوصول مباشرة إلى حالات الألبوم.

الشكل 4: مخطط الفئة بدون تغليف.

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

فئة Album:

 public class Album {
    public String name;
    public String artist;
    public double price;
    public int numberOfCopies;

    public void sellCopies () {
        if (numberOfCopies > 0 ){
            numberOfCopies--;
            System.out.println( "One album has sold!" );
        } else {
            System.out.println( "No more albums available!" );
        }
    }

    public void orderCopies ( int num) {
        numberOfCopies += num;
    }
 }

فئة StockKeeper:

 public class StockKeeper {
    public String name;

    public StockKeeper (String name) {
        this .name = name;
    }

    public void manageAlbum (Album album, String name, String artist, double price, int numberOfCopies) {
        /* Defining states and behaviors for album */
        album.name = name;
        album.artist = artist;
        album.price = price;
        album.numberOfCopies = numberOfCopies;

        /* Printing album details */
        System.out.println( "Album managed by :" + this .name);
        System.out.println( "Album details::::::::::" );
        System.out.println( "Album name : " + album.name);
        System.out.println( "Album artist : " + album.artist);
        System.out.println( "Album price : " + album.price);
        System.out.println( "Album number of copies : " + album.numberOfCopies);
    }
 }

فئة Main:

 public class Main {
    public static void main (String[] args) {
        StockKeeper johnDoe = new StockKeeper( "John Doe" );

        /* Stock keeper creates album and assigns negative values for price and number of copies available */
        johnDoe.manageAlbum( new Album(), "Slippery When Wet" , "Bon Jovi" , - 1000.00 , - 50 );
    }
 }

الناتج:

Album managed by :John Doe
Album details::::::::::
Album name : Slippery When Wet
Album artist : Bon Jovi
Album price : -1000.0
Album number of copies : -50

لا يمكن أن تكون قيمة سعر الألبوم وعدد النسخ سالبة. كيف يمكننا تجنب هذا الموقف؟ هنا نستخدم التغليف.

مخطط فئة يوضح العلاقة بين فئة Album وفئة StockKeeper مع التغليف، حيث يتم الوصول إلى حالات الألبوم عبر طرق getter و setter.

الشكل 5: مخطط الفئة مع التغليف.

في هذا السيناريو، يمكننا منع أمين المخزن من تعيين قيم سالبة. إذا حاولوا تعيين قيم سالبة لسعر الألبوم وعدد النسخ، فسنقوم بتعيينها كـ 0.0 و 0.

فئة Album (مع التغليف):

 public class Album {
    private String name;
    private String artist;
    private double price;
    private int numberOfCopies;

    public void sellCopies () {
        if (numberOfCopies > 0 ){
            numberOfCopies--;
            System.out.println( "One album has sold!" );
        } else {
            System.out.println( "No more albums available!" );
        }
    }

    public void orderCopies ( int num) {
        numberOfCopies += num;
    }

    public String getName () {
        return name;
    }

    public void setName (String name) {
        this .name = name;
    }

    public String getArtist () {
        return artist;
    }

    public void setArtist (String artist) {
        this .artist = artist;
    }

    public double getPrice () {
        return price;
    }

    public void setPrice ( double price) {
        if (price > 0 ) {
            this .price = price;
        } else {
            this .price = 0.0 ;
        }
    }

    public int getNumberOfCopies () {
        return numberOfCopies;
    }

    public void setNumberOfCopies ( int numberOfCopies) {
        if (numberOfCopies > 0 ) {
            this .numberOfCopies = numberOfCopies;
        } else {
            this .numberOfCopies = 0 ;
        }
    }
 }

فئة StockKeeper (مع التغليف):

 public class StockKeeper {
    private String name;

    StockKeeper(String name){
        setName(name);
    }

    public void manageAlbum (Album album, String name, String artist, double price, int numberOfCopies) {
        /* Defining states and behaviors for album */
        album.setName(name);
        album.setArtist(artist);
        album.setPrice(price);
        album.setNumberOfCopies(numberOfCopies);

        /* Printing album details */
        System.out.println( "Album managed by :" + getName());
        System.out.println( "Album details::::::::::" );
        System.out.println( "Album name : " + album.getName());
        System.out.println( "Album artist : " + album.getArtist());
        System.out.println( "Album price : " + album.getPrice());
        System.out.println( "Album number of copies : " + album.getNumberOfCopies());
    }

    public String getName () {
        return name;
    }

    public void setName (String name) {
        this .name = name;
    }
 }

فئة Main (مع التغليف):

 public class Main {
    public static void main (String[] args) {
        StockKeeper johnDoe = new StockKeeper( "John Doe" );

        /* Stock keeper creates album and assigns negative values for price and number of copies available */
        johnDoe.manageAlbum( new Album(), "Slippery When Wet" , "Bon Jovi" , - 1000.00 , - 50 );
    }
 }

الناتج:

Album managed by :John Doe
Album details::::::::::
Album name : Slippery When Wet
Album artist : Bon Jovi
Album price : 0.0
Album number of copies : 0

باستخدام التغليف، منعنا أمين المخزن من تعيين قيم سالبة، مما يعني أن لدينا سيطرة على البيانات.

مزايا التغليف في جافا:

  • يمكننا جعل الفئة للقراءة فقط أو للكتابة فقط: لفئة للقراءة فقط، يجب أن نوفر طريقة getter فقط. ولفئة للكتابة فقط، يجب أن نوفر طريقة setter فقط.
  • التحكم في البيانات: يمكننا التحكم في البيانات من خلال توفير منطق لطرق setter، تمامًا كما قيدنا أمين المخزن من تعيين قيم سالبة في المثال أعلاه.
  • إخفاء البيانات: لا يمكن للفئات الأخرى الوصول إلى الأعضاء الخاصة (private members) للفئة مباشرة.

الوراثة (Inheritance)

لنفترض أن متجر التسجيلات الذي ناقشناه أعلاه يبيع أيضًا أفلام Blu-ray.

مخطط فئة يوضح العلاقة بين فئات Movie و Album و StockKeeper، مع إبراز التكرار المحتمل في الكود بين Movie و Album.

الشكل 6: مخطط فئة Movie و StockKeeper.

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

الوراثة هي آلية يكتسب فيها كائن واحد جميع حالات وسلوكيات الكائن الأصل (parent object). تستخدم الوراثة علاقة الوالد بالطفل (علاقة IS-A).

إذن ما الذي يتم توريثه بالضبط؟ تؤثر معدّلات الرؤية/الوصول (Visibility/access modifiers) على ما يتم توريثه من فئة إلى أخرى. في جافا، كقاعدة عامة، نجعل متغيرات النسخة private وطرق النسخة public. في هذه الحالة، يمكننا أن نقول بأمان أن ما يلي يتم توريثه:

  • طرق النسخة العامة (public instance methods).
  • متغيرات النسخة الخاصة (private instance variables) (يمكن الوصول إلى متغيرات النسخة الخاصة فقط من خلال طرق getter و setter العامة).

أنواع الوراثة في جافا:

هناك خمسة أنواع من الوراثة في جافا. وهي الوراثة الفردية (single)، متعددة المستويات (multilevel)، الهرمية (hierarchical)، المتعددة (multiple)، والهجينة (hybrid).

  • تسمح الفئة (Class) بالوراثة الفردية، متعددة المستويات، والهرمية.
  • تسمح الواجهة (Interface) بالوراثة المتعددة والهجينة.

مخطط يوضح أنواع الوراثة في جافا: الفردية، متعددة المستويات، الهرمية، المتعددة، والهجينة.

الشكل 7: أنواع الوراثة في جافا.

  • يمكن للفئة أن ترث (extend) فئة واحدة فقط، ومع ذلك يمكنها تنفيذ (implement) أي عدد من الواجهات.
  • يمكن للواجهة أن ترث (extend) أكثر من واجهة واحدة.

مخطط يوضح الكلمات المفتاحية المستخدمة في الوراثة بجافا مثل extends و implements.

الشكل 8: يشرح الكلمات المفتاحية للوراثة.

العلاقات:

أولاً: علاقة IS-A

تشير علاقة IS-A إلى الوراثة أو التنفيذ.

أ. التعميم (Generalization)

يستخدم التعميم علاقة IS-A من فئة التخصص إلى فئة التعميم.

مخطط تعميم يوضح العلاقة الهرمية بين فئات أوسع وأكثر تخصصًا.

الشكل 9: مخطط التعميم.

ثانياً: علاقة HAS-A

نسخة من فئة واحدة HAS-A (تمتلك) مرجعًا لنسخة من فئة أخرى.

أ. التجميع (Aggregation)

في هذه العلاقة، لا يعتمد وجود الفئة A و B على بعضهما البعض. لهذا الجزء من التجميع، سنرى مثالًا لفئة Student وفئة ContactInfo.

 class ContactInfo {
    private String homeAddress;
    private String emailAddress;
    private int telephoneNumber; //12025550156
 }

 public class Student {
    private String name;
    private int age;
    private int grade;
    private ContactInfo contactInfo; //Student HAS-A ContactInfo

    public void study () {
        System.out.println( "Study" );
    }
 }

مخطط فئة يوضح علاقة التجميع (Aggregation) حيث Student HAS-A ContactInfo، مما يعني أن كلتا الفئتين يمكن أن توجدا بشكل مستقل.

الشكل 10: مخطط الفئة يوضح علاقة التعميم Student HAS-A ContactInfo. يمكن استخدام ContactInfo في أماكن أخرى – على سبيل المثال، يمكن لفئة Employee في شركة أن تستخدم أيضًا فئة ContactInfo هذه. لذا يمكن أن يوجد Student بدون ContactInfo، ويمكن أن يوجد ContactInfo بدون Student. يُعرف هذا النوع من العلاقة بالتجميع.

ب. التركيب (Composition)

في هذه العلاقة، لا يمكن للفئة B أن توجد بدون الفئة A – ولكن يمكن للفئة A أن توجد بدون الفئة B. لإعطائك فكرة عن التركيب، دعنا نرى مثالًا لفئة Student وفئة StudentId.

 class StudentId {
    private String idNumber; //A-123456789
    private String bloodGroup;
    private String accountNumber;
 }

 public class Student {
    private String name;
    private int age;
    private int grade;
    private StudentId studentId; //Student HAS-A StudentId

    public void study () {
        System.out.println( "Study" );
    }
 }

مخطط فئة يوضح علاقة التركيب (Composition) حيث Student HAS-A StudentId، مما يعني أن StudentId لا يمكن أن توجد بدون Student.

الشكل 11: مخطط الفئة يوضح علاقة التركيب Student HAS-A StudentId. يمكن أن يوجد Student بدون StudentId ولكن StudentId لا يمكن أن يوجد بدون Student. يُعرف هذا النوع من العلاقة بالتركيب.

الآن، دعنا نعود إلى مثال متجر التسجيلات السابق الذي ناقشناه أعلاه.

مخطط فئة يوضح تطبيق الوراثة في مثال متجر التسجيلات، مع فئة Product كفئة أساسية.

الشكل 12: مخطط الفئة مع الوراثة.

يمكننا تنفيذ هذا المخطط في جافا لتجنب تكرار الكود.

مزايا الوراثة:

  • إعادة استخدام الكود: ترث الفئة الفرعية جميع أعضاء النسخة من الفئة الأصل.
  • مرونة أكبر في تغيير الكود: يكفي تغيير الكود في مكان واحد.
  • يمكنك استخدام تعدد الأشكال: يتطلب تجاوز الطرق (method overriding) علاقة IS-A.

التجريد (Abstraction)

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

في جافا، يمكننا تحقيق التجريد بطريقتين: الفئة المجردة (abstract class) (من 0 إلى 100%) والواجهة (interface) (100%). يمكن تطبيق الكلمة المفتاحية abstract على الفئات والطرق. لا يمكن أن تكون abstract و final أو static معًا أبدًا.

أولاً: الفئة المجردة (Abstract class)

الفئة المجردة هي فئة تحتوي على الكلمة المفتاحية abstract. لا يمكن إنشاء نسخ من الفئات المجردة (لا يمكن إنشاء كائنات من الفئات المجردة). يمكن أن تحتوي على مُنشئات (constructors)، وطرق ثابتة (static methods)، وطرق نهائية (final methods).

ثانياً: الطرق المجردة (Abstract methods)

الطريقة المجردة هي طريقة تحتوي على الكلمة المفتاحية abstract. لا تحتوي الطريقة المجردة على تنفيذ (لا يوجد جسم للطريقة وتنتهي بفاصلة منقوطة). لا يجب أن تُعلّم كـ private.

ثالثاً: الفئة المجردة والطرق المجردة

  • إذا وجدت طريقة مجردة واحدة على الأقل داخل فئة، فيجب أن تكون الفئة بأكملها مجردة.
  • يمكن أن تكون لدينا فئة مجردة بدون طرق مجردة.
  • يمكن أن تكون لدينا أي عدد من الطرق المجردة وغير المجردة داخل فئة مجردة في نفس الوقت.
  • يجب أن توفر أول فئة فرعية ملموسة (concrete sub class) لفئة مجردة تنفيذًا لجميع الطرق المجردة. إذا لم يحدث ذلك، فيجب أيضًا تعليم الفئة الفرعية كـ abstract.

في سيناريو العالم الحقيقي، سيتم توفير التنفيذ من قبل شخص غير معروف للمستخدمين النهائيين. لا يعرف المستخدمون فئة التنفيذ والتنفيذ الفعلي.

دعنا نأخذ مثالًا على استخدام مفهوم التجريد.

 abstract class Shape {
    public abstract void draw () ;
 }

 class Circle extends Shape {
    public void draw () {
        System.out.println( "Circle!" );
    }
 }

 public class Test {
    public static void main (String[] args) {
        Shape circle = new Circle();
        circle.draw();
    }
 }

مخطط فئة يوضح العلاقة بين فئة Shape المجردة وفئة Circle الملموسة التي ترث منها.

الشكل 13: مخطط فئة يوضح العلاقة بين فئة مجردة وفئة ملموسة.

متى نرغب في تعليم فئة كـ abstract؟

  • لإجبار الفئات الفرعية على تنفيذ الطرق المجردة.
  • للتوقف عن امتلاك كائنات فعلية لتلك الفئة.
  • للحفاظ على مرجع الفئة.
  • للاحتفاظ بكود الفئة المشترك.

الواجهة (Interface)

الواجهة هي مخطط لفئة. الواجهة مجردة بنسبة 100%. لا يُسمح بأي مُنشئات هنا. إنها تمثل علاقة IS-A.

ملاحظة: تحدد الواجهات الطرق المطلوبة فقط. لا يمكننا الاحتفاظ بالكود المشترك. يمكن أن تحتوي الواجهة على طرق مجردة فقط، وليس طرقًا ملموسة. بشكل افتراضي، طرق الواجهة هي public و abstract. لذلك، داخل الواجهة، لا نحتاج إلى تحديد public و abstract. لذا، عندما تقوم فئة بتنفيذ طريقة واجهة دون تحديد مستوى الوصول لتلك الطريقة، سيقوم المترجم (compiler) بإلقاء خطأ يفيد "Cannot reduce the visibility of the inherited method from interface". لذلك يجب تعيين مستوى الوصول للطريقة المنفذة إلى public. بشكل افتراضي، متغيرات الواجهة هي public و static و final. على سبيل المثال:

 interface Runnable {
    int a = 10; //similar to: public static final int a = 10;
    void run(); //similar to: public abstract void run();
 }

 public class InterfaceChecker implements Runnable {
    public static void main (String[] args) {
        Runnable.a = 5; //The final field Runnable.a cannot be assigned.
    }
 }

دعنا نرى مثالًا يشرح مفهوم الواجهة:

 interface Drawable {
    void draw () ;
 }

 class Circle implements Drawable {
    public void draw () {
        System.out.println( "Circle!" );
    }
 }

 public class InterfaceChecker {
    public static void main (String[] args) {
        Drawable circle = new Circle();
        circle.draw();
    }
 }

مخطط فئة يوضح العلاقة بين واجهة Drawable وفئة Circle الملموسة التي تنفذها.

الشكل 14: مخطط فئة يوضح العلاقة بين واجهة وفئة ملموسة.

الطرق الافتراضية (Default) والثابتة (Static) في الواجهات

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

الطريقة الافتراضية (Default method)
 public interface DefaultInterface {
    void sleep () ;

    default void run () {
        System.out.println( "I'm running!" );
    }
 }

 public class InterfaceCheckers implements DefaultInterface {
    public void sleep () {
        System.out.println( "Sleeping..." );
    }

    public static void main (String[] args) {
        InterfaceCheckers checker = new InterfaceCheckers();
        checker.run();
        checker.sleep();
    }
 }

 /* Output:
 I'm running!
 Sleeping...
 */
الطريقة الثابتة (Static method)

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

 public interface DefaultInterface {
    void sleep () ;

    static void run () {
        System.out.println( "I'm running!" );
    }
 }

 public class InterfaceCheckers implements DefaultInterface {
    public void sleep () {
        System.out.println( "Sleeping..." );
    }

    public static void main (String[] args) {
        InterfaceCheckers checker = new InterfaceCheckers();
        DefaultInterface.run();
        checker.sleep();
    }
 }

 /* Output:
 I'm running!
 Sleeping...
 */
واجهة العلامة (Marker interface)

إنها واجهة فارغة. على سبيل المثال، واجهات Serializable و Cloneable و Remote.

 public interface Serializable {
    //No fields or methods
 }

مزايا الواجهات:

  • تساعدنا على استخدام الوراثة المتعددة في جافا.
  • توفر التجريد.
  • توفر الاقتران المرن (loose coupling): الكائنات مستقلة عن بعضها البعض.

متى نرغب في تغيير فئة إلى واجهة؟

  • لإجبار الفئات الفرعية على تنفيذ الطرق المجردة.
  • للتوقف عن امتلاك كائنات فعلية لتلك الفئة.
  • للحفاظ على مرجع الفئة.

ملاحظة: تذكر، لا يمكننا الاحتفاظ بالكود المشترك داخل الواجهة. إذا كنت ترغب في تعريف طرق محتملة مطلوبة وكود مشترك، فاستخدم فئة مجردة. إذا كنت ترغب فقط في تعريف طريقة مطلوبة، فاستخدم واجهة.

تعدد الأشكال (Polymorphism)

تعدد الأشكال هو قدرة الكائن على اتخاذ أشكال متعددة. يحدث تعدد الأشكال في البرمجة كائنية التوجه عندما تشير فئة عليا (super class) إلى كائن فئة فرعية (sub class object). تعتبر جميع كائنات جافا متعددة الأشكال لأنها تشترك في أكثر من علاقة IS-A (على الأقل ستجتاز جميع الكائنات اختبار IS-A لنوعها الخاص ولفئة Object).

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

تعدد تحميل الطرق (Method overloading)

إذا كان للفئة طرق متعددة لها نفس الاسم ولكن بمعاملات (parameters) مختلفة، يُعرف هذا بتعدد تحميل الطرق. قواعد تعدد تحميل الطرق:

  • يجب أن تحتوي على قائمة معاملات مختلفة.
  • قد تحتوي على أنواع إرجاع مختلفة.
  • قد تحتوي على معدّلات وصول مختلفة.
  • قد تلقي استثناءات مختلفة.
 class JavaProgrammer {
    public void code () {
        System.out.println( "Coding in C++" );
    }

    public void code (String language) {
        System.out.println( "Coding in " +language);
    }
 }

 public class MethodOverloader {
    public static void main (String[] args) {
        JavaProgrammer gosling = new JavaProgrammer();
        gosling.code();
        gosling.code( "Java" );
    }
 }

 /* Output:
 Coding in C++
 Coding in Java
 */

ملاحظة: يمكن أيضًا تحميل الطرق الثابتة (static methods) بشكل زائد.

 class Addition {
    public static int add ( int a, int b) {
        return a+b;
    }

    public static int add ( int a, int b, int c) {
        return a+b+c;
    }
 }

 public class PolyTest {
    public static void main (String[] args) {
        System.out.println(Addition.add( 5 , 5 ));
        System.out.println(Addition.add( 2 , 4 , 6 ));
    }
 }

ملاحظة: يمكننا تحميل طريقة main() بشكل زائد، ولكن آلة جافا الافتراضية (Java Virtual Machine - JVM) تستدعي طريقة main() التي تستقبل مصفوفة من السلاسل النصية (String arrays) كوسائط.

 public class PolyTest {
    public static void main () {
        System.out.println( "main()" );
    }

    public static void main (String args) {
        System.out.println( "String args" );
    }

    public static void main (String[] args) {
        System.out.println( "String[] args" );
    }
 }

 //Output: String[] args

قواعد يجب اتباعها لتعدد الأشكال:

قواعد وقت الترجمة (Compile time rules)
  • يعرف المترجم نوع المرجع فقط.
  • يمكنه البحث فقط في نوع المرجع عن الطرق.
  • ينتج توقيع طريقة (method signature).
قواعد وقت التشغيل (Run time rules)
  • في وقت التشغيل، تتبع JVM النوع الفعلي لوقت التشغيل (نوع الكائن) للعثور على الطريقة.
  • يجب أن يتطابق توقيع طريقة وقت الترجمة مع الطريقة في فئة الكائن الفعلي.

تجاوز الطرق (Method overriding)

إذا كانت الفئة الفرعية تحتوي على نفس الطريقة المعلنة في الفئة العليا، يُعرف هذا بتجاوز الطرق. قواعد تجاوز الطرق:

  • يجب أن تحتوي على نفس قائمة المعاملات.
  • يجب أن تحتوي على نفس نوع الإرجاع: على الرغم من أن الإرجاع المتغاير (covariant return) يسمح لنا بتغيير نوع إرجاع الطريقة المتجاوزة.
  • يجب ألا تحتوي على معدّل وصول أكثر تقييدًا: قد تحتوي على معدّل وصول أقل تقييدًا.
  • يجب ألا تلقي استثناءات جديدة أو أوسع نطاقًا (checked exceptions): قد تلقي استثناءات أضيق نطاقًا (checked exceptions) وقد تلقي أي استثناء غير مفحوص (unchecked exception).
  • يمكن تجاوز الطرق الموروثة فقط (يجب أن تكون هناك علاقة IS-A).

مثال على تجاوز الطرق:

 public class Programmer {
    public void code () {
        System.out.println( "Coding in C++" );
    }
 }

 public class JavaProgrammer extends Programmer {
    public void code () {
        System.out.println( "Coding in Java" );
    }
 }

 public class MethodOverridder {
    public static void main (String[] args) {
        Programmer ben = new JavaProgrammer();
        ben.code();
    }
 }

 /* Output:
 Coding in Java
 */

ملاحظة: لا يمكن تجاوز الطرق الثابتة (static methods) لأن الطرق يتم تجاوزها في وقت التشغيل. ترتبط الطرق الثابتة بالفئات بينما ترتبط طرق النسخة بالكائنات. لذلك في جافا، لا يمكن تجاوز طريقة main() أيضًا.

ملاحظة: يمكن تحميل المُنشئات (Constructors) بشكل زائد ولكن لا يمكن تجاوزها.

أنواع الكائنات وأنواع المراجع (Object types and reference types)

 class Person {
    void eat () {
        System.out.println( "Person is eating" );
    }
 }

 class Student extends Person {
    void study () {
        System.out.println( "Student is studying" );
    }
 }

 public class InheritanceChecker {
    public static void main (String[] args) {
        Person alex = new Person(); //New Person "is a" Person
        alex.eat();

        Student jane = new Student(); //New Student "is a" Student
        jane.eat();
        jane.study();

        Person mary = new Student(); //New Student "is a" Person
        mary.eat();
        //Student chris = new Person(); //New Person isn't a Student.
    }
 }

في السطر Person mary = new Student();، إنشاء الكائن هذا صحيح تمامًا. mary هو متغير مرجعي من نوع Person و new Student() سينشئ كائن Student جديدًا. لا يمكن لـ mary الوصول إلى طريقة study() في وقت الترجمة لأن المترجم يعرف نوع المرجع فقط. نظرًا لعدم وجود طريقة study() في فئة نوع المرجع، لا يمكنه الوصول إليها. ولكن في وقت التشغيل، ستكون mary من نوع Student (نوع وقت التشغيل / نوع الكائن).

في هذه الحالة، يمكننا إقناع المترجم بالقول “في وقت التشغيل، ستكون mary من نوع Student، لذا يرجى السماح لي باستدعائها”. كيف يمكننا إقناع المترجم بهذا الشكل؟ هنا نستخدم التحويل (casting). يمكننا جعل mary من نوع Student في وقت الترجمة ويمكننا استدعاء study() عن طريق تحويلها:

((Student)mary).study();

سنتعلم عن التحويل لاحقًا.

تحويل نوع الكائن (Object type casting)

يصنف تحويل النوع في جافا إلى نوعين:

  1. التحويل الواسع (Widening casting) (ضمني): تحويل نوع تلقائي.
  2. التحويل الضيق (Narrowing casting) (صريح): يحتاج إلى تحويل صريح.

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

أولاً: التحويل الواسع (Widening casting)

Superclass superRef = new Subclass();

ثانياً: التحويل الضيق (Narrowing casting)

Subclass ref = (Subclass) superRef;

يجب أن نكون حذرين عند إجراء التحويل الضيق. عند التحويل الضيق، نقنع المترجم بالترجمة دون أي خطأ. إذا أقنعناه بشكل خاطئ، فسنحصل على خطأ في وقت التشغيل (عادةً ClassCastException). من أجل إجراء التحويل الضيق بشكل صحيح، نستخدم عامل التشغيل instanceof. يتحقق هذا العامل من وجود علاقة IS-A.

 class A {
    public void display () {
        System.out.println( "Class A" );
    }
 }

 class B extends A {
    public void display () {
        System.out.println( "Class B" );
    }
 }

 public class Test {
    public static void main (String[] args) {
        A objA = new B();
        if (objA instanceof B){
            ((B)objA).display();
        }
    }
 }

 /**
  * Output: Class B
  */

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

الخلاصة التقنية

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

نشكركم على قراءتكم. نأمل أن يكون هذا المقال قد أفادكم. نشجعكم بشدة على قراءة المزيد من المقالات ذات الصلة بالبرمجة كائنية التوجه.

الحلم ليس ما تراه وأنت نائم، بل هو ما لا يدعك تنام.

— أ. ب. ج. عبد الكلام، أجنحة النار: سيرة ذاتية

شكرًا لكم. برمجة سعيدة!

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *