المكوّن
^title المكوّن ^section أنماط فصل الترابط
النية
اسمح لكيان واحد بامتداد عبر مجالات متعدّدة دون ربط تلك المجالات ببعضها.
الدافع
لنفترض أننا نبني لعبة منصّات (platformer). أمّا شريحة لاعبين الأنابيب هذه فقد غطّيناها، سيكون بطلنا خبّازاً دنماركياً اسمه بيورن. ومن البدهيّ أن يكون لدينا صنف يمثّل صديقنا الحلّو، وسيحتوي كل ما يفعله داخل اللعبة.
بما أن اللاعب يتحكّم به، فهذا يعني قراءة إدخال ذراع التحكّم وتحويل ذلك الإدخال إلى حركة. بالطبع أنه يحتاج إلى التفاعل مع المستوى، فتدخل بعض الفيزياء والاصطدامات هنا. وبعد أن ينتهي ذلك، عليه أن يظهر على الشاشة، فنُلقي بالحركة والعرض. ومن المرجّح أنه سيشغّل بعض الأصوات أيضاً.
لحظةً، هذا يخرج عن السيطرة. فـ«هندسة البرمجيات 101» تخبرنا أن المجالات المختلفة في البرنامج ينبغي أن تبقى معزولة عن بعضها. فإذا كنا نبني معالج نصوص، فلا ينبغي أن تتأثر شيفرة الطباعة بالشيفرة التي تحمّل المستندات وتحفظها. وللعبة نفس المجالات التي لتطبيقات الأعمال، لكن القاعدة ما تزال منطبقة.
إلى أقصى حد ممكن، لا نريد أن يعرف الذكاء الاصطناعي والفيزياء والعرض والصوت والمجالات الأخرى بعضها، لكن لدينا الآن كل ذلك محشوراً في صنف واحد. وقد رأينا إلى أين يقودنا هذا الطريق: ملف شيفرة مكبّنة سلة مهملات من خمسة آلاف سطر، ضخم إلى حدّ أن أشجع مبرمجي النينجا في فريقك فقط يجرؤ على دخوله.
هذا مصدر رزق ممتاز لقلّة ممّن يعرف كيف يروّضها، لكنه جحيم لبقيةنا. فالصنف بهذا الحجم يعني أن حتى التغييرات التي تبدو تافهة قد تكون ذات آثار بعيدة المدى. وقريباً، سيجمع الصنف أخطاء أسرع مما يجمع ميزات.
العقدة الفردية
أسوأ من مشكلة الحجم البسيط هي مسألة الترابط. فجميع أنظمة لعبتنا المختلفة شُدّت في كرة شيفرة ضخمة ومعقّدة مثل:
^code gordian
أي مبرمج يحاول إجراء تغيير في شيفرة كهذه ستحتاج إلى معرفة شيء عن الفيزياء والرسوميات والصوت، لمجرّد أن يتأكّد أنه لا يكسر شيئاً.
هاتان المشكلتان تتراكمان؛ فالصنف يلمس مجالات كثيرة لدرجة أن كل مبرمج سيضطر إلى
العمل عليه، لكنه ضخم إلى حدّ أن العمل عليه كابوس. وإذا ساءت الأمور بما يكفي،
سيبدأ المبرمجون بوضع الحيل في أجزاء أخرى من قاعدة الشيفرة لمجرّد أن يبتعدوا عن
كرّية الشعر التي أصبح عليها صنف Bjorn هذا.
قطع العقدة
يمكننا حلّ هذا كما فعل الإسكندر الأكبر -- بسيف. سنأخذ صنف Bjorn الأحادي ونقطّعه
إلى أجزاء منفصلة على حدود المجالات. مثلاً، سنأخذ كل شيفرة معالجة إدخال المستخدم
وننقلها إلى صنف InputComponent منفصل. وسيصبح Bjorn بعد ذلك مالكاً لنسخة من هذا
المكوّن. وسنكرر هذه العملية لكل مجال يلمسه Bjorn.
حين ننتهي، سنكون قد نقلنا كل شيء تقريباً خارج Bjorn. ولم يبقَ سوى قشرة رقيقة
تربط المكوّنات ببعضها. لقد حللنا مشكلة الصنف الهائلة ببساطة بقسمته إلى عدة أصناف
أصغر، لكننا حقّقنا أكثر من ذلك بكثير.
الأطراف السائبة
أصبحت أصناف مكوّناتنا الآن مفصولة الترابط. فحتى مع وجود PhysicsComponent و
GraphicsComponent في Bjorn، لا يعرف الاثنان شيئاً عن بعضهما. وهذا يعني أن من
يعمل على الفيزياء يستطيع تعديل مكوّنه دون حاجة إلى معرفة أي شيء عن الرسوميات
والعكس.
في الممارسة العملية، ستحتاج المكوّنات إلى بعض التفاعل بينها. مثلاً، قد يحتاج مكوّن الذكاء الاصطناعي إلى إخبار مكوّن الفيزياء إلى أين يحاول بيورن أن يذهب. لكن يمكننا حصر ذلك في المكوّنات التي تستحق التحدّث، بدلاً من رميها جميعاً معاً في حديقة واحدة.
إعادة التجميع معاً
من مزايا هذا التصميم أيضاً أن المكوّنات أصبحت حزماً قابلة لإعادة الاستخدام. وقد ركّزنا حتى الآن على خبّازنا، لكن لنفكّر في بضعة أنواع أخرى من الكائنات في عالم لعبتنا. فـالزخارف (decorations) هي أشياء في العالم يراها اللاعب لكنه لا يتفاعل معها: الشجيرات والحطام وغيرها من التفاصيل البصرية. والأغراض (props) مثل الزخارف لكنها قابلة للّمس: الصناديق والصخور والأشجار. أما المناطق (zones) فهي عكس الزخارف -- غير مرئية لكنها تفاعلية. وهي مفيدة لأشياء مثل إطلاق مشهد حركي حين يدخل بيورن منطقة ما.
الآن، لنفكّر كيف كان سنضع تسلسل وراثة لتلك الأصناف لو لم نستخدم المكوّنات. قد يبدو المحاولة الأولى كالتالي:
لدينا صنف أساس GameObject يضمّ أموراً مشتركة مثل الموضع والاتجاه. ويرث Zone منه
ويضيف كشف الاصطدام. وعلى حدٍّ سواء، يرث Decoration من GameObject ويضيف العرض.
ويرث Prop من Zone، فيستطيع إعادة استخدام شيفرة الاصطدام. لكن Prop لا يستطيع
أن يرث أيضاً من Decoration لإعادة استخدام شيفرة العرض دون أن يصطدم بـالماس المميت (Deadly Diamond).
يمكننا قلب الأمر فيرث Prop من Decoration، لكن عندئذٍ نضطر إلى تكرار شيفرة
الاصطدام. وعلى أي حال، لا توجد طريقة نظيفة لإعادة استخدام شيفرة الاصطدام والعرض
بين الأصناف التي تحتاج إليها دون اللجوء إلى الوراثة المتعددة. والخيار الوحيد
الآخر هو دفع كل شيء إلى GameObject، لكن عندئذٍ تكون Zone تهدر ذاكرة على بيانات
عرض لا تحتاجها، وDecoration تفعل الشيء نفسه بالفيزياء.
الآن، لنجرّب ذلك مع المكوّنات. تختفي الأصناف الفرعية
بالكامل. بدلاً من ذلك، لدينا صنف GameObject واحد وصنفا مكوّنات: PhysicsComponent
وGraphicsComponent. فالزخرفة هي ببساطة GameObject يضمّ GraphicsComponent دون
PhysicsComponent. والمنطقة هي العكس، والأغراض لديه المكوّنان معاً. لا ازدواج في
الشيفرة، ولا وراثة متعددة، وثلاثة أصناف فقط بدلاً من أربعة.
المكوّنات بالنسبة للكائنات هي أشبه بالتوصيل والتشغيل. فهي تتيح لنا بناء كيانات معقّدة ذات سلوك غني عبر توصيل كائنات مكوّنات قابلة لإعادة الاستخدام في مقابس الكيان. تخيّل فولترون برمجي.
النمط
كيان واحد يمتد عبر عدة مجالات. ولإبقاء المجالات معزولة، تُوضع شيفرة كل مجال في صنف مكوّن خاص به. ويُختزل الكيان إلى حاوية مكوّنات (container of components) بسيطة.
متى تستخدمه
أكثر ما تُوجد المكوّنات هو داخل الصنف الأساس الذي يعرّف كيانات اللعبة، لكنها قد تكون مفيدة في أماكن أخرى أيضاً. ويمكن أن يُوظَّف هذا النمط بشكل جيد حين يتحقق أيٌّ ممّا يلي:
-
لديك صنف يلمس مجالات متعدّدة تريد أن تبقى مفصولة الترابط عن بعضها.
-
أصبح صنف ما ضخماً وصعب التعامل معه.
-
تريد القدرة على تعريف مجموعة متنوعة من الكائنات التي تتشارك قدرات مختلفة، لكن استخدام الوراثة لا يسمح لك باختيار الأجزاء التي تريد إعادة استخدامها بدقة كافية.
في بالك
يضيف نمط المكوّن قدراً كبيراً من التعقيد مقارنة بمجرّد إنشاء صنف ووضع شيفرة فيه. فكل «كائن» مفاهيمي يتحوّل إلى عنقود من الكائنات التي يجب أن تُنشأ وتُهيَّأ وتُوصَل بطريقة صحيحة. ويصبح التواصل بين المكوّنات المختلفة أكثر تحدياً، ويصبح التحكّم في كيفية إشغالها للذاكرة أكثر تعقيداً.
في قاعدة شيفرة كبيرة، قد يستحق هذا التعقيد مقابل ما يتيحه من فصل الترابط وإعادة استخدام الشيفرة، لكن كن حذراً وتأكّد أنك لا تفرط في هندسة «حل» لمشكلة غير موجودة قبل تطبيق هذا النمط.
ومن النتائج الأخرى لاستخدام المكوّنات أنك غالباً ما تضطر إلى القفز عبر مستوى من التوجيه (indirection) لإنجاز أي شيء. فمع الكائن الحاوي، عليك أولاً أن تحصل على المكوّن الذي تريده، ثم تستطيع أن تفعل ما تحتاج. وفي الحلقات الداخلية الحسّاسة للأداء، قد يقود هذا التتبّع للمؤشرات إلى أداء سيّئ.
شيفرة نموذجية
أحد أكبر التحديات التي أواجهها في كتابة هذا الكتاب هو التخطيط كيفية عزل كل نمط. فهناك أنماط تصميم كثيرة موجودة لاحتواء شيفرة ليست في حدّ ذاتها جزءاً من النمط. ولكي أختزل النمط حتى جوهره، أحاول قصّ أكبر قدر ممكن من ذلك، لكن عند نقطة ما يصبح الأمر شبه بشرح كيفية تنظيم خزانة ملابس من دون عرض أي ثياب.
نمط المكوّن صعب بشكل خاص. فلا يمكنك أن تشعر به حقّاً من دون رؤية بعض الشيفرة لكل من المجالات التي يفصلها، لذا سأضطر إلى رسم قدر أكبر قليلاً من شيفرة بيورن مما أودّ. فالنمط هو في الحقيقة صنفا المكوّنات نفسها، لكن الشيفرة فيها ينبغي أن تساعد على توضيح ما الغرض من تلك الأصناف. إنها شيفرة وهمية -- تستدعي أصنافاً أخرى غير معروضة هنا -- لكن ينبغي أن تعطيك فكرة عمّا نرمي إليه.
صنف أحادي
لتكسب صورة أوضح عن كيفية تطبيق هذا النمط، سنبدأ بعرض صنف Bjorn
أحادي يقوم بكل ما نحتاجه لكن لا يستخدم هذا النمط:
^code 1
للصنف Bjorn دالة update() تُستدعى مرة واحدة في كل إطار من اللعبة:
^code monolithic-update
تقرأ ذراع التحكّم لتحديد كيفية تسريع الخبّاز. ثم تحسم موضعه الجديد بمحرّك الفيزياء. وأخيراً، ترسم بيورن على الشاشة.
التنفيذ النموذجي هنا بسيط إلى حدّ سخيف. فلا جاذبية، ولا حركة، ولا أيّ من عشرات التفاصيل الأخرى التي تجعل الشخصية ممتعة اللعب. وحتى مع ذلك، يمكننا أن نرى أننا أمامنا دالة واحدة سيضطر عدة مبرمجين مختلفين في فريقنا إلى قضاء وقت فيها، وقد بدأت تصبح فوضويّة بعض الشيء. تخيّل هذا بمقياس ألف سطر، وستحصل على فكرة عن مدى بؤسه.
إخراج مجال
لنبدأ بمجال واحد، ولنسحب جزءاً من Bjorn ندفعه في صنف مكوّن منفصل. سنبدأ بأول
مجال يُعالَج: الإدخال. فأول ما يفعله Bjorn هو قراءة إدخال المستخدم وتعديل سرعته
بناءً عليه. لننقل ذلك المنطق إلى صنف منفصل:
^code 2
بسيط إلى حدّ كبير. فقد أخذنا القسم الأول من دالة update() في Bjorn ووضعناه في
هذا الصنف. والتغييرات في Bjorn مباشرةة أيضاً:
^code 3
أصبح Bjorn الآن يملك كائن InputComponent. فحيث كان يعالج إدخال المستخدم
مباشرةً في دالة update()، أصبح الآن يفوّض الأمر إلى المكوّن:
^code 4
لم نبدأ سوى قليل، لكننا تخلّصنا بالفعل من بعض الترابط -- فصنف Bjorn الرئيس لم يعد
لديه أي إشارة إلى Controller. وسيكون هذا مفيداً لاحقاً.
إخراج البقية
الآن، لنقوم بمهمة القصّ واللصق ذاتها على شيفرة الفيزياء والرسوميات. إليك
PhysicsComponent الجديد:
^code 5
إلى جانب نقل سلوك الفيزياء خارج صنف Bjorn الرئيس، يمكنك أن ترى أننا نقلنا
البيانات أيضاً: فكائن Volume أصبح الآن مملوكاً للمكوّن.
وأخيراً وليس آخراً، إليك أين تقيم شيفرة العرض الآن:
^code 6
لقد نزعنا كل شيء تقريباً، فماذا بقي من صديقنا الحلو؟ ليس الكثير:
^code 7
أصبح صنف Bjorn يقوم بأمرين أساسيين: يحمل مجموعة المكوّنات التي تعرّفه فعلياً،
ويحمل الحالة المشتركة عبر مجالات متعدّدة. فالموضع والسرعة ما زالا في صنف Bjorn
الأساس لسببين. أولاً، هما حالة «عابرة للمجالات» -- فكل مكوّن تقريباً سيستفيد منهما،
لذلك ليس واضحاً أي مكوّن ينبغي أن يملكهما لو أردنا دفعهما إلى الأسفل.
ثانياً، وهو الأهم، أنه يتيح لنا طريقة سهلة ليتواصل المكوّنات دون أن ترتبط ببعضها. ولنرَ إن يمكننا الاستفادة من ذلك.
بيورن الآلي
حتى الآن، دفعنا سلوكنا إلى أصناف مكوّنات منفصلة، لكننا لم نستخلص السلوك
(abstraction). فـBjorn لا يزال يعرف الأصناف الملموسة تماماً حيث يُعرَّف سلوكه.
لنغيّر ذلك.
سنأخذ مكوّننا لمعالجة إدخال المستخدم ونخفيه خلف واجهة (interface). وسنحوّل
InputComponent إلى صنف أساس تجريدي:
^code 8
ثم سنأخذ شيفرة معالجة إدخال المستخدم الحالية لدينا وندفعها إلى الأسفل في صنف يطبّق تلك الواجهة:
^code 9
سنغيّر Bjorn ليحمل مؤشراً إلى مكوّن الإدخال بدلاً من أن يحتوي نسخة مضمّنة:
^code 10
والآن، حين ننشئ Bjorn، يمكننا أن نمرّر إليه مكوّن إدخال يستخدمه، كالتالي:
^code 11
يمكن أن تكون هذه النسخة أي نوع ملموس يطبّق واجهة InputComponent التجريدية لدينا.
ونحن ندفع ثمناً مقابل ذلك -- فـupdate() أصبح استدعاء دالة افتراضية (virtual)، وهو
أبطأ قليلاً. وماذا نحصل مقابل هذه الكلفة؟
تتطلب معظم الأجهزة الطرفية أن تدعم اللعبة «وضع العرض التجريبي» (demo mode). فإذا جلس اللاعب عند القائمة الرئيسة دون أن يفعل شيئاً، فستبدأ اللعبة في اللعب تلقائياً، مع الحاسوب يمثّل اللاعب. هذا يمنع اللعبة من حرق القائمة الرئيسية في تلفازك، ويجعل اللعبة تبدو أجمل أيضاً حين تعمل على جهاز عرض في متجر.
إخفاء صنف مكوّن الإدخال خلف واجهة يتيح لنا إنجاز ذلك. فلدينا بالفعل
PlayerInputComponent الملموس الذي يُستخدم عادةً عند لعب اللعبة. والآن، لنصنع واحداً
آخر:
^code 12
حين تدخل اللعبة في وضع العرض التجريبي، بدلاً من إنشاء بيورن كما فعلنا سابقاً، سنوصله بمكوّننا الجديد:
^code 13
والآن، بمجرّد تبديل مكوّن واحد، صار لدينا لاعباً يعمل بالحاسوب بالكامل لوضع العرض التجريبي. يمكننا إعادة استخدام كل بقية شيفرة بيورن -- فالوالفيزياء والرسوميات لا العرض التجريبي. يمكننا إعادة استخدام كل بقية شيفرة بيورن -- فالفيزياء والرسوميات من السرير في الصباح.
ألّا يكون هناك بيورن إطلاقاً؟
إن نظرت إلى صنف Bjorn لدينا الآن، ستلاحظ أنه لا شيء فيه «بيورن» حقّاً -- فهو مجرّد
كيس مكوّنات. وفي الواقع، يبدو مرشّحاً ممتازاً لصنف أساس «كائن لعبة» نستطيع
استخدامه لـكل كائن في اللعبة. كل ما يلزمنا فعله هو تمرير جميع المكوّنات، فيمكننا
أن نبني أي نوع من الكائنات عبر انتقاء الأجزاء وخلطها كالدكتور فرانكشتاين.
لنأخذ مكوّني الفيزياء والرسوميات الملموسين المتبقيين ونخفيهما خلف واجهات كما فعلنا مع الإدخال:
^code 14
ثم نعيد تعميد Bjorn ليصبح صنف GameObject عامّاً يستخدم
تلك الواجهات:
^code 15
أصبحت أصنافنا الملموسة القائمة ستُعاد تسميتها وتطبّق تلك الواجهات:
^code 16
والآن يمكننا بناء كائن يملك كل سلوك بيورن الأصلي دون أن نضطر إلى إنشاء صنف من أجله، تماماً كالتالي:
^code 17
وبتعريف دوال أخرى تُنشئ كائنات GameObject بمكوّنات مختلفة، يمكننا إنشاء جميع
الأنواع المختلفة من الكائنات التي تحتاجها لعبتنا.
قرارات التصميم
أهم سؤال تصميم ستحتاج إلى الإجابة عنه بهذا النمط هو: «ما مجموعة المكوّنات التي أحتاجها؟» والإجابة هنا ستعتمد على احتياجات لعبتك ونوعها. وكلما كان محرّكك أكبر وأكثر تعقيداً، زاد احتمال رغبتك في تقسيم مكوّناتك إلى قطع أدقّ.
وبعيداً عن ذلك، هناك بضعة خيارات أكثر تحديداً ينبغي التفكير فيها:
كيف يحصل الكائن على مكوّناته؟
بعد أن قسّمنا كائننا الأحادي إلى بضعة أجزاء مكوّنات منفصلة، علينا أن نقرّر مَن يعيد تجميع هذه الأجزاء معاً.
-
إذا أنشأ الكائن مكوّناته بنفسه:
-
يضمن أن الكائن لديه دائماً المكوّنات التي يحتاجها. فليس عليك أن تقلق بشأن نسيان شخص ما توصيل المكوّنات الصحيحة بالكائن وكسر اللعبة. فالكائن الحاوي نفسه يتولّى ذلك نيابةً عنك.
-
إعادة تهيئة الكائن أصعب. فإحدى الميزات القوية لهذا النمط أنه يتيح لك بناء أنواع جديدة من الكائنات ببساطة عبر إعادة تركيب المكوّنات. فإذا كان كائننا يوصّل نفسه دائماً بالمجموعة ذاتها من المكوّنات المكتوبة بشكل ثابت، فلن نستفيد من تلك المرونة.
-
-
إذا قدّمت شيفرة خارجية المكوّنات:
-
يصبح الكائن أكثر مرونة. فبإمكاننا تغيير سلوك الكائن تماماً بمنحه مكوّنات مختلفة للعمل بها. وإذا أخذنا ذلك إلى حدّه الأقصى، لصار كائننا حاوية مكوّنات عامّة نستطيع إعادة استخدامها مرّة بعد مرّة لأغراض مختلفة.
-
يمكن فصل الكائن عن الأنواع الملموسة للمكوّنات. فإذا كنّا نسمح للشيفرة الخارجية بتمرير مكوّنات، فالأرجح أننا نسمح لها أيضاً بتمرير أنواع مكوّنات مشتقّة. وعند تلك النقطة، لا يعرف الكائن سوى واجهات المكوّنات لا الأنواع الملموسة نفسها. وقد ينتج عن ذلك معمارية محاطة بشكل جيد.
-
كيف تتواصل المكوّنات مع بعضها؟
المكوّنات المفصولة تماماً ويعمل كل منها بمعزل هي مثال مثالي جميل، لكنه لا ينجح عملياً في الواقع. فكون هذه المكوّنات جزءاً من الكائن نفسه يعني أنها جزء من كل أشمل وتحتاج إلى التنسيق. أي أنها تحتاج إلى تواصل.
إذن كيف تتحدث المكوّنات إلى بعضها؟ هناك بضعة خيارات، لكن بخلاف معظم «البدائل» التصميمية في هذا الكتاب، ليست هذه بدائل حصرية -- فمن المرجّح أن تدعم أكثر من واحد في الوقت نفسه ضمن تصاميمك.
-
بتعديل حالة الكائن الحاوي:
-
يُبقي المكوّنات مفصولة الترابط. فحين يكون
InputComponentلدينا قد ضبط سرعة بيورن واستخدمهاPhysicsComponentلاحقاً، لم يكن المكوّنان يعرفان أن الآخر موجود أصلاً. فبالنسبة إليهما، كان يمكن أن تكون سرعة بيورن قد تغيّرت عبر سحر أسود. -
يتطلب أن يُدفع أي معلومة تحتاج المكوّنات إلى مشاركتها إلى أعلى الكائن الحاوي. فغالباً ما تكون هناك حالة يحتاج إليها فعلياً مجموعة جزئية فقط من المكوّنات. مثلاً، قد يحتاج مكوّن حركة ومكوّن عرض إلى مشاركة معلومات خاصة بالرسوميات. ودفع تلك المعلومات إلى أعلى الكائن الحاوي حيث يمكن لكل مكوّن الوصول إليها يشوّش صنف الكائن.
والأسوأ، إن استخدمنا صنف الكائن الحاوي نفسه مع تكوينات مكوّنات مختلفة، فقد نضيّع ذاكرة على حالة لا يحتاجها أيٌّ من مكوّنات الكائن. فإذا دفعنا بعض بيانات العرض الخاصة إلى الكائن الحاوي، فسيحرق كل كائن غير مرئي ذاكرة عليها بلا فائدة.
-
يجعل التواصل ضمنياً ومعتمداً على ترتيب معالجة المكوّنات. ففي شيفرتنا النموذجية، كانت دالة
update()الأصلية الأحادية تحمل ترتيب عمليات مدروساً بدقّة شديد. فإدخال المستخدم كان يعدّل السرعة، وهي بعد ذلك تُستخدم من شيفرة الفيزياء لتعديل الموضع، الذي يُستخدم بدوره من شيفرة العرض لرسم بيورن في المكان الصحيح. وحين فصّلنا تلك الشيفرة إلى مكوّنات، كنّا حذرين في الحفاظ على ذلك الترتيب.ولو لم نكن كذلك، لأدخلنا أخطاء خفيّة يصعب تتبّعها. مثلاً، لو حدّثنا مكوّن الرسوميات أولاً، لرسمنا بيورن بشكل خاطئ عند موضعه في الإطار الأخير لا في الإطار الحالي. وإن تخيّلت بضعة مكوّنات إضافية وشيفرة أكثر بكثير، لتحقّقت من مدى صعوبة تفادي أخطاء من هذا النوع.
-
-
بالإشارة إليهما مباشرةً:
الفكرة هنا أن المكوّنات التي تحتاج إلى التحدّث ستحتفظ بإشارات مباشرةً إلى بعضها دون الحاجة إلى المرور عبر الكائن الحاوي إطلاقاً.
لنفترض أننا نريد أن يسمح لبيورن بالقفز. فشيفرة الرسوميات تحتاج إلى معرفة ما إذا كان ينبغي رسمه باستخدام صورة قفز أم لا. ويمكنها تحديد ذلك بسؤال محرّك الفيزياء عمّا إذا كان واقفاً على الأرض الآن. ومن السهل أن نُجعل مكوّن الرسوميات يعرف مكوّن الفيزياء مباشرةً:
^code 18
وحين ننشئ
GraphicsComponentلبيورن، سنمنحه إشارة إلىPhysicsComponentالمقابل له.-
بسيط وسريع. فالتواصل هو استدعاء دالة مباشرة من كائن إلى آخر. ويستطيع المكوّن استدعاء أي دالة يدعمها المكوّن الذي لديه إشارة إليه. إنه ساحة حرة للجميع.
-
المكوّنان مترابطان بشدّة. وهذه هي تكلفة الساحة الحرة. فنحن فعلياً أخذنا خطوة إلى الوراء نحو صنفنا الأحادي. لكنه ليس سيئاً إلى حدّ الصنف الواحد الأصلي، فنحن على الأقل نقصر الترابط على أزواج المكوّنات التي تحتاج إلى التفاعل.
-
-
بإرسال الرسائل:
-
هذا هو البديل الأكثر تعقيداً. يمكننا في الواقع بناء نظام مراسلة صغير في الكائن الحاوي نفسه ونترك المكوّنات تبثّ المعلومات لبعضها.
إليك أحد التنفيذات الممكنة. سنبدأ بتعريف واجهة
Componentأساس يطبّقها جميع مكوّناتنا:^code 19
لديها دالة
receive()واحدة تطبّقها أصناف المكوّنات للاستماع إلى رسالة واردة. هنا نستخدم مجرّدintلتحديد الرسالة، لكن تنفيذاً أشمل يمكن أن يرفق بيانات إضافية بالرسالة.ثم سنضيف دالة إلى الكائن الحاوي لإرسال الرسائل:
^code 20
الآن، إذا كان لدى مكوّن ما وصول إلى حاويه، فيستطيع إرسال رسائل إلى الحاوي، الذي سيعيد بثّ الرسالة إلى جميع المكوّنات المحتواة. (وهذا يشمل المكوّن الأصلي الذي أرسل الرسالة؛ كن حذراً من أن تعلق في حلقة تغذية راجعة!) ولهذا عدة نتائج:
-
المكوّنات الأشقاء مفصولة الترابط. فعبر المرور من خلال الكائن الحاوي الأصل، تماماً كما في بديل الحالة المشتركة، نضمن أن المكوّنات تبقى مفصولة عن بعضها. ومع هذا النظام، فإن الترابط الوحيد بينها هو قيم الرسائل نفسها.
-
الكائن الحاوي بسيط. فعلى خلاف استخدام الحالة المشتركة حيث يملك الكائن الحاوي نفسه البيانات ويعرفها ويستخدمها المكوّنات، فهنا كل ما يفعله هو تمرير الرسائل بشكل أعمى. وقد يكون ذلك مفيداً لمكالمة مكوّنين يتبادلان معلومات خاصة جداً بمجاليهما دون أن يتسرّب ذلك إلى الكائن الحاوي.
-
لا مفاجأة في عدم وجود إجابة واحدة أفضل هنا. فما ستفعله على الأرجح هو استخدام قدر من كل منها. فالحالة المشتركة مفيدة للأشياء الأساسية جداً التي يمكنك افتراض أن كل كائن يملكها -- مثل الموضع والحجم.
وبعض المجالات متمايزة لكنها مرتبطة عن قرب. فكّر في الحركة والعرض، وإدخال المستخدم والذكاء الاصطناعي، أو الفيزياء والاصطدام. فإذا كان لديك مكوّنات منفصلة لكل نصف من تلك الأزواج، فقد تجد أن الأسهل هو جعلهما يعرفان نصفهما الآخر مباشرةً.
أما الرسائل فمفيدة للتواصل «أقل أهمية». فطبيعتها التي ترسل وتنسى تناسب أشياء مثل جعل مكوّن صوت يعزف صوتاً حين يرسل مكوّن الفيزياء رسالة تفيد بأن الكائن قد اصطدم بشيء ما.
وكما دائماً، أنصحك بالبدء ببساطة ثم إضافة مسارات تواصل إضافية إن احتجت إليها.
انظر أيضاً
-
صنف
GameObjectالرئيس في إطار عمل Unity مصمَّم بالكامل حول المكوّنات. -
محرّك Delta3D مفتوح المصدر يملك صنف أساس
GameActorيطبّق هذا النمط مع صنف أساسActorComponentالمناسب التسمية. -
يأتي إطار ألعاب XNA من مايكروسوفت مع صنف رئيس
Game. وهو يملك مجموعة من كائناتGameComponent. فبينما يستخدم مثالنا المكوّنات على مستوى كيان اللعبة الفردي، فإن XNA يطبّق النمط على مستوى كائن اللعبة الرئيس نفسه، لكن الغرض هو نفسه. -
يشبه هذا النمط نمط الاستراتيجية عند «الرباعي». فكلا النمطين يتعلق بأخذ جزء من سلوك كائن وتفويضه إلى كائن تابع منفصل. والفرق أن كائن «الاستراتيجية» المنفصل يكون عادةً بلا حالة -- فهو يجسّد خوارزمية لا بيانات. فهو يعرّف كيف يتصرّف الكائن، لا ما هو.
أما المكوّنات فتُعطي نفسها أهمية أكبر قليلاً. فهي غالباً تحمل حالة تصف الكائن وتساعد في تعريف هويته الفعلية. لكن الخط قد يتداخل. فقد تكون لديك بعض المكوّنات التي لا تحتاج أي حالة محلية. وفي تلك الحالة، يحقّ لك استخدام نفس نسخة المكوّن عبر كائنات حاوية متعددة. وعند تلك النقطة، بالنسبة له، يتصرّف أقرب إلى استراتيجية.