أنماط برمجة الألعاب › الحالة الواحدة (singleton) 7 / 26 السابق التالي

المفرد

^title Singleton ^section Design Patterns Revisited

هذا الفصل حالة شاذّة. فكل فصل آخر في هذا الكتاب يريك كيفية استخدام نمط تصميم. أما هذا الفصل فيريك كيف لا تستخدمه.

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

ومثل أي نمط، فإن استخدام المفرد في غير موضعه لا يفيد أكثر مما يفيد معالجة جرح طلقة بتجبير. ولأنه مُفرط في الاستخدام، فسيتناول معظم هذا الفصل تجنّب المفردات، لكن لنراجع النمط نفسه أولاً.

نمط المفرد

يلخّص أنماط التصميم المفرد هكذا:

تأكّد أن للصنف نسخة واحدة، ووفّر نقطة وصول عامة إليها.

وسنفصل ذلك عند «و» وننظر في كل نصف على حدة.

تقييد الصنف بنسخة واحدة

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

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

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

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

توفير نقطة وصول عامة

ستستخدم عدة أنظمة مختلفة في اللعبة غلاف نظام الملفات لدينا: التسجيل، وتحميل المحتوى، وحفظ حالة اللعبة، وما شابه. فإذا لم تستطع تلك الأنظمة إنشاء نسخها الخاصة من غلاف نظام الملفات، فكيف تحصل على نسخة منه؟

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

^code 1

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

أما المقاربة الحديثة فهكذا:

^code local-static

تشترط C++11 بأن المُهيّئ المتغيّر الساكن المحلي لا يُنفَّذ إلا مرة واحدة، حتى في وجود التزامن. لذا، إذا افترضنا أن لديك مصرّف C++ حديثاً، تكون هذه الشيفرة آمنة تجاه الخيوط، خلافاً للمثال الأول.

لماذا نستخدمه

يبدو أننا وجدنا فائزاً. فغلاف نظام الملفات لدينا متاح أينما احتجنا إليه من دون ملل تمريره في كل مكان. والصنف نفسه يضمن بذكاء أننا لن نُفسد الأمور بإنشاء نسختين. ولديه أيضاً بعض الميزات الأخرى اللطيفة:

  • لا ينشئ النسخة إن لم يستعملها أحد. فتوفير الذاكرة ودورات المعالج أمر جيد دائماً. ولأن المفرد لا يهيَّأ إلا عند أول وصول إليه، فلن يُنشأ إطلاقاً إن لم تطلبه اللعبة أبداً.

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

    وتحلّ التهيئة الكسولة مشكلتيهما معاً. فسيُهيَّأ المفرد في أبكر وقت ممكن، بحيث تكون أي معلومات يحتاجها قد أصبحت متاحة عندئذٍ. وطالما لم تكن هناك اعتماديات دائرية، فيمكن لمفرد أن يشير إلى مفرد آخر أثناء تهيئة نفسه.

  • يمكنك أن تجعل المفرد يُورَث. وهذه قدرة قوية لكن كثيراً ما تُنسى. لنقل أننا نحتاج غلاف نظام الملفات لدينا أن يكون متعدد المنصات. ولتحقيق ذلك، نريده أن يكون واجهة مجرّدة لنظام ملفات لها أصناف فرعية تنفّذ الواجهة لكل منصة. وإليك الصنف الأساسي:

    ^code 2

    ثم نعرّف أصنافاً مشتقة ل منصتين اثنتين:

    ^code derived-file-systems

    بعد ذلك، نحوّل FileSystem إلى مفرد:

    ^code 3

    والجزء الذكي هو كيفية إنشاء النسخة:

    ^code 4

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

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

لماذا نأسف على استخدامه

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

إنه متغير عام

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

