الگوی طراحی Iterator در زبان C#

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

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

ایده اصلی این است که یک کلاس به نام Iterator داشته باشیم که حاوی یک مرجع به یک شیء مجموعه‌ی مرتبط باشد و این Iterator بتواند بر روی مجموعه پیمایش کند تا اشیاء منفرد را بازیابی کند.

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

مفهوم مسئله

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

انواع مختلف مجموعه

مجموعه‌ها معمولاً عناصر خود را در یک لیست ساده ذخیره می‌کنند. اما مجموعه‌هایی وجود دارند که بر اساس ساختارهایی مثل پشته‌ها (Stacks)، درخت‌ها (Trees)، گراف‌ها (Graphs) و نمایش‌های پیچیده‌تر سازماندهی می‌شوند.

با این حال، همه مجموعه‌ها باید راهی برای دسترسی به عناصر خود ارائه دهند. باید راهی وجود داشته باشد که بتوان از هر عنصر مجموعه عبور کرد، بدون اینکه عناصر تکراری بارها و بارها خوانده شوند.

این مسئله زمانی که مجموعه بر اساس لیست باشد ساده به نظر می‌رسد؛ می‌توانیم تمام عناصر را با یک حلقه پیمایش کنیم. اما چگونه عناصر یک ساختار داده‌ی پیچیده، مانند درخت را به صورت ترتیبی پیمایش کنیم؟
برای مثال ممکن است یک روز به پیمایش عمقی (Depth-First Traversal) در یک درخت نیاز داشته باشیم و روز دیگر ممکن است به پیمایش سطحی (Breadth-First Traversal) نیاز پیدا کنیم یا هفته بعد به چیزی متفاوت مثل دسترسی تصادفی به عناصر درخت نیاز داشته باشیم.

یک درخت واحد را می‌توان به روش‌های مختلف پیمایش کرد

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

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

ایده اصلی الگوی Iterator این است که رفتار پیمایشی مجموعه را به اشیاء جداگانه‌ای به نام Iterator استخراج کنیم.

پیمایش‌های مختلف در درخت‌ها

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

معمولاً Iteratorها یک متد اصلی برای بازیابی عناصر فراهم می‌کنند. کلاینت می‌تواند از این متد استفاده کند تا زمانی که Iterator همه عناصر را پیمایش کند.

همچنین، همه Iteratorها باید یک رابط (Interface) مشترک را پیاده‌سازی کنند. این کار کد کلاینت را با هر نوع مجموعه یا هر الگوریتم پیمایشی سازگار می‌کند، به شرطی که یک Iterator مناسب وجود داشته باشد. اگر نیاز به روشی برای پیمایش مجموعه داشته باشیم که توسط Iteratorهای موجود پیاده‌سازی نشده باشد، می‌توانیم یک کلاس Iterator جدید ایجاد کنیم، بدون اینکه نیازی به تغییر مجموعه یا کد کلاینت باشد.

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

در پیاده‌سازی پایه، الگوی Iterator پنج بخش اصلی دارد:

  • IIterator: رابط Iterator عملیات لازم برای پیمایش مجموعه، بازیابی عنصر بعدی، دریافت موقعیت فعلی و شروع مجدد پیمایش را اعلام می‌کند.

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

  • IIterableCollection: رابط Collection یک یا چند متد برای دریافت Iteratorهایی که با مجموعه سازگار هستند، اعلام می‌کند. نوع بازگشتی این متدها باید به عنوان رابط Iterator اعلام شود، به طوری که مجموعه‌های مشخص بتوانند انواع مختلف Iteratorها را بازگردانند.

  • ConcreteCollection: این کلاس هر بار که کلاینت درخواست یک Iterator جدید می‌کند، نمونه‌های جدیدی از یک کلاس Iterator خاص برمی‌گرداند. کدهای دیگر مجموعه نیز باید در همین کلاس باشند، اما از آنجایی که این جزئیات برای الگو ضروری نیستند، آنها را حذف می‌کنیم.

  • Client: کلاینت با استفاده از رابط‌های مجموعه و Iterator کار می‌کند. به این ترتیب کلاینت به کلاس‌های خاص متصل نمی‌شود و می‌توان از مجموعه‌ها و Iteratorهای مختلف با همان کد کلاینت استفاده کرد.

برای نشان دادن نحوه کار الگوی Iterator، مثالی از یک میان‌وعده در رمان‌های فانتزی مطرح می‌کنیم: جعبه آب‌نبات‌های طعم‌دار Bertie Bott’s!

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

				
					namespace Iterator.Collection
{
    public class EveryFlavorBean
    {
        private readonly string flavor;

        public EveryFlavorBean(string flavor)
        {
            this.flavor = flavor;
        }

        public string Flavor
        {
            get { return flavor; }
        }
    }
}

				
			

سپس باید شرکت‌کننده IIterableCollection را ایجاد کنیم که آن را ICandyCollection می‌نامیم، و شرکت‌کننده ConcreteCollection را که آن را BertieBottsEveryFlavorBeanBox می‌نامیم. این کلاس‌ها نمایانگر مجموعه‌ای از آب‌نبات‌ها هستند.

				
					using Iterator.Iterator;

