الگوی طراحی پَروَزن (Flyweight) در زبان C#

توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.

الگوی طراحی Flyweight یک الگوی ساختاری است که استفاده از حافظه را با اشتراک‌گذاری حالت مشترک اشیای مختلف بهینه می‌کند.

هدف اصلی الگوی Flyweight این است که با اجازه دادن به اشیای مشابه برای اشتراک‌گذاری بیشترین میزان اطلاعات ممکن، استفاده از حافظه را کاهش دهد. این کار با تقسیم یک مورد به حالت‌های ذاتی (intrinsic) و بیرونی (extrinsic) انجام می‌شود. حالت ذاتی داده‌ای است که یک شیء برای عملکرد مستقل خود به آن نیاز دارد، در حالی که حالت بیرونی داده‌ای است که موجودیت‌های مرتبط می‌توانند آن را به اشتراک بگذارند. با اشتراک‌گذاری حالت بیرونی و جدا نگه داشتن حالت ذاتی، Flyweight می‌تواند بدون تغییر در عملکرد برنامه، مصرف کلی حافظه را کاهش دهد.

شما می‌توانید کد نمونه مربوط به این مطلب را در GitHub پیدا کنید.

درک مسئله

اگر تاکنون بازی Minecraft را تجربه نکرده‌اید، هیچ ایده‌ای از آنچه از دست داده‌اید ندارید؛ اما بقیه ما تجربه زیادی با این بازی داریم. به دلیل استقبال زیاد از آن، شرکت ما تصمیم گرفت ایده ساخت بازی‌ای مشابه آن را مطرح کند و نام آن را CraftMine بگذارد. ازآنجاکه به این نقطه رسیده بودیم، تیم ما به این نتیجه رسید که بهترین راه برای ایجاد سیاره‌ای بزرگ با مناظر زیبا، استفاده از مربع‌ها به‌عنوان پایه است. کوه‌های سنگی، دره‌های عمیق، رودخانه‌ها و دره‌ها، تونل‌ها و معادن ترکیب می‌شوند تا ماجراجویی خیره‌کننده‌ای را برای بازیکن فراهم کنند.

پس از آماده‌سازی همه‌چیز، آخرین commit را انجام دادیم، بازی را ساختیم و آن را به چند نفر دادیم تا امتحان کنند. بااینکه بازی روی سیستم‌های ما به‌راحتی اجرا می‌شد، دوستان ما نتوانستند آن را برای مدت طولانی اجرا کنند. پس از تنها چند دقیقه بازی روی کامپیوترهای آن‌ها، بازی بدون هشدار قبلی اغلب متوقف می‌شد.

پس از تحلیل logهای اشکال‌زدایی، متوجه شدیم که بازی به دلیل کمبود RAM متوقف می‌شود. مشخص شد که کامپیوترهای دوستان ما نسبت به کامپیوترهای شرکت ما قدرت کمتری دارند و به همین دلیل این مشکل زودتر روی دستگاه‌های آن‌ها بروز می‌کرد. کامپیوترهای ما قدرت پردازشی بیشتری نسبت به دستگاه‌های دوستانمان دارند.

مشکل واقعی در سیستم بلوک ما بود. هر بلوک، مانند بلوک گل، بلوک چوب یا بلوک برگ، به‌صورت یک شیء واحدی نمایش داده می‌شد که می‌توانست مقدار زیادی داده ذخیره کند. وقتی بلوک‌های روی صفحه نمایش بازیکن به آستانه معینی می‌رسیدند، بلوک‌های جدید نمی‌توانستند در RAM باقی‌مانده جا شوند و برنامه متوقف می‌شد.

اگر به کلاس Block با دقت نگاه کنید، متوجه می‌شوید که فیلدهای StepSound و Texture نسبت به سایر فیلدها حافظه بیشتری مصرف می‌کنند. بدتر اینکه این دو فیلد تقریباً داده‌های یکسانی در بین تمام ذرات دارند. برای مثال، تمام بلوک‌های چمن دارای یک اثر صوتی و بافت یکسان هستند.

