نمط وزن الذبابة (Flyweight)

من موسوعة حسوب

نمط وزن الذبابة (Flyweight) هو نمط تصميم هيكلي يسمح لك بإدخال كائنات أكثر داخل المساحة المتاحة لديك من الذاكرة العشوائية RAM من خلال مشاركة أجزاء مشتركة بين عدة كائنات بدلًا من إبقاء نسخة من البيانات داخل كل كائن.

المشكلة

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

ثم إنك أرسلتها إلى صديق لك ليجربها عندما أنهيتها، لكنه لم يستطع متابعة اللعب لوقت طويل بسبب انهيار اللعبة مرة بعد مرة -رغم أنها تعمل بسلاسة على حاسوبك-، ولما بحثت في سجلات التنقيح (Debug logs) وجدت أن اللعبة تتعطل بسبب عدم كفاية الذاكرة العشوائية، لأن حاسوب صديقك أضعف من حاسوبك، لهذا ظهرت المشكلة عنده سريعًا.

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

ضع الصورة.

الحل

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

ضع الصورة

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

ويطلق على هذه البيانات الثابتة للجزء عادة اسم الحالة الجوهرية (intrinsic state)، وهي توجد داخل الكائن، وتستطيع الكائنات الأخرى قراءتها فقط دون تغييرها، أما بقية حالة الكائن فإنها تُغيَّر من الخارج فقط في الغالب بواسطة كائنات أخرى، ويطلق عليها عندها الحالة المؤقتة (extrinsic state).

ويقترح نمط وزن الذبابة أن تتوقف عن تخزين الحالة المؤقتة داخل الكائن، وإنما تمرر تلك الحالة إلى أساليب محددة تعتمد عليها، ولا يبقى داخل الكائن سوى الحالة الجوهرية فقط، مما يسمح لك باستخدامه في سياقات مختلفة. وكنتيجة لهذا فستحتاج إلى كائنات أقل بما أنها تختلف فقط في الحالة الجوهرية، والتي بها متغيرات أقل بكثير من الحالة العارضة.

ضع الصورة.

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

تخزين الحالة المؤقتة

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

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

ضع الصورة.

ترجمة الصورة. 1: اذهب إلى مصفوفة particles وحاول إيجاد جزء باللون والنقش المطلوبين، إن لم تجد فأنشئ واحدًا جديدًا.

2. أنشئ جزءًا متحركًا بالبيانات المعطاة وكائن الجزء من الخطوة الأولى.

وهناك حل أفضل بأن تنشئ فئة سياق منفصلة تخزن الحالة المؤقتة مع مرجع إلى كائن وزن الذبابة، وسيتطلب هذا المنظور أن يكون لديك مصفوفة وحيدة في الفئة الحاوية (container class).

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

وزن الذبابة والجمود

بما أن نفس كائن وزن الذبابة يمكن إعادة استخدامه في سياقات مختلفة إلا أنك يجب أن تتأكد أن حالته لا يمكن تغييرها، ذلك أن هذا الكائن يجب أن يبدأ حالته (initialize) مرة واحدة فقط من خلال معامِلاتِ منشئ (Constructor Parameters)، ولا ينبغي أن يكشف أي محدِّدات أو حقول عامة للكائنات الأخرى.

مصنع وزن الذبابة

يمكنك إنشاء أسلوب مصنع (Factory method) يدير حقلًا من كائنات وزن الذبابة الموجودة، من أجل تسهيل الوصول إلى الأنواع المختلفة من تلك الكائنات، ويقبل الأسلوبُ الحالةَ الجوهريةَ (intrinsic state) لكائن وزن الذبابة المرغوب فيه من عميل ما، ويبحث عن كائن وزن ذبابة في الموجودين لديه يكون مماثلًا لهذه الحالة ويعيده إن وجد، أو ينشئ واحدًا جديدًا ويضيفه إلى الحقل.

هناك عدة أماكن يمكن استخدام هذه الطريقة فيها، أحد أبرز هذه الأماكن هو حاوية وزن الذبابة (flyweight container)، لكن تستطيع إنشاء فئة مصنع جديد كحل بديل، أو تجعل أسلوب المصنع ثابتًا (static) وتضعه داخل فئة وزنِ ذبابةِ حقيقية.

البنية

ضع الصورة.

  1. نمط وزن الذبابة هو مجرد تحسين أو تطوير للحل الموجود مسبقًا، فتأكد قبل استخدامه أن البرنامج يعاني فعلًا من مشكلة في استهلاك ذاكرة RAM بسبب احتوائه على عدد كبير من الكائنات المتشابهة داخل الذاكرة في نفس الوقت، وتأكد أيضًا أن هذه المشكلة لا يمكن حلها بأي شكل آخر.
  2. تحتوي فئة Flyweight (وزن الذبابة) على جزء من حالة الكائن الأصلي يمكن مشاركته بين عدة كائنات ، ويمكن استخدام كائن وزن الذبابة نفسه في سياقات مختلفة، وتسمى الحالة المخزَّنة داخل كائن وزن ذبابة بالجوهرية (intrinsic)، أما الحالة التي تُمرَّر إلى أساليب وزن الذبابة فيطلق عليها المؤقتة أو العابرة (extrinsic).
  3. فئة Context (السياق) تحتوي الحالة المؤقتة الفريدة لكل الكائنات الأصلية، وحين يُقرن سياق بأحد كائنات وزن الذبابة فإنه يمثل الحالة الكاملة للكائن الأصلي.
  4. عادة يظل سلوك الكائن الأصلي في فئة وزن الذبابة (flyweight class)، وفي تلك الحالة فيجب على من يستدعي أسلوب وزن الذبابة تمرير الأجزاء المناسبة من الحالة المؤقتة (extrinsic state) إلى معامِلات الأسلوب. لكن من الناحية الأخرى فيمكن نقل السلوك إلى فئة السياق، والتي ستستخدم كائن وزن الذبابة المرتبط بالكاد على أنه كائن بيانات.
  5. يحسب العميل (Client) الحالة المؤقتة لكائنات وزن الذبابة أو يخزنها، ويرى كائنَ وزن الذبابة على أنه كائنٌ قالبٌ (Template Object) يمكن تهيئته أثناء وقت التشغيل (runtime) بتمرير بعض البيانات السياقية (Contextual Data) إلى معامِلات من أساليبه.
  6. يدير مصنعُ وزنِ الذبابةِ حقلًا من كائنات وزن الذبابة الموجودة فعلًا، ولا ينشئ العملاء كائنات وزن ذبابة بوجود