فانتقلنا إلى لغات مثل C++ وبدأنا نطبّق بعض الحكمة المكتشفة بشقّ الأنفس من أسلافنا من مهندسي البرمجيات. وأحد الدروس التي تعلّمناها أن المتغيّرات العامة سيّئة لأسباب متعددة:

  • تجعل التفكير في الشيفرة أصعب. لنقل إننا نتتبّع خللاً في دالة كتبها شخص آخر. فإذا كانت تلك الدالة لا تلمس أي حالة عامة، يمكننا أن نفهمها بمجرد فهم جسم الدالة والمعاملات الممرَّرة إليها.

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

  • تشجّع على الترابط. فالمبرمج الجديد في فريقك غير معتاد على بنية لعبتك الجميلة القابلة للصيانة والمنفتحة على الترابط، لكنه توُ JUST Given his first task: أن تُصدر الصخور أصواتاً عند ارتطامها بالأرض وأنت وأنا نعرف أننا لا نريد أن ترتبط شيفرة الفيزياء بـالصوت بكل ما تحمله الكلمة، لكنه يحاول فقط إنجاز مهمّته. وللأسف، فإن نسخة AudioPlayer لدينا مرئية عالمياً. لذا، بعد #include صغير واحد، يكون زميلنا الجديد قد أفسد بنية بُنيت بعناية.

    ودون نسخة عامة من مشغّل الصوت، لما استطاع فعل شيء به حتى لو #include ملفه. وهذا الصعوبة ترسل إليه رسالة واضحة بأن هذين الوحدة يجب ألا يعرف كل منهما الآخر وأنه يحتاج إلى طريقة أخرى لحل مشكلته. بالتحكّم في الوصول إلى النسخ، تتحكّم في الترابط.

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

يحلّ مشكلتين حتى إن كانت لديك واحدة فقط

كلمة «و» في وصف الرباعية للمفرد غريبة نوعاً ما. فهل هذا النمط حلّ لمشكلة واحدة أم لمشكلتين؟ وماذا لو كانت لدينا إحداهما فقط؟ فضمان نسخة واحدة مفيد، لكن من قال إننا نريد أن نسمح للجميع بالعبث بها؟ وعلى نحو مماثل، الوصول العام مريح، لكن هذا صحيح حتى مع صنف يسمح بنسخ متعددة.

والمشكلة الثانية منهما، أي الوصول المريح، هي السبب الذي يجعلنا نلجأ إلى نمط المفرد في الغالبية العظمى من الأحوال. لنقل صنف تسجيل. فمعظم وحدات اللعبة يمكن أن تستفيد من القدرة على تسجيل معلومات تشخيصية. لكن تمرير نسخة من صنف Log إلى كل دالة على حدة يُثقل توقيع الدالة ويشتّت عن قصد الشيفرة.

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

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

ولنودّ إصلاح ذلك بتقسيم التسجيل إلى عدة ملفات. وللقيام بذلك، سنحتاج إلى مُسجِّلات منفصلة لمجالات اللعبة المختلفة: المتصل، وواجهة المستخدم، والصوت، واللعب. لكننا لا نستطيع ذلك. فصنف Log لم يعد يسمح لنا بإنشاء نسخ متعددة، كما أن ذلك القيد التصميمي قد ترسّخ في كل موضع استدعاء يستخدمه:

Log::instance().write("Some event.");

ولكي ندعم صنف Log على تعدّد النسخ (كما كان يفعل أصلاً)، سنحتاج إلى إصلاح الصنف نفسه وكل سطر من الشيفرة يذكره. فلن يصبح وصولنا المريح مريحاً بعد الآن.

التهيئة الكسولة تأخذ التحكم منك

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

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

ولأجل هاتين المشكلتين، فإن معظم الألعاب التي رأيتها لا تعتمد على التهيئة الكسولة. وبدلاً من ذلك، تنفّذ نمط المفرد هكذا:

^code 5

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