جنبه‌های دیگری از حالت یک بلوک برای هر بلوک منحصربه‌فرد است، مانند مختصات، قابلیت تخریب یا سطح نور آن. به هر حال، مقادیر این فیلدها با گذشت زمان تغییر می‌کنند. این داده‌ها نشان‌دهنده زمینه متغیری است که بلوک در آن وجود دارد، هرچند که جلوه‌های صوتی و بافت بلوک ثابت می‌مانند.

حالت ذاتی یک موجودیت به داده‌های ثابتی اشاره دارد که درون شیء نهفته است و سایر اشیاء نمی‌توانند آن را تغییر دهند. سایر بخش‌های حالت شیء، که اغلب توسط عوامل خارجی تحت تأثیر قرار می‌گیرند، به‌عنوان حالت بیرونی شناخته می‌شوند.

الگوی طراحی Flyweight پیشنهاد می‌کند که از ذخیره حالت بیرونی در داخل شیء خودداری کنیم. در عوض، این حالت باید به روش‌هایی خاص که به آن متکی هستند منتقل شود. از سوی دیگر، حالت ذاتی درون شیء باقی می‌ماند و به ما اجازه می‌دهد از آن در زمینه‌های مختلف استفاده کنیم. درنتیجه، ما به تعداد کمتری از این اشیاء نیاز خواهیم داشت، زیرا تنها تفاوت آن‌ها در حالت ذاتی است که تنوع بسیار کمتری نسبت به حالت بیرونی دارد.

ذخیره حالت بیرونی

اکنون که حالت بیرونی را از بلوک‌های خود حذف کرده‌ایم، باید آن‌ها را جایی ذخیره کنیم. اغلب، حالت بیرونی به یک شیء مخزن منتقل می‌شود که اشیاء را پیش از اعمال الگو جمع‌آوری می‌کند.

در پروژه CraftMine خود، می‌توانیم از کلاس Game برای ذخیره تمام بلوک‌ها در فیلد blocks استفاده کنیم. برای افزودن حالت بیرونی بلوک‌ها، ابتدا باید آرایه‌های متعددی برای ذخیره مختصات، سطح نور و سایر ویژگی‌های هر بلوک منحصربه‌فرد اعلام کنیم. علاوه بر این، به آرایه دیگری نیاز داریم تا یک مرجع به یک flyweight خاص که نماینده یک بلوک است ذخیره کند. سپس باید این آرایه‌ها را همگام‌سازی کنیم تا بتوانیم با استفاده از یک اندیس مشترک به تمام داده‌های یک بلوک دسترسی پیدا کنیم. این راه‌حل چندان ظریف نیست.

یک راه‌حل زیباتر این است که یک کلاس زمینه‌ای جدید تعریف کنیم که حالت بیرونی را همراه با مرجعی به شیء flyweight ذخیره کند. این راه‌حل نیاز به یک آرایه واحد در کلاس مخزن خواهد داشت.

این ممکن است به همان تعداد نمونه‌های زمینه‌ای که قبلاً داشتیم نیاز داشته باشد؛ اما این نمونه‌ها به‌مراتب حافظه کمتری مصرف می‌کنند. فیلدهای سنگین‌تر اکنون در تعداد کمی از اشیاء flyweight متمرکز شده‌اند. به‌جای نگهداری از یک میلیون نسخه از داده‌های یکسان، اکنون یک میلیون شیء کوچک زمینه‌ای می‌توانند از یک شیء flyweight بزرگ استفاده کنند.

اشیاء Flyweight و تغییرناپذیری

