الگوی طراحی کامپوزیت (Composite) در C#

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

الگوی طراحی مرکب (Composite) یک الگوی ساختاری است که به ما امکان می‌دهد اشیاء را در ساختارهای درختی ترکیب کرده و با این ساختارها به‌گونه‌ای کار کنیم که گویی اشیاء منفرد هستند.

کامپوزیت نمایش‌دهنده سلسله‌مراتب جز-کل است. این بیان فنی بدین معناست که می‌توانیم تمامی بخش‌های یک سلسله‌مراتب را با ساده کردن اجزای آن به مولفه‌های مشترک نشان دهیم.

شما می‌توانید کد نمونه این پست را در گیت‌هاب پیدا کنید.

مفهوم‌سازی مسئله

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

تصور کنید که قصد داریم نسخه منحصربه‌فردی از یک سیستم فایل ایجاد کنیم. در این سیستم دو نوع شیء داریم: Files و Folders. یک پوشه می‌تواند شامل چندین Files و همچنین Folders دیگر باشد. این Folders نیز می‌توانند Files یا حتی Folders دیگری را در خود جای دهند و این روند ادامه یابد.

فرض کنید تصمیم داریم تابعی ایجاد کنیم که اندازه یک دایرکتوری را محاسبه کند. یک دایرکتوری ممکن است تنها یک فایل داشته باشد بدون هیچ پوشه‌ای، یا شامل پوشه‌هایی پر از فایل‌ها… و پوشه‌های دیگر باشد. چگونه می‌توان اندازه کل چنین دایرکتوری را تعیین کرد؟

می‌توانیم روش مستقیم را امتحان کنیم: تمام فایل‌ها را از پوشه‌هایشان خارج کنیم، هر فایل را بررسی کنیم و سپس اندازه کل را محاسبه کنیم. این کار در دنیای واقعی امکان‌پذیر است؛ به‌عنوان مثال، فایل‌ها را خارج کنید و تعداد صفحات را بشمارید.

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

الگوی طراحی کامپوزیت پیشنهاد می‌دهد که با فایل‌ها و پوشه‌ها از طریق یک رابط مشترک کار کنیم. این رابط یک متد برای محاسبه اندازه کل اعلام می‌کند.

عملکرد این متد چگونه خواهد بود؟ برای یک فایل، تنها اندازه فایل را بازمی‌گرداند. برای یک پوشه، تمام آیتم‌های موجود در آن را بررسی کرده، اندازه آن‌ها را می‌پرسد و سپس مجموع اندازه‌ها را بازمی‌گرداند. اگر یکی از این آیتم‌ها پوشه دیگری باشد، آن پوشه نیز محتوای خود را بررسی کرده و این فرآیند ادامه می‌یابد تا زمانی که اندازه تمام اجزای داخلی محاسبه شود.

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

ساختاردهی الگوی طراحی کامپوزیت

در پیاده‌سازی پایه‌ای، الگوی طراحی کامپوزیت دارای ۴ بخش اصلی است:

  1. کامپوننت: اینترفیس کامپوننت عملیاتی را توضیح می‌دهد که بین عناصر ساده و پیچیده درخت مشترک است.
  2. برگ (Leaf): برگ یک عنصر اساسی درخت است که عناصر فرعی ندارد. معمولاً اجزای برگ بیشترین منطق کاری را دارند، زیرا وظایف خود را به کسی تفویض نمی‌کنند.
  3. کانتینر: کانتینر، که به‌عنوان کامپوزیت نیز شناخته می‌شود، عنصری است که دارای عناصر فرعی است. کانتینر از پیاده‌سازی‌های مشخص اجزای فرعی خود اطلاعی ندارد و فقط از طریق رابط کامپوننت با آن‌ها کار می‌کند. هنگامی که درخواستی دریافت می‌کند، کانتینر وظیفه را به فرزندان خود تفویض کرده، نتایج میانی را پردازش و سپس نتیجه نهایی را به مشتری بازمی‌گرداند.
  4. مشتری: مشتری با همه عناصر از طریق رابط کامپوننت کار می‌کند. به همین دلیل، مشتری می‌تواند به‌طور یکسان با عناصر ساده یا پیچیده درخت تعامل کند.

نمودار کلاس الگوی مرکب (Composite)

برای نشان دادن نحوه کار الگوی طراحی کامپوزیت، می‌خواهیم یک میکسر Nuka-Cola طراحی کنیم.

برای کسانی که طرفدار بازی Fallout 4 نیستند، Nuka Mixer Station یک دستگاه است که به شما امکان می‌دهد طعم‌های مختلف Nuka-Cola را ترکیب کنید تا یک Nuka-Cola جدید بسازید، مشابه این دستگاه‌های پخش‌کننده.