وبدلاً من إنشاء مفرد، ما لدينا هنا في الحقيقة هو صنف ساكن بسيط. وهذا ليس بالضرورة شيئاً سيئاً، لكن إن كان كل ما تحتاجه هو صنف ساكن، فلماذا لا تتخلّص من دالة instance() تماماً وتستخدم دوال ساكنة بدلاً منها؟ فاستدعاء Foo::bar() أبسط من Foo::instance().bar()، ويوضّح أيضاً أنك تتعامل فعلاً مع ذاكرة ساكنة.

ما الذي يمكننا فعله بدلاً من ذلك

إن كنت قد حقّقت هدفي حتى الآن، فسوف تفكّر مرتين قبل أن تُخرج المفرد من صندوق أدواتك في المرة القادمة التي تواجه فيها مشكلة. لكن لديك مشكلة تحتاج إلى حل. فما الأداة التي ينبغي أن تُخرجها؟ فاحسب ما تحاول فعله، لديّ بعض الخيارات لتفكّر فيها، لكن أولاً...

تحقّق ما إذا كنت تحتاج الصنف أصلاً

معظم أصناف المفردات التي أراها في الألعاب هي «مُدبِّرين» — تلك الأصناف الغامضة التي لا وجود لها إلا لمراقبة كائنات أخرى. وقد رأيت قواعد شيفرة يبدو فيها أن كل صنف له مُدبِّر: Monster وMonsterManager وParticle وParticleManager وSound وSoundManager وManagerManager. وأحياناً، للتنويع، يضعون «نظاماً» أو «محرّكاً» في الأمر، لكن الفكرة تبقى نفسها.

وبينما تكون أصناف الرعاية مفيدة أحياناً، فإنها غالباً ما تعكس عدم الإلمام بالبرمجة الكائنية. خذ هذين الصنفين المصطنعين:

^code 8

ربما يكون هذا المثال غبياً بعض الشيء، لكنني رأيت الكثير من الشيفرة التي تكشف عن تصميم كهذا بعد كشط التفاصيل المتقشّرة. فإذا نظرت إلى هذه الشيفرة، يكون من الطبيعي أن تفكر بأن BulletManager يجب أن يكون مفرداً. فعلى كل حال، أي شيء لديه Bullet سيحتاج إلى المُدبِّر أيضاً، وكم نسخة من BulletManager تحتاج؟

والجواب هنا هو صفر في الواقع. وهكذا نحلّ مشكلة «المفرد» في صنف المُدبِّر:

^code 9

وهكذا. لا مُدبّر، لا مشكلة. إلى صنف آخر. فإذا استطعت، انقل كل ذلك السلوك إلى الصنف الذي يساعده. فبعد كل شيء، البرمجة الكائنية تدور حول ترك الكائنات تعتني بأنفسها.

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

لتقييد صنف بنسخة واحدة

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

ونريد طريقة لضمان الإنشاء الوحيد من دون توفير وصول عام. وهناك طريقتان لتحقيق ذلك. وهذه إحداهما:

^code 6

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

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

لتوفير وصول مريح إلى نسخة