باید اطمینان حاصل کنیم که حالت شیء flyweight قابل تغییر نیست، زیرا می‌تواند در زمینه‌های مختلف استفاده شود. حالت flyweight فقط یک‌بار از طریق پارامترهای سازنده مقداردهی اولیه می‌شود. سایر اشیاء نباید به هیچ Setter یا فیلد عمومی دسترسی داشته باشند.

کارخانه Flyweight

برای دسترسی راحت‌تر، می‌توانیم یک متد کارخانه ایجاد کنیم که یک مجموعه از اشیاء flyweight موجود را مدیریت کند. این متد حالت ذاتی flyweight موردنظر را از مشتری دریافت می‌کند، سپس به دنبال یک شیء flyweight موجود که با این حالت مطابقت دارد می‌گردد و درنهایت اگر پیدا شد، آن را بازمی‌گرداند. اگر پیدا نشد، flyweight به مجموعه اضافه می‌شود.

ساختار الگوی طراحی Flyweight

در پیاده‌سازی پایه‌ای، الگوی طراحی Flyweight چهار شرکت‌کننده اصلی دارد:

  1. Flyweight: بخشی از حالت شیء اصلی که می‌تواند بین اشیاء متعدد به اشتراک گذاشته شود، در کلاس Flyweight قرار می‌گیرد. همان شیء flyweight می‌تواند در موقعیت‌های مختلف اعمال شود. حالت نگهداری‌شده در flyweight به‌عنوان “ذاتی” شناخته می‌شود. حالت بیرونی به حالت‌هایی اشاره دارد که به متدهای flyweight منتقل شده‌اند.
  2. Concrete Flyweight: حالت بیرونی که مختص تمام اشیاء اصلی است، در کلاس ConcreteFlyweight ذخیره می‌شود. یکی از اشیاء flyweight و یک flyweight مشخص با هم نمایانگر حالت کامل شیء اصلی هستند.
  3. کارخانه Flyweight: کارخانه Flyweight مسئول مدیریت مجموعه‌ای از flyweightهای موجود است. مشتریان مستقیماً flyweightها را در کارخانه تولید نمی‌کنند. در عوض، آن‌ها به کارخانه مراجعه می‌کنند و بخش‌هایی از حالت ذاتی flyweight موردنظر را ارسال می‌کنند. کارخانه پایگاه داده flyweightهای تولیدشده قبلی خود را جستجو می‌کند و یا یکی از موجود را که با معیار جستجو مطابقت دارد بازمی‌گرداند یا اگر موردی یافت نشد، یک flyweight جدید ایجاد می‌کند.
  4. مشتری (Client): حالت بیرونی flyweightها توسط مشتری محاسبه یا ذخیره می‌شود. از دیدگاه مشتری، یک flyweight یک شیء الگو است که می‌تواند در زمان اجرا با انتقال داده‌های زمینه‌ای به پارامترهای متدهای آن تنظیم شود.

نمودار کلاس الگوی طراحی Flyweight

برای نشان دادن نحوه عملکرد الگوی طراحی Flyweight، قصد داریم یک برنامه طراحی ایجاد کنیم. برای مثال، فرض کنید باید طراحی‌ای شامل اشکال مختلف، خطوط، بیضی‌ها، مثلث‌ها و مربع‌ها ایجاد کنیم. بنابراین ابتدا باید یک رابط IShape ایجاد کنیم:

				
					namespace Flyweight.Shapes;

public interface IShape
{
    void Draw(Graphics g, int x, int y, int width, int height, Color color);
}

				
			

حالا بیایید اشکال خود را تعریف کنیم. کلاس Line هیچ ویژگی ذاتی ندارد، اما کلاس‌های Oval، Square و Triangle همگی یک ویژگی ذاتی دارند که مشخص می‌کند آیا شکل با رنگ داده‌شده پر شود یا خیر. همچنین توجه داشته باشید که در پیاده‌سازی‌ها، یک دستور Sleep اضافه کرده‌ام تا محاسبات سنگین را نشان دهم. این موضوع در ادامه کمی واضح‌تر خواهد شد.

