كائن النوع
^title Type Object ^section Behavioral Patterns
النية
اسمح بإنشاء «أصناف» جديدة بمرونة عبر إنشاء صنف واحد، كل نسخة من نسخه تمثّل نوعاً مختلفاً من الكائنات.
الدافع
تخيّل أننا نعمل على لعبة أدوار خيالية. ومهمتنا كتابة شيفرة لأسراب الوحوش الشريرة التي تسعى إلى قتل بطلنا الشجاع. وللوحشات صفات مختلفة كثيرة: الصحة، والهجمات، والرسوميات، والأصوات، وغيرها، لكن ولأغراض المثال سنكتفي بأول صفتين.
لكل وحش في اللعبة قيمة لصحة الحالية. يبدأ بكامل صحته، وفي كل مرة يُجرح فيها الوحش تخبو تلك القيمة. وللوحثات أيضاً سلسلة هجوم. وحين يهاجم الوحش بطلنا، يُعرض ذلك النص على المستخدم بطريقة ما. (ولن نهتم بكيفية ذلك هنا.)
يُخبرنا المصمّمون أن الوحوش تأتي بمتنوّع من السلالات (breeds) المختلفة، مثل «تنين» أو «غول». وكل سلالة تصف نوعاً من الوحوش موجود في اللعبة، ويمكن أن يوجد عدة وحوش من السلالة نفسها تتجوّل في الزنقة في الوقت نفسه.
وتحدّد السلالة صحة البداية للوحش — فالتنانير تبدأ بصحة أكبر من الغيول، ما يجعلها أصعب في القتل. وهي تحدّد أيضاً سلسلة الهجوم — فكل وحوش السلالة نفسها تهاجم بالطريقة نفسها.
جواب البرمجة الكينونية المعتاد
وانطلاقاً من تصميم اللعبة هذا، نشغّل محرّر النصوص ونبدأ البرمجة. ووفقاً للتصميم،
التنين نوع من الوحث، والغول نوع آخر، وهكذا مع بقية السلالات.
والتفكير الكينوني يقودنا إلى صنف أساس Monster:
^code 1
تتيح دالة getAttack() العامة لشيفرة القتال أن تحصل على السلسلة التي ينبغي
عرضها حين يهاجم الوحش البطل. وسيتجاوز كل صنف سلالة مشتق هذه الدالة ليقدّم رسالة
مختلفة.
والباني محميّ ويأخذ صحة البداية للوحش. وسنمتلك أصنافاً مشتقة لكل سلالة تقدّم بانياتها العامة الخاصة التي تستدعي هذا الباني، وتمرّر صحة البداية المناسبة لتلك السلالة.
ولنرَ الآن بعض أصناف السلالات الفرعية:
^code 2
كل صنف مشتق من Monster يمرّر صحة البداية ويتجاوز getAttack() ليعيد سلسلة
الهجوم لتلك السلالة. كل شيء يعمل كما هو متوقّع، وقبل أن يمر وقت طويل يكون
بطلنا يجري ويقتل تنوّعاً من الوحوش. ونواصل صبّ الشيفرة، وقبل أن ندري يكون لدينا
عشرات من أصناف الوحوش الفرعية، من اللزجات الحمضية إلى ماعز الزومبي.
ثم، ولسبب غريب، تبدأ الأمور بالتثقل. فمصمّونا يريدون في النهاية أن يكون لديهم مئات السلالات، فنجد أنفسنا نصرف كل وقتنا في كتابة هذه الأصناف الفرعية الصغيرة التي تتكوّن من سبعة أسطر وإعادة الترجمة. والأمر يسوء — يريد المصمّمون أن يبدأوا بضبط السلالات التي برمجناها بالفعل. فيتحوّل يوم عملنا الذي كان مُنتجاً إلى مجرّد:
-
استلم بريداً من المصمّم يطلب تغيير صحة الغول من 48 إلى 52.
-
أخرِج
Troll.hوغيّره. -
أعد ترجمة اللعبة.
-
سجّل التغيير.
-
رد على البريد.
-
كرّر.
نقضي اليوم في الإحباط لأننا تحوّلنا إلى قرود بيانات. والمصمّمون محبطون لأنهم يحتقرون وقتاً طويلاً ليعدّلوا رقماً بسيطاً. وما نحتاجه هو القدرة على تغيير إحصاءات السلالات من غير إعادة ترجمة اللعبة كلها في كل مرة. والأفضل بعد ذلك، أننا نريد أن يستطيع المصمّمون إنشاء السلالات وضبطها من غير أي تدخّل من المبرمج على الإطلاق.
صنف لصنف
على مستوى عالٍ جداً، فإن المشكلة التي نحاول حلها بسيطة إلى حد ما. فلدينا حفنة من الوحوش المختلفة في اللعبة، ونريد مشاركة بعض الصفات بينها. فزرب من الوحوش يضرب البطل، ونريد بعضها أن يستخدم النص نفسه في هجومه. ونعرّف ذلك بالقول إن كل تلك الوحوش هي السلالة نفسها، وأن السلالة تحدّد سلسلة الهجوم.
وقد قررنا تنفيذ هذا المفهوم باستخدام الوراثة لأنها تتوافق مع حدسنا تجاه الأصناف.
فالتنين وحش، وكل تنين في اللعبة هو نسخة من «صنف» التنين هذا. وتعريف كل سلالة
كصنف فرعي من صنف أساس مجرّد Monster، وجعل كل وحش في اللعبة نسخة من ذلك الصنف
الفرعي المشتق، يعكس هذا كله. وننتهي إلى تسلسل أصناف على هذا النحو:
كل نسخة من الوحش في اللعبة ستكون من أحد أنواع الوحوش المشتقة. وكلما زاد عدد السلالات زاد حجم تسلسل الأصناف. وتلك هي المشكلة بالطبع: فإضافة سلالات جديدة تعني إضافة شيفرة جديدة، وكل سلالة يجب أن تُترجَم بوصفها نوعاً خاصاً بها.
وهذا يعمل، لكنه ليس الخيار الوحيد. يمكننا أيضاً أن نُنسّق شيفرتنا بحيث يكون لكل
وحث سلالة. فبدلاً من اشتقاق صنف فرعي من Monster لكل سلالة، لدينا صنف واحد
Monster وصنف واحد Breed:
هذا كل شيء. صنفان. ولاحظ أنه لا توجد وراثة على الإطلاق. مع هذا النظام، كل وحش في
اللعبة ليس سوى نسخة من صنف Monster. ويحتوي صنف Breed على المعلومات المشتركة بين
كل وحوش السلالة نفسها: صحة البداية وسلسلة الهجوم.
ولربط الوحوش بالسلالات، نعطي كل نسخة Monster مرجعاً إلى كائن Breed يحتوي
معلومات تلك السلالة. ولمعرفة سلسلة الهجوم، يستدعي الوحث دالة على سلالته فقط. وصنف
Breed يعرّف فعلياً «نوع» الوحش. وكل نسخة من السلالة هي كائن يمثّل نوعاً
مفاهيمياً مختلفاً، ومن هنا اسم النمط: كائن النوع.
القوة لافتة لهذا النمط تحديداً هي أننا الآن نستطيع تعريف أنواع جديدة من الأشياء من غير تعقيد قاعدة الشيفرة إطلاقاً. فقد رفعنا فعلياً جزءاً من نظام الأنواع إلى الخارج من تسلسل الأصناف المكتوب يدوياً إلى بيانات يمكننا تعريفها وقت التشغيل.
ويمكننا إنشاء مئات السلالات المختلفة عبر إنشاء نسخ أكثر من Breed بقيم مختلفة. وإذا
أنشأنا السلالات عبر تهيئتها من بيانات مقروءة من ملف إعدادات ما، فسنكون قادرين
على تعريف أنواع جديدة من الوحوش بالكامل في بيانات. الأمر سهل إلى حد أن يستطيع
المصمّم فعله!
النمط
عرّف صنف كائن النوع (type object) وصنف الكائن المُنمَّط (typed object). تمثّل كل نسخة من كائن النوع نوعاً منطقياً مختلفاً. ويخزّن كل كائن مُنمَّط مرجعاً إلى كائن النوع الذي يصف نوعه.
تُخزَّن البيانات الخاصة بنسخة بعينها في نسخة الكائن المُنمَّط، وتُخزَّن البيانات أو السلوك الذي ينبغي أن يكون مشتركاً بين كل نسخ النوع المفاهيمي نفسه في كائن النوع. وستعمل الكائنات التي تشير إلى كائن النوع نفسه كما لو كانت من النوع نفسه. وهذا يتيح لنا مشاركة البيانات والسلوك عبر مجموعة من الكائنات المتشابهة، تماماً كما تتيح لنا الأصناف الفرعية ذلك، لكن من غير مجموعة ثابتة من أصناف فرعية مكتوبة يدوياً.
متى تستخدمه
هذا النمط مفيد كلما احتجت إلى تعريف متنوّع من «أنواع» الأشياء المختلفة، لكن تثبيت هذه الأنواع في نظام أنواع لغتك جامد أكثر من اللازم. وعلى وجه الخصوص، فهو مفيد حين يصدق أحد الشرطين التاليين:
-
لا تعرف مسبقاً ما هي الأنواع التي ستحتاج إليها. (فمثلاً، ماذا لو احتاجت لعبتنا إلى دعم تنزيل محتوى يحوي سلالات جديدة من الوحوش؟)
-
تريد أن تتمكّن من تعديل الأنواع أو إضافة أنواع جديدة من غير إعادة ترجمة أو تغيير الشيفرة.
ضع في اعتبارك
يتعلّق هذا النمط بنقل تعريف «النوع» من لغة الشيفرة الأمريكية الجامدة إلى عالم الكائنات في الذاكرة، وهو أكثر مرونة لكن أقل سلوكية. المرونة أمر جيد، لكنك تفقد بعض الأشياء حين ترفع أنواعك إلى مستوى البيانات.
يجب تتبّع كائنات النوع يدوياً
إحدى مزايا استخدام شيء مثل نظام أنواع ++C++ هو أن المترجِّم يتولّى كل أعمال الدفاتر على الأصناف تلقائياً. فالبيانات التي تعرّف كل صنف تُترجَم تلقائياً إلى القطاع الساكن من الذاكرة في الملف التنفيذي وتعمل كما هي.
ومع نمط كائن النوع، نصبح نحن مسؤولين عن إدارة وحوشنا في الذاكرة، لا عن أنواعها فقط — علينا التأكد من إنشاء كل كائنات السلالات وإبقائها في الذاكرة طالما تحتاج إليها وحوشنا. وكلما أنشأنا وحشاً جديداً، فالأمر مسؤوليتنا أن نتأكد من تهيئته على النحو الصحيح بمرجع إلى سلالة صالحة.
لقد تحرّرنا من بعض قيود المترجِّم، لكن الثمن هو أننا علينا إعادة تنفيذ بعض ما كان يفعله نيابةً عنا.
تعريف السلوك لكل نوع أصعب
مع الأصناف الفرعية، يمكنك تجاوز دالة وفعل ما تشاء — حساب قيم إجرائياً، واستدعاء شيفرة أخرى، إلخ. والحد هو الخيال. يمكننا أن نعرّف صنف وحش فرعي تتغيّر فيه سلسلة الهجوم حسب طور القمر إن أردنا. (مفيد للذئاب الرؤساء، على ما أظن.)
وحين نستخدم نمط كائن النوع بدلاً من ذلك، فإننا نستبدل دالة متجاوزة بمتغيّر عضو. فبدلاً من وجود أصناف وحش فرعية تتجاوز دالة لحساب سلسلة هجوم باستخدام شيفرة مختلفة، لدينا كائن سلالة يخزّن سلسلة هجوم في متغيّر مختلف.
وهذا يجعل استخدام كائنات النوع لتعريف بيانات خاصة بالنوع سهلاً جداً، لكنه يجعل تعريف سلوك خاص بالنوع صعباً. فإذا، على سبيل المثال، احتاجت سلالات مختلفة من الوحوش إلى استخدام خوارزميات ذكاء اصطناعي مختلفة، فإن استخدام هذا النمط يصبح أكثر تعقيداً.
وهناك بضع طرق يمكننا تجاوز هذا القيد بها. وأحد الحلول البسيطة هو أن تكون لديك مجموعة ثابتة من السلوكيات المعرَّفة مسبقاً ثم تستخدم البيانات في كائن النوع لمجرد اختيار أحدها. فمثلاً، لنقل إن ذكاء الوحش في لعبتنا سيكون دائماً إما «قف مكانك»، أو «طارد البطل»، أو «أنّ وتخور خائفاً» (يا ترى، لا يمكن أن يكونوا كلهم تنانير عظام). يمكننا أن نعرّف دوال لتنفّذ كل واحد من تلك السلوكيات. ثم يمكننا ربط خوارزمية ذكاء اصطناعي بسلالة بأن تجعلها تخزّن مؤشراً إلى الدالة المناسبة.
والحل الأقوى هو أن ندعم فعلياً تعريف السلوك بالكامل في بيانات. فكلاً من نمطي المُفسِّر (Interpreter) وشيفرة البايت (Bytecode) يتيح لنا بناء كائنات تمثّل السلوك. وإذا قرأنا ملف بيانات واستخدمنا ذلك لإنشاء بنية بيانات لأحد هذين النمطين، فقد نقلنا تعريف السلوك من الشيفرة تماماً إلى المحتوى.
شيفرة العيّنة
ولمحاولتنا الأولى في التنفيذ، لنبدأ ببساطة ونبنِ النظام الأساسي الموصوف في قسم
الدافع. وسنبدأ بصنف Breed:
^code 3
بسيط جداً. فهو في جوهره مجرّد حاوية لحقلي بيانات: صحة البداية وسلسلة الهجوم. ولنرَ كيف تستخدمه الوحوش:
^code 4
حين نبني وحشاً، فإننا نعطيه مرجعاً إلى كائن سلالة. وهذا يعرّف سلالة الوحث بدلاً من
الأصناف الفرعية التي كنّا نستخدمها سابقاً. وفي الباني، يستخدم Monster السلالة
لتحديد صحة بدايته. أما للحصول على سلسلة الهجوم، فيكفي أن يمرّر الوحث الاستدعاء إلى
سلالته.
هذه الشيفرة البسيطة جداً هي الفكرة الجوهرية للنمط. وكل ما يأتي بعد ذلك هو إضافات.
جعل كائنات النوع أقرب إلى الأنواع: البانيات
مع ما لدينا الآن، نبني وحشاً مباشرةً ونتحمّل مسؤولية تمرير سلالته. وهذا معكوس بعض الشيء مقارنة بكيفية إنشاء الكائنات العادية في معظم اللغات الكينونية — فغالباً ما لا نخصّص مقطعاً من الذاكرة ثم نمنح الكائن صنفه. وبدلاً من ذلك، فإننا نستدعي دالة باني على الصنف نفسه، وهي مسؤولة عن أن تمنحنا نسخة جديدة.
ويمكننا تطبيق هذا النمط نفسه على كائنات النوع لدينا:
^code 5
والصنف الذي يستخدمها:
^code 6
الفرق الأساسي هو دالة newMonster() في Breed. فتلك هي «الباني»
الخاص بدالة المصنع. ومع تنفيذنا الأصلي، كان إنشاء الوحش يبدو هكذا:
^code 7
وبعد تغييراتنا، صار هكذا:
^code 8
إذن، لماذا نفعل هذا؟ فهناك خطوتان لإنشاء كائن: التخصيص والتهيئة. وباني
Monster’ يتيح لنا أن ننفّذ كل التهيئة التي نحتاجها. وفي مثالنا، لا يزيد ذلك
على تخزين السلالة، لكن اللعبة الكاملة ستُحمّل الرسوميات، وتهيّئ ذكاء الوحش
الاصطناعي، وتنفّذ أعمال تهيئة أخرى.
غير أن كل ذلك يحدث بعد التخصيص. فقد حصلنا بالفعل على مقطع من الذاكرة نضع فيه وحشنا قبل استدعاء بانيه. وفي الألعاب، نريد غالباً أن نتحكّم في ذلك الجانب من إنشاء الكائن أيضاً: فعادة ما نستخدم أشياء مثل مخصِّصات مخصّصة أو نمط مجموعة الكائنات (Object Pool) للتحكّم في المكان الذي تنتهي إليه كائناتنا في الذاكرة.
وتعريف دالة «باني» في Breed يمنحنا موضعاً لوضع تلك المنطق. فبدلاً من مجرد
استدعاء new، يمكن لدالة newMonster() أن تسحب الذاكرة من مجموعة كائنات أو من كومة
ذاكرة مخصّصة قبل أن تسلّم التحكم إلى Monster للتهيئة. وبوضع هذا المنطق داخل
Breed، في الدالة الوحيدة التي تملك القدرة على إنشاء الوحوش، فإننا نتأكد من أن
كل وحش يمرّ بمخطّط إدارة الذاكرة الذي نريده.
مشاركة البيانات عبر الوراثة
ما لدينا حتى الآن هو نظام كائنات نوع عملي تماماً، لكنه أساسي إلى حد ما. فستُبح لعبتنا في النهاية مئات السلالات المختلفة، لكل منها عشرات الصفات. وإذا أرادت مصمّمة أن تضبط كل سلالات الغول الثلاثين المختلفة لتجعلها أقوى قليلاً، فسيكون أمامها إدخال بيانات مملّاً لا ينتهي.
وما سيساعد هو القدرة على مشاركة الصفات عبر عدة سلالات بالطريقة نفسها التي تتيح بها السلالات نفسها مشاركة الصفات عبر عدة وحوش. تماماً كما فعلنا مع حلّنا الكينوني الأصلي، يمكننا حلّ هذا باستخدام الوراثة. لكن هذه المرة، بدلاً من استخدام آلية الوراثة في لغتنا، سننفّذها بأنفسنا داخل كائنات النوع لدينا.
ولنبقِ الأمور بسيطة، سندعم الوراثة المفردة فقط. وعلى الطريقة نفسها التي يمكن أن يكون فيها للصنف صنف أساس أب، سنسمح للسلالة بأن تكون لها سلالة أب:
^code 9
حين نبني سلالة، فإننا نعطيها أباً ترث منه. ويمكننا تمرير NULL لسلالة أساس ليس لها
أسلاف.
ولجعل هذا مفيداً، تحتاج السلالة الفرعية إلى التحكّم في الصفات التي ترثها من أبيها
والصفات التي تتجاوزها وتحدّدها بنفسها. في نظامنا المثال، سنقول إن السلالة تتجاوز
صحة الوحث بأن تكون لها قيمة غير صفرية، وتتجاوز الهجوم بأن يكون لها سلسلة نصية غير
NULL. وإلا، فإن الصفة ستُورَث من أبيها.
وهناك طريقتان يمكننا تنفيذ هذا بهما. إحداهما هي معالجة التفويض ديناميكياً في كل مرة تُطلب فيها الصفة، على هذا النحو:
^code 10
مع أن هذا يعني أن تفعل الشيء الصحيح إذا عُدِّلت سلالة وقت التشغيل لتتجاوز صفة ما أو تتخلى عن وراثتها. ومن الجهة الأخرى، فإنها تستهلك ذاكرة أكبر قليلاً (فعلينا الاحتفاظ بمؤشر إلى أبيها)، وهي أبطأ. إذ عليها أن تسير في سلسلة الوراثة في كل مرة تبحث فيها عن صفة.
وإذا كان بإمكاننا الاعتماد على أن صفات السلالة لن تتغيّر، فإن الحل الأسرع هو تطبيق الوراثة في وقت البناء (construction time). ويُسمّى هذا التفويض «النسخ إلى الأسفل» (copy-down)، لأننا ننسخ الصفات الموروثة إلى الأسفل داخل النوع المشتق حين يُنشأ. يبدو هكذا:
^code copy-down
لاحظ أننا لم نعد بحاجة إلى حقل للسلالة الأب. فبعد انتهاء الباني، يمكننا أن ننسى الأب لأننا نسخنا كل صفاته بالفعل. وللوصول إلى صفة السلالة، نعيد الحقل الآن فحسب:
^code copy-down-access
جيّد وسريع!
لنقل إن محرّك لعبتنا مُهيّأ لإنشاء السلالات عبر تحميل ملف JSON يعرّفها. وقد يبدو على النحو التالي:
:::json
{
"Troll": {
"health": 25,
"attack": "The troll hits you!"
},
"Troll Archer": {
"parent": "Troll",
"health": 0,
"attack": "The troll archer fires an arrow!"
},
"Troll Wizard": {
"parent": "Troll",
"health": 0,
"attack": "The troll wizard casts a spell on you!"
}
}
وسنكون لدينا قطعة شيفرة تقرأ كل مُدخَل سلالة وتُنشئ نسخة سلالة جديدة ببياناتها. وكما
ترى من حقول "parent": "Troll"، فإن سلالتَي Troll Archer وTroll Wizard ترثان من
السلالة الأساسية Troll.
ولأن كليهما بصفر في الصحة، فسيَرِثانها من السلالة الأساسية Troll بدلاً من ذلك.
وهذا يعني أن مصمّمنا يستطيع الآن ضبط الصحة في Troll وستُحدَّث السلالات الثلاث كلها.
وكما يزداد عدد السلالات وعدد الصفات المختلفة لكل سلالة، يمكن أن يكون هذا موفّراً
للوقت بشكل كبير. الآن، وبقطعة شيفرة صغيرة إلى حد ما، لدينا نظام مفتوح النهاية يضع
التحكّم في أيدي مصمّمينا ويجعل أفضل استثمار لوقتهم. وفي الوقت نفسه، يمكننا أن نعود
إلى برمجة الميزات الأخرى.
القرارات التصميمية
يتيح لنا نمط كائن النوع أن نبني نظام أنواع كما لو كنا نصمّم لغة برمجة خاصة بنا. ففضاء التصميم مفتوح على مصراعيه، يمكننا أن نفعل فيه أشياء ممتعة من كل الأنواع.
ولكن في الواقع، فإن بضعة أمور تقيّد خيالنا. فالوقت وقابلية الصيانة سيثبّطانا عن أي شيء معقّد بشكل خاص. والأهم من ذلك، مهما كان نظام كائنات الأنواع الذي نصمّمه، فسنحتاج مستخدمنا (كثيراً ما يكونون غير مبرمجين) إلى أن يكونوا قادرين على فهمه بسهولة. وكلما تمكّنّا من تبسيطه، زادت قابليته للاستخدام. لذا سنغطّي هنا فضاء التصميم المُسالَك على نطاق واسع، ونترك الأطراف البعيدة للباحثين والمستكشفين.
هل كائن النوع مُغلَّف أم مكشوف؟
في تنفيذنا للعيّنة، لدى Monster مرجع إلى سلالة، لكنه لا يعرضها للعامة. فلا
تستطيع الشيفرة الخارجية الوصول مباشرةً إلى سلالة الوحش. ومن منظور قاعدة الشيفرة،
فالوحوش عديمة النوع إلى حد ما، وكونها تمتلك سلالات هو تفصيل تنفيذي.
ويمكننا تغيير ذلك بسهولة والسماح لـMonster بأن يعيد Breed:
^code 11
وفعل ذلك يغيّر تصميم Monster. فكون كل الوحوش لها سلالات يصبح الآن جزءاً علنياً
مرئياً من واجهته البرمجية. وهناك فوائد لكل خيار.
-
إذا كان كائن النوع مُغلَّفاً:
-
يكون تعقيد نمط كائن النوع مخفياً عن بقية قاعدة الشيفرة. فيصبح تفصيلاً تنفيذياً لا يقلق بشأنه إلا الكائن المُنمَّط.
-
يستطيع الكائن المُنمَّط أن يتجاوز سلوكاً من كائن النوع بشكل انتقائي. فلنقل إننا أردنا تغيير سلسلة هجوم الوحث عندما يقترب من الموت. وبما أن سلسلة الهجوم يُت إليها دائماً عبر
Monster، فلنا موضع مريح لوضع تلك الشيفرة:^code 12
لو كانت الشيفرة الخارجية تستدعي
getAttack()مباشرةً على السلالة، لما أتيح لنا الفرصة لإدراج ذلك المنطق. -
علينا كتابة دوال تمرير لكل ما يعرضه كائن النوع. وهذه هي الجزء المملّ من هذا التصميم. فإذا كان صنف كائن النوع لدينا لديه عدد كبير من الدوال، فسيحتاج صنف الكائن إلى دواله الخاصة لكل دالة نريدها أن تكون مرئية للعامة.
-
-
إذا كان كائن النوع مكشوفاً:
-
تستطيع الشيفرة الخارجية التفاعل مع كائنات النوع من غير أن تكون لها نسخة من الصنف المُنمَّط. فإذا كان كائن النوع مُغلَّفاً، فلا سبيل إلى استخدامه من غير أن يكون لديك كائن مُنمَّط يغلّفه. وهذا يمنعنا، على سبيل المثال، من استخدام نمط الباني حيث تُنشأ الوحوش الجديدة باستدعاء دالة على السلالة. فإذا لم يستطع المستخدمون الوصول إلى السلالات مباشرةً، لما استطاعوا استدعائها.
-
يصبح كائن النوع الآن جزءاً من الواجهة العامة للكائن. عموماً، تكون الواجهات الضيّقة أسهل في الصيانة من الواجهات الواسعة — فكلما قلّ ما تعرّضه لبقية قاعدة الشيفرة، قلّ التعقيد والصيانة التي عليك التعامل معها. وبكشف كائن النوع، فإننا نوسّع واجهة الكائن لتشمل كل ما يوفّره كائن النوع.
-
كيف تُنشأ الكائنات المُنمَّطة؟
مع هذا النمط، صار كل «كائن» زوجاً من الكائنات: الكائن الرئيسي وكائن النوع الذي يستخدمه. فكيف ننشئ الاثنين ونربطهما؟
-
ابنِ الكائن ومرّر إليه كائن نوعه:
- تستطيع الشيفرة الخارجية التحكّم في التخصيص. وبما أن الشيفرة المستدعية هي التي تبني الكائنين نفسها، فيستطيعها التحكّم في المكان الذي يحدث فيه ذلك في الذاكرة. وإذا أردنا لكائناتنا أن تكون قابلة للاستخدام في مجموعة متنوّعة من سيناريوهات الذاكرة (مخصِّصات مختلفة، على المكدّس، إلخ) فإن هذا يمنحنا المرونة اللازمة لذلك.
-
استدعِ دالة «باني» على كائن النوع:
- يتحكّم كائن النوع في تخصيص الذاكرة. وهذه هي الجهة الأخرى للعملة. فإذا كنا لا نريد أن نختار للمستخدمين مكان إنشاء كائناتنا في الذاكرة، فإن اشتراط المرور عبر دالة مصنع على كائن النوع يمنحنا التحكّم في ذلك. وقد يكون هذا مفيداً إذا أردنا أن نتأكد من أن كل كائناتنا تأتي من مجموعة الكائنات (Object Pool) أو من مخصّص ذاكرة آخر.
هل يمكن أن يتغيّر النوع؟
حتى الآن، افترضنا أن ما إن يُنشأ الكائن ويرتبط بكائن نوعه، فإن ذلك الارتباط لن يتغيّر أبداً. فالنوع الذي يُنشأ به الكائن هو النوع الذي يموت به. وهذا ليس ضرورياً تماماً، ف-يمكننا السماح لكائن بأن يغيّر نوعه مع مرور الوقت.
ولنُعِد النظر إلى مثالنا. حين يموت الوحش، يخبرنا المصمّمون أنهم أحياناً يريدون أن تصير جثته زومبياً مُبعثاً. يمكننا تنفيذ ذلك بإنشاء وحش جديد بسلالة زومبي حين يموت وحش، لكن خياراً آخر هو ببساطة أن نأخذ الوحش القائم ونغيّر سلالته إلى سلالة زومبي.
-
إذا كان النوع لا يتغيّر:
-
الأمر أبسط في البرمجة وفي الفهم معاً. فعلى المستوى المفاهيمي، «النوع» شيء لن يتوقّع معظم الناس تغيّره. وهذا النمط يُثبّت ذلك الافتراض.
-
أسهل في تصحيح الأخطاء. فإذا كنّا نحاول تتبّع خطأ جعل الوحش يدخل في حالة غريبة ما، فإن الأمر يُسهّل عملنا إن استطعنا أن نأخذ على عاتقنا أن سلالة الوحش الذي ننظر إليه الآن هي السلالة التي كان يملكها الوحش دائماً.
-
-
إذا كان النوع يمكن أن يتغيّر:
-
إنشاء الكائنات أقلّ. ففي مثالنا، إذا كان النوع لا يمكن أن يتغيّر، فسنُجبر على حرق دورات معالج لإنشاء وحش زومبي جديد، ونسخ أي صفات من الوحش الأصلي تحتاج إلى الحفاظ عليها، ثم حذفه. وإذا استطعنا تغيير النوع، فإن كل ذلك العمل يُستبدل بإسناد بسيط.
-
علينا أن نكون حذرين من تحقّق الافتراضات. فهناك اقتران متين نوعاً ما بين الكائن ونوعه. فسلالة مثلاً قد تفترض أن صحة الوحش الحالية لا تتجاوز أبداً صحة البداية التي تأتي من السلالة.
وإذا سمحنا للسلالة بالتغيير، فعلينا أن نتأكد من أن متطلبات النوع الجديد يحققها الكائن القائم. وحين نغيّر النوع، من المرجّح أن نضطر إلى تنفيذ شيفرة تحقّق ما نتأكد من أن الكائن أصبح الآن في حالة منطقية مع النوع الجديد.
-
ما نوع الوراثة المدعومة؟
-
بلا وراثة:
-
الأمر بسيط. فالأبسط هو الأفضل غالباً. فإذا لم تكن لديك كمية هائلة من البيانات التي تحتاج إلى مشاركتها بين كائنات النوع، فلماذا تجعل الأمور صعبة على نفسك؟
-
قد يؤدي إلى تكرار الجهد. فلم أرَ حتى اليوم نظام تأليف لا يريد فيه المصمّمون نوعاً ما من الوراثة. فحين لديك خمسون نوعاً مختلفاً من الأقزام، فإن ضبط صحتهم بتغيير الرقم نفسه في خمسين موضعاً مختلفاً أمر فظيع.
-
-
وراثة مفردة:
-
لا تزال بسيطة نسبياً. فتنفيذها سهل، لكن وهو الأهم أنها سهلة الفهم أيضاً. فإذا كان المستخدمون غير التقنيين هم من سيعمل مع النظام، فكلما قلّت الأجزاء المتحركة كان أفضل. وهناك سبب في أن كثيراً من لغات البرمجة لا تدعم سوى الوراثة المفردة. يبدو أنه التوازن المناسب بين القوة والبساطة.
-
البحث عن الصفات أبطأ. فللحصول على قطعة بيانات بعينها من كائن نوع، قد نضطر إلى الصعود في سلسلة الوراثة للعثور على النوع الذي يحسم القيمة في النهاية. فإذا كنا في شيفرة حرجة من حيث الأداء، فقد لا نرغب في إضاعة الوقت هنا.
-
-
وراثة متعددة:
-
يمكن تجنب تكرار كل البيانات تقريباً. فمع نظام وراثة متعددة جيد، يستطيع المستخدمون بناء تسلسل لكائنات نوع خالٍ من التكرار تقريباً. وحين يأتي وقت ضبط الأرقام، يمكننا تفادي الكثير من النسخ واللصق.
-
معقّدة. ولسوء الحظ، تبدو فوائد هذا أكثر نظرية منها عملية. فالوراثة المتعددة صعبة الفهم والتفكير فيها.
إذا كان نوع زومبي التنين لدينا يرث من زومبي ومن تنين معاً، فأي الصفات تأتي من زومبي وأيها يأتي من تنين؟ ولاستخدام النظام، سيحتاج المستخدمون إلى فهم كيف يُجتاز رسم الوراثة وأن يسبق البصيرة تصميم تسلسل ذكي.
معظم معايير ++C التي أراها اليوم تميل إلى منع الوراثة المتعددة، وJava وC# تفتقر إليها تماماً. وهذا اعتراف بحقيقة مؤلمة: فالأمر صعب إلى حد أن الأفضل غالباً ألا تستخدمه على الإطلاق. ورغم أنه يستحق التفكير، فمن النادر جداً ما سترغب في استخدام الوراثة المتعددة لكائنات النوع في ألعابك. وكما دائماً، الأبسط هو الأفضل.
-
انظر أيضاً
-
المشكلة عالية المستوى التي يعالجها هذا النمط هي مشاركة البيانات والسلوك بين عدة كائنات. ونمط آخر يعالج المشكلة نفسها بطريقة مختلفة هو النموذج الأولي (Prototype).
-
كائن النوع قريبٌ من الوزن الخفيف (Flyweight). فكلاهما يتيح لك مشاركة البيانات بين النسخ. ومع الوزن الخفيف، تكون النيّة في توفير الذاكرة، وقد لا تمثّل البيانات المشتركة أي نوع مفاهيمي من الكائنات. ومع نمط كائن النوع، يكون التركيز على التنظيم والمرونة.
-
هناك قدر كبير من التشابه بين هذا النمط ونمط الحالة (State). فكلاهما يتيح لكائن أن يفوّض جزءاً مما يعرّف به نفسه إلى كائن آخر. ومع كائن النوع، فإننا نفوّض عادةً ما هو عليه الكائن: بيانات ثابتة تصف الكائن وصفاً عاماً. ومع الحالة، فإننا نفوّض ما هو عليه الكائن الآن: بيانات زمنية تصف تكوينه الحالي.
وحين ناقشنا جعل كائن يغيّر نوعه، فيمكنك أن ترى في ذلك أن يخدم كائن النوع غرضين بوصفه حالة أيضاً.