می‌توانیم مدل نحوه کار این دستگاه را طراحی کنیم. میکسر Nuka می‌تواند ۵ نوع کامپوننت را بپذیرد: Nuka Cola معمولی، Nuka Cola طعم‌دار، Nuka Cola واریانتی، داروها و مواد خوراکی. این امر به‌طور مؤثر سلسله‌مراتبی ایجاد می‌کند که در آن “نوشابه” خود مولفه ریشه است، انواع کامپوننت‌ها مولفه‌های فرعی هستند و طعم‌های مختلف برگ‌ها را تشکیل می‌دهند.

نسخه ساده‌ای از این سلسله‌مراتب ممکن است این‌گونه باشد:

طبقه‌بندی مرکب

بیایید این سلسله‌مراتب را مدل‌سازی کنیم. برای تمام طعم‌های احتمالی نوشابه که دستگاه ما می‌تواند توزیع کند، باید بدانیم هر طعم چند امتیاز ضربه (HitPoints) فراهم می‌کند. بنابراین، در کلاس انتزاعی که نشان‌دهنده تمام نوشیدنی‌های گازدار است، نیاز به یک ویژگی برای HitPoints داریم.

				
					namespace Composite
{
    /// <summary>
    /// Component abstract class
    /// </summary>
    public abstract class Component
    {
        public int HitPoints { get; set; }

        public List<Component> Flavors { get; set; }

        public Component(int hitPoints)
        {
            HitPoints = hitPoints;
            Flavors = new List<Component>();
        }

        /// <summary>
        /// Return all available flavours and their hitpoints
        /// </summary>
        public void DisplayHitPoints()
        {
            foreach(var drink in this.Flavors)
            {
                drink.DisplayHitPoints();
                this.HitPoints += drink.HitPoints;
            }

            Console.WriteLine($"{this.GetType().Name}: {this.HitPoints} hitpoints.");
        }
    }
}
				
			

به متد DisplayHitPoints توجه کنید. این متد بازگشتی است که امتیاز ضربه (HitPoints) تمام اجزا را نمایش می‌دهد و در نهایت مجموع امتیازات ضربه شیء مرکب را نشان می‌دهد.

در گام بعدی، باید چندین شرکت‌کننده برگ (Leaf Participants) برای طعم‌های خاص نوشابه Nuka-Cola پیاده‌سازی کنیم.

				
					namespace Composite.Leaves
{
    /// <summary>
    /// Leaf class
    /// </summary>
    public class NukaCola : Component
    {
        public NukaCola(int hitPoints) : base(hitPoints)
        {
        }
    }
}

namespace Composite.Leaves
{
    public class NukaGrape : Component
    {
        public NukaGrape(int hitPoints) : base(hitPoints)
        {
        }
    }
}

namespace Composite.Leaves
{
    public class NukaCherry : Component
    {
        public NukaCherry(int hitPoints) : base(hitPoints)
        {
        }
    }
}

namespace Composite.Leaves
{
    public class NukaWild : Component
    {
        public NukaWild(int hitPoints) : base(hitPoints)
        {
        }
    }
}
				
			

اکنون باید شرکت‌کننده مرکب (Composite Participant) را پیاده‌سازی کنیم، که نمایانگر سلسله‌مراتبی است که شامل فرزندان می‌شود.

				
					namespace Composite.Composites
{
    public class NewkaCola : Component
    {
        public NewkaCola(int hitPoints) : base(hitPoints)
        {
        }
    }
}

namespace Composite.Composites
{
    public class NukaBerry : Component
    {
        public NukaBerry(int hitPoints) : base(hitPoints)
        {
        }
    }
}

namespace Composite.Composites
{
    public class NukaFancy : Component
    {
        public NukaFancy(int hitPoints) : base(hitPoints)
        {
        }
    }
}
				
			

کلاس‌های مرکب و برگ یکسان هستند، و این تصادفی نیست.

در نهایت، متد Main() نشان می‌دهد که چگونه می‌توانیم یک سلسله‌مراتب جدید از نوشابه کولا را با چندین طعم مختلف مقداردهی اولیه کنیم و سپس تمام کالری‌های مربوط به هر طعم را نمایش دهیم:

				
					using Composite.Composites;
using Composite.Leaves;

var newkaCola = new NewkaCola(0);
newkaCola.Flavors.Add(new NukaCola(150));
newkaCola.Flavors.Add(new NukaCherry(150));

var nukaBerry = new NukaBerry(0);
nukaBerry.Flavors.Add(new NukaCola(150));
nukaBerry.Flavors.Add(new NukaCherry(150));
nukaBerry.Flavors.Add(new NukaGrape(100));

