محدّد الخدمات
^title محدّد الخدمات ^section أنماط فصل الترابط
النية
وفّر نقطة وصول عامة إلى خدمة دون ربط المستخدمين بالصنف الملموس الذي ينفّذها.
الدافع
تميل بعض الكائنات أو الأنظمة في اللعبة إلى الانتشار، فتزور كل زاوية تقريباً من قاعدة الشيفرة. ومن الصعب العثور على جزء من اللعبة لن يحتاج إلى مخصّص ذاكرة أو تسجيل (logging) أو أرقام عشوائية في وقت ما.
ويمكن اعتبار الأنظمة من هذا النوع خدمات (services) تحتاج إلى أن تكون متاحة للعبة بأكملها.
ولنأخذ الصوت مثالاً. فليس له مدى الانتشار نفسه الذي لمخصّص ذاكرة أدنى مستوى، لكنه يظل يلمس حزمة من أنظمة اللعبة. فصخرة متساقطة ترتطم بالأرض باحتكاك (الفيزياء). وشخصية غير قابلة للعب (NPC) من نوع قنّاص تُطلق بنديقتها فيرنّ صوت الطلقة (الذكاء الاصطناعي). والمستخدم يختار عنصراً من القائمة مع صوت تأكيد (واجهة المستخدم).
وكل هذه المواضع ستحتاج إلى القدرة على الاستدعاء في نظام الصوت بشيء من هذا الشكل:
^code 15
كلاهما يوصلنا إلى حيث نريد، لكننا اصطدمنا ببعض الترابط اللزج في الطريق. فكل موضع في
اللعبة يستدعي نظام الصوت مباشرةً يشير إلى الصنف الملموس AudioSystem وإلى آلية
الوصول إليه -- سواء كصنف ساكن أو نمط
Singleton.
ومع ذلك، لا بدّ لمواضع الاستدعاء هذه أن ترتبط بـشيء ما كي يُشغَّل صوت، لكن تركها تنقر في تنفيذ الصوت الملموس مباشرةً يشبه إعطاء مئة غريب العنوان بيتك حتى يضع كل منهم رسالة على عتبتك. ليس الأمر شخصياً أكثر من اللازم فحسب، بل هو كذلك عبء حقيقي حين تنتقل ويضطر كل شخص أن تُحدّثه بالعنوان الجديد.
وهناك حلّ أفضل: دليل الهاتف. فمن يحتاج إلى الاتصال بنا يستطيع أن يبحث عنا بالاسم ويحصل على عنواننا الحالي. وحين ننتقل، نخبر شركة الهاتف. فهي تحدّث الدليل، فيحصل الجميع على العنوان الجديد. بل إننا لا نضطر حتى إلى إعطاء عنواننا الحقيقي، إذ يمكننا أن ندرج صندوق بريد أو شكلاً آخر من «التمثيل» لأنفسنا. وبجعل المستدعين يمرّون عبر الدليل للعثور علينا، كان لدينا مكان ملائم وحيد نتحكّم فيه بكيفية العثور علينا.
هذا هو نمط محدّد الخدمات (service locator) في سطور -- فهو يفصل الشيفرة التي تحتاج إلى خدمة عن كلٍّ من من هي (نوع التنفيذ الملموس) وأين هي (كيف نصل إلى نسختها).
النمط
يعرّف صنف الخدمة (service) واجهة تجريدية لمجموعة عمليات. ويطبّق مزوّد خدمة ملموس (service provider) هذه الواجهة. ويوفّر محدّد خدمات (service locator) منفصل الوصول إلى الخدمة عبر إيجاد مزوّد مناسب مع إخفاء كلٍّ من النوع الملموس للمزوّد والعملية المستخدمة في تحديد موقعه.
متى تستخدمه
في كل مرة تجعل فيها شيئاً متاحاً لكل جزء من برنامجك، فأنت تطلب المتاعب. فهذه هي المشكلة الرئيسة في نمط Singleton، وهذا النمط لا يختلف عنها. فأبسط نصيحتي بشأن متى تستخدم محدّد الخدمات هي: بشكل محدود.
وبدلاً من استخدام آلية عامة لتمنح بعض الشيفرة الوصول إلى كائن تحتاجه، فكّر أولاً في تمرير الكائن إليها بدلاً من ذلك. هذا سهل تماماً، ويجعل الترابط واضحاً تماماً. وسيغطي معظم احتياجاتك.
لكن... هناك بعض الأوقات التي يكون فيها تمرير الكائن يدوياً غير ضروري أو يجعل الشيفرة أصعب في القراءة فعلياً. فبعض الأنظمة، مثل التسجيل أو إدارة الذاكرة، لا ينبغي أن تكون جزءاً من الواجهة البرمجية العامة لوحدة ما. فمعاملات شيفرة العرض لديك ينبغي أن تتعلّق بـالعرض، لا بأشياء مثل التسجيل.
وكذلك، تمثّل أنظمة أخرى مرافق بطبيعتها فريدة. فرم لعبة لديك جهاز صوتي واحد أو نظام عرض واحد لا تستطيع التحدث إليه. إنه خاصية محيطة بالبيئة، فإن شقّطه عبر عشر طبقات من الدوال لمجرّد أن يستطيع استدعاء عميق الوصول إليه هو ضياع تعقيد بلا داعٍ.
في تلك الحالات، يمكن لهذا النمط أن يساعد. وكما سنرى، فإنه يعمل بوصفه قريباً أكثر مرونة وقابلية للتهيئة لنمط Singleton. وحين يُستخدم بصورة جيدة، يستطيع جعل قاعدة شيفرتك أكثر مرونة بتكلفة تشغيل زهيدة.
في بالك
الصعوبة الجوهرية في محدّد الخدمات أنه يأخذ اعتمادية -- أي جزء من الترابط بين قطعتَي شيفرة -- ويؤجل توصيلها حتى وقت التشغيل. وهذا يمنحك مرونة، لكن الثمن الذي تدفعه هو صعوبة معرفة ما هي اعتمادياتك بمجرد قراءة الشيفرة.
يجب فعلياً تحديد موقع الخدمة
مع النمط الأحادي (singleton) أو الصنف الساكن، لا مجال إطلاقاً لأن تكون النسخة التي نحاها غير متاحة. فشيفرة الاستدعاء تستطيع أن تفترض وجودها. لكن بما أن هذا النمط عليه أن يحدّد موقع الخدمة، فقد نضطر إلى معالجة حالات يفشل فيها ذلك. ولحسن الحظ، سنغطي استراتيجية لاحقًا للتعامل مع هذا تمنحك ضمان الحصول على بعض خدمة حين تحتاجها.
الخدمة لا تعرف مَن يحدّد موقعها
بما أن المحدّد متاح عالمياً، فيمكن لأي شيفرة في اللعبة أن تطلب خدمة ثم تنقر فيها. وهذا يعني أن الخدمة يجب أن قادرة على العمل بشكل صحيح في أي ظرف. فمثلاً، قد لا يعمل صنف يفترض استخدامه فقط في جزء المحاكاة من حلقة اللعبة وليس أثناء العرض كخدمة -- إذ لن يستطيع التأكّد من أنه يُستخدم في الوقت المناسب. لذا، إذا كان صنف ما يفترض استخدامه فقط في سياق معيّن، فمن الأسلم ألّا تعرّضه للعالم كله بهذا النمط.
شيفرة نموذجية
لنعد إلى مشكلتنا في نظام الصوت، ولنعالجها بأن نعرّض النظام لبقية قاعدة الشيفرة عبر محدّد خدمات.
الخدمة
سنبدأ بواجهة الصوت البرمجية. وهذه هي الواجهة التي ستعرّضها خدمتنا:
^code 9
بالطبع، محرك صوت حقيقي أكثر تعقيداً من هذا بكثير، بالطبع، لكن هذا يوضّح الفكرة الأساسية. والمهم أنها صنف واجهة تجريدية لا تنفيذ مربوط به.
مزوّد الخدمة
وحده، واجهتنا الصوتية ليست مفيدة جداً. فنحتاج إلى تنفيذ ملموس. وهذا الكتاب ليس عن كيفية كتابة شيفرة صوتية لأجهزة الألعاب، لذا سيتعيّن عليك أن تتخيّل أن هناك شيفرة فعلية في أجسام هذه الدوال، لكنك فهمت الفكرة:
^code 10
الآن لدينا واجهة وتنفيذ. والقطعة المتبقّية هي محدّد الخدمات -- الصنف الذي يربط الاثنين معاً.
محدّد بسيط
التنفيذ هنا هو أبسط أنواع محدّدات الخدمات التي يمكنك تعريفها:
^code 1
الدالة الساكنة getAudio() هي التي تحدّد الموقع. يمكننا استدعاؤها من أي موضع في قاعدة
الشيفرة، وستعيد لنا نسخة من خدمة Audio لاستخدامها:
^code 5
أما طريقة «تحديد الموقع» فبسيطة جداً -- فهي تعتمد على شيفرة خارجية لتسجيل مزوّد خدمة قبل أن يحاول أي شيء استخدام الخدمة. وحين تبدأ اللعبة بالتشغيل، فإنها تستدعي شيفرة من هذا الشكل:
^code 11
الجزء المفتاح الذي ينبغي ملاحظته هنا هو أن الشيفرة التي تستدعي playSound() ليست
واعيةً بالصنف الملموس ConsoleAudio؛ فهي لا تعرف سوى الواجهة التجريدية Audio.
ومهمّ بالقدر نفسه أن صنف المحدّد نفسه غير مرتبط بمزوّد الخدمة الملموس. فـالمكان
الوحيد في الشيفرة الذي يعرف عن الصنف الملموس الفعلي هو شيفرة التهيئة التي توفّر
الخدمة.
وهناك مستوى إضافي من فصل الترابط هنا: فالواجهة Audio ليست واعيةً بأنها يتم
الوصول إليها في معظم المواضع عبر محدّد خدمات. فبالنسبة إليها، إنها مجرّد صنف أساس
تجريدي عادي. وهذا مفيد لأنه يعني يمكننا تطبيق هذا النمط على أصناف موجودة لم
تُصمَّم بالضرورة حوله. وهذا على النقيض من نمط Singleton الذي يؤثّر في تصميم صنف «الخدمة» نفسه.
خدمة فارغة
تنفيذنا حتى الآن بسيط إلى حدّ كبير، وهو مرن أيضاً إلى حدّ كبير. لكن لديه عيب كبير
واحد: إن حاولنا استخدام الخدمة قبل أن يُسجَّل مزوّد،
فإنها تُعيد NULL. وإذا لم تفحص شيفرة الاستدعاء ذلك، فسنُسقط اللعبة.
ولحسن الحظ، هناك نمط تصميم آخر اسمه «كائن الفارغ» (Null Object) يمكننا استخدامه
لمعالجة هذا. وتتمثل الفكرة الأساسية في أنه في المواضع التي كنا سنُعيد فيها NULL
حين نفشل في العثور على كائن أو إنشائه، فإننا نُعيد بدلاً من ذلك كائناً خاصاً يطبّق
الواجهة ذاتها التي يطبّقها الكائن المطلوب. وتنفيذه لا يفعل شيئاً في الأساس، لكنه
يسمح للشيفرة التي تستقبل الكائن بأن تتابع بأمان وكأنها استلمت كائناً «حقيقياً».
ولاستخدام هذا، سنعرّف مزوّد خدمة «فارغاً» آخر:
^code 7
وكما ترى، فهو يطبّق واجهة الخدمة لكنه لا يفعل شيئاً فعلياً. والآن نغيّر محدّدنا ليصبح كالتالي:
^code 8
لن تعرف شيفرة الاستدعاء أبداً أنه لم يُعثر على خدمة «حقيقية»، ولا عليها أن تقلق بشأن
معالجة NULL. فمضمون لها أن تتلقّى دائماً كائناً صالحاً.
وهذا مفيد أيضاً للفشل المقصود في العثور على الخدمات. فإذا أردنا أن نُعطِّل نظاماً مؤقتاً، فقد صار لدينا الآن طريقة سهلة لذلك: ببساطة لا تُسجّل مزوّداً للخدمة، وسيُرجع المحدّد مزوّداً فارغاً افتراضياً.
ديكورور التسجيل
الآن، وبعد أن أصبح نظامنا متيناً إلى حدٍّ كبير، فلنتناقش في تحسين آخر يتيحه لنا هذا النمط -- الخدمات المزخرفة. وسأشرح ذلك بمثال.
أثناء التطوير، يمكن لسجلّ بسيط حين تحدث أحداث مثيرة للاهتمام أن يساعدك على معرفة ما يجري في باطن محرّك لعبتك. فإذا كنت تعمل على الذكاء الاصطناعي، فأنت تودّ معرفة متى يغيّر كيان حالة ذكائه الاصطناعي. وإن كنت مبرمج الصوت، فقد ترغب في سجلّ لكل صوت أثناء تشغيله كي تتحقّق من أنها تُطلق بالترتيب الصحيح.
والحلّ المعتاد هو نثر استدعاءات إلى دالة ما اسمها log() في كل مكان. لكن هذا
يستبدل مشكلة بمشكلة أخرى -- فلدينا الآن تسجيل زائد عن اللزوم. فمبرمج الذكاء
الاصطناعي لا يهتمّ بحين تشغيل الأصوات، ومسؤول الصوت لا يهتمّ بانتقالات حالة الذكاء
الاصطناعي، لكنهما الآن يضطران إلى تصفّح رسائل بعضهما.
في الحالة المثلى، نودّ أن نتمكّن من تفعيل التسجيل للأشياء التي نهمّنا فحسب، وأن تكون اللعبة النهائية خالية من التسجيل تماماً. فإذا كانت الأنظمة المختلفة التي نريد تسجيلها شرطياً معرَّضة كخدمات، فيمكننا حلّ ذلك باستخدام نمط الديكور (Decorator). ولنعرّف تنفيذاً آخر لمزوّد خدمة الصوت كالتالي:
^code 12
وكما ترى، فهو يلفّ مزوّد صوت آخر ويعرّض الواجهة ذاتها. يمرّر سلوك الصوت الفعلي إلى المزوّد الداخلي، لكنه يسجّل أيضاً كل استدعاء صوت. فإذا أراد مبرمج تفعيل تسجيل الصوت، يستدعي هذا:
^code 13
الآن، أي استدعاءات لخدمة الصوت ستُسجَّل قبل المتابعة كما كانت من قبل. وبالطبع، فهذا يتعايش بشكل جيّد مع خدمتنا الفارغة، فيمكنك تعطيل الصوت ومع ذلك تسجيل الأصوات التي كان سيلعبها لو كان الصوت مُفعَّلاً.
قرارات التصميم
لقد غطّينا تنفيذاً نموذجياً، لكن هناك بضع طرق يمكن أن يختلف فيها بناءً على إجابات مختلفة لأسئلة جوهرية قليلة:
كيف يتم تحديد موقع الخدمة؟
-
تسجّلها شيفرة خارجية:
هذه هي الآلية التي تستخدمها شيفرتنا النموذجية لتحديد موقع الخدمة، وهي التصميم الأكثر شيوعاً الذي أراه في الألعاب:
-
سريع وبسيط. فدالة
getAudio()تعيد مجرّد مؤشر. وغالباً ماwill يقوم المصرّف بتضمينها (inlining)، فنحصل على طبقة تجريد لطيفة تقريباً بلا أي كلفة أداء. -
نتحكّم بكيفية بناء المزوّد. لنفترض خدمةً للوصول إلى ذراعَي تحكّم اللعبة. لدينا مزوّدان ملموسان: واحد للألعاب العادية وآخر للعب عبر الإنترنت. فيمرّر المزوّد الخاص بالإنترنت إدخال ذراع التحكّم عبر الشبكة بحيث تبدو للاعبين البعيدين، من منظور بقية اللعبة، وكأنهم يستخدمون أذرع تحكّم محلية.
ولجعل هذا يعمل، يحتاج المزوّد الملموس الخاص بالإنترنت إلى معرفة عنوان IP للاعب البعيد الآخر. فإذا كان المحدّد نفسه هو من يبني الكائن، فكيف سيعرف ما الذي يمرّره؟ فصنف
Locatorلا يعرف أي شيء عن اللعب عبر الإنترنت أصلاً، فضلاً عن عنوان IP الخاص بمستخدم آخر.يتفادى المزوّدون المسجَّلون خارجياً هذه المشكلة. فبدلاً من أن يبني المحدّد الصنف، تُنشئ شيفرة الشبكات في اللعبة مزوّد الخدمة الخاص بالإنترنت، وتمرّر عنوان IP الذي تحتاجه. ثم تعطيه للمحدّد، وهو لا يعرف سوى الواجهة التجريدية للخدمة.
-
يمكنك تغيير الخدمة أثناء تشغيل اللعبة. قد لا نستخدم هذا في اللعبة النهائية، لكنه حيلة أنيقة أثناء التطوير. فأثناء الاختبار يمكننا، مثلاً، استبدال خدمة الصوت بخدمة الفارغ التيtalked عنها سابقاً لتعطيل الصوت مؤقتاً بينما اللعبة ما زالت تعمل.
-
المحدّد يعتمد على شيفرة خارجية. وهذه هي sided المعاكسة. فأي شيفرة تصل إلى الخدمة تفترض أن شيفرة ما في مكان ما سجّلتها بالفعل. وإن لم يحدث هذا التهيئة، فسينتهي بنا المطاف إما إلى إيقاف اللعبة أو إلى خدمة لا تعمل على نحو غامض.
-
-
الربط بها في وقت الترجمة (compile time):
الفكرة هنا أن عملية «تحديد الموقع» تحدث فعلاً في وقت الترجمة باستخدام وحدات المعالج المسبق (macros). كالتالي:
^code 2
ويترتّب على تحديد موقع الخدمة على هذا النحو أمور قليلة:
-
سريع. وبما أن كل العمل الحقيقي يُنجَز في وقت الترجمة، فلا يبقى شيء لتنفيذه في وقت التشغيل. وس يُضمّن المصرّف على الأرجح استدعاء
getAudio()، مما يمنحنا حلاً سريعاً بقدر ما نستطيع أمله. -
يمكنك ضمان توفّر الخدمة. وبما أن المحدّد يملك الخدمة الآن ويختارها في وقت الترجمة، يمكننا أن نطمئن إلى أنه ما دامت اللعبة قد تُرجمت فلن نضطر للقلق بشأن توفّر الخدمة.
-
لا يمكنك تغيير الخدمة بسهولة. وهذه هي العيب الأكبر. فبما أن الربط يحدث في وقت البناء، فمتى أردت تغيير الخدمة وجب عليك إعادة الترجمة وإعادة تشغيل اللعبة.
-
-
تهيئتها في وقت التشغيل:
في عالم برمجيات الأعمال المؤسّسية، فإن قلتَ «محدّد خدمات» فسيكون هذا ما يقصدونه. وحين تُطلب الخدمة، يقوم المحدّد ببعض السحر في وقت التشغيلResearch للعثور على التنفيذ الفعلي المطلوب.
وعادةً ما يعني هذا تحميل ملف تهيئة يحدّد المزوّد ثم استخدام الانعكاس لإنشاء ذلك الصنف في وقت التشغيل. وهذا يفعل بنا عدة أمور:
-
يمكننا استبدال الخدمة دون إعادة الترجمة. هذا أكثر مرونة قليلاً من خدمة مربوطة في وقت الترجمة، لكنه ليس بمرونة الخدمة المسجَّلة حيث يمكنك فعلاً تغيير الخدمة أثناء تشغيل اللعبة.
-
يمكن لغير المبرمجين تغيير الخدمة. هذا لطيف حين يريد المصمّمون القدرة على تشغيل بعض ميزات اللعبة وإيقافها دون أن يكونوا مرتاحين للتنقّل في شيفرة المصدر. (أو، على الأرجح أكثر، المبرمجون غير مرتاحين لهم في تصفّحها.)
-
قاعدة الشيفرة ذاتها يمكنها دعم تكوينات متعدّدة في آن واحد. وبما أن عملية تحديد الموقع نُقلت تماماً خارج قاعدة الشيفرة، يمكننا استخدام الشيفرة ذاتها لدعم تكوينات خدمة متعدّدة في آن واحد.
وهذه إحدى الأسباب التي تجعل هذا النموذج جذاباً في عالم تطبيقات الويب المؤسّسي: يمكنك نشر تطبيق واحد يعمل على إعدادات خوادم مختلفة بمجرّد تغيير بعض التهيئات. تاريخياً، كان هذا أقل فائدة في الألعاب لأن عتاد الأجهزة الطرفية معياري إلى حدٍّ كبير، لكن مع استهداف المزيد من الألعاب لمجموعة هائلة من الأجهزة المحمولة المتنوّعة، أصبح هذا أكثر صلة.
-
معقّد. وعلى عكس الحلول السابقة، فإن هذا ثقيل الوزن إلى حدٍّ كبير. فعليك إنشاء نظام تهيئة،Judge ورّبما كتابة شيفرة لتحميل ملف وتحليله، وأن تفعل بعضThings عموماً لتحديد موقع الخدمة. والوقت الذي تقضيه في كتابة هذه الشيفرة هو وقت لا تقضيه في ميزات اللعبة الأخرى.
-
تحديد موقع الخدمة يستغرق وقتاً. والآن turns turns الوجوه إلى عبوس. فالانتقال إلى التهيئة في وقت التشغيل يعني أنك تحرق بعض دورات المعالج في تحديد موقع الخدمة. ويمكن للتخزين المؤقت أن يقلّل ذلك، لكن ذلك لا يزال يعني أن في المرة الأولى التي تستخدم فيها الخدمة، على اللعبة أن تنطلق وتستهلك بعض الوقت في البحث عنها. ومطوّرو الألعاب يكرهون حرق دورات المعالج في أمر لا يحسّن تجربة اللاعب.
-
ماذا يحدث إذا تعذّر تحديد موقع الخدمة؟
-
دع المستخدم يتعامل معها:
أبسط حلّ هو تمرير المسؤولية. فإذا لم يجد المحدّد الخدمة، فإنه يعيد مجرّد
NULL. وهذا يعني:-
يترك للمستخدمين تحديد كيفية التعامل مع الفشل. فقد يعتبر بعض المستخدمين الفشل في العثور على خدمة خطأً حرجاً يجب أن يوقف اللعبة. بينما قد يستطيع آخرون تجاهلها بأمان والمتابعة. فإذا لم يستطع المحدّد وضع سياسة عامة صحيحة في جميع الحالات، فإن تمرير الفشل إلى الأسفل يتيح لكل موضع استدعاء أن يقرّر بنفسه ما الاستجابة الصحيحة.
-
على مستخدمي الخدمة التعامل مع الفشل. وبالطبع، فإن نتيجة ذلك أن كل موضع استدعاء يجب أن يفحص عن الفشل في العثور على الخدمة. فإذا تعامل معها كل واحد منهم بالطريقة ذاتها تقريباً، فذلك قدر كبير من الشيفرة المكرّرة موزّعة في أنحاء قاعدة الشيفرة. وإذا فشل موضع استدعاء واحد فقط من المواضع المحتملة التي قد المواضع المحتملة التي قد تبلغ المئات استخداماً للخدمة في إجراء ذلك الفحص، فسينتهي الأمر بأن تُسقط اللعبة.
-
-
إيقاف اللعبة:
قلتُ إننا لا نستطيع أن نثبت أن الخدمة ستكون متاحة دائماً في وقت الترجمة، لكن ذلك لا يعني أننا لا نستطيع أن نعلن أن التوفّر جزء من العقد التشغيلي للمحدّد. وأبسط طريقة لل doing ذلك هي عبر تحقّق (assertion):
^code 4
إن لم يتم تحديد موقع الخدمة، تتوقف اللعبة قبل أن تحاول أي شيفرة لاحقة استخدامها. فاستدعاء
assert()هناك لا يحلّ مشكلة الفشل في تحديد موقع الخدمة، لكنه يوضّح مَن صاحب المشكلة. وبإجراء التحقّق هنا، فإننا نقول: «الفشل في تحديد موقع خدمة هو خطأ في المحدّد».إذاً، ماذا يفيدنا هذا؟
-
لا يحتاج المستخدمون إلى التعامل مع خدمة مفقودة. وبما أنه قد تُستخدم خدمة واحدة في مئات المواضع، فقد يكون هذا وفراً كبيراً في الشيفرة. فإعلان أن توفير الخدمة دائماً هو مهمة المحدّد، نوفّر على مستخدمي الخدمة عناء سدّ تلك الفجوة.
-
ستتوقف اللعبة إذا لم يُعثر على الخدمة. في احتمال نادر أن يتعذّر العثور على خدمة حقّاً، ستتوقف اللعبة. وهذا أمر جيد insofar أنه يفرض علينا معالجة الخطأ الذي يمنع تحديد موقع الخدمة (غالباً ما تكون شيفرة تهيئة ما لا تُستدعى حين ينبغي)، لكنه عبء حقيقي على كل الآخرين الذين يشاركونك الانتظار حتى يُصلَح. ومع فريق تطوير كبير، قد تتسبّب مشكلة من هذا النوع في بعض فترات توقّف مؤلمة للمبرمجين.
-
-
إعادة خدمة فارغة:
عرضنا هذا التحسين في تنفيذنا النموذجي. واستخدامه يعني:
-
لا يحتاج المستخدمون إلى التعامل مع خدمة مفقودة. تماماً مثل الخيار السابق، نضمن إعادة كائن خدمة صالح دائماً، مما يبسّط الشيفرة التي تستخدم الخدمة.
-
ستستمر اللعبة إذا كانت الخدمة غير متاحة. وهذا مكسب ولعنة في آن. فهو مفيد insofar أنه يتيح لنا إبقاء اللعبة تعمل حتى حين لا تكون هناك خدمة. ويمكن أن يكون مفيداً حقّاً في فريق كبير حين تعتمد ميزة نعمل عليها على نظام آخر لم يُوضع في مكانه بعد.
والعيب هو أن تنقيح خدمة مفقودة عَرضاً قد يكون أصعب. لنفترض أن اللعبة تستخدم خدمة للوصول إلى بيانات ما ثم تتخذ قراراً بناءً عليها. فإذا فشلنا في تسجيل الخدمة الحقيقية وحصلت تلك الشيفرة على خدمة فارغة بدلاً منها، فقد لا تتصرّف اللعبة كما نريد. وسيحتاج الأمر إلى بعض العمل لتتبّع المسألة عائدةً إلى أن الخدمة لم تكن موجودة حين ظننّا أنها ستكون موجودة.
-
ومن بين هذه الخيارات، الخيار الذي أراه الأكثر استخداماً هو ببساطة التأكّد من أن الخدمة سيُعثر عليها. فحين تخرج اللعبة من الباب، تكون قد اختُبرت إلى حدٍّ كبير جداً، ومن المرجّح أن تُشغَّل على عتاد موثوق. فاحتمالات تعذّر العثور على خدمة بحلول ذلك الحين ضئيلة جداً.
وعلى فريق أكبر، أحثّك على إضافة خدمة فارغة. فهي لا تحتاج إلى جهد كبير لتنفيذها، ويمكن أن تنقذك من بعض فترات التوقّف أثناء التطوير حين لا تكون الخدمة متاحة. كما تمنحك طريقة سهلة لإيقاف خدمة ما إن كانت بها أخطاء أو كانت تشتّتك عمّا تعمل عليه.
ما نطاق الخدمة؟
حتى هذه النقطة، افترضنا أن المحدّد سيوفّر الوصول إلى الخدمة لـأي شخص يريدها. ورغم أن هذه هي الطريقة المعتادة لاستخدام النمط، فإن خياراً آخر هو حصر الوصول في صنف واحد وحدَّائه، كالتالي:
^code 3
مع هذا، يقصر الوصول إلى الخدمة على الأصناف التي ترث Base. وهناك مزايا في كلا
الحالتين:
-
إذا كان الوصول عاماً:
-
يشجّع قاعدة الشيفرة بأكملها على استخدام الخدمة ذاتها. فمعظم الخدمات مُعدّة لتكون فريدة. وبالسماح لقاعدة الشيفرة كاملةً بالوصول إلى الخدمة ذاتها، يمكننا تفادي مواضع عشوائية في الشيفرة تُنشئ مزوّداتها الخاصة لأنها لا تستطيع الوصول إلى المزوّد «الحقيقي».
-
نفقد التحكّم في أين ومتى تُستخدَم الخدمة. وهذه هي الكلفة الواضحة لجعل شيء ما عامّاً -- فأي شيء يستطيع الوصول إليه. وفي فصل Singleton تشكيلة كاملة من الشخصيات لعرض الرعب الذي يمكن أن يولّده النطاق العام.
-
-
إذا كان الوصول محصوراً في صنف:
-
نتحكّم بالترابط. وهذه هي الميزة الرئيس. فبحصر خدمة في فرع من شجرة الوراثة، يمكننا التأكّد من بقاء الأنظمة التي ينبغي أن تكون مفصولة مفصولة فعلاً.
-
قد يقود إلى تكرار الجهد. والعيب المحتمل هو أن إذا كان SOME من أصناف لا صلة لها بالفعل تحتاج إلى الوصول إلى الخدمة، فسيحتاج كل منها إلى مرجع خاص به إليها. وستحتاج أي عملية تُستخدم لتحديد موقع الخدمة أو تسجيلها إلى التكرار بين تلك الأصناف.
(والخيار الآخر هو إعادة ترتيب شجرة الأصناف لتمنح تلك الأصناف صنف أساس مشترك، لكن ذلك على الأرجح أكثر إزعاجاً مما يستحق.)
-
والمبدأ العام الذي أميل إليه هو: إذا كانت الخدمة محصورة في مجال واحد في اللعبة، فاحصر نطاقها في صنف. فمثلاً، الخدمة المخصّصة للحصول على الوصول إلى الشبكة يمكن على الأرجح أن تُحصر في أصناف اللعب عبر الإنترنت. أما الخدمات التي تُستخدم على نطاق أوسع مثل التسجيل فينبغي أن تكون عامة.
انظر أيضاً
-
نمط محدّد الخدمات شقيق لنمط Singleton في جوانب كثيرة، لذا يجدر بك النظر في كليهما لتحديد أيّهما أنسب لاحتياجاتك.
-
يستخدم إطار عمل Unity هذا النمط بالتوازي مع نمط المكوّن في دالته
GetComponent(). -
إطار XNA من مايكروسوفت لتطوير الألعاب يحتوي هذا النمط مدمجاً في صنفه الرئيس
Game. ولكل نسخة كائنGameServicesيمكن استخدامه لتسجيل الخدمات وتحديد مواقعها من أي نوع.