بیایید با کلاس Line شروع کنیم:

				
					namespace Flyweight.Shapes;

public class Line : IShape
{
    public Line()
    {
        Console.WriteLine("Creating a new line object");

        // Simulate heavy computation
        System.Threading.Thread.Sleep(1000);
    }

    public void Draw(Graphics g, int x1, int y1, int x2, int y2, Color color)
    {
        g.DrawLine(new Pen(color), x1, y1, x2, y2);
    }
}

				
			

بعد کلاس Oval. به مقدار ذاتی _fill توجه کنید.

				
					namespace Flyweight.Shapes;

public class Oval : IShape
{

    private bool _fill;

    public Oval(bool fill)
    {
        _fill = fill;
        Console.WriteLine($"Creating Oval object with fill: {fill}");

        // Simulate heavy computational work
        System.Threading.Thread.Sleep(2000);
    }

    public void Draw(Graphics g, int x, int y, int width, int height, Color color)
    {
        g.DrawEllipse(new Pen(color), x, y, width, height);
        if (_fill)
            g.FillEllipse(new SolidBrush(color), x, y, width, height);
    }
}

				
			

بعدی، کلاس Triangle:

				
					namespace Flyweight.Shapes;

public class Triangle : IShape
{
    private bool _fill;

    public Triangle(bool fill)
    {
        _fill = fill;
        Console.WriteLine($"Creating triangle with fill: {_fill}");

        System.Threading.Thread.Sleep(800);
    }

    public void Draw(Graphics g, int x, int y, int width, int height, Color color)
    {
        var points = new Point[3];
        points[0] = new Point(x + width / 2, y);
        points[1] = new Point(x, y + height);
        points[2] = new Point(x + width, y + height);

        g.DrawPolygon(new Pen(color), points);

        if (_fill)
        {
            g.FillPolygon(new SolidBrush(color), points);
        }
    }
}

				
			

و در نهایت کلاس Square:

				
					namespace Flyweight.Shapes;

public class Square : IShape
{
    private bool _fill;

    public Square(bool fill)
    {
        _fill = fill;
        Console.WriteLine($"Creating triangle with fill: {_fill}");

        System.Threading.Thread.Sleep(800);
    }

    public void Draw(Graphics g, int x, int y, int width, int height, Color color)
    {
        g.DrawRectangle(new Pen(color), x, y, width, height);
        if(_fill)
            g.FillRectangle(new SolidBrush(color), x, y, width, height);
    }
}

				
			

حالا وقت آن رسیده که به بخش کارخانه Flyweight بپردازیم. ما به نوعی ثبت‌نام نیاز داریم تا Flyweightها را ذخیره کنیم. همچنین می‌خواهیم این ثبت‌نام برای برنامه مشتری غیرقابل‌دسترس باشد. به همین دلیل، قصد داریم کلاسی ایجاد کنیم که شامل یک Dictionary باشد. اگر مشتری شکلی درخواست کند که در ثبت‌نام موجود باشد، همان بازگردانده می‌شود. اگر موجود نباشد، بلافاصله ایجاد و بازگردانده خواهد شد.

بیایید کلاس ShapeFactory را ببینیم:

				
					using Flyweight.Shapes;

namespace Flyweight;

public class ShapeFactory
{
    private static readonly Dictionary<ShapeType, IShape> shapes = new Dictionary<ShapeType, IShape>();

    public static IShape GetShape(ShapeType type)
    {
        IShape _concreteShape = shapes.GetValueOrDefault(type, null);

        if (_concreteShape != null) 
            return _concreteShape;

        _concreteShape = type switch
        {
            ShapeType.LINE => new Line(),
            ShapeType.OVAL_FILL => new Oval(true),
            ShapeType.OVAL_NOFILL => new Oval(false),
            ShapeType.SQUARE_FILL => new Square(true),
            ShapeType.SQUARE_NOFILL => new Square(false),
            ShapeType.TRIANGLE_FILL => new Triangle(true),
            ShapeType.TRIANGLE_NOFILL => new Triangle(false),
            _ => _concreteShape
        } ?? throw new InvalidOperationException();

        shapes.Add(type, _concreteShape);

        return _concreteShape;
    }
}

				
			

