رغم أن الأفكار التي كانت وراء البرمجة الكائنية التوجه طُورت في ستينيات القرن الماضي إلا أنها لم تشتهر في الوسط البرمجي إلا بعد ذلك بعقدين، أي في الثمانينيات، بعد إطلاق Smalltalk-80 ومجموعة متنوعة من تطبيقات لغة Lisp، ولم تكن وقتها اتجاهًا سائدًا في البرمجة وإنما كانت تثير الفضول فقط، ثم تغير ذلك عندما انتشرت الواجهات الرسومية في الحواسيب الشخصية على حواسيب أبل أولًا، ثم على الحواسيب العاملة بنظام ويندوز ونظام نوافذ X في يونكس، إلى أن اقتربنا من نهاية الألفية السابقة، حيث صارت البرمجة الكائنية التوجه Object Oriented Programming -والتي تعرف اختصارًا OOP- التقنية الأبرز لتطوير البرمجيات.
وتتجسد مفاهيم البرمجة الكائنية التوجه في لغات مثل جافا وC++ وبايثون بحيث لا تكاد تفعل شيئًا فيها إلا وتقابل كائنًا في مكان ما، ونريد في هذا المقال أن نتعرف على هذه التقنية وننظر في المفاهيم الأساسية لها، مثل تعريف الكائن والصنف وتعددية الأشكال polymorphism والوراثة inheritance، وكيفية إنشاء الكائنات وتخزينها واستخدامها، والبرمجة الكائنية التوجه موضوع كبير قد كُتبت فيه كتب، فإذا أردت التعمق فيه أكثر مما هو مذكور في هذا المقال فانظر هذه الكتب باللغة الإنجليزية:
- كتاب Object Oriented Analysis لبيتر كود Peter Coad وإد يوردون Ed Yourdon.
- كتاب Object Oriented Analysis and Design with Applications لجريدي بوش Grady Booch (الطبعة الأولى أو الثالثة).
- كتاب Object Oriented Software Construction لبرتراند ماير Berterand Meyer (الطبعة الثانية).
تختلف هذه الكتب عن بعضها في العمق والحجم والدقة الأكاديمية بالترتيب، فالكتاب الأول مناسب للأغراض العامة التي هي خارج نطاق العمل البرمجي، والحق أنها كلها ليست كتبًا في البرمجة، بل كتب تحليل وتصميم برمجي، لأن أفضل تطبيق للبرمجة الكائنية التوجه يكون بتطبيق المبادئ خلال دورة حياة المشروع، وهذه طريقة مختلفة لحل المشاكل عن الطريقة العادية للبرمجة.
أما في هذا المقال فسنتحدث باختصار عن مفاهيم البرمجة الكائنية التوجه التي ذكرناها، ولا مشكلة إذا لم تستوعبها في البداية، فلا تزال تستطيع استخدام الكائنات بدون أن تستوعب المفهوم الذي بُنيت عليه، ثم ستتضح الأمور بالتدريج مع التدرب والوقت،كما يمكن استخدام تصميم كائني التوجه في لغة غير كائنية من خلال الاصطلاحات البرمجية، لكن لا يُنصح بهذا الأسلوب، وإنما يستخدَم ملاذًا أخيرًا عندما لا نجد حلًا آخر، حيث تُستخدم التقنيات الكائنية التوجه إذا كانت المشكلة توافقها وتُحل بها، ويُفضل أن تُستخدم لغة كائنية التوجه أيضًا عندها، وتدعم أغلب اللغات الحديثة البرمجة الكائنية التوجه جيدًا، بما فيها اللغات الثلاثة التي نتدرب عليها، لكننا سنستخدم بايثون في الأمثلة الواردة هنا، ثم سنعرض المفاهيم الأساسية فقط في جافاسكربت وVBScript.
جمع البيانات والدوال
الكائنات هي تجميعات من البيانات والدوال التي تنفذ مهامًا على تلك البيانات، وتوضعان معًا بحيث يمكن تمرير كائن من جزء ما في البرنامج كي نحصل تلقائيًا على وصول إلى العمليات المتاحة وسمات البيانات، وهذا الجمع بين البيانات والدوال هو أصل البرمجة الكائنية التوجه، ويُعرف باسم التغليف encapsulation، لأن بعض لغات البرمجة تخفي البيانات عن مستخدمي الكائن، وبناءً عليه تتطلب توابع الكائن للوصول إليها، وتسمى هذه التقنية باسم إخفاء البيانات، ويطلق عليها التغليف أحيانًا، وكمثال على التغليف، قد يخزن كائن سلسلة نصية سلسلة محارف، لكنه يوفر كذلك توابع للعمل على هذه السلسلة؛ لتنفيذ أمور مثل البحث وتغيير حالة الأحرف وحساب الطول وغير ذلك، وتستخدم الكائنات مجازًا تمرير الرسالة message passing، حيث يمرر كائنٌ رسالةً إلى كائن آخر، ويرد الكائن المستقبل بتنفيذ تابع -وهو إحدى عملياته-، وهكذا يُستدعى التابع عند استقبال الرسالة الموافقة له بواسطة الكائن المالك، ويمكن تمثيل ذلك بصيغ مختلفة، وأكثرها شيوعًا الصيغة النقطية .
التي تحاكي الوصول إلى العناصر التي في الوحدات، فبالنسبة إلى صنف widget وهمي:
w = Widget() # widget جديدة من w أنشئ نسخة w.paint() # paint أرسل إليها الرسالة
ستستدعي هذه التعليمات التابع paint الخاص بكائن widget.
تعريف الأصناف
يمكن للكائنات أن تحتوي على أنواع مختلفة، كما أن للبيانات أنواعًا مختلفةً، وتُعرَّف تجميعات الكائنات التي لها صفات متطابقة باسم الأصناف classes، ونستطيع تعريف هذه الأصناف وإنشاء نسخ منها، حيث تكون تلك النسخ هي الكائنات الفعلية، كما يمكن تخزين مراجع إلى تلك الكائنات في المتغيرات داخل برامجنا، لننظر الآن في مثال حقيقي لنرى إن كنا نستطيع تفسيره وشرحه، حيث سننشئ صنف رسالة يحتوي على سلسلة نصية -تمثل نص الرسالة- وتابع لطباعة الرسالة.
class Message: def __init__(self, aString): self.text = aString def printIt(self): print( self.text )
-
الملاحظة الأولى: يسمى أحد توابع هذا الصنف باسم
__init__
، وهو تابع خاص يسمى الباني constructor، وسبب هذا الاسم أنه يُستدعى عند إنشاء أو بناء نسخة جديدة من كائن ما، وستكون المتغيرات المسندة داخل هذا التابع -والتي أنشئت داخل بايثون- متغيرات فريدة للنسخة الجديدة، وتوجد عدة توابع خاصة مثل هذا التابع في بايثون، وجميعها مميزة بصيغة التسمية التي فيها شرطتان سفليتان عن يمينها وشمالها__xxx__
، ويطلق عليها مستخدمو بايثون أحيانًا اسم التوابع السحرية magic methods أو التوابع المحاطة بشرطين سفليتين dunder methods (إذ dunder اختصار إلى double under)؛ أما وقت استدعاء الباني الدقيق فيختلف بين اللغات، حيث يُستدعى التابعinit
في بايثون بعد إنشاء النسخة في الذاكرة، لكنه في لغات أخرى يعيد النسخة نفسها، والفرق في هذا بين اللغات طفيف ولا يستحق الانتباه له. -
الملاحظة الثانية: يحتوي كلا التابعين المعرفين على معامل أول هو
self
، والاسم مجرد اصطلاح يشير إلى نسخة الكائن، وسنرى قريبًا أن هذا المعامِل لا يملؤه المبرمج، بل يُملأ بواسطة المفسر في وقت التشغيل، وعلى هذا يُستدعىprintIt
على نسخة للصنف -انظر أدناه-، بدون وسطاء بالشكلm.printIt()
. -
الملاحظة الثالثة: لقد استدعينا الصنف
Message
بحرف M كبير كما نرى، وهذا للسهولة فقط، لكن هذا الاصطلاح يُستخدم بكثرة في لغات البرمجة الكائنية التوجه، وليس في بايثون وحدها، ويوجد اصطلاح قريب من هذا يقتضي أن تبدأ أسماء التوابع بحرف صغير ثم تبدأ الكلمات التالية في الاسم بحرف كبير، فإذا كان لدينا تابع اسمه calculate current balance، فسيُكتب بالشكلcalculateCurrentBalance
.
ننصحك عند هذه النقطة بالعودة إلى مقال البيانات وأنواعها، لقراءة قسم الأنواع المعرَّفة من قِبل المستخدم، حيث ستفهم مثال دليل جهات الاتصال في بايثون فهمًا أفضل بعد هذا الشرح هنا، فالنوع الوحيد الذي يعرّفه المستخدم في بايثون هو الصنف، والصنف الذي له سمات attributes وليس له توابع -ما عدا __init__
- يكافئ الباني المسمى record
أو struct
في بعض لغات البرمجة.
الصيغة الرسومية
تبنّى مجتمع هندسة البرمجيات صيغةً مرئيةً لوصف الأصناف والكائنات وعلاقاتها بعضها ببعض، وتسمى هذه الصيغة باسم لغة النمذجة الموحدة Unified Modelling Language أو UML اختصارًا، وهي أداة تصميم قوية وتحتوي على العديد من المخططات والأيقونات، وسننظر في بعضها هنا بما يعيننا على فهم المبادئ التي نريد شرحها فقط، وأول أيقونة سنقابلها في UML هي وصف الصنف، وهي أهم الأيقونات، وتتكون من صندوق من ثلاثة أجزاء، يحتوي الجزء العلوي على اسم الصنف، والأوسط على سماته أو البيانات فيه، أما الجزء السفلي فيحتوي على توابع الصنف أو دواله، وبناءً عليه سيبدو صنف Message المعرَّف أعلاه كما يلي:
سنرى في هذا المقال أيقونات UML أخرى، ونتعرض لمفاهيم جديدة تدعمها هذه الصيغة.
استخدام الأصناف
بما أننا عرّفنا الصنف Message فنستطيع إنشاء نسخ منه الآن والعمل عليها:
m1 = Message("Hello world") m2 = Message("So long, it was short but sweet") notes = [m1, m2] # ضع الكائنات في قائمة for msg in notes: msg.printIt() # اطبع الرسائل متتابعة
وبهذا نعامل الصنف كما لو كان نوع بيانات قياسيًا في بايثون، وهو الغرض من التدريب ابتداءً.
كما توجد أيقونة للكائن أو النسخة في UML، وهي مثل أيقونة الصنف إلا أننا نترك الجزئين السفليين في الصندوق فارغين، ويتكون الاسم من اسم الكائن أو النسخة متبوعًا بنقطتين رأسيتين ثم اسم الصنف، وعليه فإن m1:Message
تخبرنا أن m1
ما هي إلا نسخة من الصنف Message
، ويمكن رسم مثال الرسالة الخاص بنا الآن كما يلي:
نلاحظ أن الصنف List
يمثل نوع القائمة القياسي في بايثون، كما هو موضح من وضع كلمة builtin
بين أقواس حادة، وهي بنية معروفة في UML باسم القالب النمطي stereotype، وتشير الخطوط ذوات الرؤوس الماسية في الصورة إلى القائمة التي تحتوي على كائنات Message
، وبالمثل فإن كائن MyProg
يُنمَّط stereotyped على أنه صنف مساعد utility class، مما يعني في هذه الحالة أنه غير موجود مثل صنف داخل البرنامج، لكنه منتج من منتجات البيئة نفسها، وتُظهَر أدوات نظام التشغيل عادةً بهذه الطريقة، مثل مكتبات لدوال.
أما الخطوط المستقيمة التي تخرج من myProg
إلى Message
فتوضح أن الكائن myProg
يرتبط بـكائنات Message
أو يشير إليها، وتشير الأسهم المرافقة لتلك الخطوط أن كائن myProg
يرسل رسالة printIt
إلى كل كائن من كائنات Message
، وتُنقل رسائل الكائنات من خلال ارتباطات associations.
المعامل self
يطرح من يبدأ حديثًا في البرمجة الكائنية التوجه ببايثون سؤالًا هو: ما هو المعامل self؟ لأن تعريف أي تابع في صنف ما في بايثون يبدأ به، ويجب أن نبين أن الاسم نفسه مجرد اصطلاح، ولم يتغير إلى الآن لأن الثبات أمر محمود في الاصطلاحات البرمجية، فلغة جافاسكربت مثلًا لديها مفهوم مشابه لكنها تستخدم اسم this
بدلًا من self.
أما self نفسه فهو مرجع إلى النسخة الحالية، فحين ننشئ نسخةً من الصنف فإنها تحتوي على بياناتها الخاصة كما أنشأها الباني، لكنها لا تحوي التوابع، لذا عندما نرسل رسالةً إلى نسخة وتستدعي التابع الموافق؛ فإنها تستدعيه إلى الصنف من خلال مرجع داخلي، فتمرر مرجعًا إلى نفسها self إلى التابع لتعرف شيفرة الصنف أي نسخة يجب أن تستخدمها.
لننظر الآن في مثال مألوف، فإذا كان لدينا تطبيق رسومي فيه كائنات أزرار كثيرة، فسيُفعَّل التابع المرتبط بكل زر عندما يضغطه المستخدم، ويعرف تابع الزر أي زر ضغِط بالإشارة إلى قيمة self التي ستكون مرجعًا إلى نسخة الزر الحقيقي الذي ضُغط، وسنرى ذلك عمليًا في مقال لاحق.
وعند إرسال رسالة إلى كائن يحدث ما يلي:
- تستدعي شيفرة العميل النسخة، أي ترسل الرسالة إذا أردنا الحديث باصطلاح البرمجة الكائنية التوجه.
- تستدعي النسخة تابع الصنف، وتمرر مرجعًا إلى نفسها self.
- يستخدم تابع الصنف بعدها المرجع الممرَّر ليأخذ بيانات النسخة للكائن المستقبِل.
يمكن رؤية تلك النقاط عمليًا في تسلسل الشيفرة التالي، ونلاحظ أننا نستطيع استدعاء تابع الصنف صراحةً كما فعلنا في السطر الأخير:
>>> class C: ... def __init__(self, val): self.val = val ... def f(self): print ("hello, my value is:", self.val) ... >>> # create two instances >>> a = C(27) >>> b = C(42) >>> # first try sending messages to the instances >>> a.f() hello, my value is 27 >>> b.f() hello, my value is 42 >>> # now call the method explicitly via the class >>> C.f(a) hello, my value is 27
نستطيع استدعاء التوابع كما نرى في المثال أعلاه من خلال النسخة، وتملأ بايثون معامِل self في تلك الحالة لنا، أو من خلال الصنف صراحةً، وفي تلك الحالة نحتاج إلى تمرير قيمة self صراحةً.
لدينا سؤال يطرح نفسه الآن، فإذا كانت بايثون تستطيع توفير مرجع بين النسخة وصنفها، ألا تستطيع أن تملًا self بنفسها أيضًا؟ ربما يكون هذا سؤالًا منطقيًا لكن الإجابة عليه هي أن Guido Van Rossum -منشئ اللغة- صممها هكذا. لكن مع هذا فإن العديد من لغات البرمجة الكائنية التوجه تخفي معامِل self، لكن بايثون تعتمد الصراحة explicity وتفضلها على الضمنية implicity، ويتعود المبرمج على هذا المبدأ مع كثرة العمل.
تعددية الأشكال polymorphism
لدينا الآن القدرة على تعريف أصنافنا الخاصة وإنشاء نسخ منها وإسنادها إلى متغيرات، ثم تمرير رسائل إلى تلك الكائنات تؤدي إلى تشغيل التوابع التي عرفناها، لكن هناك عنصر أخير يتعلق بالبرمجة الكائنية التوجه هنا، وهو الأهم فيها من نواحٍ عدة، فإذا كان لدينا كائنان من صنفين مختلفين لكنهما يدعمان نفس مجموعة الرسائل، لكن مع التوابع الموافقة لها، فنستطيع عندئذ أن نجمع تلك الكائنات معًا ونعاملها معاملةً واحدةً في برنامجنا، لكنها ستتصرف تصرفًا مختلفًا، وتُعرف تلك القدرة على التصرف المختلف لنفس رسائل الدخل باسم تعددية الأشكال، تُستخدم هذه الخاصية عادةً لجعل عدد من الكائنات الرسومية المختلفة ترسم نفسها عند استلام رسالة paint، فترسم الدائرة شكلًا مختلفًا تمامًا عن المثلث، لكن بما أن لهما نفس تابع paint فنستطيع -نحن المبرمجين- أن نتجاهل الاختلافات ونراهما على أنهما مجرد أشكال، لننظر الآن في مثال نحسب فيه مساحة الأشكال بدلًا من رسمها:
ننشئ أولًا الصنفين Square
وCircle
:
class Square: def __init__(self, side): self.side = side def calculateArea(self): return self.side**2 class Circle: def __init__(self, radius): self.radius = radius def calculateArea(self): import math return math.pi*(self.radius**2)
نستطيع الآن أن ننشئ قائمةً من الأشكال -دوائر أو مربعات- ثم نطبع مساحاتها:
shapes = [Circle(5),Circle(7),Square(9),Circle(3),Square(12)] for item in shapes: print "The area is: ", item.calculateArea()
إذا جمعنا هذه الأفكار مع وحدات modules فسنحصل على آلية بالغة القوة لإعادة استخدام الشيفرة، حيث نضع تعريفات الصنف في وحدة ولتكن shapes.py
مثلًا، ثم نستورد تلك الوحدة حين نرغب في التعديل على الأشكال، وهذا بالضبط ما حصل مع العديد من وحدات بايثون القياسية، وهو السبب الذي يجعل الوصول إلى توابع كائن ما يشبه استخدام الدوال في وحدة.
نرى في الصورة أعلاه مخطط كائن أكثر تعقيدًا، ونلاحظ أن الكائنات التي داخل القائمة في هذه الحالة ليس لها أسماء لأننا لم ننشئ متغيرات لها صراحةً، ففي تلك الحالة نعرض مسافةً فارغةً قبل النقطين الرأسيتين واسم الصنف، لكن هذا يجعل المخطط مزدحمًا، لذا لا نرسم مخططات الكائنات إلا عند الضرورة لتوضيح بعض المزايا غير المألوفة للتصميم، أما في الأحوال العادية فنستخدم خصائص معقدةً أكثر من مخططات الأصناف لعرض العلاقات التي لدينا، كما سنرى في الأمثلة التالية.
الوراثة inheritence
تُستخدم الوراثة (أو الاكتساب) عادةً لتنفيذ تعددية الأشكال واستخدامها، وقد تكون هي الآلية الوحيدة لذلك في العديد من لغات البرمجة الكائنية، ويمكن للصنف أن يرث السمات والعمليات من صنف أب parent class أو صنف رئيسي super class، وهذا يعني أن الصنف الجديد المطابق لصنف آخر في أغلب جوانبه لا يجب أن يعيد تنفيذ جميع التوابع التي في الصنف الأول، بل يمكن أن يرث تلك الإمكانيات ثم يغيرها لتنفيذ أمور مختلفة، كما في تابع calculateArea
أعلاه، وسنستخدم للتوضيح مثالًا فيه هرمية أصناف حسابات بنكية، حيث نستطيع إيداع المال والحصول على الرصيد والقيام بعمليات سحب، ولبعض الحسابات نسبة ربوية (فائدة) سنفترض أنها تُحسب عند كل إيداع، إضافةً إلى بعض الرسوم الأخرى لعمليات السحب.
الصنف BankAccount
لنرى الآن كيف سيبدو هذا المثال، سننظر في السمات والعمليات الخاصة بالحساب البنكي في أكثر مستوىً عام له، ومن الأفضل هنا أن ننظر في العمليات أولًا، ثم نوفر السمات حسب الحاجة لدعم تلك العمليات، فمع الحساب المصرفي نستطيع القيام بما يلي:
- إيداع المال.
- سحب المال.
- التحقق من الرصيد الحالي.
- تحويل الأموال إلى حساب آخر.
وسنحتاج إلى معرِّف الحساب المصرفي ID للحساب الآخر والرصيد الحالي، بالنسبة للمعرِّف فسنستخدم المتغير الذي نسند الكائن إليه، لكن إذا كنا في مشروع حقيقي فيجب إنشاء سمة خاصة بالمعرِّف تخزن مرجعًا فريدًا، كما سنحتاج إلى تخزين الرصيد، وعند تمثيل ذلك بلغة النمذجة الموحدة UML فسيبدو كما يلي:
نستطيع الآن أن ننشئ صنفًا يدعم ذلك:
# ننشئ صنف اعتراض Exception مخصص class BalanceError(Exception): value = "Sorry you only have $%6.2f in your account" class BankAccount: def __init__(self, initialAmount): self.balance = initialAmount print( "Account created with balance %5.2f" % self.balance ) def deposit(self, amount): self.balance = self.balance + amount def withdraw(self, amount): if self.balance >= amount: self.balance = self.balance - amount else: raise BalanceError() def checkBalance(self): return self.balance def transfer(self, amount, account): try: self.withdraw(amount) account.deposit(amount) except BalanceError: print( BalanceError.value % self.balance )
-
الملاحظة الأولى: نتحقق من الرصيد قبل السحب، ونستخدم اعتراضًا لمعالجة الأخطاء، وبما أنه لا يوجد خطأ من النوع
BalanceError
في بايثون فسنحتاج إلى إنشاء واحد، وهو صنف فرعي من الصنفException
مع قيمة نصية، وتُعرَّف قيمة السلسلةvalue
سمةً لصنف الاعتراض لمجرد الاصطلاح فقط، وهي تضمن أننا نولد رسائل خطأ في كل مرة نرفع فيها اعتراضًا، ونلاحظ هنا أننا لم نستخدمself
عند تعريف القيمة فيBalanceError
لأنvalue
سمة مشتركة بين كل النسخ، وهي معرَّفة على مستوى الصنف وتُعرف بمتغير الصنف، ونصل إليها باستخدام اسم الصنف متبوعًا بنقطةBalanceError.value
كما رأينا أعلاه، فعندما يولّد خطأ التعقب العكسي traceback -أي مسار مكان وقوع الخطأ ورجوعًا ضمن سلسلة الاستدعاءات- فسينتهي بطباعة سلسلة الخطأ المصاغة مع عرض الرصيد الحالي. -
الملاحظة الثانية: يستخدم التابع
transfer
الدالة التابعةwithdraw/deposit
الخاصة بالصنفBankAccount
أو توابعه لتنفيذ عملية التحويل، وهذا أمر شائع في البرمجة الكائنية التوجه ويُعرف بالمراسلة الذاتية self messaging، ويعني أن الأصناف المشتقة تستطيع تنفيذ نسخها الخاصة منdeposit/withdraw
لكن يظل التابعtransfer
كما هو لجميع أنواع الحسابات.
بما أننا عرّفنا BankAccount
صنفًا قاعديًا فنستطيع أن نعود إلى الوراثة التي كنا نشرحها، ولننظر في أول صنف فرعي لنا فيما يلي.
الصنف InterestAccount
نستخدم الوراثة الآن لتوفير حساب يضيف نسبة ربوية -سنفترض أنها 3%- عند كل عملية إيداع، وستكون مطابقةً لصنف BankAccount القياسي عدا تابع الإيداع وبدء معدل النسبة، لذا نعيد كتابة تنفيذ هذه التوابع كما يلي:
class InterestAccount(BankAccount): def __init__(self, initialAmount, interest=0.03): super().__init__(initialAmount) self.interest = interest def deposit(self, amount): super().deposit(amount) self.balance = self.balance * (1 + self.interest)
-
الملاحظة الأولى: نمرر الصنف الرئيسي (أو الأب) معامِلًا في تعريف الصنف، ويكون هذا الصنف الأب في حالتنا هو
BankAccount
. -
الملاحظة الثانية: نستدعي
super().__init__()
في بداية التابع__init__()
، والدالةsuper()
هي دالة خاصة وظيفتها معرفة الصنف الرئيسي، ويفيدنا ذلك عند وراثة أكثر من صنف رئيسي واحد فيما يسمى بالوراثة المتعددة، حيث نتجنب بعض المشاكل الغريبة التي قد تظهر إذا حاولنا استدعاء الصنف الرئيسي باسمه، لذلك يُفضل استخدامsuper()
.
ونبدأ الصنف الموروث باستدعاء التابع __init__
الخاص بالصنف الرئيسي، ولا نحتاج هنا إلا إلى بدء السمة interest
التي قدمناها هنا، وبالمثل في استخدام super()
في تابع الإيداع، إذ يستدعي التابع deposit
الخاص بالصنف الأب فلا نحتاج إلا إلى إضافة المزايا الجديدة للصنف InterestAccount
.
وهكذا نرى قوة البرمجة الكائنية التوجه وإمكانياتها، فبما أننا وضعنا BankAccount
داخل الأقواس بعد اسم الصنف فقد صارت جميع التوابع موروثةً من BankAccount
، ونلاحظ أن deposit
تستدعي التابع deposit
الخاص بالصنف الرئيسي بدلًا من نسخ الشيفرة، وسيحصل الصنف الفرعي على تلك التعديلات تلقائيًا إذا عدّلنا deposit
الخاص بالصنف BankAccount
بحيث يحوي تحققًا من بعض الأخطاء.
الصنف ChargingAccount
هذا الصنف مطابق للصنف BankAccount
عدا أنه يطلب رسومًا افتراضية مقدارها 3$ لكل عملية سحب، وبالنسبة لـ InterestAccount
فيمكن إنشاء صنف يرث من BankAccount
ويعدّل التابعين init
و withdraw
:
class ChargingAccount(BankAccount): def __init__(self, initialAmount, fee=3): super().__init__(initialAmount) self.fee = fee def withdraw(self, amount): super().withdraw(amount+self.fee)
-
الملاحظة الأولى: نخزن الرسوم مثل متغير نسخة intance variable لنستطيع تغييره لاحقًا عند الحاجة، ونلاحظ أننا نستدعي
__init__
مرةً أخرى مثل أي تابع آخر. -
الملاحظة الثانية: نضيف الرسوم إلى عملية السحب المطلوبة في استدعاء التابع الموروث
withdraw
الذي ينجز العمل الفعلي. - الملاحظة الثالثة: نضيف أثرًا جانبيًا هنا حيث تُفرض رسوم تلقائيًا على عمليات التحويل، لكن هذا مطلوب على الأرجح لذا لا بأس به، وتجدر الإشارة هنا إلى أن إعادة الاستخدام هذه تحمل في طياتها احتمالية الآثار الجانبية غير المتوقعة التي يجب الحذر منها.
ونمثل هذه الوراثة في UML بسهم مصمت من الصنف الفرعي إلى الصنف الرئيسي، فتُمثَّل هرمية الحساب البنكي الآن كما يلي:
نلاحظ أننا سردنا التوابع والسمات التي تغيرت فقط أو أضيفت إلى الأصناف الفرعية.
اختبار النظام
للتحقق من عمل الهرمية السابقة بكفاءة، جرب تنفيذ الشيفرة التالية في محث بايثون أو بإنشاء ملف اختبار منفصل:
from bankaccount import * # الحساب البنكي العادي a = BankAccount(500) b = BankAccount(200) a.withdraw(100) # a.withdraw(1000) a.transfer(100,b) print( "A = ", a.checkBalance() ) print( "B = ", b.checkBalance() ) # حساب للنسبة الربوية c = InterestAccount(1000) c.deposit(100) print( "C = ", c.checkBalance() ) # حساب للرسوم المفروضة d = ChargingAccount(300) d.deposit(200) print( "D = ", d.checkBalance() ) d.withdraw(50) print( "D = ", d.checkBalance() ) d.transfer(100,a) print( "A = ", a.checkBalance() ) print( "D = ", d.checkBalance() ) # حوّل من حساب الرسوم إلى حساب النسبة الربوية # حساب الرسوم سيطلب رسومًا، وحساب النسبة الربوية # يضيف نسبة ربوية print( "C = ", c.checkBalance() ) print( "D = ", d.checkBalance() ) d.transfer(20,c) print( "C = ", c.checkBalance() ) print( "D = ", d.checkBalance() )
أزل علامة التعليق الآن من السطر (a.withdraw(1000
لترى الاعتراض عمليًا.
وبهذا نكون أتممنا مثالًا بسيطًا يظهر كيفية استخدام الوراثة لتوسيع إطار بسيط وإضافة مزايا قوية إليه، وقد رأينا كيف يبنى المثال على مراحل ويوضع برنامج اختبار للتحقق من نجاحه، مع أن اختباراتنا لم تكن كاملةً لأننا لم نغطِّ كل الحالات الممكنة، وكان من الممكن إدراج المزيد من الاختبارات، كما في حالة إنشاء حساب برصيد سالب.
اقتباسالتطوير الموجه بالاختبارات
يستخدم العديد من المبرمجين العاملين تقنيةً تسمى التطوير الموجَّه بالاختبار test driven development، حيث يكتبون اختباراتهم قبل كتابة الشيفرة البرمجية نفسها، مما يسمح لهم باختبار شيفرتهم مرةً بعد مرة أثناء تطويرها، وينتقلون تدريجيًا من فشل الاختبارات المتكرر إلى نجاحها، وإذا نجحت جميع الاختبارات فإن هذا يعني أن البرنامج يعمل بكفاءة. وهذا النهج شائع جدًا في البرمجة لدرجة تطوير أدوات خاصة لتساعد فيه، ولدى بايثون أدوات مثل هذه موجودة في وحدة
unittest
في المكتبة القياسية، وهذا النهج -وإن كان مفيدًا في كتابة شيفرة حقيقية- لن يفيدنا في شرحنا كثيرًا لأنه يخفي الشيفرة الأساسية وحالات اختبار كثيرة نحاول دراستها، لذا لن نستخدمه هنا، لكن قد ننظر فيه في مقال لاحق.
تجميعات الكائنات
إحدى المشاكل التي قد تواجهها هي كيفية التعامل مع كائنات كثيرة، أو كيفية التعامل مع كائنات تنشئها في وقت التشغيل، فمن السهل إنشاء حسابات بنكية ثابتة كما فعلنا أعلاه:
acc1 = BankAccount(...) acc2 = BankAccount(...) acc3 = BankAccount(...) etc...
لكن في العالم الحقيقي لا تكون لدينا بيانات عن عدد الحسابات التي يجب إنشاؤها، فكيف نحل هذه المشكلة؟ لننظر فيها بتفصيل أكبر:
نريد شكلًا ما من قواعد البيانات التي تسمح لنا بإيجاد أي حساب بنكي باسم مالكه أو رقم حسابه -بما أنه قد يكون للشخص الواحد عدة حسابات-، والعكس صحيح، ولكن ألا يشبه البحث عن شيء له معرّف خاص به القاموس؟ لنجرب استخدام قاموس في بايثون للاحتفاظ بكائنات منشأة ديناميكيًا:
from bankaccount import BankAccount import time # أنشئ دالة جديدة لتوليد أرقام معرّفات فريدة def getNextID(): ok = input("Create account[y/n]? ") if ok[0] in 'yY': # check valid input id = time.time() # use current time as basis of ID id = int(id) % 10000 # حول إلى عدد صحيح وقلله إلى 4 أرقام else: id = -1 # وذلك سيوقف الحلقة التكرارية return id # أنشئ بعض الحسابات وخزنها في القاموس accountData = {} # قاموس جديد while True: # كرر حلقيًا بلا نهاية id = getNextID() if id == -1: break # تخرج إجباريًا من الحلقة التكرارية bal = float(input("Opening Balance? ")) # حول السلسلة إلى عدد ذي فاصلة عائمة accountData[id] = BankAccount(bal) # استخدم المعرِّف لإنشاء إدخال جديد في القاموس print( "New account created, Number: %04d, Balance %0.2f" % (id, bal) ) # دعنا نصل الآن إلى بعض الحسابات for id in accountData.keys(): print( "%04d\t%0.2f" % (id, accountData[id].checkBalance()) ) # ونبحث عن واحد فيها # أدخل محرفًا غير رقمي لرفع اعتراض وإنهاء البرنامج while True: id = int(input("Which account number? ")) if id in accountData: print( "Balance = %0.2d" % accountData[id].checkBalance() ) else: print( "Invalid ID" )
لا شك أن المفتاح الذي نستخدمه للقاموس قد يكون أي شيء يعرّف الكائن تعريفًا فريدًا، فقد يكون أحد سماته -مثل الرصيد balance
- لكن الرصيد قد يتشابه بين الحسابات، ويتغير بالزيادة والنقص، وهكذا نفكر في الخيارات المتاحة إلى أن نصل إلى معرّف لا يتكرر، وهنا يمكن الرجوع إلى مقال البيانات وأنواعها، لقراءة القسم الخاص بالقاموس مرةً أخرى.
يمثَّل هذا مرئيًا في UML باستخدام مخطط الصنف، ويُعرض القاموس مثل صنف له علاقة مع العديد من الحسابات البنكية، ونرى ذلك موضحًا بمحرف النجمة على الخط الموصل بين الأصناف، ونستخدم محرف النجمة هنا لأنه الرمز المستخدم في التعابير النمطية للإشارة إلى عدد من العناصر مقداره صفر أو أكثر، وهذا يُعرف بعدد عناصر العلاقة cardinality of the relationship، ويمكن رؤيته بعد طرق، لكن المجالات العددية للتعابير النمطية هي المستخدمة بكثرة لثرائها ومرونتها.
نلاحظ استخدام القالب النمطي stereotype على القاموس Dictionary
لإظهار أنه صنف مضمَّن، كما نلاحظ وجود الصندوق الملحق بالارتباط، والذي يوضح أن المفتاح هو قيمة المعرّف ID
، فإذا كنا نستخدم قائمةً بسيطةً فلن يكون لدينا الصندوق ولكان الخط وصل بين الصنفين مباشرةً، وبهذا يتضح أننا نتجنب الحاجة إلى مخططات الكائنات الكبيرة والمعقدة باستخدام علاقات الأصناف وعدد العناصر في المجموعة cardinality، حيث نركز على العلاقات المجردة بين الأصناف بدلًا من التعامل مع عدد كبير من العلاقات الحقيقية بين النسخ المفردة.
حفظ الكائنات
إن فقد البيانات عند انتهاء المخطط هو أحد مشاكل السلوك السابق، لذا نريد طريقةً لحفظ الكائنات، ويمكن فعل ذلك باستخدام قواعد البيانات لكنه أسلوب متقدم، أما الآن فسنستخدم ملفًا نصيًا بسيطًا لحفظ الكائنات واسترجاعها، ورغم أن بايثون تحتوي على وحدتين لتنفيذ ذلك بكفاءة، وهما pickle
وshelve
، إلا أننا سنشرح الطريقة العامة التي تصلح لأي لغة، والطريقة العامة التي نقصدها هي إنشاء التابعين save
وrestore
في الكائن ذي المستوى الأعلى، ثم إعادة كتابتهما في كل صنف ليستدعيا النسخة الموروثة ثم يضيفا السمات المعرفة محليًا، وبالمناسبة يُستخدم مصطلح الثبات Persistence للإشارة إلى القدرة على حفظ الأشياء واستعادتها.
class A: def __init__(self,x,y): self.x = x self.y = y self.f = None def save(self,fn): f = open(fn,"w") f.write(str(self.x)+ '\n') # convert to a string and add newline f.write(str(self.y)+'\n') return f # for child objects to use def restore(self, fn): f = open(fn) self.x = int(f.readline()) # convert back to original type self.y = int(f.readline()) return f class B(A): def __init__(self,x,y,z): super().__init__(x,y) self.z = z def save(self,fn): f = super().save(fn) # call parent save f.write(str(self.z)+'\n') return f # in case further children exist def restore(self, fn): f = super().restore(fn) self.z = int(f.readline()) return f # أنشئ النُسخ a = A(1,2) b = B(3,4,5) # احفظ النُسخ a.save('a.txt').close() # تذكر أن تغلق الملف b.save('b.txt').close() # اجلب النسخ newA = A(5,6) newA.restore('a.txt').close() # تذكر أن تغلق الملف newB = B(7,8,9) newB.restore('b.txt').close() print( "A: ",newA.x,newA.y ) print( "B: ",newB.x,newB.y,newB.z )
نلاحظ أن القيم المطبوعة هي القيم المسترجعة، وليست القيم التي استخدمناها لإنشاء النسخ.
والمهم هنا هو إعادة كتابة التابع save والتابع restore في كل صنف واستدعاء التابع الرئيسي في خطوة أولى، ثم لا نتعامل في الصنف الفرعي إلا مع سمات ذلك الفرعي فقط، ومن البديهي أن تحويل السمة إلى سلسلة نصية وحفظها أمر متروك لك كونك مبرمجًا، لكن يجب إخراجها على سطر واحد، أما عند الاستعادة فببساطة يمكن عكس عملية التخزين، لكن تظهر هنا عقبة كبيرة في هذا الأسلوب، وهي أننا نحتاج إلى إنشاء ملف منفصل لكل كائن، وهذا قد يعني آلاف الملفات الصغيرة إذا كنا في بيئة برمجية لسوق حقيقي، حيث سيتعقد العمل بوتيرة متسارعة، وسنحتاج إلى استخدام قاعدة بيانات لحفظ الكائنات، وسنبحث في ذلك في مقال لاحق، أما الآن فيكفي أن تعلم أن المفاهيم الأساسية ستظل كما هي.
دمج الأصناف والوحدات
توفر الأصناف والوحدات آليات للتحكم في تعقيد البرنامج، ومن المنطقي مع ازدياد حجم البرنامج ازدياد الحاجة إلى دمج المزايا بوضع أصناف داخل وحدات، وتنصح بعض الهيئات بوضع كل صنف في وحدة منفصلة، لكن هذا يؤدي إلى وحدات كثيرة جدًا ويزيد التعقيد بدلًا من تقليله، والبديل الذي لدينا هو تجميع الأصناف معًا، ووضع المجموعة في وحدة، وإذا عدنا إلى مثالنا أعلاه فسنضع كل تعريفات أصناف الحساب المصرفي في وحدة واحدة هي bankaccount
مثلًا، ثم ننشئ وحدةً منفصلةً لشيفرة التطبيق التي تستخدم الوحدة.
يمكن تمثيل ذلك مرئيًا بواسطة UML بطريقتين، حيث يمكن تمثيل الجمع المنطقي للأصناف باستخدام حزمة، أو نستطيع تمثيل الملف الحقيقي مثل مكوّن:
والهدف هنا أن تبدو أيقونة الحزمة مثل مجلد في أي برنامج مدير ملفات، أما الأيقونة الصغيرة التي في أعلى اليمين في أيقونة المكون فهي رمز المكون القديم في UML، وبما أن رسمها صعب في المخططات عند رسم الخطوط التي تظهر العلاقات بين المكونات فقد صغِّر شكلها في UML 2.0.
هذا ما سنغطيه حول UML في هذه السللسلة، ويمكن الرجوع لمحركات البحث والويب للاستزادة من المراجع والتدريبات وأدوات رسم UML، رغم أن الأشكال سهلة الرسم في أي برنامج رسم متجهي.
إذا أردنا تمثيلًا بسيطًا لذلك التصميم فسيكون كما يلي:
# File: bankaccount.py # # Implements a set of bank account classes ################### class BankAccount: .... class InterestAccount: ... class ChargingAccount: ...
ثم إذا أردنا استخدامه:
import bankaccount newAccount = bankaccount.BankAccount(50) newChrgAcct = bankaccount.ChargingAccount(200) # هنا يمكن تنفيذ المهام التي تريدها
لكن ماذا لو أراد صنفان في وحدتين الوصول إلى بيانات بعضهما بعضًا؟ إن أبسط حل لهذا هو استيراد كلتا الوحدتين وإنشاء نُسخ للأصناف التي نريدها، وتمرير نُسخ أحد الصنفين إلى توابع النسخة الأخرى، وتمرير الكائنات كاملةً من مكان لآخر ما هو إلا برمجة كائنية التوجه، وهنا ندرك أحد أسباب التسمية لهذا النوع من البرمجة، فلا نحتاج إلى استخراج سمات كائن وتمريرها إلى كائن آخر، بل نمرر الكائن كله، وإذا كان الكائن يستخدم رسالةً متعددة الأشكال للوصول إلى المعلومات التي يحتاج إليها فسيصلح التابع مع أي نوع من الكائنات التي تدعم الرسالة.
لننظر في مثال واقعي لتوضيح ذلك، حيث ننشئ وحدةً قصيرةً نسميها logger
تحتوي صنفين، هما Logger
الذي يسجل النشاط داخل ملف، ويحتوي هذا الصنف على تابع واحد هو log()
الذي يأخذ معاملًا كائنًا قابلًا للتسجيل، أما الصنف الآخر فهو Loggable
الذي تستطيع الأصناف الأخرى أن ترثه لتعمل مع logger:
# File: logger.py # # Create Loggable and Logger classes for logging activities # of objects ############ class Loggable: def activity(self): return "This needs to be overridden locally" class Logger: def __init__(self, logfilename = "logger.dat"): self._log = open(logfilename,"a") def log(self, loggedObj): self._log.write(loggedObj.activity() + '\n') def __del__(self): self._log.close()
نلاحظ أننا وفرنا تابع تدمير destructor هو __del__
لإغلاق الملف عند حذف كائن التسجيل أو كنسه garbage collected، وهو أحد التوابع السحرية الموجودة في بايثون كما نرى من الشرطتين السفليتين حوله، واللتين تشبهان __init__()
، مع فرق أن init
يُستدعى عند إنشاء نسخة ما، أما del
فيستدعى عندما يحذف كانس المخلفات النسخة، وقد لا يُستدعى إذا خرجت بايثون خروجًا غير متوقع، حيث سيكون لدينا في هذه الحالة مشاكل أكبر من استدعاء del
أو عدم استدعائه.
كما استدعينا سمة السجل _log
مع شرطة سفلية قبلها، وهو اصطلاح للتسمية في بايثون، كما في استخدام الكلمات ذات الأحرف الكبيرة لأسماء الأصناف، فالشرطة السفلية المفردة تعني أن السمة ليست مصممةً لنصل إليها مباشرةً، بل من خلال توابع الصنف.
سننشئ الآن وحدةً جديدةً تعرِّف النسخ القابلة للتسجيل لأصناف حساباتنا المصرفية السابقة، لنستطيع استخدام وحدتنا:
# File: loggablebankaccount.py # # Extend Bank account classes to work with logger module. ############################### import bankaccount, logger class LoggableBankAccount(bankaccount.BankAccount, logger.Loggable): def activity(self): return "Account balance = %d" % self.checkBalance() class LoggableInterestAccount(bankaccount.InterestAccount, logger.Loggable): def activity(self): return "Account balance = %d" % self.checkBalance() class LoggableChargingAccount(bankaccount.ChargingAccount, logger.Loggable): def activity(self): return "Account balance = %d" % self.checkBalance()
استخدمنا الوراثة المتعددة في المثال أعلاه، حيث ورثنا صنفين رئيسيين وليس صنفًا واحدًا، وهذا غير ضروري في بايثون بما أننا نستطيع إضافة التابع activity()
إلى أصنافنا الأصلية ونحقق نفس الأثر، أما في لغات برمجة كائنية التوجه ثابتة مثل جافا أو C++ فسيكون هذا ضروريًا، لذا سنشرح التقنية هنا، قد تلاحظ أن التابع activity()
متطابق في الأصناف الثلاثة، وهذا يعني أننا نستطيع توفير بعض الكتابة على أنفسنا بإنشاء نوع وسيط من صنف حساب قابل للتسجيل يرث Loggable
وله تابع activity
فقط، ثم ننشئ ثلاثة أنواع حسابات قابلة للتسجيل بوراثتها من ذلك الصنف الجديد ومن صنف Loggable
، كما يلي:
class LoggableAccount(logger.Loggable): def activity(self): return "Account balance = %d" % self.checkBalance() class LoggableBankAccount(bankaccount.BankAccount, LoggableAccount): pass class LoggableInterestAccount(bankaccount.InterestAccount, LoggableAccount): pass class LoggableChargingAccount(bankaccount.ChargingAccount, LoggableAccount): pass
لا يوفر هذا علينا الكثير من الكتابة، لكنه يعني أن علينا اختبار تعريف تابع واحد فقط والاحتفاظ به بدلًا من ثلاثة توابع متطابقة، وهذا النوع من البرمجة الذي يكون فيه لصنف رئيسي وظيفةً مشتركةً يسمى بالبرمجة الخليطة mixin programming، ويسمى الصنف الأدنى بالصنف الخليط mixin class، والناتج المتوقع من ذلك الأسلوب أن يكون لتعريفات الصنف النهائي متن صغير أو ليس لها متن أصلًا، لكنها تحوي قائمةً طويلةً من الأصناف الموروثة كما رأينا هنا.
ومن الشائع أيضًا أن الأصناف المختلطة لا ترث من أي شيء بنفسها، رغم أننا فعلنا هذا هنا، فما هي إلا طريقة لإضافة تابع مشترك أو مجموعة من التوابع إلى صنف أو مجموعة أصناف باستخدام الوراثة، ويأتي مصطلح المختلطة mixin، من مجال صناعة المثلجات، حيث تضاف نكهات مختلفة إلى الفانيليا لإنتاج نكهة جديدة، وكانت أول لغة تدعم هذا الأسلوب هي لغة Flavors، والتي كانت إحدى لغات Lisp المشهورة وقتها.
نأتي الآن إلى النقطة التي نُظهر فيها شيفرة التطبيق الخاص بنا بإنشاء كائن مسجل logger وبعض الحسابات المصرفية، ثم تمرير الحسابات إلى المسجل، رغم أنها معرّفة في وحدات مختلفة:
# Test logging and loggable bank accounts. ############# import logger import loggablebankaccount as lba log = logger.Logger() ba = lba.LoggableBankAccount(100) ba.deposit(700) log.log(ba) intacc = lba.LoggableInterestAccount(200) intacc.deposit(500) log.log(intacc)
نلاحظ هنا كلمة as
المفتاحية التي تُستخدم لإنشاء اسم مختصر عند استدعاء loggablebankaccount
.
لا نحتاج إلى استخدام سابقة الوحدة module prefix بعد إنشاء النسخ المحلية، وبما أنه لا يوجد وصول مباشر من كائن إلى آخر بل من خلال الرسائل، فلا حاجة أن تشير وحدتا تعريف الصنف إلى بعضهما البعض مباشرةً، كما يعمل المسجل مع نسخ كل من LoggableBankAccount
وLoggableInterestAccount
لأنهما يدعمان واجهة Loggable
، فالتوافق بين واجهات الكائنات من خلال تعددية الأشكال هو الأساس الذي تبنى عليه جميع البرامج الكائنية التوجه.
يوجد نظام تسجيل أكثر تعقيدًا في وحدة المكتبة القياسية logging
، لإظهار بعض التقنيات فقط، ونذكره هنا لتكون هذه المكتبة أول خيار نبحث فيه عند الحاجة إلى أدوات تسجيل في البرامج التي نكتبها.
نأمل أن يكون هذا الشرح كافيًا لفهم البرمجة كائنية التوجه، مع الاستزادة من المصادر الموجودة في الويب أو قراءة أحد الكتب المذكورة في بداية المقال، أما الآن فسننظر في كيفية تنفيذ البرمجة كائنية التوجه في جافاسكربت وVBScript.
البرمجة الكائنية في VBScript
تدعم VBScript مفهوم الكائنات وتسمح بتعريف الأصناف وإنشاء نسخ منها، لكنها لا تدعم مفهوم الوراثة ولا تعددية الأشكال، وعلى هذا تكون لغة VBScript لغةً كائنية الأساس object based وليست كائنية التوجه، لكن مفهوم دمج البيانات والدوال في كائن واحد لا يزال صالحًا هنا، ويمكن تنفيذ صورة محدودة من الوراثة باستخدام تقنية تسمى التفويض delegation.
تعريف الأصناف
يُعرَّف الصنف في VBScript باستخدام تعليمة Class
كما يلي:
<script type=text/VBScript> Class MyClass Private anAttribute Public Sub aMethodWithNoReturnValue() MsgBox "MyClass.aMethodWithNoReturnValue" End Sub Public Function aMethodWithReturnValue() MsgBox "MyClass.aMethodWithReturnValue" aMethodWithReturnValue = 42 End Function End Class </script>
وهذا يعرِّف صنفًا جديدًا اسمه MyClass
مع سمة تسمى anAttribute
تكون مرئيةً فقط للتوابع التي في الصنف، كما نرى من كلمة Private
المفتاحية، ومن المتعارف عليه أن نصرح عن سمات البيانات بأنها Private
، وعن أغلب التوابع بأنها Public
، ويُعرف هذا باسم إخفاء البيانات، وميزته أنه يسمح لنا بالتحكم في الوصول إلى البيانات بإجبار استخدام التوابع التي تتحقق من جودة البيانات على القيم التي تمرَّر إلى داخل وخارج الكائن، وتوفر بايثون آليةً خاصةً بها لتنفيذ هذا لكنها خارج نطاق شرحنا.
إنشاء النسخ
ننشئ النسخ في VBScript بدمج الكلمتين المفتاحيتين Set
وNew
، ويجب أن يكون المتغير الذي أُسندت إليه النسخة الجديدة قد صرِّح عنه باستخدام كلمة Dim
كما هو متبع في VBScript:
<script type=text/VBScript> Dim anInstance Set anInstance = New MyClass </script>
يؤدي هذا إلى إنشاء نسخة من الصنف مصرح عنها في القسم السابق وإسنادها إلى متغير anInstance
، لاحظ أننا يجب أن نسبق اسم المتغير بـ Set
، وأننا نستخدم كلمة New
لإنشاء الكائن.
إرسال الرسائل
تُرسَل الرسائل إلى النسخ باستخدام نفس الصيغة النقطية .
التي تستخدمها بايثون:
<script type=text/VBScript> Dim aValue anInstance.aMethodWithNoReturnValue() aValue = anInstance.aMethodWithReturnValue() MsgBox "aValue = " & aValue </script>
يُستدعى التابعان المصرح عنهما في تعريف الصنف، ولا توجد في الحالة الأولى قيمة معادة، أما في الحالة الثانية فسنسند القيمة المعادة إلى المتغير aValue
، ولا يوجد شيء غير اعتيادي هنا باستثناء أن البرنامج الفرعي subroutine والدالة مسبوقان باسم النسخة.
الوراثة وتعددية الأشكال
لا تدعم VBScript أي آلية للوراثة أو تعددية الأشكال، لكن نستطيع محاكاة ذلك إلى حد ما باستخدام تقنية تسمى التفويض، وهذا يعني أننا نعرف سمةً للصنف الفرعي ليكون نسخةً من الصنف الرئيسي المفترض، ثم نعرف تابعًا لجميع التوابع الموروثة التي تستدعي تابع النسخة الرئيسية أو تفوض إليه، لننشئ صنفًا فرعيًا من MyClass
كما هو معرَّف أعلاه:
<script type=text/VBScript> Class SubClass Private parent Private Sub Class_Initialize() Set parent = New MyClass End Sub Public Sub aMethodWithNoReturnValue() parent.aMethodWithNoREturnVAlue End Sub Public Function aMethodWithReturnValue() aMethodWithReturnValue = parent.aMethodWithReturnValue End Function Public Sub aNewMethod MsgBox "This is unique to the sub class" End Sub End Class Dim inst,aValue Set inst = New SubClass inst.aMethodWithNoReturnVAlue aValue = inst.aMethodWithReturnValue inst.aNewMethod MsgBox "aValue = " & CStr(aValue) </script>
نلاحظ هنا استخدام السمة الخاصة parent
والتابع الخاص المميز Class_Initialise
، فالأولى هي سمة تفويض الصنف الأب، والثاني هو المكافئ للتابع __init__
في بايثون لبدء النسخ عند إنشائها، أي أنه الباني في لغة VBScript.
البرمجة الكائنية في جافاسكربت
تدعم جافاسكربت الكائنات باستخدام تقنية تسمى النمذجة الأولية prototyping، وهذا يعني عدم وجود بنية صنف صريحة في جافاسكربت، بل نستطيع تعريف الصنف بمجموعة من الدوال أو مثل مفهوم شبيه بالقاموس يُعرف بالمهيئ initializer.
تعريف الأصناف
من أكثر الطرق شيوعًا لتعريف الأصناف في جافاسكربت هي إنشاء دالة بنفس اسم الصنف، ويكون هو الباني، لكنه لا يُحتوى داخل أي بنية أخرى:
<script type=text/JavaScript> function MyClass(theAttribute) { this.anAttribute = theAttribute; }; </script>
ربما تلاحظ كلمة this
التي تُستخدم بنفس طريقة استخدام self
في بايثون مثل مرجع نائب placeholder reference إلى النسخة الحالية، رغم أننا لا نحتاج في الغالب إلى إدراج this
صراحةً في قائمة المعامِلات لتوابع الصنف، ونستطيع إضافة سمات جديدة إلى الصنف لاحقًا باستخدام السمة prototype
المضمَّنة كما يلي:
<script type=text/JavaScript> MyClass.prototype.newAttribute = null; </script>
ويعرِّف هذا سمةً جديدةً لـ MyClass
اسمها newAttribute
، وتضاف التوابع من خلال تعريف دالة عادية؛ ثم إسناد اسم الدالة إلى سمة جديدة مع اسم التابع، ويكون للتابع والدالة نفس الاسم عادةً، لكن لا مانع من تغيير اسم التابع إلى شيء مختلف، كما يلي:
<script type=text/JavaScript> function oneMethod(){ return this.anAttribute; } MyClass.prototype.getAttribute = oneMethod; function printIt(){ document.write(this.anAttribute + "<BR>"); }; MyClass.prototype.printIt = printIt; </script>
ولا شك أن الأسهل تعريف الدوال ثم الباني، ثم إسناد التوابع داخل الباني، وهذا هو الأسلوب الافتراضي، لذا سيبدو تعريف الصنف كاملًا كما يلي:
<script type=text/JavaScript> function oneMethod(){ return this.anAttribute; }; function printIt(){ document.write(this.anAttribute + "<BR>"); }; function MyClass(theAttribute) { this.anAttribute = theAttribute; this.getAttribute = oneMethod; this.printIt = printIt; }; </script>
لكن ثمة طريقة أخرى في جافاسكربت، باستخدام صيغة مختلفة قليلًا لإنشاء دوال التابع، حيث تسمح جافاسكربت بتعريف الدالة كما يلي:
square = function(x){ return x*x;}
ثم نستدعي ذلك كما يلي:
document.write("The square of 5 is: " + square(5))
فإذا طبقناه على تعريف الصنف الخاص بنا نحصل على:
<script type=text/JavaScript> function MyClass(theAttribute) { this.anAttribute = theAttribute; this.getAttribute = function(){ return this.anAttribute; }; this.printIt = function printIt(){ document.write(this.anAttribute + "<BR>"); }; }; </script>
يفضل بعض المبرمجين هذا الأسلوب لأنه يبقي تعريفات التابع داخل الصنف دون تلويث فضاء الاسم الخارجي، بينما يرى آخرون أن هذا أسلوب فوضوي وأصعب في القراءة، وأنت حر في اختيار أي الأسلوبين تفضل.
إنشاء النسخ
تُنشأ نسخ الأصناف باستخدام كلمة new
المفتاحية كما يلي:
<script type=text/JavaScript> var anInstance = new MyClass(42); </script>
مما ينشئ نسخةً جديدةً اسمها anInstance
.
إرسال الرسائل
لا يختلف إرسال الرسائل في جافاسكربت عن اللغات الأخرى، إذ نستخدم الصيغة النقطية:
<script type=text/JavaScript> document.write("The attribute of anInstance is: <BR>"); anInstance.printIt(); </script>
الوراثة وتعددية الأشكال
يمكن استخدام آلية النمذجة الأولية في جافاسكربت للوراثة من صنف آخر -على عكس VBScript-، مع أنها أعقد من تقنية بايثون لكن يمكن التعامل معها وضبطها، لكنها ليست منتشرةً بين مبرمجي جافاسكربت، إن أساس الوراثة في جافاسكربت هو الكلمة المفتاحية prototype
التي استخدمناها في التمرير في الشيفرة أعلاه، حيث يمكن إضافة مزايا إلى كائن ما بعد تعريفه، كما يلي:
<script type="text/javascript"> function Message(text){ this.text = text; this.say = function(){ document.write(this.text + '<br>'); }; }; msg1 = new Message('This is the first'); msg1.say(); Message.prototype.shout = function(){ alert(this.text); }; msg2 = new Message('This gets the new feature'); msg2.shout(); /* msg1 وبالمثل بالنسبة لـ ...*/ msg1.shout(); </script>
الملاحظة الأولى: أضفنا تابع alert
جديد باستخدام prototype
بعد إنشاء نسخة msg1
للصنف، غير أن الخاصية كانت متاحةً للنسخة الحالية ونسخة msg2
التي أنشئت بعد الإضافة، أي أن الخاصية الجديدة تضاف إلى جميع نسخ Message
الحالية والجديدة، وتؤدي خاصية النمذجة الأولية هذه إلى إمكانية تغيير سلوك كائنات جافاسكربت المضمَّنة، بإضافة مزايا جديدة أو تغيير الطريقة التي تتصرف بها المزايا الحالية، لذا استخدمها بحرص إذا لم ترد أن تضيع وقتك مع زلات برمجية يصعب تعقبها. ومع ذلك لاستخدام prototype
آليةً لإضافة وظائف إلى الأصناف الحالية عيوبه، التي منها تغيير سلوك النسخة الحالية، وتغيير تعريف الصنف الأصلي. ويوجد أسلوب تقليدي أكثر من الوراثة يمكن استخدامه، كما يلي:
<script type="text/javascript"> function Parent(){ this.name = 'Parent'; this.basemethod = function(){ alert('This is the parent'); }; }; function Child(){ this.parent = Parent; this.parent(); this.submethod = function(){ alert('This from the child'); }; }; var aParent = new Parent(); var aChild = new Child(); aParent.basemethod(); aChild.submethod(); aChild.basemethod(); </script>
يجب أن نلاحظ أن كائن child
هنا له وصول إلى basemethod
، دون أن يُعطى ذلك الوصول صراحةً، وإنما يرثه من الصنف الرئيسي بحكم أسطر الإسناد/الاستدعاء داخل تعريف الصنف Child
:
this.parent = Parent; this.parent();
وبهذا نكون ورثنا basemethod
من الصنف الرئيسي Parent
.
اقتباسالخلاف حول جافاسكربت
لجافاسكربت سمعة سيئة وسط المبرمجين لأسباب كثيرة، أحدها إمكانية إنشاء كائنات دون استخدام الكلمة المفتاحية
new
، ودون إرسال أي خطأ، لذا ستكون النتيجة غير التي نريدها، وسنواجه زلات برمجيةً يصعب إيجادها، أحد أسبابها أن جافاسكربت ليس لديها مفهوم الصنف نفسه، وإنما تحاكيه محاكاةً فقط من خلال آلية النموذج الأولي، وليتغلب مبرمجو جافاسكربت على هذه المشكلة طوروا بعض المصطلحات التي تنتج شيفرات أكثر موثوقيةً، لكن تفسيرها يتطلب بعض المفاهيم المتقدمة من علوم الحاسوب التي يقصر عنها شرحنا، فإذا أردت استخدام جافاسكربت للبرمجة الكائنية التوجه ننصحك بقراءة الكتاب القصير Javascript the Good Parts الذي كتبه Douglas Crockford، والذي يشرح السبب الذي يجعل التقنيات المشروحة أعلاه غير مثالية، وكيفية تنفيذها بشكل أفضل.
يمكن استخدام نفس حيلة التفويض التي استخدمناها في VBScript، كما يلي:
<script type=text/JavaScript> function noReturn(){ this.parent.printIt(); }; function returnValue(){ return this.parent.getAttribute(); }; function newMethod(){ document.write("This is unique to the sub class<BR>"); }; function SubClass(){ this.parent = new MyClass(27); this.aMethodWithNoReturnValue = noReturn; this.aMethodWithReturnValue = returnValue; this.aNewMethod = newMethod; }; var inst, aValue; inst = new SubClass(); // عرِّف الصنف الرئيسي document.write("The sub class value is:<BR>"); inst.aMethodWithNoReturnValue(); aValue = inst.aMethodWithReturnValue(); inst.aNewMethod(); document.write("aValue = " + aValue); </script>
سنرى استخدام الكائنات والأصناف في دراسات الحالات والفصول التالية، ورغم أنه من الصعب أن يرى المبرمج المبتدئ كيف أن هذه البنية المعقدة تسهل كتابة وفهم البرامج، إلا أننا نأمل أن يتضح هذا المفهوم إذا رأيت استخدام الأصناف في البرامج الحقيقية، على أن ذلك ليس له فائدة كبيرة في البرامج الصغيرة، وإنما سيجعلها أعقد وأطول، لكن كلما زاد حجم البرنامج -أكثر من 100 سطر مثلًا-، فستعين الأصناف والكائنات على تنظيمه وتقليل كمية الشيفرات المكتوبة، ومن المهم أن تعلم أنه من الممكن تعلم البرمجة وكتابة برامج دون الحاجة إلى مفهوم البرمجة الكائنية التوجه أصلًا، فكم من مبرمج كتب برامج دون إنشاء صنف واحد طيلة حياته، لكن إذا استطعت استيعابها فإن فيها مزايا وتقنيات قويةً ومفيدةً.
خاتمة
نأمل في نهاية هذا المقال أن تكون تعلمت ما يلي:
- تغلِّف الأصناف البيانات والدوال في كيان واحد.
- تشبه الأصناف قاطعات البسكويت، حيث تُستخدم لإنشاء النسخ أو الكائنات.
- تتواصل الكائنات من خلال إرسال رسائل إلى بعضها البعض.
- عندما يستقبل كائن ما رسالةً فإنه ينفذ تابعًا موافقًا لها.
- التوابع هي دوال تُخزَّن مثل سمات للصنف.
- تستطيع الأصناف أن ترث التوابع والبيانات من أصناف أخرى، مما يسهل توسيع إمكانيات الصنف دون تغيير الصنف الأصلي.
- تعددية الأشكال هي القدرة على إرسال نفس الرسالة إلى عدة أنواع مختلفة من الكائنات، وسيستجيب كل منها بطريقته الخاصة.
- التغليف وتعددية الأشكال والوراثة كلها خصائص للغات البرمجة كائنية التوجه.
- تسمى لغة VBScript لغةً كائنية الأساس، لأنها لا تدعم الوراثة وتعددية الأشكال دعمًا كاملًا، رغم دعمها للتغليف.
ترجمة -بتصرف- للفصل السابع عشر: Object Oriented Programming من كتاب Learning To Program لصاحبه Alan Gauld.
اقرأ أيضًا
- المقال التالي: البرمجة الحدثية Event Driven Programming المساقة بالأحداث
- المقال السابق: التعابير النمطية في البرمجة
- التوابع السحرية (Magic Methods) في PHP
- البرمجة كائنية التوجه (Object Oriented Programming) في PHP
- البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C
- تطبيق البرمجة كائنية التوجه في لغة سي شارب #C - الجزء الثالث
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.