الخميس، 28 مارس 2013

طرقٌ مختلفةٌ لدعم الأحداث events و مُتعهِّداتها events handlers

في لغات البرمجة هناك مفهومان هامان للغاية هما: الأحداث events و مُتعهِّدات الأحداث events handlers؛ و يُقصَد بالحدث "وقوع أمرٍ ما يحتاج لاتخاذ رد فعلٍ مناسبٍ تجاهه"، و مُتعهِّد الحدث هو "رد الفعل الذي يرغب المبرمج في تنفيذه عند وقوع حدثٍ معين"، 
و تزداد أهمية الأحداث و مُتعهِّداتها أضعافاً مُضاعفةً حينما يأتي ذِكر برمجة الواجهات الرسومية graphical user interfaces لما تلعبانه من دورٍ مِحوريٍ في تسهيل بنائها؛ فالحاجة إليهن ماسَّةٌ نظراً لأن الفكرة الرئيسة من وراء الواجهات الرسومية هي القدرة علي اتخاذ ردود أفعالٍ معينةٍ عند حدوث شيءٍ معين، مثل عرض رسالةٍ حينما يضغط المستخدم علي زرٍ معين، أو إغلاق البرنامج حينما يضغط المستخدم علي زر x الموجود في الواجهة الرئيسة لذلك البرنامج.

و عملياً فإن هناك طرقٌ عديدةٌ لبناء مفهومَيْ الأحداث و مُتعهِّداتها في لغات البرمجة، و هنا سأقدم مجموعةً صغيرةً للغاية منهن، ثم سأتحدث عن الطريقة التي تم بها الاستغناء عنهن في إبداع بشكلٍ بسيطٍ جداً و قوي للغاية. 

إلا أنه ينبغي التنبه إلي كون الشرح الموجود هنا مُختصَرٌ بشكلٍ كبير، و يحتاج كذلك لمعرفةٍ جيدةٍ ببعض قواعد لغات الـ: java و #C و visual basic .NET و إبداع. و ربما يتسبب هذا في غموض بعض النقاط و الحاجة إلي القراءة المستفيضة فيهن حتي يتم استيعابهن. و لم أستطع الإسهاب في الشرح حتي لا يزداد حجم المقال إلي ما لا يُطاق.

أسلوب الـjava:

في الـjava ليس هناك مُكوِّنٌ قائمٌ بذاته يُمثِّل الأحداث أو المُتعهِّدات، بل إن الـjava تستخدم آليات الأصناف classes و الوراثة inheritance لمُحاكاة طريقة عمل الأحداث و المُتعهِّدات. 
فلنفترض أنك تريد عمل مُتعهِّداتٍ لأحداث الفأرة mouse بحيث يحدث رد فعلٍ معينٍ عند نقر الزر الأيمن أو الزر الأيسر أو غيرهن من الأحداث الأخري، هنا يجب عليك في البداية أن تقوم بعمل صنفٍ class يبني واجهةً interface خاصةً بأحداث الفأرة هي الواجهة (MouseListener)، و  التي تحتوي علي مجموعةٍ من الدوال تختص كل واحدةٍ منهن بتعهد حدثٍ معينٍ من أحداث الفأرة، و بالطبع سيتعين عليك أن تقوم ببناء implement كل تلك الدوال (حتي و إن لم ترغب في تعهُّد كل أحداث الفأرة !).
مثال:
الصنف my_mouse_events_handler هو الذي سنبني فيه كل أكواد معالَجة الأحداث المُختلِفة للفأرة:


public class my_mouse_events_handler  implements MouseListener {
    public void mouseClicked(MouseEvent e) {
        // code to handle mouse clicking
    }
    public void mousePressed(MouseEvent e) {
        // code to handle mouse pressing
    }
    public void mouseReleased(MouseEvent e) {
        // code to handle mouse releasing after being pressed
    }
    public void mouseEntered(MouseEvent e) {
        // code to handle having the  mouse entering a specific area
    }
    public void mouseExited(MouseEvent e) {
        // code to handle having the  mouse exiting the area it was in
    }
}

و حينما نرغب في تعهُّد أحداث الفأرة في مُكوِّنٍ معينٍ (مثلاً:
my_component) باستخدام الدوال الموجودة في الصنف my_mouse_events_handler سنقوم بفعل التالي:

my_mouse_events_handler handler = new my_mouse_events_handler();
my_component.addMouseListener(
handler);

و هكذا حينما يقع أي حدثٍ من أحداث الفأرة للمُكوِن my_component سيتم استدعاء الدالة التي تعمل كمُتعهِّدٍ للحدث في كائن الصنف الذي سجَّلناه كمُستمِعٍ للحدث (أقصد الكائن handler)، و بالتالي ستقوم دالة الحدث باستدعاء البناء implementation الذي كتبناه للدالة المُتعهِّدة.