همچنین یک enum به نام ShapeType ایجاد کرده‌ام تا اشکال مناسب را از رجیستری انتخاب کنم:

				
					namespace Flyweight;

public enum ShapeType
{
    LINE, 
    OVAL_NOFILL, 
    OVAL_FILL, 
    TRIANGLE_NOFILL, 
    TRIANGLE_FILL, 
    SQUARE_NOFILL, 
    SQUARE_FILL
}

				
			

در نهایت، به چیزی نیاز داریم که طراحی کنیم. در زیر کلاس Painter قرار دارد. این کلاس ممکن است کمی پیچیده به نظر برسد، اما در واقع فقط یک قاب برای طراحی تنظیم می‌کند و سپس به‌صورت تصادفی ترکیبی از شکل و رنگ را انتخاب کرده و آن را روی صفحه نمایش ترسیم می‌کند:

				
					namespace Flyweight;

public partial class Painter : Form
{
    private readonly int WIDTH;
        private readonly int HEIGHT;

        private static readonly ShapeType[] shapes = { ShapeType.LINE, ShapeType.OVAL_FILL, ShapeType.OVAL_NOFILL, ShapeType.SQUARE_FILL, ShapeType.SQUARE_NOFILL, ShapeType.TRIANGLE_FILL, ShapeType.TRIANGLE_NOFILL };
        private static readonly Color[] colors = { Color.Red, Color.Green, Color.Yellow, Color.Aquamarine, Color.Chartreuse, Color.Black, Color.Indigo };

        public Painter(int width, int height)
        {
            this.WIDTH = width;
            this.HEIGHT = height;

            var contentPane = Controls;

            var startButton = new Button { Text = "Draw" };
            var panel = new Panel { Dock = DockStyle.Fill };

            contentPane.Add(panel);
            contentPane.Add(startButton);
            startButton.Dock = DockStyle.Bottom;

            Visible = true;
            Size = new Size(WIDTH, HEIGHT);
            FormBorderStyle = FormBorderStyle.FixedSingle;
            MaximizeBox = false;
            StartPosition = FormStartPosition.CenterScreen;

            startButton.Click += (sender, e) =>
            {
                var g = panel.CreateGraphics();
                for (int i = 0; i < 20; ++i)
                {
                    var shape = ShapeFactory.GetShape(GetRandomShape());
                    shape.Draw(g, GetRandomX(), GetRandomY(), GetRandomWidth(),
                            GetRandomHeight(), GetRandomColor());
                }
            };
        }

        private ShapeType GetRandomShape()
        {
            var random = new Random();
            return shapes[random.Next(shapes.Length)];
        }

        private int GetRandomX()
        {
            var random = new Random();
            return random.Next(WIDTH);
        }

        private int GetRandomY()
        {
            var random = new Random();
            return random.Next(HEIGHT);
        }

        private int GetRandomWidth()
        {
            var random = new Random();
            return random.Next(WIDTH / 10);
        }

        private int GetRandomHeight()
        {
            var random = new Random();
            return random.Next(HEIGHT / 10);
        }

        private Color GetRandomColor()
        {
            var random = new Random();
            return colors[random.Next(colors.Length)];
        }

        [STAThread]
        private static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Painter(1024, 768));
        }
}

				
			

اگر پروژه را اجرا کنید و دکمه Draw را فشار دهید، متوجه خواهید شد که برنامه برای شروع طراحی به زمان نیاز دارد. این به این دلیل است که ما در حال نمونه‌سازی از اشیای Flyweight هستیم:

خروجی الگوی طراحی Flyweight

