اذهب إلى المحتوى

مفاهيم أساسية في البرمجة كائنية التوجه وتحقيقها في جافاسكريبت


ابراهيم الخضور

البرمجة كائنية التوجه Object-Oriented programming واختصارًا OOP هي مصطلح برمجي أساسي في الكثير من لغات البرمجة مثل جافا و ++C. ونحاول في هذا المقال تزويدك بإحاطة شاملة عن أساسيات مفهوم البرمجة كائنية التوجه، ونشرح مفاهيمها اﻷساسية، ونوضح مفهوم اﻷصناف classes والنسخ instances والوراثة inheritance والتغليف encapsulation. ولن نخص في شرحنا لهذه المفاهيم لغة جافا سكريبت حاليًا، وستكون اﻷمثلة جميعها مكتوبة بلغة معممة (أو بالشيفرة الوهمية pseudo-code).

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

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

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

  1. أساسيات جافا سكريبت كما شرحناها في سلسلة المقالات السابقة.
  2. أساسيات البرمجة كائنية التوجه في جافا سكريبت، كما شرحناها في مقال أساسيات العمل مع الكائنات في جافا سكريبت، ومقال استخدام كائنات Prototype في جافا سكريبت.

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

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

اﻷصناف Classes والنُّسَخ Instances

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

لهذا يمكن أن يكون المدرّس ضنفًا في المنظومة باسم Professor، ويٌعرّف الصنف بداخله مجموعة من البيانات والتوابع التي يمتلكها كل مدرّس في المدرسة.

على سبيل المثال يمكن أن يُعرّف الصنف Professor بالشيفرة الوهمية التالية:

class Professor
    properties
        name
        teaches
    methods
        grade(paper)
        introduceSelf()

تُعرّف الشيفرة السابقة الصنف Professor كالتالي:

  • خاصيتين أو سمتين تحملان بيانات المدرس وهما name التي تمثل اسم المدرس و teaches التي تمثل المواد التي يقوم بتدريسها.
  • تابعين هما ()grade لتصحيح ورقة، و ()introduceSelf للتعريف عن أنفسهم.

كما تلاحظ ليس للصنف وظيفة قائمة بحد ذاتها، بل هو أقرب إلى قالب ﻹنشاء كائنات objects من هذا النوع أو الصنف. فكل مدرّس ننشئه وفق القالب Professor يُدعى نسخة instance عن هذا القالب.

تُنشأ نسخة عن صنف باستخدام نوع خاص من الدوال تُدعى بالدوال البانية constructors. إذ نمرر قيمًا إلى الدوال البانية لتهيئة النسخة بقيم تضبط حالتها الداخلية اﻷساسية.

تُكتب الدوال البانية عمومًا كجزء من تعريف الصنف، ولها عادة نفس اسم الصنف، لاحظ الشيفرة التالية:

class Professor
    properties
        name
        teaches
    constructor
        Professor(name, teaches)
    methods
        grade(paper)
        introduceSelf()

تأخذ الدالة البانية في الشيفرة السابقة معاملين أو وسيطين، لهذا بإمكاننا تهيئة الخاصيتين name و teaches عند إنشاء نسخة جديدة أو كائن عن صنف المدرّس. وطالما أن لدينا دالة بانية اﻵن، سنتمكن من إنشاء بعض المدرسين، وتستخدم بعض لغات البرمجة عادة الكلمة المحجوزة new للإشارة إلى استدعاء الدالة البانية:

walsh = new Professor("Ahmad", "Psychology");
lillian = new Professor("Lyla", "Poetry");

walsh.teaches; // 'Psychology'
walsh.introduceSelf(); // 'My name is Professor Ahmad and I will be your Psychology professor.'

lillian.teaches; // 'Poetry'
lillian.introduceSelf(); // 'My name is Professor Lyla and I will be your Poetry professor.'

تنشئ شيفرة جافا سكريبت السابقة كائنين، وكلاهما نسخة عن الصنف Professor.

الوراثة Inheritance

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

class Student
    properties
        name
        year
    constructor
        Student(name, year)
    methods
        introduceSelf()

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

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

class Person
    properties
        name
    constructor
        Person(name)
    methods
        introduceSelf()

class Professor : extends Person
    properties
        teaches
    constructor
        Professor(name, teaches)
    methods
        grade(paper)
        introduceSelf()

class Student : extends Person
    properties
        year
    constructor
        Student(name, year)
    methods
        introduceSelf()

وهكذا يمكن القول أن الصنف Person هو صنف أعلى super class أو صنف أب parent class لكل من الصنف Professor والصنف Student اللذان يُدعيان في هذه الحالة بأصناف فرعية sub classes أو أصناف أبناء child class.

ولاحظ كيف عُرِّف التابع ()introduceSelf في جميع الأصناف الثلاث، والسبب هو اختلاف الطريقة التي يُقدّم فيها كل صنف نفسه:

ahmad = new Professor("Ahmad", "Psychology");
ahmad.introduceSelf(); // 'My name is Professor Ahmad and I will be your Psychology professor.'

summers = new Student("Summers", 1);
summers.introduceSelf(); // 'My name is Summers and I'm in the first year.'

وباﻹمكان أيضًا كتابة تابع افتراضي ()introduceSelf للتعريف عن أشخاص ليسوا طلابًا ولا مدرسين:

passam = new Person("Passam");
passam.introduceSelf(); // 'My name is Passam.'

تُدعى فكرة وجود تابع بنفس الاسم في عدة أصناف، لكنه ينفّذ وظيفة خاصة في كل صنف بتعدد الأشكال polymorphism، حيث يمكن تعريف تابع بنفس الاسم في الصنف الأب والابن وفي هذه الحالة يحل التابع المعرف في الصنف الابن محل التابع المعرف في الصنف الابن، ونقول في هذه الحالة أننا تجاوزنا overrides نسخة التابع الموجودة في الصنف اﻷب.

التغليف Encapsulation

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

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

فلو سمُح لطلاب السنة الثانية وما فوق دراسة الرماية، باﻹمكان تنفيذ اﻷمر بالاستفادة من الخاصية year، وستتمكن بقية الشيفرة من تحديد إمكانية تسجيل الطالب في صف الرماية أو لا:

if (student.year > 1) {
  // allow the student into the class
}

لكن المشكلة ستقع إذا غيرنا معيار السماح للطلاب بالتسجيل في درس الرماية، كأن يحتاج إلى موافقة ولي أمره، عندها علينا تغيير الشيفرة التي تتحقق من إمكانية تسجيل الطالب في كل مكان. لهذا من اﻷفضل إنشاء تابع ()canStudyArchery في الكائنات Student لتنفيذ منطق العملية.

class Student : extends Person
    properties
       year
    constructor
       Student(name, year)
    methods
       introduceSelf()
       canStudyArchery() { return this.year > 1 }
if (student.canStudyArchery()) {
  // allow the student into the class
}

وهكذا، سيكون علينا تغيير الصنف Student فقط إذا أردنا تغيير شروط دراسة الرماية، وستعمل بقية الشيفرة في كل مكان كما يجب.

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

class Student : extends Person
    properties
       private year
    constructor
        Student(name, year)
    methods
       introduceSelf()
       canStudyArchery() { return this.year > 1 }

student = new Student('Wael', 1)
student.year // error: 'year' is a private property of Student

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

تحقيق البرمجة كائنية التوجه في لغة جافا سكريبت

ناقشنا حتى اللحظة في مقالنا الميزات اﻷساسية للبرمجة كائنية التوجه بالعموم والتي تعتمدها لغات برمجة عديدة مثل ++C وجافا، وكنا قد ألقينا النظرة في مقالي أساسيات الكائنات في جافا سكريبت وكائنات prototype عن مفهومي الدوال البانية والكائنات prototype. وترتبط هاتان الميزتان بالتأكيد مع ميزات البرمجة كائنية التوجه إلى حد ما.

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

كما تبدي سلسلة prototype chain سلوكًا يشبه سلوك الوراثة، فلو كان لدينا كائن من الصنف Student يمتلك الكائن Person ككائن prototype، فسيرث الخاصية name والتابع ()introduceSelf.

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

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

ثانيًا، على الرغم من أن سلسلة كائنات prototype قريبة من مفهوم الوراثة، وتسلك السلوك نفسه بشكل أو بآخر، لكنهما مفهومان مختلفان. فعند إنشاء كائنات من صنف ابن subclass سنحصل على كائن واحد يجمع بين الخاصيات المعرفة في الصنف الابن والخاصيات المعرّفة في الصنف اﻷب. بينما يتميز نموذج prototyping بأن كل مستوى من مستويات الوراثة الهرمية في سلسلة يمثل بكائن مستقل، وسترتبط هذه الكائنات ببعضها عبر الخاصية _proto_.

فالكائنات في سلسلسة prototype chain هي أقرب إلى مفهوم التفويض delegation من مفهوم الوراثة. والتفويض هو نمط برمجي يعطي الكائن القدرة على تنفيذ مهمة ما توكل إليه بنفسه أو تفويض كائن آخر لتنفيذ هذه المهمة. وفي الكثير من اﻷحيان، نرى أن التفويض أكثر مرونة في ربط الكائنات مع بعضها مقارنة بالوراثة (لسبب مهم وهو إمكانية تغيير أو استبدال الكائن المفوَّض كليًا أثناء تنفيذ البرنامج).

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

الخاتمة

تحدثنا في هذا المقال عن الميزات اﻷساسية للبرمجة كائنية التوجه OOP وطريقة تحقيقها في جافا سكريبت، كما ألقينا نظرة سريعة على مواطن الشبه بين الدوال البانية وكائنات prototype في جافا سكريبت وبين مميزات البرمجة بالكائنات.

ترجمة-وبتصرف- للمقال Object-Oriented programming

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...