الوصول المريح هو السبب الرئيسي الذي نلجأ إليه إلى المفردات. فهي تجعل من السهل أن تضع يدك على كائن تحتاجه في أماكن مختلفة كثيرة. لكن هذا السهولة لها ثمن — إذ يصبح من السهل أن تضع يدك على الكائن في أماكن لا نريده فيها أن يُستخدم.

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

  • مرّره كمعامل. فالحلّ الأبسط، وكثيراً ما يكون الأفضل، هو أن تمرّر ببساطة الكائن الذي تحتاجه كمعامل للدوال التي تحتاجه. ويستحق التفكير فيه قبل أن نستبعده باعتباره مرهقاً أكثر من اللازم.

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

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

  • احصل عليه من الصنف الأساسي. فليست كثير من بنيات الألعاب عميقة الوراثة، فكثيراً ما تكون واسعة النطاق، وغالباً بعمق مستوى واحد فقط. فمثلاً، قد يكون لديك صنف أساسي GameObject له أصناف مشتقة لكل عدو أو كائن في اللعبة. ومع بنى كهذه، ستعيش نسبة كبيرة من شيفرة اللعبة في هذه الأصناف المشتقة «الورقية». وهذا يعني أن جميع هذه الأصناف تملك بالفعل وصولاً إلى الشيء نفسه: صنف GameObject الأساسي. ويمكننا أن نُغتنم ذلك:

    ^code 10

    وهذا يضمن أن لا شيء خارج GameObject يصل إلى كائن Log الخاص به، لكن كل كيان مشتق يفعل ذلك باستخدام getLog(). وقد غُطّي هذا النمط — أي ترك الكائنات المشتقة تنفّذ نفسها بدلالة دوال محميّة تُقدَّم إليها — في فصل صندوق الصنف الفرعي (Subclass Sandbox).

  • احصل عليه من شيء عام بالفعل. فهدف إزالة كل الحالة العامة جدير بالثناء، لكنه نادراً ما يكون عملياً. فمعظم قواعد الشيفرة ستحتفظ بكائنين أو ثلاثة متاحين عالمياً، مثل كائن Game أو World واحد يمثّل حالة اللعبة بأكملها.

    يمكننا تقليل عدد الأصناف العامة بالركوب على أكتاف الأصناف العامة الموجودة فعلاً. فبدلاً من جعل Log وFileSystem وAudioPlayer مفردات، افعل هذا:

    ^code 11

    وبهذا لا يكون Game وحده متاحاً عالمياً. ويمكن للدوال الوصول إلى الأنظمة الأخرى عن طريقه:

    ^code 12

    وإذا غُيِّرت البنية لاحقاً لدعم نسخ Game متعددة (ربما لأغراض البثّ أو الاختبار)، فإن Log وFileSystem وAudioPlayer لن تتأثر جميعها — لن تعرف حتى أن الفرق حدث. ومن عيوب هذا بالطبع أن مزيداً من الشيفرة تنتهي إلى ارتباطها بـGame نفسه. فإذا كان الصنف يحتاج فقط إلى تشغيل صوت، فإن مثالنا ما زال يتطلب أن يعرف العالم كي يصل إلى مشغّل الصوت.

    ونحلّ ذلك بحلّ هجين. فالشيفرة التي تعرف Game أصلاً تستطيع الوصول إلى AudioPlayer مباشرةً منه. أما الشيفرة التي لا تعرفه، فنوفّر لها الوصول إلى AudioPlayer باستخدام أحد الخيارات الأخرى الموصوفة هنا.

  • احصل عليه من محدِّد خدمة. حتى الآن، كنا نفترض أن الصنف العام صنف ملموس عادي مثل Game. والخيار الآخر هو تعريف صنف ليس له سبب وجود إلا توفير وصول عام إلى الكائنات. ويُسمّى هذا النمط الشائع محدِّد الخدمة (Service Locator)، وقد خُصِّص له فصل خاص به.

ما تبقّى للمفرد

يبقى السؤال: أين ينبغي أن نستخدم نمط المفرد الحقيقي؟ وبصراحة، لم أستخدم أبداً تنفيذ الرباعية الكامل في لعبة. ولضمان الإنشاء الوحيد، أستخدم عادةً ببساطة صنفاً ساكناً. وإن لم ينجح ذلك، فسأستخدم راية ساكنة للتحقّق وقت التنفيذ من أن نسخة واحدة فقط من الصنف قد بُنيت.

وهناك فصلان آخران في هذا الكتاب يمكنهما المساعدة هنا. فنمط صندوق الصنف الفرعي (Subclass Sandbox) يمنح نسخ الصنف وصولاً إلى حالة مشتركة دون جعلها متاحة عالمياً. ونمط محدِّد الخدمة (Service Locator) يفعل جعل الكائن متاحاً عالمياً، لكنه يمنحك مرونة أكبر في كيفية ضبط ذلك الكائن.