نمط وزن الذبابة (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) وتضعه داخل فئة وزنِ ذبابةِ حقيقية.
البنية
ضع الصورة.
- نمط وزن الذبابة هو مجرد تحسين أو تطوير للحل الموجود مسبقًا، فتأكد قبل استخدامه أن البرنامج يعاني فعلًا من مشكلة في استهلاك ذاكرة RAM بسبب احتوائه على عدد كبير من الكائنات المتشابهة داخل الذاكرة في نفس الوقت، وتأكد أيضًا أن هذه المشكلة لا يمكن حلها بأي شكل آخر.
- تحتوي فئة Flyweight (وزن الذبابة) على جزء من حالة الكائن الأصلي يمكن مشاركته بين عدة كائنات ، ويمكن استخدام كائن وزن الذبابة نفسه في سياقات مختلفة، وتسمى الحالة المخزَّنة داخل كائن وزن ذبابة بالجوهرية (intrinsic)، أما الحالة التي تُمرَّر إلى أساليب وزن الذبابة فيطلق عليها المؤقتة أو العابرة (extrinsic).
- فئة Context (السياق) تحتوي الحالة المؤقتة الفريدة لكل الكائنات الأصلية، وحين يُقرن سياق بأحد كائنات وزن الذبابة فإنه يمثل الحالة الكاملة للكائن الأصلي.
- عادة يظل سلوك الكائن الأصلي في فئة وزن الذبابة (flyweight class)، وفي تلك الحالة فيجب على من يستدعي أسلوب وزن الذبابة تمرير الأجزاء المناسبة من الحالة المؤقتة (extrinsic state) إلى معامِلات الأسلوب. لكن من الناحية الأخرى فيمكن نقل السلوك إلى فئة السياق، والتي ستستخدم كائن وزن الذبابة المرتبط بالكاد على أنه كائن بيانات.
- يحسب العميل (Client) الحالة المؤقتة لكائنات وزن الذبابة أو يخزنها، ويرى كائنَ وزن الذبابة على أنه كائنٌ قالبٌ (Template Object) يمكن تهيئته أثناء وقت التشغيل (runtime) بتمرير بعض البيانات السياقية (Contextual Data) إلى معامِلات من أساليبه.
- يدير مصنعُ وزنِ الذبابةِ حقلًا من كائنات وزن الذبابة الموجودة فعلًا، ولا ينشئ العملاء كائنات وزن ذبابة بوجود المصنع، وإنما يستدعونه ويمررون إليه أجزاءً من الحالة الجوهرية لكائن وزن الذبابة المطلوب، ويبحث المصنع في الكائنات الموجودة والتي أنشئت من قبل، ويعيد أحدها مما يطابق المواصفات المذكورة في البحث، أو ينشئ واحدًا جديدًا إن لم يجد.
مثال توضيحي
في هذا المثال، يساهم نمط وزن الذبابة في تقليل استهلاك الذاكرة عند معالجة ملايين الكائنات من الشجرة في مساحة ما.
ضع الصورة.
يستخرج النمط الحالة الجوهرية المتكررة من فئة Tree الأساسية، وينقلها إلى فئة وزن الذبابة TreeType. والآن بدلًا من تخزين نفس البيانات في عدة كائنات فإنها تُحفظ في كائنات قليلة من نوع وزن الذبابة، وتُربط بكائنات Tree المناسبة والتي تتصرف كسياقات (Contextes). وتنشئ شيفرة العميل كائنات شجرية جديدة باستخدام مصنع وزن الذبابة الذي يغلف تعقيد البحث عن الكائن المناسب ويعيد استخدامه إن دعت الحاجة لذلك.
// تحتوي فئة وزن الذبابة على جزء من حالة الشجرة، وهذه الحقول تخزن قيمًا
// فريدة لكل شجرة، فمثلًا لن تجد هنا إحداثيات الشجرة، لكن النقش واللون
// المشترك بين عدة شجرات يكون هنا.
// وبما أن هذه البيانات تكون كبيرة في العادة، فستضيع ذاكرة كثيرة إن
// حفظتها في كل كائن في الشجرة، وبدلًا من ذلك فإننا نستخرج النقش واللون
// وأي بيانات متكررة إلى كائن منفصل يمكن أن ترجع إليه كائنات كثيرة
// من الشجرة.
class TreeType is
field name
field color
field texture
constructor TreeType(name, color, texture) { ... }
method draw(canvas, x, y) is
// 1. من معطيات اللون والنوع والنقش bitmap أنشئ ملف.
// 2. داخل مساحة الرسم Y و X تلك في إحداثيات bitmap ارسم صورة.
// (Canvas)
// يقرر مصنع وزن الذبابة ما إن كان سيستخدم كائن وزن الذبابة الموجود
// أم ينشئ واحدًا جديدًا.
class TreeFactory is
static field treeTypes: collection of tree types
static method getTreeType(name, color, texture) is
type = treeTypes.find(name, color, texture)
if (type == null)
type = new TreeType(name, color, texture)
treeTypes.add(type)
return type
// يحتوي الكائن السياقي على الجزء المؤقت من حالة الشجرة، ويستطيع
// التطبيق أن ينشئ مليارات من هذه الكائنات بما أنها صغيرة الحجم،
// فلا تتكون إلا من إحداثيين رقميين وحقل مرجعي واحد.
class Tree is
field x,y
field type: TreeType
constructor Tree(x, y, type) { ... }
method draw(canvas) is
type.draw(canvas, this.x, this.y)
// فئتي Forest و Tree هما عملاء لكائن وزن الذبابة، ويمكنك دمج هذين
// إن لم تكن تنوي تطوير فئة Tree أكثر مما هي عليه.
class Forest is
field trees: collection of Trees
method plantTree(x, y, name, color, texture) is
type = TreeFactory.getTreeType(name, color, texture)
tree = new Tree(x, y, type)
trees.add(tree)
method draw(canvas) is
foreach (tree in trees) do
tree.draw(canvas)
قابلية التطبيق
استخدم نمط وزن الذبابة حين يتحتم على تطبيقك أن يدعم عددًا ضخمًا من الكائنات التي تكفيها الذاكرة الحالية بالكاد.
فائدة استخدام النمط تعتمد بشكل كبير على كيف وأين يُستخدم، فهو أكثر فائدة في الحالات الآتية:
تطبيق يحتاج إلى إنتاج عدد كبير من الكائنات المتشابهة.
هذا يستهلك ذاكرة RAM كبيرة على الجهاز الهدف.
وتحتوي الكائنات على حالات متكررة يمكن استخراجها ومشاركتها بين كائنات متعددة.
كيفية الاستخدام
- قسّم حقول الفئة التي ستصبح وزن ذبابة إلى جزئين.
- الحالة الجوهرية: الحقول التي تحتوي بيانات لا تتغير ومكررة في كائنات كثيرة.
- الحالة المؤقتة: الحقول التي تحتوي على بيانات سياقية فريدة لكل كائن.
- اترك الحقول التي تمثل الحالة الجوهرية في الفئة، لكن تأكد أنها لا يمكن تغييرها، إذ ينبغي أن تأخذ قيماها الأولية (initial values) داخل المنشئ (Constructor) فقط.
- اذهب إلى الأساليب التي تستخدم حقول الحالة المؤقتة، وأدخل معامِلًا جديدًا لكل حقل مستخدم في الأسلوب، واستخدم ذلك المعامل بدلًا من الحقل.
- لديك الخيار في إنشاء فئة مصنع لإدارة حقل من كائنات وزن الذبابة، ينبغي أن تتفقد وجود كائنات وزن ذبابة قبل إنشاء جديد منها. أيضًا، يجب أن يطلب العملاء كائنات وزن الذبابة من خلال المصنع فقط بمجرد أن يكون جاهزًا، فيصفوا الكائن المطلوب بتمرير حالته الجوهرية إلى المصنع.
- يجب أن يخزن العميل أو يحسب قيم الحالة المؤقتة (السياق) ليستطيع استدعاء أساليب كائنات وزن الذبابة. وقد تُنقل الحالة المؤقتة مع الحقل المرجعي لكائن وزن الذبابة بداعي التيسير إلى فئة سياق (context) منفصلة.
المزايا والعيوب
المزايا
- تستطيع حفظ مقدار كبير من ذاكرة RAM بافتراض أن برنامجك به كائنات كثيرة متشابهة.
العيوب
- قد يعني استخدامك لنمط وزن الذبابة تفضيلك للذاكرة على المعالج إن احتاجت بعض البيانات السياقية إلى إعادة الحساب في كل مرة يستدعي أحدهم أسلوب وزن الذبابة.
- تصبح الشيفرة أكثر تعقيدًا، ولا يعرف الأفراد الجدد في الفريق سبب وجود حالة كيان ما بشكل منفصل مثلًا.
العلاقات مع الأنماط الأخرى
تستطيع استخدام عُقد ورقية مشتركة (shared leaf nodes) من شجرة لنمط المركب، تستطيع استخدامها ككائنات وزن ذبابة من أجل توفير ذاكرة RAM.
يوضح نمط وزن الذبابة كيف تصنع الكثير من الكائنات الصغيرة، في حين يريك نمط الواجهة كيف تصنع كائنًا واحدًا يمثل النظام الفرعي بأكمله.
إن تمكنت من تقليل كل الحالات المشتركة للكائنات إلى كائن وزن ذبابة واحد، فحينها سيكون وزن الذبابة مشابهًا لنمط المفردة. لكن هناك اختلافان أساسيان بين هذين النمطين:
- يجب أن تكون هناك نسخة واحدة فقط من المفردة، بينما يمكن لفئة وزن الذبابة أن تكون لها نسخ كثيرة وبحالات جوهرية مختلفة.
- يمكن أن يكون كائن المفردة متغيرًا، بينما كائنات وزن الذبابة غير قابلة للتغير.
الاستخدام في لغة جافا
المستوى: ★ ★ ☆
الانتشار: ★ ☆ ☆
أمثلة الاستخدام: هدف نمط وزن الذبابة هو تقليل استهلاك الذاكرة، فإن كان برنامجك لا يعاني من قصور في الذاكرة العشوائية فربما تود تجاهل هذا النمط إلى حين. تجد أمثلة لنمط وزن الذبابة في مكتبات جافا التالية:
(java.lang.Intefer#valueof(int (وكذلك Boolean - Byte - Character - Short - Long - BigDecimal).
يمكن ملاحظة نمط وزن الذبابة من خلال أسلوب إنشائي يعيد كائنات محفوظة مسبقًا (cached) بدلًا من إنشاء كائنات جديدة.
إخراج غابة (Rendering a forest)
في هذا المثال سنخرِج غابة من مليون شجرة، وكل شجرة ستُمثَّل بكائنها الخاص الذي فيه بعض بيانات حالتها (الإحداثيات والنقوش، ..) ورغم أن البرنامج ينفذ وظيفته الأساسية إلا أنه يستهلك ذاكرة RAM بشراهة.
وسبب ذلك هو وجود عدد ضخم من كائنات الشجرة التي تحتوي على بيانات متكررة مثل الاسم والنقوش واللون، ولهذا السبب تحديدًا نستطيع تطبيق نمط وزن الذبابة على هذا المثال وتخزين هذه القيم داخل كائنات وزن ذبابة منفصلة (فئة TreeType).
وسنجعل أحد كائنات وزن الذبابة كائنًا مرجعيًا بمجموعة محددة من البيانات تُخزَّن فيه، بدلًا من تخزين نفس البيانات في آلاف من كائنات Tree. ولن تلاحظ شيفرة العميل أي تغيير بما أن تعقيد إعادة استخدام كائنات وزن الذبابة يختفي داخل مصنع وزن ذبابة (flyweight factory).
الأشجار
trees/Tree.java: تحتوي حالة فريدة لكل شجرة
package refactoring_guru.flyweight.example.trees;
import java.awt.*;
public class Tree {
private int x;
private int y;
private TreeType type;
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void draw(Graphics g) {
type.draw(g, x, y);
}
}
trees/TreeType.java: تحتوي حالة مشتركة بين عدة أشجار
package refactoring_guru.flyweight.example.trees;
import java.awt.*;
public class TreeType {
private String name;
private Color color;
private String otherTreeData;
public TreeType(String name, Color color, String otherTreeData) {
this.name = name;
this.color = color;
this.otherTreeData = otherTreeData;
}
public void draw(Graphics g, int x, int y) {
g.setColor(Color.BLACK);
g.fillRect(x - 1, y, 3, 5);
g.setColor(color);
g.fillOval(x - 5, y - 10, 10, 10);
}
}
trees/TreeFactory.java: يغلِّف التعقيد الكامن في إنشاء كائن وزن الذبابة
package refactoring_guru.flyweight.example.trees;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
public class TreeFactory {
static Map<String, TreeType> treeTypes = new HashMap<>();
public static TreeType getTreeType(String name, Color color, String otherTreeData) {
TreeType result = treeTypes.get(name);
if (result == null) {
result = new TreeType(name, color, otherTreeData);
treeTypes.put(name, result);
}
return result;
}
}
الغابة
forest/Forest.java: الغابة التي نرسمها
package refactoring_guru.flyweight.example.forest;
import refactoring_guru.flyweight.example.trees.Tree;
import refactoring_guru.flyweight.example.trees.TreeFactory;
import refactoring_guru.flyweight.example.trees.TreeType;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
public class Forest extends JFrame {
private List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, Color color, String otherTreeData) {
TreeType type = TreeFactory.getTreeType(name, color, otherTreeData);
Tree tree = new Tree(x, y, type);
trees.add(tree);
}
@Override
public void paint(Graphics graphics) {
for (Tree tree : trees) {
tree.draw(graphics);
}
}
}
Demo.java: شيفرة العميل
package refactoring_guru.flyweight.example;
import refactoring_guru.flyweight.example.forest.Forest;
import java.awt.*;
public class Demo {
static int CANVAS_SIZE = 500;
static int TREES_TO_DRAW = 1000000;
static int TREE_TYPES = 2;
public static void main(String[] args) {
Forest forest = new Forest();
for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) {
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Summer Oak", Color.GREEN, "Oak texture stub");
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Autumn Oak", Color.ORANGE, "Autumn Oak texture stub");
}
forest.setSize(CANVAS_SIZE, CANVAS_SIZE);
forest.setVisible(true);
System.out.println(TREES_TO_DRAW + " trees drawn");
System.out.println("---------------------");
System.out.println("Memory usage:");
System.out.println("Tree size (8 bytes) * " + TREES_TO_DRAW);
System.out.println("+ TreeTypes size (~30 bytes) * " + TREE_TYPES + "");
System.out.println("---------------------");
System.out.println("Total: " + ((TREES_TO_DRAW * 8 + TREE_TYPES * 30) / 1024 / 1024) +
"MB (instead of " + ((TREES_TO_DRAW * 38) / 1024 / 1024) + "MB)");
}
private static int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}
}
OutputDemo.png: لقطة للشاشة
ضع الصورة
OutputDemo.txt: إحصاءات استهلاك RAM
1000000 trees drawn
---------------------
Memory usage:
Tree size (8 bytes) * 1000000
+ TreeTypes size (~30 bytes) * 2
---------------------
Total: 7MB (instead of 36MB)