var nukaFancy = new NukaFancy(0);
nukaFancy.Flavors.Add(new NukaWild(50));
nukaFancy.Flavors.Add(new NukaCherry(150));

newkaCola.DisplayHitPoints();
Console.WriteLine("--------------------------");

nukaBerry.DisplayHitPoints();
Console.WriteLine("--------------------------");

nukaFancy.DisplayHitPoints();
				
			

انواع الگوی طراحی مرکب (Composite)

الگوی مرکب در اشکال مختلفی ارائه می‌شود، اما در یک سطح کلی می‌توان آن‌ها را به دو دسته تقسیم کرد: صورت مواد اولیه (Bill of Material) و رده‌بندی (Taxonomy).

صورت مواد اولیه (Bill of Material)

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

هنگام پیاده‌سازی صورت مواد اولیه، گزینه‌های مختلفی برای نحوه رفتار این ساختار داریم که هر کدام پیامدهای متفاوتی دارند:

  1. رفتار تجمیعی (Aggregating Behavior):
    اگر بخواهیم وزن خودرو را تعیین کنیم، باید وزن تمام اجزای فرعی را به‌صورت بازگشتی جمع کنیم. معمولاً این رفتار می‌تواند به نحوی که گره‌های تشکیل‌دهنده (Component) پیاده‌سازی شده‌اند، از دید موجودیت‌های مصرف‌کننده مخفی بماند.

  2. رفتار بهترین/بدترین حالت (Best/Worst Case Behavior):
    اگر بخواهیم وضعیت کلی سلامت خودرو را مشخص کنیم، ممکن است قانونی داشته باشیم که خودرو تنها در صورتی سالم است که تمام اجزای آن سالم باشند. اگر حتی یک جزء مشکل داشته باشد، ممکن است کل خودرو را مشکل‌دار بدانیم. بنابراین، می‌توان گفت که خودرو بدترین حالت همه اجزای خود را نمایان می‌کند. باز هم این رفتار می‌تواند از دید کلاس مصرف‌کننده مخفی بماند.

  3. تحقیقی (Investigatory):
    اگر بخواهیم ویژگی خاصی از یک جزء مشخص را تعیین کنیم، ممکن است نیاز باشد به مشتری اجازه دهیم تا در اجزا جست‌وجو کند و اجزای واجد شرایط را پیدا کند.

رده‌بندی (Taxonomy)

شکل رده‌بندی روابط منطقی بین انواع عمومی‌تر و اختصاصی‌تر را ثبت می‌کند. به‌عنوان مثال، دسته‌بندی‌های زیستی به این صورت عمل می‌کنند. چهاراندامان (Tetrapoda) شامل دوزیستان (Amphibia)، خزندگان (Reptilia) و پستانداران (Mammalia) می‌شوند. پستانداران نیز به دو دسته تخم‌گذارها (Prototheria) و زنده‌زایان (Theria) تقسیم می‌شوند و به همین ترتیب ادامه می‌یابد.

هنگام پیاده‌سازی یک ساختار مرکب رده‌بندی، هدف اصلی تقریباً همیشه این است که به مشتری اجازه دهیم ساختار را پیمایش کند، اطلاعات بیابد یا یک موجودیت را در زمینه ساختار موجود طبقه‌بندی کند.

گزینه‌های پیمایش (Iteration Options)

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

  • در صورت مواد اولیه، ممکن است متدهای پیمایش لازم باشند یا نه، بسته به رفتار مطلوب.
  • در رده‌بندی، متدهای پیمایش تقریباً همیشه لازم هستند، زیرا هدف اصلی ساختار مرکب این است که مشتری بتواند آن را پیمایش کند.

مزایا و معایب الگوی مرکب

مزایا

  • کار با ساختارهای پیچیده درختی راحت‌تر می‌شود. می‌توانیم از چندریختی و بازگشت استفاده کنیم.
  • امکان معرفی انواع جدیدی از اجزا در برنامه وجود دارد بدون اینکه کد موجود که با درخت شیء کار می‌کند، شکسته شود. این موضوع اصل باز-بسته بودن (Open-Closed Principle) را رعایت می‌کند.

معایب

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

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

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

  2. الگوی طراحی Chain of Responsibility:
    اغلب در کنار الگوی مرکب استفاده می‌شود. در این حالت، زمانی که یک جزء برگ درخواست دریافت می‌کند، ممکن است آن را از طریق زنجیره‌ای از تمام اجزای والد به درخت شیء منتقل کند.

  3. الگوی طراحی Iterator:
    می‌توان از آن برای پیمایش درخت‌های مرکب استفاده کرد.

نکات پایانی

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

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

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