اگر به فشار دادن دکمه Draw ادامه دهید، مشاهده خواهید کرد که اشکال به‌صورت فوری ظاهر می‌شوند و می‌توانید چیزی شبیه به هنر انتزاعی دهه 80 ایجاد کنید:

مزایا و معایب الگوی طراحی Facade

مزایا

  • کاهش مصرف حافظه برنامه.
  • جداسازی Flyweightها به برنامه اجازه می‌دهد بدون اختلال، موجودیت‌ها یا مدل‌های جدیدی اضافه کند.

معایب

  • ممکن است حافظه را با چرخه‌های CPU مبادله کنیم، اگر وضعیت بیرونی (Extrinsic) به‌طور مکرر محاسبه شود.
  • افزایش پیچیدگی کد.

ارتباط با الگوهای دیگر

برای کاهش استفاده از RAM، گره‌های مشترک درخت Composite می‌توانند به‌عنوان Flyweight پیاده‌سازی شوند.
Flyweight نشان می‌دهد که چگونه می‌توان تعداد زیادی شیء کوچک ایجاد کرد، در حالی که Facade نشان می‌دهد که چگونه می‌توان یک شیء واحد ایجاد کرد که نماینده یک زیرسیستم کامل باشد.

اگر ممکن باشد که همه وضعیت‌های مشترک اشیا را در یک شیء Flyweight واحد ادغام کنیم، Flyweight به شیوه‌ای مشابه Singleton عمل خواهد کرد. از طرف دیگر، دو تفاوت مهم بین این الگوها وجود دارد:

  • تنها یک نمونه از کلاس Singleton باید وجود داشته باشد، در حالی که یک کلاس Flyweight ممکن است شامل نمونه‌های متعددی باشد که هر کدام می‌توانند وضعیت ذاتی (Intrinsic) منحصربه‌فردی داشته باشند.
  • شیء Singleton ممکن است به‌نوعی تغییر کند. اشیایی که دارای Flyweight هستند، غیرقابل تغییر (Immutable) هستند.

نتیجه‌گیری

در این مقاله، بررسی کردیم که الگوی طراحی Flyweight چیست، چه زمانی باید از آن استفاده کرد و مزایا و معایب استفاده از این الگوی طراحی چیست. سپس، به بررسی برخی موارد استفاده از این الگو و نحوه ارتباط Flyweight با سایر الگوهای طراحی کلاسیک پرداختیم.

قابل ذکر است که الگوی طراحی Flyweight، همراه با سایر الگوهای طراحی ارائه‌شده توسط Gang of Four، یک راه‌حل همه‌جانبه یا قطعی برای طراحی برنامه نیست. دوباره این وظیفه مهندسان است که تصمیم بگیرند چه زمانی باید از یک الگوی خاص استفاده کنند. در نهایت، این الگوها زمانی مفید هستند که به‌عنوان یک ابزار دقیق استفاده شوند، نه یک چکش بزرگ.

ما به پایان سری مقالات الگوهای طراحی Gang of Four رسیدیم! امیدوارم به اندازه من که از تحقیق و نوشتن این مقالات لذت بردم، از خواندن آنها لذت برده باشید. تکمیل این سری برای من یک دستاورد بزرگ محسوب می‌شود، زیرا این اولین سری قابل‌توجهی است که از زمان شروع وبلاگ‌نویسی‌ام به پایان رسانده‌ام. از شما بابت همراهی در این مسیر بسیار سپاسگزارم و امیدوارم در ادامه نیز همراه من باشید!

©دوات با هدف دسترس‌پذیر کردن دانش انگلیسی در حوزه صنعت نرم‌افزار وجود آمده است. در این راستا از هوش مصنوعی برای ترجمه گلچینی از مقالات مطرح و معتبر استفاده می‌شود. با ما در تماس باشید و انتقادات و پیشنهادات خود را از طریق صفحه «تماس با ما» در میان بگذارید.