زمان تخمینی مطالعه: 13 دقیقه
الگوی Flyweight یکی از الگوهای طراحی Structural است زیرا این الگو راههایی را برای کاهش تعداد اشیا ارائه میدهد و در نتیجه ساختار اشیاء مورد نیاز کاربرد را بهبود میبخشد. الگوی Flyweight زمانی که ما نیاز به ایجاد تعداد زیادی از اشیاء مشابه داشته باشیم (مثلاً تعداد 213) استفاده میشود. یکی از ویژگیهای مهم اجسام با وزن سبک تغییر ناپذیر بودن آنهاست. این بدان معنی است که آنها را نمیتوان پس از ساخت تغییر داد. الگوی Flyweight که با نام Cache هم شناخته میشود یک الگوی طراحی است که به کاربر امکان میدهد اشیاء بیشتری را در مقدار RAM در دسترس قرار دهد. این الگو با به اشتراک گذاشتن قسمتهای مشترک حالت بین چندین شیء به جای نگه داشتن تمام دادهها در هر شی، کار میکند.
بیان مسئله: فرض کنید تصمیم گرفتید یک بازی ویدیویی ساده بسازید که در آن بازیکنان بر روی نقشه حرکت میکنند و به یکدیگر تیراندازی میکنند. در این بازی شما تصمیم گرفتید که یک سیستم ذرات واقعی را پیادهسازی کنید و آن را به یکی از ویژگیهای متمایز بازی تبدیل کنید. در ساختار طراحی شده مقادیر زیادی گلوله، موشک و ترکشهای ناشی از انفجارها باید در سراسر نقشه پرواز کنند و تجربهای هیجان انگیز را به بازیکن ارائه دهند. پس از اتمام مراحل، آخرین commit را انجام داده و بازی را ساختید و آن را برای دوست خود برای تست اجرا فرستادید. اگرچه بازی بدون نقص روی دستگاه شما اجرا میشد، اما دوست شما نمیتوانست برای مدت طولانی بازی کند. در رایانه او، متاسفانه بازی پس از چند دقیقه گیم پلی از کار میافتد. پس از صرف چندین ساعت جستجو در لاگهای اشکال زدایی، متوجه شدید که بازی به دلیل مقدار ناکافی RAM از کار افتاده است. معلوم شد که سیستم گرافیکی رایانه دوست شما بسیار ضعیفتر از رایانه شما است و به همین دلیل است که مشکل در دستگاه او ظاهر شده است.
با بررسی بیشتر متوجه میشوید که مشکل واقعی مربوط به سیستم ذرات طراحی شده توسط شما است. هر ذره، مانند یک گلوله، یک موشک یا یک قطعه ترکش با یک جسم جداگانه نشان داده میشد که حاوی دادههای زیادی بود. زمانی که صحنههای اکشن و درگیری روی صفحه نمایش به اوج خودمیرسد، ذرات تازه ایجاد شده دیگر در حافظه رم باقیمانده نمیگنجید، بنابراین برنامه از کار میافتاد.
با بررسی دقیقتر کلاس Particle، ممکن است متوجه شوید که فیلدهای رنگ و sprite(تصویری که ذرات را نمایش میدهد) نسبت به سایر فیلدها حافظه بیشتری مصرف میکنند. بدتر این است که این دو فیلد دادههای تقریباً یکسانی را در همه ذرات ذخیره میکنند. به عنوان مثال، همه گلولهها دارای رنگ و sprite یکسان هستند.
بخشهای دیگر حالت یک ذره، مانند مختصات، بردار حرکت و سرعت، برای هر ذره منحصر به فرد است. به هر حال، مقادیر این فیلدها در طول زمان تغییر میکنند. این دادهها نشان دهنده زمینه(Context) همیشه در حال تغییری است که ذره در آن وجود دارد، در حالی که رنگ و sprite برای هر ذره ثابت میماند. دادههای ثابت یک شی معمولاً حالت ذاتی(intrinsic state) نامیده میشود. این حالت درون جسم زندگی میکند و اشیاء دیگر فقط میتوانند آن را بخوانند و نه اینکه آن را تغییر دهند. بقیه حالات جسم که اغلب توسط اجسام دیگر “از بیرون” تغییر میکند، حالت بیرونی(extrinsic state) نامیده میشود. الگوی Flyweight به شما پیشنهاد میکند که ذخیره حالت بیرونی را در داخل جسم متوقف کنید. در عوض، شما باید این حالت را به روشهای خاصی که بر آن تکیه دارند، منتقل کنید. فقط حالت ذاتی درون شی باقی میماند و به شما امکان میدهد از آن در زمینه های مختلف استفاده مجدد کنید. در نتیجه، شما به تعداد کمتری از این اشیاء نیاز خواهید داشت، زیرا آنها فقط در حالت ذاتی متفاوت هستند، که تغییرات بسیار کمتری نسبت به حالت بیرونی دارد.
اجازه دهید به بازی خودمان برگردیم با فرض اینکه حالت بیرونی را از کلاس ذرات خود استخراج کرده باشیم، تنها سه جسم مختلف برای نشان دادن همه ذرات در بازی کافی است که شامل یک گلوله، یک موشک و یک قطعه ترکش است. همانطور که احتمالاً تا به حال حدس زدهاید، جسمی که فقط حالت ذاتی را ذخیره می کند، Flyweight نامیده میشود.
– ذخیرهسازی حالت بیرونی
در حالت کلی مقصد حالت بیرونی کجاست؟ آیا باید برخی از کلاسها هنوز باید آن را ذخیره کنند یا نه؟ در بیشتر موارد، به شی کانتینر منتقل میشود، که قبل از اعمال الگو، اشیاء را جمع میکند. در مورد مثال ما، این شیء اصلی Game است که تمام ذرات را در فیلد particles ذخیره میکند. برای انتقال حالت بیرونی به این کلاس، باید چندین فیلد آرایه برای ذخیره مختصات، بردارها و سرعت هر ذره ایجاد کنید. اما این موارد در واقع همه چیز را شامل نمیشود. شما به آرایه دیگری برای ذخیره ارجاعات به یک Flyweight خاص که نشان دهنده یک ذره است نیاز دارید. این آرایهها باید همگام باشند تا بتوانید با استفاده از شاخص یکسان به تمام دادههای یک ذره دسترسی داشته باشید.
یک راه حل زیباتر ایجاد یک کلاس زمینه(Context) جداگانه است که حالت بیرونی را همراه با ارجاع به جسم Flyweight ذخیره میکند. این رویکرد مستلزم داشتن تنها یک آرایه در کلاس کانتینر است. در این جا سوالی پیش میآید که آیا نیازی نیست که به همان اندازه که در ابتدا داشتیم از این اشیاء زمینهای(contextual) داشته باشیم؟ از نظر فنی بله اما نکته اینجاست که این اجسام بسیار کوچکتر از قبل هستند. فیلدها با بیشترین مصرف حافظه به چند جسم Flyweight منتقل شدهاند. اکنون، هزاران شیء زمینهای کوچک میتوانند از یک شیء سنگین Flyweight به جای ذخیره هزاران نسخه از دادههای آن، دوباره استفاده کنند.
– Flyweight و تغییر ناپذیری
از آنجایی که یک شی Flyweight میتواند در زمینههای مختلف استفاده شود، باید مطمئن شوید که حالت آن قابل تغییر نیست. یک الگوی Flyweight باید حالت خود را فقط یک بار، از طریق پارامترهای سازنده، مقداردهی اولیه کند. نباید هیچ تنظیم کننده یا فیلد عمومی را در معرض اشیاء دیگر قرار دهد.
– کارخانه Flyweight
برای دسترسی راحتتر به Flyweight های مختلف، میتوانید یک روش Factory ایجاد کنید که مجموعهای از اشیاء با Flyweight موجود را مدیریت میکند. این روش حالت ذاتی Flyweight مورد نظر را از مشتری میپذیرد، به دنبال یک شی Flyweight موجود مطابق با این حالت میگردد و در صورت یافتن آن را بر میگرداند. در غیر این صورت، Flyweight جدیدی ایجاد میکند و آن را به استخر اضافه میکند.
چندین گزینه وجود دارد که این روش را میتوان در آنها قرار داد. واضحترین مکان یک کانتینر Flyweight است. از طرف دیگر، میتوانید یک کلاس Factory جدید ایجاد کنید. یا میتوانید روش Factory ای را ثابت کنید و آن را در یک کلاس Flyweight واقعی قرار دهید.
ساختار الگوی Flyweight
در این بخش به بررسی ساختار الگوی Flyweight میپردازیم و پیادهسازی آن را به صورت UML خواهیم دید.
‘
- گام 1: الگوی Flyweight صرفا یک بهینهسازی است. قبل از اعمال آن، مطمئن شوید که برنامه شما مشکل مصرف RAM مربوط به داشتن تعداد زیادی از اشیاء مشابه در حافظه را به طور همزمان دارد. اطمینان حاصل کنید که این مشکل را نمیتوان به روش معنی دار دیگری حل کرد.
- گام 2: کلاس Flyweight شامل بخشی از حالت اصلی شی است که میتواند بین چندین شی به اشتراک گذاشته شود. یک جسم Flyweight یکسان را میتوان در زمینههای مختلف استفاده کرد. حالتی که در داخل یک Flyweight ذخیره میشود، ذاتی (intrinsic) نامیده میشود. حالتی که به متدهای Flyweight منتقل میشود، بیرونی(extrinsic) نامیده میشود.
- گام 3: کلاس Context شامل حالت بیرونی است که در تمام اشیاء اصلی منحصر به فرد است. هنگامی که یک Context با یکی از اشیاء Flyweight وابسته میشود، وضعیت کامل جسم اصلی را نشان میدهد.
- گام 4: معمولاً رفتار جسم اصلی در کلاس flyweight باقی میماند. در این حالت، کسی که متد flyweight را فراخوانی میکند، باید بیتهای مناسبی از حالت بیرونی را نیز به پارامترهای متد ارسال کند. از سوی دیگر، رفتار را میتوان به کلاس زمینه منتقل کرد، که از flyweight مرتبط صرفاً به عنوان یک شی داده استفاده میکند.
- گام 5: مشتری وضعیت بیرونیflyweight را محاسبه یا ذخیره میکند. از دیدگاه مشتری، flyweight یک شی الگو است که میتواند در زمان اجرا با ارسال برخی از دادههای متنی به پارامترهای متدهای آن پیکربندی شود.
- گام 6: کارخانه Flyweight مجموعهای از Flyweight موجود را مدیریت میکند. در factory، مشتریان مستقیماً الگوی Flyweight ایجاد نمیکنند. درعوض، آنها کارخانه را صدا میزنند و قطعاتی از حالت ذاتی Flyweight مورد نظر را به آن منتقل میکنند. کارخانه به Flyweight ای که قبلاً ایجاد شده بود نگاه میکند و یا Flyweight موجود را برمیگرداند که با معیارهای جستجو مطابقت دارد یا اگر چیزی پیدا نشد، وزن جدیدی ایجاد میکند.
شبه کد(Pseudocode)
در این مثال، الگوی Flyweight به کاهش استفاده از حافظه هنگام رندر کردن میلیونها شی درخت روی بوم کمک میکند.
این الگو حالت ذاتی مکرر را از یک کلاس Tree اصلی استخراج میکند و آن را به کلاس TreeType میبرد. اکنون به جای ذخیره دادههای مشابه در چندین شیء، فقط در چند شیء با Flyweight نگهداری میشود و به اشیاء Tree مناسب که به عنوان زمینه عمل میکنند، پیوند داده میشود. کد مشتری اشیاء درختی جدیدی را با استفاده از کارخانه flyweight ایجاد میکند، که پیچیدگی جستجوی شی مناسب و استفاده مجدد از آن در صورت نیاز را در بر میگیرد.
// The flyweight class contains a portion of the state of a
// tree. These fields store values that are unique for each
// particular tree. For instance, you won't find here the tree
// coordinates. But the texture and colors shared between many
// trees are here. Since this data is usually BIG, you'd waste a
// lot of memory by keeping it in each tree object. Instead, we
// can extract texture, color and other repeating data into a
// separate object which lots of individual tree objects can
// reference.
class TreeType is
field name
field color
field texture
constructor TreeType(name, color, texture) { ... }
method draw(canvas, x, y) is
// 1. Create a bitmap of a given type, color & texture.
// 2. Draw the bitmap on the canvas at X and Y coords.
// Flyweight factory decides whether to re-use existing
// flyweight or to create a new object.
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
// The contextual object contains the extrinsic part of the tree
// state. An application can create billions of these since they
// are pretty small: just two integer coordinates and one
// reference field.
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)
// The Tree and the Forest classes are the flyweight's clients.
// You can merge them if you don't plan to develop the Tree
// class any further.
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)
قابلیتها و کاربردها
- از الگوی Flyweight فقط زمانی استفاده کنید که برنامه شما باید تعداد زیادی از اشیاء را پشتیبانی کند که به سختی در RAM موجود قرار میگیرند. مزیت اعمال الگو به شدت به نحوه و مکان استفاده از آن بستگی دارد و زمانی مفیدتر است که:
- یک برنامه باید تعداد زیادی از اشیاء مشابه را ایجاد کند
- تمام RAM موجود در دستگاه مورد نظر را تخلیه میکند
- اشیاء حاوی حالتهای تکراری هستند که میتوانند استخراج شوند و بین چندین شی به اشتراک گذاشته شوند
نحوه پیادهسازی
- فیلدهای یک کلاس که تبدیل به Flyweight میشود را به دو قسمت تقسیم کنید:
- حالت ذاتی: فیلدهایی که حاوی دادههای بدون تغییر هستند که در بسیاری از اشیاء کپی شدهاند
- حالت بیرونی: فیلدهایی که حاوی دادههای متنی منحصر به فرد برای هر شی هستند
- فیلدهایی را که حالت ذاتی را در کلاس نشان میدهند، رها کنید، اما مطمئن شوید که تغییر ناپذیر هستند. آنها باید مقادیر اولیه خود را فقط در داخل سازنده بگیرند.
- متدهایی را که از فیلدهای حالت بیرونی استفاده میکنند، مرور کنید. برای هر فیلد استفاده شده در متد، یک پارامتر جدید معرفی کنید و به جای فیلد از آن استفاده کنید.
- به صورت اختیاری، یک کلاس کارخانه برای مدیریت مجموعه Flyweight ایجاد کنید. قبل از ایجاد یک Flyweight جدید، باید Flyweight موجود را بررسی کند. هنگامی که کارخانه راه اندازی شد، مشتریان فقط باید Flyweight را از طریق آن درخواست کنند. آنها باید Flyweight مورد نظر را با انتقال حالت ذاتی آن به کارخانه توصیف کنند.
- مشتری باید مقادیر حالت بیرونی (زمینه) را ذخیره یا محاسبه کند تا بتواند متدهای اشیاء Flyweight را فراخوانی کند. به منظور راحتی، حالت بیرونی همراه با میدان مرجع Flyweight ممکن است به یک کلاس زمینه جداگانه منتقل شود.
مزایا و معایب الگوی Flyweight
این الگوی طراحی دارای مزایا و معایبی به شرح زیر است:
– مزایا
- شما میتوانید مقدار زیادی RAM را حفظ کنید(صرفه جویی)، با این فرض که برنامه شما دارای تعداد زیادی آبجکت مشابه است.
– معایب
- ممکن است زمانی که برخی از دادههای زمینه باید هر بار که یک متد flyweight را فراخوانی میکند، مجدداً محاسبه شوند، RAM را روی چرخههای CPU معامله میکنید.
- کد بسیار پیچیدهتر میشود. اعضای جدید تیم همیشه متعجب خواهند بود که چرا وضعیت یک موجودیت به این شکل جدا شده است.
ارتباط با الگوهای دیگر
- میتوانید گرههای برگ مشترک درخت Composite را به عنوان Flyweights پیادهسازی کنید تا مقداری RAM ذخیره کنید.
- Flyweight نحوه ساخت تعداد زیادی اشیاء کوچک را نشان میدهد، در حالی که Facade نشان میدهد که چگونه یک شی منفرد بسازید که یک زیر سیستم کامل را نشان میدهد.
- Flyweight شبیه Singleton خواهد بود اگر به نحوی بتوانید تمام حالات مشترک اشیاء را به یک جسم با Flyweightکاهش دهید. اما دو تفاوت اساسی بین این الگوها وجود دارد:
- فقط یک نمونه Singleton باید وجود داشته باشد، در حالی که یک کلاس Flyweight میتواند چندین نمونه با حالتهای ذاتی مختلف داشته باشد.
- شی Singleton میتواند قابل تغییر باشد. اجسام Flyweight تغییرناپذیر هستند.