المراقب
^title Observer ^section Design Patterns Revisited
لا يمكنك أن ترمي حجراً نحو حاسوب دون أن تصيب تطبيقاً مبنياً على بنية Model-View-Controller، وأنت خلف ذلك هو نمط المراقب
(Observer). وانتشار المراقب واسع إلى حدّ أن Java وضعته في مكتبتها الأساسية
(java.util.Observer)، ودجنت C# الفكرة في اللغة نفسها (الكلمة المفتاحية
event).
المراقب واحد من أكثر أنماط الرباعية الأصلية استخداماً وشهرةً، لكن عالم تطوير الألعاب مغلق إلى حدّ ما في أحيان كثيرة، لذا قد يكون كل هذا جديداً عليك. في حال لم تخرج من الدير منذ مدة، دعني أمشي بك عبر مثال تحفيزي.
إنجاز مفتوح
لنفترض أننا نضيف نظام إنجازات إلى لعبتنا. وسيضم عشرات الشارات المختلفة التي يمكن للاعبين كسبها عند إتمام محطات بعينها مثل «اقتل 100 شيطان قرود» أو «اسقُط من جسر» أو «أكمل مستوى حاملاً سنجاباً ميتاً لا غير».
تنفيذ هذا بشكل نظيف أمر صعب، لأن لدينا نطاقاً واسعاً جداً من الإنجازات التي تُفتح
بسلوكيات مختلفة تماماً. فإذا لم نكن حذرين، فإن خيوط نظام الإنجازات لدينا ستلتفّ
وتتسلّل عبر كل ركن مظلم في قاعدة شيفرتنا. صحيح أن «اسقُط من جسر» مرتبط بطريقة ما
بـمحرك الفيزياء، لكن هل نريد حقاً أن نرى استدعاءً لـunlockFallOffBridge()
في وسط الجبر الخطي داخل خوارزمية حلّ التصادمات لدينا؟
ومثلما هو الحال دائماً، نريد أن تكون كل الشيفرة المتعلقة بوجه واحد من أوجه اللعبة مجمّعة في مكان واحد. والتحدّي هو أن الإنجازات تُطلقها جوانب مختلفة كثيرة من اللعب. فكيف يمكن أن يعمل ذلك دون ربط شيفرة الإنجاز بكل تلك الجوانب؟
هذا هو ما وُجد نمط المراقب من أجله. فهو يتيح لقطعة شيفرة واحدة أن تُعلن أن شيئاً ثيراً للاهتمام قد حدث دون أن يهتمّ فعلياً بمن يستقبل الإشعار.
فمثلاً، لدينا شيفرة فيزياء تتعامل مع الجاذبية وتتتبّع أي الأجسام تستقرّ على أسطح مستوية لطيفة وأيها يتساقط نحو حتميّة هلاكه. ولتنفيذ شارة «اسقُط من جسر»، يمكننا أن ندفع بشيفرة الإنجاز إلى هناك مباشرةً، لكن ذلك فوضى. وبدلاً من ذلك، يمكننا أن نفعل الآتي:
^code physics-update
كل ما تفعله هو أن تقول: «آه، لا أعرف إن كان أحد سيهتمّ، لكن هذا الشيء سقط للتو. فافعل ما تشاء بذلك.»
يسجّل نظام الإنجازات نفسه، بحيث يستقبل الإشعار كلما أرسلته شيفرة الفيزياء. ثمّ يستطيع أن يتحقّق ممّا إذا كان الجسم الساقط هو بطَلنا الأقل حظاً، وإن كان موضع ارتكازه قبل هذا اللقاء الجديد المؤسف مع الميكانيكا الكلاسيكية هو جسر. وإذا كان كذلك، فإنه يفتح الإنجاز المناسب مع الألعاب النارية والضجيج المصاحب، وكل ذلك من دون أي مشاركة من شيفرة الفيزياء.
والحقيقة، يمكننا تغيير مجموعة الإنجازات أو نزع نظام الإنجازات كله من دون لمس سطر واحد في محرك الفيزياء. فسيظل يرسل إشعاراته، غافلاً عن حقيقة أن لا أحد يستقبلها بعد الآن.
كيف يعمل
إن لم تكن تعرف بالفعل كيف يُنفَّذ هذا النمط، فقد تتمكّن من استنتاجه من الوصف السابق، لكنني سأمشي بك فيه سريعاً تيسيراً لأمرك.
المراقب
سنبدأ بالصنف الفضولي الذي يريد أن يعرف متى يفعل كائن آخر شيئاًثيراً. وتُعرَّف هذه الكائنات الاستفسارية بهذه الواجهة:
^code observer
وأي صنف ملموس ينفّذ هذه الواجهة يصبح مراقباً. وفي مثالنا، هو نظام الإنجازات، فسنحصل على شيء كهذا:
^code achievement-observer
الموضوع
دالة الإشعار يستدعيها الكائن الذي تتم مراقبته. وفي لغة الرباعية، يُسمّى ذلك الكائن «الموضوع» (subject). وله مهمّتان. أولاً، إنه يحتفظ بقائمة المراقبين الذين ينتظرون بصبر شديد رسالة منه:
^code subject-list
الجزء المهم هو أن الموضوع يعرض واجهة برمجية عامة لتعديل تلك القائمة:
^code subject-register
يتيح ذلك للشيفرة الخارجية أن تتحكّم في من يستقبل الإشعارات. فالموضوع يتواصل مع المراقبين، لكنه ليس مربوطاً بهم. وفي مثالنا، لن يذكر أي سطر من شيفرة الفيزياء الإنجازات. ومع ذلك، لا يزال بإمكانه التحدث إلى نظام الإنجازات. وهذا هو الجزء الذكي في هذا النمط.
ومن المهم أيضاً أن يكون للموضوع قائمة من المراقبين بدل مراقب واحد. فهذا يضمن ألّا يرتبط المراقبون ببعضهم ارتباطاً ضمنياً. فمثلاً، لنقل إن محرّك الصوت يراقب أيضاً حدث السقوط كي يشغّل صوتاً مناسباً. فإذا كان الموضوع يدعم مراقباً واحداً فقط، فإن تسجيل محرّك الصوت لنفسه سيُلغي تسجيل نظام الإنجازات. وهذا يعني أن النظامين سيتداخلان مع بعضهما — وبطريقة خبيثة على وجه الخصوص، لأن الثاني سيعطّل الأول. ودعم قائمة من المراقبين يضمن معاملة كل مراقب باستقلال عن الآخرين. فمن وجهة نظر كل منهم، هو الشيء الوحيد في العالم الذي يراقب الموضوع.
والمهمّة الثانية للموضوع هي إرسال الإشعارات:
^code subject-notify
فيزياء قابلة للمراقبة
الآن، نحتاج فقط إلى ربط كل هذا بمحرك الفيزياء كي يتمكّن من إرسال الإشعارات، وليتمكّن
نظام الإنجازات من ربط نفسه لاستقبالها. وسنلتزم قريباً بوصفة أنماط التصميم
الأصلية ونجعل محرك الفيزياء يرث من Subject:
^code physics-inherit
يتيح لنا هذا جعل notify() في Subject محميّة. وبهذا يستطيع صنف محرك الفيزياء
المشتق استدعاءها لإرسال الإشعارات، لكن الشيفرة الخارجية منه لا تستطيع. وفي المقابل،
addObserver() وremoveObserver() عامّتان، فكل ما يستطيع الوصول إلى نظام الفيزياء
يستطيع مراقبته.
والآن، عندما يفعل محرك الفيزياء شيئاً يستحقّ الانتباه، فإنه يستدعي notify() كما في
المثال التحفيزي السابق. وذلك يجوب قائمة المراقبين ويخبرهم جميعاً مقدّماً.
بسيطة جداً، أليس كذلك؟ صنف واحد فقط يحتفظ بقائمة مؤشرات إلى نسخ من واجهة ما. ومن بسيطة جداً، أليس كذلك؟ صنف واحد فقط يحتفظ بقائمة مؤشرات إلى نسخ من واجهة ما. ومن الصعب تصديق أن أمراً بهذه البساطة هو عمود التواصل في آلاف البرامج وأطر التطبيقات.
لكن لنمط المراقب منتقدين أيضاً. فعندما سألت مبرمجي ألعاب آخرين عن رأيهم في هذا النمط، أثاروا بعض الشكاوى. لنرى ما يمكننا فعله لمعالجتها، إن أمكن.
«إنه بطيء جداً»
أسمع هذا كثيراً، وغالباً من مبرمجين لا يعرفون في الواقع تفاصيل النمط. لديهم افتراض افتراضي بأن كل ما يشبه «نمط تصميم» لا بدّ أن يتضمن أكواماً من الأصناف والتوجيهات وطرقاً إبداعية أخرى لتبديد دورات المعالج.
يحصل نمط المراقب على سمعة سيئة تحديداً هنا، لأنه عُرف بأنه يتسكّر مع شخصيات مشبوهة اسمها «الأحداث» و«الرسائل» بل وحتى «ربط البيانات». بعض تلك الأنظمة قد تكون بطيئة (غالباً عن قصد وسبب وجيه). فهي تتضمّن أموراً مثل الوضع في طابور أو إجراء تخصيص ديناميكي لكل إشعار.
لكن، الآن وقد رأيت كيف يُنفَّذ النمط فعلياً، فأنت تعرف أن الأمر ليس كذلك. فإرسال إشعار ليس سوى المرور على قائمة واستدعاء بعض الدوال الافتراضية. صحيح، إنها أبطأ قليلاً من استدعاء يُوجَّه ساكناً، لكن هذه الكلفة مهملة في كل الشيفرة عدا الأكثر حساسية للأداء.
وجدت أن هذا النمط يناسب المسارات غير الحرجة على أي حال، لذا يمكنك عادةً تحمل كلفة التوجيه الديناميكي. عدا ذلك، فلا توجد أي كلفة تُذكر تقريباً. فنحن لا نخصّص كائنات للرسائل. ولا يوجد طابور. كل ما في الأمر هو توجيه إضافي فوق استدعاء دالة متزامن.
أليس سريعاً أكثر من اللازم؟
في الواقع، عليك توخّي الحذر لأن نمط المراقب متزامن. فالموضوع يستدعي مراقبيه مباشرةً، مما يعني أنه لن يستأنف عمله حتى يعود جميع المراقبين من دوال الإشعار لديهم. والمراقب البطيء يستطيع أن يوقف الموضوع.
يبدو هذا مخيفاً، لكن في الواقع العملي لا نهاية للعالم. كل ما عليك هو أن تكون واعياً له. أمّا مبرمجو واجهات المستخدم — الذين ظلّوا ينفّذون البرمجة القائمة على الأحداث هكذا منذ زمن طويل — فلديهم شعار عتيق في هذا الباب: «ابتعد عن خيط واجهة المستخدم».
إن كنت تستجيب لحدث بشكل متزامن، فيجب أن تُنهي وتعيد التحكم بأسرع وقت ممكن كي لا تتجمّد واجهة المستخدم. وحين يكون أمامك عمل بطيء، فادفعه إلى خيط آخر أو إلى طابور عمل.
لكن عليك توخّي الحذر عند مزج المراقبين مع الخيوط والأقفال الصريحة. فإذا حاول مراقب أن يمسك قفلاً يحتفظ به الموضوع، فقد تتجمّد اللعبة إلى الأبد. وفي محرّك شديد التعدّد، قد يكون من الأفضل لك استخدام التواصل غير المتزامن عبر طابور الأحداث (Event Queue).
«إنه يفرغ ذاكرة كثيرة ديناميكياً»
قبائل كاملة من عشيرة المبرمجين — بما في ذلك كثير من مطوّري الألعاب — انتقلت إلى اللغات التي تجمع القمامة تلقائياً، ولم يعد التخصيص الديناميكي هو الشرير الذي كان. لكن في البرمجيات الحرجة للأداء مثل الألعاب، ما زال تخصيص الذاكرة مهمّاً، حتى في اللغات المُدارة. فالتخصيص الديناميكي يستهلك وقتاً، وكذلك استرداد الذاكرة، حتى لو حدث ذلك تلقائياً.
في شيفرة المثال السابقة، استخدمت مصفوفة ثابتة لأنني أحاول إبقاء الأمور بسيطة تماماً. وفي التنفيذات الحقيقية، تكون قائمة المراقبين مجموعة مخصّصة ديناميكياً في الغالب، تنمو وتصغر مع إضافة المراقبين وإزالتهم. وهذا التذبذب في الذاكرة يقلق بعض الناس.
وبالطبع، أول ما تلاحظه هو أنه لا يخصّص ذاكرة إلا عندما يجري ربط المراقبين. فـإرسال إشعار لا يحتاج إلى أي تخصيص للذاكرة على الإطلاق؛ كل ما هو استدعاء دالة. وإن ربطت مراقبيك في بداية اللعبة ولم تعبث بهم كثيراً، فسيكون مقدار التخصيص ضئيلاً.
وإن ظلّ ذلك مشكلة، فسأمشي بك عبر طريقة لتنفيذ إضافة المراقبين وإزالتهم من دون أي تخصيص ديناميكي على الإطلاق.
مراقبون في قائمة مترابطة
في الشيفرة التي رأيناها حتى الآن، يملك Subject قائمة مؤشرات إلى كل Observer يراقبه.
وصنف Observer نفسه لا يملك أي مرجع إلى هذه القائمة. إنه مجرد واجهة افتراضية بحتة.
والواجهات مفضّلة على الأصناف الملموسة ذات الحالة، لذا فهذا أمر جيد عموماً.
لكن إن كنا مستعدين لوضع بعض الحالة في Observer، فيمكننا حلّ مشكلة التخصيص بأن
نمرّر قائمة الموضوع عبر المراقبين أنفسهم. وبدلاً من أن يملك الموضوع مجموعة منفصلة
من المؤشرات، تصير كائنات المراقب عقداً في قائمة مترابطة:
ولتنفيذ ذلك، سنبدأ بالتخلّص من المصفوفة في Subject واستبدالها بمؤشر إلى رأس قائمة
المراقبين:
^code linked-subject
ثمّ نجعل Observer يحمل مؤشراً إلى المراقب التالي في القائمة:
^code linked-observer
ونحن هنا نجعل Subject صنفاً صديقاً أيضاً. فالموضوع يملك الواجهة البرمجية لإضافة
المراقبين وإزالتهم، لكن القائمة التي سيديرها صارت الآن داخل صنف Observer نفسه.
وأبسط طريقة لمنحه القدرة على التلاعب بهذه القائمة هي جعله صديقاً.
وتسجيل مراقب جديد ليس سوى ربطه في القائمة. وسنختار الخيار السهل وندرجه في مقدّمتها:
^code linked-add
والخيار الآخر هو إضافته إلى نهاية القائمة المترابطة. ويفرض ذلك قدراً أكبر من التعقيد.
فعلى Subject إمّا أن يجوب القائمة ليصل إلى النهاية، أو أن يحتفظ بمؤشر tail_
منفصل يشير دائماً إلى العقدة الأخيرة.
أما إضافته إلى مقدّمة القائمة فأبسط، لكن له أثراً جانبياً واحداً. فحين نجوب القائمة لإرسال إشعار إلى كل مراقب، يستقبل المراقب المسجَّل حديثاً الإشعار أولاً. فإذا سجّلت المراقبين A وB وC بهذا الترتيب، فسيستقبلون الإشعارات بترتيب C ثم B ثم A.
نظرياً، لا يهمّ هذا الأمر في اتجاه أو آخر. فمن قواعد الانضباط الجيد في المراقبة أن يكون المراقبان اللذان يراقبان الموضوع نفسه بلا اعتماديات في الترتيب بينهما. وإذا كان الترتيب مهمّاً فعلاً، فهذا يعني أن بين المراقبَين ربطاً خفياً قد ينقض ظهرك لاحقاً.
لنجعل الإزالة تعمل:
^code linked-remove
ولأن لدينا قائمة مترابطة باتجاه واحد، علينا أن نجوبها لنجد المراقب الذي نزاله. ولو كنا نستخدم مصفوفة عادية لحصلنا على الشيء ذاته. وإن استخدمنا قائمة مترابطة باتجاهين، حيث يملك كل مراقب مؤشراً إلى المراقب الذي يليه وآخر يسبقه، يمكننا إزالة مراقب في زمن ثابت. ولو كانت هذه شيفرة حقيقية لفعلت ذلك.
والشيء الوحيد المتبقّي هو إرسال إشعار. وهو بسيط تماماً كالمرور على القائمة:
^code linked-notify
ليس سيئاً إلى هذا الحد، أليس كذلك؟ يمكن أن يكون للموضوع أي عدد من المراقبين يشاء، دون ذرّة ذاكرة ديناميكية واحدة. والتسجيل وإلغاء التسجيل سريعان كما كانا مع مصفوفة بسيطة. لكننا ضحّينا بميزة صغيرة.
ولأننا نستخدم كائن المراقب نفسه كعقدة قائمة، فإن ذلك يعني أنه لا يمكن أن يكون جزءاً من قائمة مراقبين لموضوع واحد أكثر من ذلك. بعبارة أخرى، لا يستطيع المراقب أن يراقب أكثر من موضوع واحد في الوقت ذاته. أما في التنفيذ التقليدي أكثر، حيث يملك كل موضوع قائمته المستقلة، فيمكن للمراقب أن يكون في أكثر من قائمة في آن واحد.
قد تستطيع التعايش مع هذا القيد. فأجد أن الأمر أكثر شيوعاً أن يكون للموضوع مراقبون متعدّدون لا العكس. وإذا كان الأمر مشكلة عندك، فهناك حلّ آخر أكثر تعقيداً يمكنك استخدامه، ولا يزال بلا حاجة إلى تخصيص ديناميكي. فهو طويل جداً ليدمج في هذا الفصل، لكنني سأرسمه وأترك لك الفجوات...
مجموعة من عقد القائمة
وكما في السابق، سيكون لكل موضوع قائمة مترابطة من المراقبين. لكن عقد تلك القائمة لن تكون كائنات المراقب نفسها. بدلاً من ذلك، ستكون كائنات «عقدة قائمة» صغيرة منفصلة تحتوي مؤشراً إلى المراقب ثم مؤشراً إلى العقدة التالية في القائمة.
ولأن عدة عقد تستطيع كلّها أن تشير إلى المراقب نفسه، فهذا يعني أن المراقب يمكن أن يكون في قوائم أكثر من موضوع في الوقت نفسه. فعُدنا إلى القدرة على مراقبة عدة مواضيع في آن واحد.
وطريقة تجنّب التخصيص الديناميكي بسيطة: بما أن كل تلك العقد لها الحجم والنوع نفسيهما، فأنت تخصّص مسبقاً مجموعة كائنات (Object Pool) منها. وهذا يمنحك كومة ثابتة الحجم من عقد القائمة تعمل بها، وتستطيع استخدامها وإعادة استخدامها حسب الحاجة دون أن تضطر إلى مخصّص ذاكرة حقيقي.
المشكلات المتبقية
أعتقد أننا طردنا الشرور الثلاثة التي كانت تُستخدم لإخافة الناس عن هذا النمط. وكما رأينا، فهو بسيط وسريع، ويمكن جعله يتعامل بلطف مع إدارة الذاكرة. لكن هل يعني ذلك أنك ينبغي أن تستخدم المراقبين في كل وقت؟
الآن، هذا سؤال مختلف. فمثل كل أنماط التصميم، ليس نمط المراقب علاجاً لكل شيء. وحتى حين يُنفَّذ بشكل صحيح وكفء، فقد لا يكون الحلّ المناسب. والسبب في أن أنماط التصميم تنال سمعة سيئة هو أن الناس يطبّقون أنماطاً جيدة على مشكلات خاطئة فينتهي الأمر إلى أن يسوءوا الأمور.
بقي تحدّيان، أحدهما تقني والآخر أقرب إلى مستوى قابلية الصيانة. وسنتناول التقني أولاً لأنهما عادةً الأسهل.
إتلاف الموضوعات والمراقبين
شيفرة المثال التي مشينا عبرها متينة، لكنها تتجاوز مسألة
مهمة: ماذا يحدث حين تحذف موضوعاً أو مراقباً؟ فإن استدعيت delete على مراقب دون انتباه،
فقد يكون الموضوع ما زال يملك مؤشراً إليه. وهذا الآن مؤشر معلّق إلى ذاكرة محرَّرة. وحين
يحاول ذلك الموضوع إرسال إشعار، حسناً... لن يكون وقتك ممتعاً.
أما إتلاف الموضوع فأسهل، لأن المراقب لا يملك في معظم التنفيذات أي مراجع إليه. لكن حتى في هذه الحالة، قد يسبّب إرسال أجزاء الموضوع إلى سلة إعادة التدوير لدى مدير الذاكرة بعض المشاكل. فقد يظلّ هؤلاء المراقبون يتوقّعون استلام إشعارات في المستقبل، ولا يعرفون أن ذلك لن يحدث أبداً الآن. وهم ليسوا مراقبين في الحقيقة، إنما يظنّون أنهم كذلك.
يمكنك التعامل مع هذا بطرق مختلفة. وأبسطها ما فعلته أنا: التأجيل. فمن مهمة المراقب أن
يُلغي تسجيل نفسه من أي موضوعات عندما يُحذف. وفي أغلب الأحيان، يعرف المراقب أيّ
الموضوعات يراقب، لذا فالأمر عادةً مجرّد إضافة استدعاء
removeObserver() إلى مُتلِفه.
وإن لم ترغب في ترك المراقبين معلّقين حين يموت الموضوع، فالإصلاح سهل. فقط اجعل الموضوع يرسل إشعار «أنفاسه الأخيرة» الأخير قبل أن يُتلف. وبهذه الطريقة، يستطيع أي مراقب أن يستقبله ويتخذ أي إجراء يراه مناسباً.
الناس — حتى أولئك الذين أمضوا وقتاً طويلاً في صحبة الآلات فتخمّلت منهم بعض طبيعتها الدقيقة — سيئون دائماً في الالتزام. لهذا اخترعنا الحواسيب: فهي لا ترتكب الأخطاء التي نرتكبها نحن في أغلب الأحيان.
أما الجواب الأأمن فهو أن تجعل المراقبين يُلغون تسجيلهم تلقائياً من كل موضوع عندما يُتلفون. وإن نفّذت منطق ذلك مرة واحدة في صنف المراقب الأساسي، فلا على كل من يستخدمه أن يتذكّر أن يفعله بنفسه. لكن هذا يضيف بعض التعقيد. فهو يعني أن كل مراقب سيحتاج إلى قائمة الموضوعات التي يراقبها. وهكذا تنتهي بك إلى مؤشرات في الاتجاهين.
لا تقلق، لديّ جامع قمامة
أنتم الشباب الرائعون بلغاتكم العصرية التي فيها جامعات القمامة، تشعرون الآن بأنكم في منتهى الرضا. تظنّون أنكم لا تحتاجون للقلق من هذا لأنكم لا تحذفون أي شيء صريحاً؟ فكّروا من جديد!
تخيّل هذا: لديك شاشة واجهة تُظهر مجموعة من الإحصاءات عن شخصية اللاعب مثل صحّته وغيرها. حين يفتح اللاعب تلك الشاشة، أنت تنشئ كائناً جديداً من أجلها. وحين يغلقها، تنسى الكائن فحسب وتترك جامع القمامة ينظّفه.
في كل مرة تتلقّى فيها الشخصية ضربة على الوجه (أو في مكان آخر، كما أفترض)، ترسل إشعاراً. وشاشة الواجهة تراقب ذلك وتحدّث شريط الصحّة الصغير. رائع. والآن، ماذا يحدث حين يغلق اللاعب الشاشة، لكنك لا تُلغي تسجيل المراقب؟
فواجهة المستخدم لم تعد مرئية، لكنها لن تُجمع من القمامة ما دامت قائمة مراقبي الشخصية ما زالت تحوي مرجعاً إليها. وفي كل مرة تُحمَّل فيها الشاشة، نضيف نسخة جديدة منها إلى تلك القائمة التي تطول بلا توقف.
طوال الوقت الذي يقضيه اللاعب في اللعب والركض والدخول في المعارك، ترسل الشخصية إشعارات تستقبلها كل تلك الشاشات. فهي ليست على الشاشة، لكنها تستقبل الإشعارات وتبدّد دورات المعالج في تحديث عناصر واجهة غير مرئية. وإن فعلت أشياء أخرى مثل تشغيل أصوات، فسنحصل على سلوك خاطئ ملحوظ.
هذه مشكلة شائعة إلى هذا الحدّ في أنظمة الإشعارات حتى أنها لها اسم: مشكلة المستمع المنسي (lapsed listener problem). ولأن الموضوعات تحتفظ بمراجع إلى مستمعيها، فقد ينتهي بك إلى كائنات واجهة زومبية تظل قائمة في الذاكرة. والدرس هنا هو أن تكون منضبطاً في إلغاء التسجيل.
ماذا يجري هنا؟
المسألة الأخرى والأعمق مع نمط المراقب هي نتيجة مباشرة لغرضه المقصود. فنحن نستخدمه لأنه يساعدنا على فكّ الترابط بين قطعتين من الشيفرة. فهو يتيح لموضوع أن يتواصل بشكل غير مباشر مع مراقب ما دون أن يكون مربوطاً به ربطاً ساكناً.
وهذا مكسب حقيقي حين تحاول التفكير في سلوك الموضوع، فأي متدخّل آخر سيكون تشتيتاً مزعجاً. فإن كنت تتلاعب بمحرك الفيزياء، فلن ترغب حقاً في أن يزدحم محرّكك — أو ذهنك — بشيء عن الإنجازات.
وفي المقابل، إن كان برنامجك لا يعمل وكان الخلل ممتداً عبر سلسلة من المراقبين، فالتفكير في تدفّق التواصل هذا يصبح أصعب بكثير. ومع الربط الصريح، صار الأمر بمجرد النظر إلى الدالة التي تُستدعى. وهذا من متناول أي بيئة تطوير متوسّط، لأن الربط ساكن.
لكن إن حدث هذا الربط عبر قائمة مراقبين، فالطريقة الوحيدة لمعرفة من سيُشعر هي أن ترى أي المراقبين يوجد في تلك القائمة وقت التشغيل. وبدلاً من أن تستطيع التفكير ساكناً في بنية التواصل في البرنامج، عليك التفكير في سلوكه الأمري المتغيّر في وقت التنفيذ.
وإرشادي في التعامل مع هذا بسيط جداً. فإذا كنت تحتاج غالباً إلى التفكير في جانبَي بعض التواصل لفهم جزء من البرنامج، فلا تستخدم نمط المراقب التعبير عن ذلك الربط. فضّل شيئاً أكثر وضوحاً.
وحين تشتغل على برنامج كبير، فأنت تميل إلى أن تكون لديك كتل منه تعمل عليها معاً. ولدينا مصطلحات كثيرة لذلك مثل «فصل المسؤوليات» و«التماسك والترابط» و«التنظيم المعياري»، لكنها تختزل في عبارة: «هذا الشيء يجتمع مع بعضه ولا يجتمع مع ذاك الشيء».
ونمط المراقب طريقة رائعة لتلك الكتل التي لا علاقة بينها في الغالب كي تتحدث مع بعضها دون أن تندمج في كتلة واحدة كبيرة. وهو أقلّ نفعاً داخل كتلة شيفرة واحدة مخصّصة لميزة أو وجه واحد.
ولهذا يناسب مثالنا جيداً: فالت إنجازات والفيزياء مجالان لا علاقة بينهما تقريباً، ويُرجَّح أن ينفّذهما أشخاص مختلفون. ونريد أقلّ قدر ممكن من التواصل بينهما، بحيث لا يتطلّب العمل على أحدهما معرفة كبيرة بالآخر.
المراقبون اليوم
صدر أنماط التصميم عام 1994. وفي ذلك الوقت، كانت البرمجة الكائنية هي النموذج الساخن. وكل مبرمج على الأرض يريد أن «يتعلّم البرمجة الكائنية في 30 يوماً»، وكان المديرون في المنتصف يدفعون لهم بناءً على عدد الأصناف التي ينشئونها. وكان المهندسون يقيسون مكانتهم بعمق التسلسلات الوراثية التي يكتبونها.
ونمط المراقب أصبح شائعاً في تلك الحقبة، لذا فلا غرابة أنه معني بالأصناف. لكن المبرمجين في التيار الرئيسي مرتاحون الآن أكثر للبرمجة الوظيفية. فتنفيذ واجهة كاملة لمجرد استلام إشعار لا يناسب الذوق الحديث.
ويبدو ثقيلاً جامداً. وهو فعلاً ثقيل جامد. فمثلاً، لا يمكنك أن يكون لديك صنف واحد يستخدم دوال إشعار مختلفة لمواضيع مختلفة.
أما المقاربة الأحدث، فأن يكون «المراقب» مجرّد مرجع إلى دالة أو دالة. وفي اللغات ذات الدوال من الطبقة الأولى، وخاصة ذات الإغلاقات، فهذه طريقة أكثر شيوعاً لتنفيذ المراقبين.
فمثلاً، في C# «الأحداث» جزء من اللغة نفسها. ومعها، يكون المراقب الذي تسجّله «مندوباً»
(delegate)، وهي تسمية اللغة للمرجع إلى دالة. وفي نظام الأحداث في JavaScript، يستطيع
المراقبون أن يكونوا كائنات تدعم بروتوكول EventListener خاصاً، لكنهم يستطيعون أيضاً
أن يكونوا مجرّد دوال. وهذا الأخير هو ما يستخدمه الناس في الغالبية العظمى من الأحوال.
ولو كنت أصمّم نظام مراقبة اليوم، لجعلته قائماً على الدوال لا على الأصناف. وحتى في C++، سأميل إلى نظام
يتيح لك تسجيل مؤشرات الدوال الأعضاء كمراقبين بدلاً من نسخ من واجهة Observer ما.
المراقبون غداً
أنظمة الأحداث والأنماط الأخرى الشبيهة بالمراقبة شائعة إلى حدّ لا يُصدّق هذه الأيام. فهي طريق مُستهلك كثيراً. لكن إن كتبت بضعة تطبيقات كبيرة باستخدامها، فستبدأ بملاحظة شيء. فكثير من شيفرة مراقبيك تنتهي بأن يبدو متطابقة. وهي عادةً شيء كهذا:
-
تستلم إشعاراً بأن بعض الحالة تغيّرت.
-
تعدّل بأسلوب الأمر جزءاً من واجهة المستخدم ليعكس الحالة الجديدة.
كل شيء هنا هو «آه، صحّة البطل صارت 7 الآن؟ دعيني أضبط عرض شريط الصحّة على 70 بكسل». وبعد فترة، يصبح ذلك مرهقاً إلى حدّ كبير.
ولقد حاول علماء الحاسوب ومهندسو البرمجيات للتخلص من هذا الرتابة مدة طويلة جداً. وقد حملت محاولاتهم أسماء مختلفة كثيرة: «برمجة التدفّق» و«البرمجة التفاعلية الوظيفية» وغيرها.
وبينما كانت هناك بعض النجاحات، عادةً في مجالات محدودة مثل معالجة الصوت أو تصميم الرقاقات، فإن الكأس المقدسة لم تُكتشف بعد. وفي هذه الأثناء، بدأ منهج أقلّ طموحاً في اكتساب أرض.
وكثير من أطر التطبيقات الحديثة تستخدم الآن «ربط البيانات». فعلى خلاف النماذج الأكثر تطرّفاً، لا يحاول ربط البيانات إلغاء الشيفرة الأمرية تماماً، ولا يحاول أن يبني تطبيقك بالكامل حول رسم بياني ضخم لتدفّق بيانات وصفي. وكل ما يفعله هو أتمتة الأعمال الروتينية حين تعدّل عنصر واجهة أو خاصية محسوبة لتعكس تغيّراً في قيمة ما.
وكما في الأنظمة الوصفية الأخرى، من المرجّح أن يكون ربط البيانات أبطأ قليلاً وأكثر تعقيداً من أن يناسب جوهر محرّك الألعاب.
لكنني سأتفاجأ لو لم أرَه يبدأ في التسلّل إلى المناطق الأقلّ أهمية في اللعبة مثل واجهة المستخدم.
وفي هذه الأثناء، سيبقى نمط المراقب القديم الجيد هنا في انتظارنا. صحيح، إنه ليس مثيراً بقدر بعض التقنيات الساخنة التي تدّعي أنها تجمع بين «وظيفي» و«تفاعلي» في اسمها، لكنه بسيط تماماً ويعمل. ومن وجهة نظري، غالباً ما يكون هذان المعياران هما الأهم في أي حلّ.