لكن هناك حلٌ لمشكلة "وجوب بناء كل الدوال في الواجهة (MouseListener)"، و هذا يكون عن طريق الاستغناء عن استخدام الواجهة (MouseListener) نفسها، و القيام باستخدام الصنف المُجرَّد (MouseAdapter) بدلاً منها؛ فهذا الصنف يبني الواجهة نفسها و يحتوي علي دوالٍ فارغةٍ تبني كل الدوال الموجودة فيها، و حينما نرغب نحن في تعهُّد حدثٍ معينٍ للفأرة نقوم بعمل صنفٍ جديدٍ يرث الصنف (MouseAdapter) و يُغطِّي override الدالة التي نرغب في بنائها بأنفسنا، أما بقية الدوال فقد كفانا الصنف (MouseAdapter) مؤونة بنائها منذ البداية.

المثال السابق بعد التعديل و استخدام الصنف MouseAdapter :

public class my_mouse_events_handler  extends MouseAdapter {
    // code to handle only mouse clicking
    public void mouseClicked(MouseEvent e) {
        // implementation
    }
}

ثم كما فعلنا المرة السابقة:

my_mouse_events_handler handler = new my_mouse_events_handler();
my_component.addMouseListener(
handler);

و هذا هو أسلوب الـjava مع كل الأحداث المختلفة و ليس فقط الأحداث الخاصة بالفأرة، أي أنه في الـjava لكي تقوم بتعهُّد أحداثٍ معينةٍ فيجب أن تقوم بالخطوات التالية:
  • عمل صنفٍ معينٍ إما: يبني الواجهة الخاصة بالأحداث المرغوب في تعهُّدها (مثل واجهة الـMouseListener في المثال السابق)، أو يرث الصنف الـadapter الخاص بتلك الواجهة (مثل الـMouseAdapter في ذات المثال)،
  • إضافة كائنٍ من الصنف الجديد إلي المُكوِّن الذي نرغب في تعهد الأحداث لصالحه.

أسلوب الـ#C:
الأمر في لغة الـ#C ليس أقل تعقيداً من الـjava، و إن كان تعقيداً من نوعٍ آخر؛ فلكي يتم عمل حدثٍ جديدٍ و عمل مُتعهِّداتٍ له فيجب علينا أن نقوم بالخطوات التالية:
- تعريف وسيط delegate لكي نستطيع استخدامه في تحديد الدوال التي سنستخدمها كمُتعهِّدات أحداث (يجب أن تكون مُلِمَّاً بمفهوم الوسيط حتي تستوعب علاقته بالموضوع):
// create the delegate that will determine types of parameters of the event
delegate <return_type> <delegate_name>(<parameters_types>);

مثال:
delegate void my_event_handler(object sender, EventArgs e);

- يتم تعريف الحدث ككائنٍ قائمٍ بذاته (مثل المتغيرات بالضبط)، و يتم ربطه بالوسيط الذي عرَّفناه من قبل:
// define the event and attach it to the delegate
event <delegate_name> <event_name>;
مثال:
event my_event_handler my_event;
- ثم نضم الدوال التي ستعمل كمُتعهِّداتٍ للحدث (مع ملاحظة أنه يجب أن تكون مُعامِلات الدوال التي ستتعهد الحدث مشابهةً  في النوع و الترتيب لمُعامِلات ذلك الحدث):
// Add function(s) to the event; to serve as event handler(s)
<object_name>.<event_name> += new <delegate_name>(<function_name>);

مثال:
my_object.my_event += new my_event_handler(my_function);
مع الانتباه إلي أن الدالة my_function لابد و أن يكون لها نوع الخرج (void)، و أن تكون مُعامِلاتها كما يلي (object sender, EventArgs e).
- الآن يمكننا أن نُطلِق الحدث (أي أن نخبر الجميع أن الحدث قد وقع، و بالتالي يتم تنفيذ مُتعهِّداته):
// firing the event
<event_name>(<values passed to event>);
مثال:
my_event(this, e);

و هنا سيتم استدعاء كل الدوال المُسجَّلة في الوسيط my_event_handler الخاص بالحدث my_event، و في هذا المثال هي فقط الدالة my_function، و سيتم تمرير القِيم (this, e) لمُعامِلات تلك الدالة لكي تعمل عليهن.

أسلوب الـvisual Basic .NET:
و قد جعلتُه الأخير في الترتيب لأنه يُقدِّم دليلاً مادياً علي نظرتي للغة الـvisual Basic .NET علي أنها لغةٌ متضخمةٌ ضخامةً سرطانية لا لزوم لها !
ففي visual Basic .NET هناك أسلوبان لتعهد الأحداث و ليس أسلوباً واحداً، و تفصيل ذلك كما يلي:
- في البداية يجب عليك تعريف الحدث بالشكل:

Event <name> (<parameters_types>)

مثال:

Event my_event(int)
ثم بعد ذلك يمكننا استخدام أحد أسلوبَيْ تعهد الأحداث:
- الأسلوب الأول لتعهُّد الحدث:
استخدام الصياغة:
Dim WithEvents <object_name> As New <class_name>(<values for constructor calling>)
Sub <sub_name>(<same parameters>) Handles <object_name>.<event_name>

' body of the sub
End Sub

مثال:

Dim WithEvents my_object As New my_class(4, 10)
Sub my_sub(Dim x As int) Handles my_object.my_event
' body of the sub
End Sub
و هذا معناه أنه عند وقوع الحدث my_event في الكائن my_object فإنه سيتم استدعاء الإجراء my_sub علي أنه المُتعهِّد لذلك الحدث. لكن مع الانتباه إلي أنه:
  • يُشترَط في الكائن الذي سنستخدم معه صفة WithEvents (مثل my_object في المثال السابق) ألا يكون كائناً محلياً local، بل يجب أن يكون علي مستوي النموذج module أو الصنف أو أن يكون له مستوي الوصول عام global، و ألا يكون مصفوفةً بل كائناً مفرداً.
  • يمكن للإجراء الواحد تعهُّد أكثر من حدثٍ واحد، مثل:

    Sub my_sub(Dim x As int) Handles my_object.my_event, another_object.another_event

    و لكن يجب أن تكون مُعامِلات الأحداث متطابقةً مع بعضها البعض، و في كل الأحوال يجب أن تُطابِق مُعامِلاتُ الأحداث مُعاملات الإجراء المُتعهِّد (أي تكون كلها متماثلة الأنواع و الترتيب).
  • لو وضعنا NoThing في my_object

    my_object  =  NoThing

    فسيتوقف تعهُّد my_sub للحدث my_event حتي و إن كان هناك اسمٌ آخرٌ لنفس الكائن !

- الأسلوب الثاني لتعهُّد الحدث:
و فيه نقوم بربط إجراءٍ معينٍ بالحدث في زمن التشغيل، و يمكن فك الارتباط بشكلٍ سهل:

Dim <object_name> As New <class_name>(<values for constructor calling>)
' to add a handler 
AddHandler <object_name>.<event_name>, AddressOf <sub_name>
' to remove a handler
 RemoveHandler <object_name>.<event_name>, AddressOf <sub_name>
مثال:

Dim my_object As New my_class(4, 10)
AddHandler my_object.my_event, AddressOf my_sub
RemoveHandler my_object.my_event, AddressOf my_sub
و يمكن استخدام هذه الطريقة مع المصفوفات كما يلي:

For <counter_name> = 0 To UBound(<array_name>)
    ' initialize that element
    <array_name>(<counter_name>) = new <class_name>(<parameters>)
    ' add a handler to the element
    AddHandler <array_name>(<counter_name>).<event_name>, AddressOf <sub_name>
    ' remove a handler
    RemoveHandler <array_name>(<counter_name>).<event_name>, AddressOf <sub_name>
Next
مثال:

For my_counter = 0 To UBound(my_array)
    my_array(my_counter) = new my_class(4, 10)
    AddHandler my_array(my_counter).my_event, AddressOf my_sub
    RemoveHandler my_array(my_counter).my_event, AddressOf my_sub
Next


مما فات نري أنه من الممكن دعم الأحداث و مُتعهِّداتها عن طريق المُكوِّنات الموجودة فعلياً في اللغة، مثل حالة الـjava التي تم فيها الدعم عن طريق الواجهات interfaces و الأصناف classes و تعدد الأوجه polymorphism.