namespace Iterator.Collection
{
    public interface ICandyCollection
    {
        public IBeanIterator CreateIterator();
    }
}

				
			
				
					using Iterator.Iterator;

namespace Iterator.Collection
{
    public class BertieBottsEveryFlavorBeanBox : ICandyCollection
    {
        private List<EveryFlavorBean> items = new();

        public IBeanIterator CreateIterator()
        {
            return new BeanIterator(this);
        }

        public int Count
        {
            get { return items.Count; }
        }

        public void Add(params string[] beans)
        {
            foreach(string bean in beans)
                items.Add(new EveryFlavorBean(bean));
        }

        public object this[int index]
        {
            get { return items[index]; }
            set { items.Add((EveryFlavorBean)value); }
        }
    }
}

				
			

اکنون می‌توانیم شرکت‌کنندگان IIterator و ConcreteIterator را تعریف کنیم.

				
					using Iterator.Collection;

namespace Iterator.Iterator
{
    public interface IBeanIterator
    {
        public EveryFlavorBean? First();
        public EveryFlavorBean? Next();
        public bool IsDone { get; }
        public EveryFlavorBean CurrentBean { get; }
    }
}

				
			
				
					using Iterator.Collection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Iterator.Iterator
{
    public class BeanIterator : IBeanIterator
    {
        private BertieBottsEveryFlavorBeanBox bertieBottsEveryFlavorBeanBox;
        private int current = 0;
        private int step = 1;

        public BeanIterator(BertieBottsEveryFlavorBeanBox bertieBottsEveryFlavorBeanBox)
        {
            this.bertieBottsEveryFlavorBeanBox = bertieBottsEveryFlavorBeanBox;
        }

        public bool IsDone => current >= bertieBottsEveryFlavorBeanBox.Count;

        public EveryFlavorBean? First()
        {
            current = 0;
            return bertieBottsEveryFlavorBeanBox[current] as EveryFlavorBean;
        }

        public EveryFlavorBean? Next()
        {
            current += step;
            if (!IsDone)
                return bertieBottsEveryFlavorBeanBox[current] as EveryFlavorBean;
            else
                return null;
        }

        public EveryFlavorBean CurrentBean => bertieBottsEveryFlavorBeanBox[current] as EveryFlavorBean;
    }
}

				
			

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

در نهایت، در متد Main() خود، ما یک مجموعه از آب‌نبات‌های ژله‌ای ایجاد کرده و سپس روی آن‌ها تکرار خواهیم کرد.

				
					using Iterator.Collection;
using Iterator.Iterator;

BertieBottsEveryFlavorBeanBox beanBox = new();
beanBox.Add("Banana",
            "Black Pepper",
            "Blueberry",
            "Booger",
            "Candyfloss",
            "Cherry",
            "Cinnamon",
            "Dirt",
            "Earthworm",
            "Earwax",
            "Grass",
            "Green Apple",
            "Marshmallow",
            "Rotten Egg",
            "Sausage",
            "Lemon",
            "Soap",
            "Tutti-Frutti",
            "Vomit",
            "Watermelon");

BeanIterator iterator = (BeanIterator)beanBox.CreateIterator();

for(EveryFlavorBean item = iterator.First();
    !iterator.IsDone;
    item = iterator.Next())
{
    Console.WriteLine(item.Flavor);
}

				
			

اگر این برنامه را اجرا کنیم، خروجی زیر را مشاهده خواهیم کرد:

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

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

مزایا

  • ما می‌توانیم با استخراج الگوریتم‌های سنگین پیمایش به کلاس‌های جداگانه، کد مشتری و مجموعه را مرتب کنیم و بدین ترتیب اصل مسئولیت‌پذیری یکتا (Single Responsibility Principle) را رعایت کنیم.
  • ما می‌توانیم انواع جدیدی از مجموعه‌ها و پیمایش‌گرها را پیاده‌سازی کنیم و آن‌ها را بدون ایجاد مشکل به کدهای موجود اضافه کنیم و بدین ترتیب اصل باز/بسته (Open/Closed Principle) را رعایت کنیم.
  • می‌توانیم به طور موازی روی یک مجموعه تکرار کنیم، زیرا هر شیء پیمایش‌گر حالت پیمایش خود را نگه می‌دارد.
  • به همین دلیل می‌توانیم یک پیمایش را به تعویق بیندازیم و در زمان نیاز آن را ادامه دهیم.

معایب

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

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

  • می‌توان از پیمایش‌گرها برای پیمایش درخت‌های الگوی طراحی Composite استفاده کرد.
  • می‌توان از  Factory method همراه با الگوی Iterator استفاده کرد تا زیرکلاس‌های مجموعه، انواع مختلفی از پیمایش‌گرها را که با مجموعه‌ها سازگار هستند برگردانند.
  • می‌توان از الگوی Memento همراه با Iterator استفاده کرد تا حالت فعلی پیمایش را ذخیره و در صورت نیاز بازگردانی کنیم.
  • می‌توان از الگوی Visitor همراه با Iterator استفاده کرد تا یک ساختار داده پیچیده را پیمایش کرده و عملیات خاصی روی عناصر آن انجام دهیم.

نکات پایانی

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

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

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

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