أو يمكننا الدعم عن طريق عمل مكوناتٍ جديدةٍ خاصةٍ بالأحداث و طريقة تعهُّدها، مثل حالة الـ#C التي تم دعم الأحداث فيها عن طريق عمل مكونٍ خاصٍ هو الـ"event"، و تم دعم التعهُّد عن طريق مكونٍ خاصٍ هو الـ"delegate".
أما في حالة الـvisual basic.NET فقد تم دعم الأحداث نفسها كمُكوِّنٍ منفصل (نفس أسلوب الـ#C)، أما طريقة التعهُّد فليست إلا قاعدةً من ضمن قواعد اللغة يتم فيها ربط مكونٍ موجودٍ بالفعل (sub) بالحدث.

و الآن يمكنكم معرفة مدي التعقيد الذي لا لازمة له في الطرق السابقة لدعم الأحداث و المُتعهِّدات حينما نقارنها بطريقة الدعم في إبداع؛ ففي إبداع ليس هناك مُكوِّنٌ خاصٌ يُسمَّي "حدث"؛ لأن الحدث في الأصل كما قلنا ليس إلا "وقوع أمرٍ ما يجب علي المبرمج اتخاذ رد فعلٍ تجاهه" و هذا مشابهٌ لتعريف الإجراء الحر؛ لأن الإجراء الحر هو "حدثٌ أو مجموعةٌ من الأحداث التي يمكن اتخاذ رد فعلٍ تجاهها عن طريق إضافة نداءٍ للإجراء الحر". 
و مُعالجة الحدث تكون باستخدام النداءات التي يمكن إضافتها في زمن التشغيل لذلك الإجراء الحر؛ حيث أن مُتعهِّد الحدث كما قلنا أيضاً ليس إلا "مجموعةً من الأوامر التي يتم تنفيذها عند حدوث أمرٍ ما"، و في إبداع يكون بإمكانك ربط أي إجراء (مهما كان نوعه) بالإجراء الحر الذي يمثل تنبيهاً لوقوع الحدث.

مثال:


تجربة.الأحداث  الأحداث =  تجربة.الأحداث()
\ الربط بين المتعهد و الحدث /
في الأحداث_الحدث  أضيف  المتعهد

\ إطلاق الحدث /

الأحداث_الحدث(15)



إجراء المتعهد (رقم س):
    \ أوامر الإجراء /

صنف  تجربة.الأحداث:
    إجراء حر  الحدث
(رقم س):   
        \ مجموعة من الأوامر الثابتة /

و في هذا المثال قمنا بعمل حدثٍ يُسمَّي "الحدث" داخل الصنف "تجربة.الأحداث"، و حينما أردنا عمل مُتعهِّدٍ لنسخة الحدث الخاصة بالكائن "الأحداث" تم ذلك بمنتهي البساطة بوضع نداءٍ للإجراء "المتعهد" في الأحداث_الحدث حتي يتم استدعاؤه عند إعلان وقوع ذلك الحدث، 
 هكذا فقط بدون تعقيدٍ أو تضخيمٍ للقواعد و الأكواد.

أضف إلي هذا أن التحكم في الإجرءات الحرة أسهل و أكثر مرونةً من التحكم في متعهدات الأحداث العادية في اللغات الأخري؛ ففي إبداع يمكنك:
  •  إضافة نداء لإجراءٍ بحيث يكون للإجراء المُنادَي نفس المُعامِلات التي تم تمريرها للإجراء الحر عند استدعائه، و هذا هو الأمر الوحيد الذي تستطيع اللغات الأخري فعله فيما أعلم، و هو ما فعلناه في المثال السابق،
  •  إضافة نداء لإجراءٍ بحيث تُمرَّر للإجراء المُنادَي قِيم مُعامِلاتٍ تختلف عن تلك التي تم تمريرها للإجراء الحر عند استدعائه،
    مثال:
    في
    الأحداث_الحدث  أضيف(المتعهد(10)
    )
    و هنا سيتم دائماً استدعاء الإجراء "المتعهد" بقيمة 10 كقيمةٍ لمُعامِله الوحيد.
  •  حذف آخر نداءٍ لإجراءٍ معينٍ من قائمة النداء،
    مثال:

    في
    الأحداث_الحدث  أحذف.آخر.نداء(المتعهد
    )
  •  حذف كل النداءات الموجودة لإجراءٍ معينٍ من قائمة النداء،مثال:
    في
    الأحداث_الحدث  أحذف.كل.نداء(المتعهد
    )
  •  تفريغ قائمة النداءات كلها.مثال:
    في
    الأحداث_الحدث  أحذف.الكل()

هناك تعليقان (2):

  1. بسم الله الرحمن الرحيم
    جزاك الله خيرا، على هذه الكنوز الثمينة. بارك الله فيكم وفي ذريتكم، ورفع درجتكم في الأولى والآخرة، وألحقنا بكم إنه على كل شيء قدير.

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

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

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

    لن نتطور أبدا بثقافات أعدائنا مهما حاولنا، مثل تطورنا بلغتنا.

    ولذلك كنت أنادي منذ أمد بتطوير لغات برمجة عربية 100%، والتسهيل على طلاب العلم، والتركيز على العربية في جميع أمورنا.

    نعم نتعلم اللغات الغربية، ولكن لا يمنعنا ذلك من التركيز على تعريب كل شيء لتقدم شعوبنا الإسلامية... ويجب على الحكومات العربية دعم مشاريع ترجمة العلوم والتقنية، بدلا من دعم لاعبي الكرة والراقصات.

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

    وصلى الله وسلم على رسوله محمد وعلى آله وأصحابه
    والحمد لله رب العالمين.

    ردحذف
    الردود
    1. جزاك الله خيراً و بارك فيك :)

      حقاً و صدقاً، و أعلم عن تجربةٍ صدق ما تقول و لا حول و لا قوة إلا بالله.

      حذف