الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

Iterator Pattern in .Net

بدأه AliBazzi في 18 سبتمبر 2010 · 0 رد · 1,875 مشاهدة · في قسم تكنولوجيا Microsoft .NET العام
مشاركة: واتساب X فيسبوك تيليجرام
#1

Iterator Pattern هو نمط تصميم شائع الوجود في حياتنا البرمجية اليومية , من منا لم يشاهد Object يحوي أو يشير إلى Objects عديدة , وكم مرة أردنا أن نقوم بعمليات عدة على هذه المجموعة من العناصر , لكي نطبعها أو نجد أكبر عنصر أو ... , ببساطة الـ Iterator Pattern يعيش معنا دون أن ندرك وجوده .

قبل البدأ فقط يجب التنويه إلى أن هذه المقالة تحتاج من القارئ دراية بالأمور التقنية التالية:

#C

Delegates Concept

Generic Concept

فلنبدأ ...

الـ Iterator pattern هو بنية مشتركة بين كل الـ Objects التي نعاملها على أنها مجموعات (Collections) بشكل أساسي , فيستوحى من الإسم فكرة قابلية تفكيك الـ Object والدوران على عناصره بالطريقة التقليدية التي نعرفها ( إحدى حلقات الدوران) ...

مثال:

int[] array  = new int[] {1,2,3,4,7,8,9,10} ;
foreach(int num in array) 
{
 	Console.WriteLine(num);        
}

فلو لم يكن من الممكن تفكيك الـ array منطقيا إلى عدة عناصر و الدوران عليها بحلقة For each, لما أمكن إعتبار الـ Array as Collection ,وبالتالي ممكن لأي Object يشبه الـ Array في فكرته (أي أنه حاوية تشير لعدة عناصر , ممكن الوصول إليهم في أي وقت ) يستطيع تطبيق الـ Iterator Pattern ويعطي أي Type يطبق الـ Iterator Pattern خدمات رائعة لأي مستخدم له ... يمكن مشاهدتها لاحقا ...

فلنبدا بإستكشاف بنية أي Iteratable Type (الصنف الذي ممكن أن نلُف على عناصر يخزنها بطريقة ما , يا لها من ترجمة ! icon_rolleyes.gif ).

في كل سيناريو مشابه نجد هناك طرفين أساسيين في هذه اللعبة , مقدم الخدمة ( الذي يقوم بإعطائنا عنصر جديد كلما طلبنا منه ذلك) ومستهلك العناصر ( من يقوم بطلب العناصر واحد تلو الاّخر , بعد إستهلاك كل عنصر حالي) وبالتالي الواجهة الأساسية ستكون تشبه شيئا كالتالي :

ClassDiagram.png

كما يلاحظ من الشكل السابق , إن الـ Iterate Class يمثل الـ Consumer (المستهلك , الذي سيستهلك أي Object يطبق الـ Iteratable<T> Interface ) و القصود في الإستهلاك هنا , هو عمل شيء ما على كل العناصر الموجودة في الـ Iteratable Object كـ طباعته عن طريق PrintAll , إلى عمل شيء معين على كل عنصر عن طريق Do Method .

والـ MyClollection Class هو الـ Producer هنا ( من يقوم بإنتاج العناصر الذي يحتويها الـ Object عند الطلب )

الـ Iteratable<T> Interface هي البروتوكول السائد هنا , وهي صلة الوصل الموجودة بين الـ Consumer والـ Producer عن طريق الـ Members التالية :

GetNext():bool Method :

تقوم هذه الـ Method بإعادة قيمة True إذا أمكن إنتاج عنصر جديد في السلسلة , وتعيد False إذا وصلنا إلى نهاية السلسلة ...

Current Property :

تعيد العنصر الحالي الذي تقف عنده السلسلة ... ولنكون أكثر وضوحا , عند كتابتنا لـ Consumer فسنقوم بطلب الـ GetNext وإذا حصلنا على جواب إيجابي (True) سنحصل على العنصر الحالي عن طريق هذه الـ Property , إنها Current

Reset():void Method :

سنطلبها عندما ننتهي من قراءة العناصر (ولا يشترط إنتهائها هنا , فربما نريد أن نقف هنا , ونعيد كل شيء إلى حالته ,اي أول عنصر ) , فنستدعي هذه الـ Method لنعيد الحالة الداخلية إلى نقطة البداية (إي العنصر الأول ) .

الاّن سأعرض الكود الخاص بالمكون الرئيسي Iteratable<T> Interface :

public  interface Iteratable<T>
    {
        T Current { get; }

 		bool GetNext();

        void Reset();
    }

وهنا كود الـ Consumer وفي حالتنا هذه إنه Static Iterate Class :

   public static class Iterate
    {
 		public static void Over<T>(Iteratable<T>  iteratable,Action<T> action) 
        {
            while  (iteratable.GetNext())
 				action.Invoke(iteratable.Current);
        }
        public static  void PrintAll<T>(Iteratable<T> iteratable)
        {
 			while (iteratable.GetNext())
 				Console.WriteLine(iteratable.Current.ToString());
        }
    }

الكود السابق يعرض ما تم توضيحه مسبقا , في كلا المثالين , نحاول أن نتبين إذا ما وصلنا إلى نهاية السلسة عن طريق الطلب من الـ Producer بشكل دائم في الإنتقال إلى العنصر التالي , إذا نجح في ذلك نقوم بقراءة قيمته و طبعها في الـ Console أو كما هو في Do Method نقوم بتنفيذ كود خاص تم تمريره عن طريق الـ Action Delegate ... (لا نحتاج هنا إلى عمل Reset لأننا نقوم بإستهلاك السلسلة بالكامل ) .

الاّن هو إستعراض لمُنتِجنا الخاص ( Custom producer ) والذي يسمى MyCollection ,وهو بسيط للغاية , لايقوم بأي شيء سوى تخزين العناصر في Array داخليه , لكن الجديد في الموضوع أنه يقوم بإنتاج العناصر عند الطلب من أي مستهلك لـ Iteratable<T> Interface :

	public class MyCollection : Iteratable<int>
 	{
        private int[] array;

        public  MyCollection(params int[] items)
        {
            array =  items;
        }

        public int Current
        {
 			get 
            {
                if (flag)
 				{
                    return array[count];
                }
 				throw new InvalidProgramException("Iterator is not  running");
            }
        }

        private bool  flag = false;
        private int count = 0;
        public bool  GetNext()
        {
            if (!flag)
            {
 				if (array.Length == 0) return false;

 				flag = true;
                return true;
            }
 			else
            {
                if (count <  array.Length-1)
                {
                    count++;
 					return true;
                }
            }
 			this.Reset();
            return false;
        }

 		public void Reset()
        {
            flag = false;
 			count = 0;
        }
    }

الكود السابق يطرخ خوارزميات بسيطة , تقوم بعمل ما تم شرحه سابقا ...

والاّن لكي نتأكد من عملنا , سنقوم بتجربة ما سبق :

MyCollection  collection = new MyCollection(1, 2, 3, 4, 5, 8, 8, 8, 8, 6, 4, 3);
 			Iterate.PrintAll<int>(collection);
 			Iterate.Over<int>(collection, (item) => Console.WriteLine(item *  10));

نلاحظ أننا قمنا بطباعة كل العناصر الموجود في Collection Object والتي تم تلقيمها للـ Constructor , وبعدها قمنا بتطبيق سلوك خاص (تمثل في طبع كل رقم مضروبا بعشرة ) لكي نجرب Do Method أيضا ... الأمور بسيطة إلى الاّن !

والاّن لندخل عالم الـ Net. عن طريق شرح الواجهات التي قامت مايكروسوفت بتصميمها و تضمنيها ضمن إطار العمل لتدعم الـ Iterator Pattern

في الـ Net. هناك واجهتان يجب الإلمام بهما لنتعامل مع الـ Iterator Pattern عوضا عن واحدة كما في حالتنا السابقة في Iteratable<T> Interface

IEnumerable.png

السبب وراء وجود واجهتين أساسيتين هو رغبة المصممين في مايكروسفت في السماح لأي مبرمج بالفصل بين الـ Object الذي يقدم خدماته في كونه Enumerable (ولا تسألوني عن الترجمة , قربها بعقلك فقط من كلمة enumerate والتي تعني يَعُد أو يٌعَدّد , وسيتم إستعمال مسطلح Enumerable بدأً من الاّن ) , وبين الـ Object الذي سيقوم بعملية التعداد فعليا Enumerator ...

لكن الاّن نبدأ مع مثال بسيط , سنقوم بإعادة كتابة MyCollection لكن هذه المرة بإستعمال IEnumerable<T> Interface و IEnumerator<T> Interface :

	public  class MyCollection : IEnumerable<int>
    {
        internal  int[] array;

        public MyCollection(params int[] items)
 		{
            array = items;
        }

 		System.Collections.IEnumerator  System.Collections.IEnumerable.GetEnumerator()
        {
 			return (this as IEnumerable<int>).GetEnumerator();
        }

 		System.Collections.Generic.IEnumerator<int>  IEnumerable<int>.GetEnumerator()
        {
 			return new MyCollectionIterator(this);
        }
    }

internal class MyCollectionIterator :  System.Collections.Generic.IEnumerator<int> 
    {
 		private int[] array;

        internal  MyCollectionIterator(MyCollection collection)
        {
 			this.array = collection.array;
        }

        private bool  flag = false;
        private int count = 0;
        public int  Current
        {
            get
            {
 				if (flag)
                {
                    return  array[count];
                }
                throw new  InvalidProgramException("Iterator is not running");
            }
 		}

        public bool MoveNext()
        {
 			if (!flag)
            {
                if (array.Length == 0)  return false;

                flag = true;
 				return true;
            }
            else
            {
 				if (count < array.Length - 1)
                {
 					count++;
                    return true;
 				}
            }
            this.Reset();
 			return false;
        }

        public void Reset()
 		{
            flag = false;
            count = 0;
        }

 		public void Dispose()
        {
            this.Reset();
 		}

        object IEnumerator.Current // This Property  exists for Historical Reasons ...
        {
            get { return  this.Current; } 
        }
    }

بسبب خيار مايكروسوفت في تصميم واجهتين , إضطررنا إلى إلى كتابة Two Types هما MyCollection والذي سيعلن من خلالها عن قدرته في أن تستعمله حلقة foreach عن طريق تضمين الـ IEnumerable<T> Interface , و الـ MyCollectionIterator الذي يستطيع تعداد العناصر الموجودة في أي MyCollection Object عن طريق تضمينه لـ IEnumerator<T> Interface .

ولا نحتاج هنا إلى كتابة Consumer كما هو حال Iterate Class في الأعلى لأن الـ #C تحوي مستهلك جاهز لأي صنف يطبق الـ IEnumerable<T> Interface هو الـ foreach وبالتالي نحن محظوظون لذلك :

MyCollection  collection = new MyCollection(1, 2, 3, 4, 5, 8, 8, 8, 8, 6, 4, 3);
 			foreach (int num in collection)
 				Console.WriteLine(num * 100);

والاّن فهمنا لماذا foreach تقبل فقط types معينة , وإكتشفنا أنها لم تكن تستعمل المحسوبيات في ذلك icon_e_wink.gif , بل السر كان في أنهم يستعملون IEnumerble<T> Interface !

نستطيع أن نقوم بتصميم Type وحيد يقوم بتضمين الواجهتين معا : IEnumerable<T> , IEnumerater<T> Interfaces ولكن على ما أذكر مايكروسوفت لا تنصح بذلك (ولا أذكر السبب أيضا ... icon_redface.gif ) , لكن من الممكن عمل ذلك كما يلي :

public class MyCollection :  IEnumerable<int>,IEnumerator<int>
    {
     	internal int[] array;

        public MyCollection(params int[]  items)
        {
            array = items;
        }

     	IEnumerator IEnumerable.GetEnumerator()// This Method exists for  Historical Reasons :)
        {
            return (this as  IEnumerable<int>).GetEnumerator();
        }

     	IEnumerator<int> IEnumerable<int>.GetEnumerator()
     	{
            return (IEnumerator<int>)this;
        }

     	private bool flag = false;
        private int count = 0;

     	public int Current
        {
            get
         	{
                if (flag)
                {
                 	return array[count];
                }
                throw  new InvalidProgramException("Iterator is not running");
            }
     	}

        public bool MoveNext()
        {
         	if (!flag)
            {
                if (array.Length == 0)  return false;

                flag = true;
             	return true;
            }
            else
            {
             	if (count < array.Length - 1)
                {
                 	count++;
                    return true;
             	}
            }
            this.Reset();
         	return false;
        }

        public void Reset()
     	{
            flag = false;
            count = 0;
        }

     	public void Dispose()
        {
            this.Reset();
     	}

        object IEnumerator.Current // This Property  exists for Historical Reasons :)
        {
            get {  return this.Current; }
        }
    }

وسيظل الـ Type يعمل كما تتوقع ذلك , وجربه بحلقة Foreach إذا أردت التأكد ...

ربما الكثير من التعقيد قدم مسبقا , والسبب في ذلك ليس في تعقيد التصميم الذي إتبعته مايكروسفت , بل لأن هناك الكثير من الضجة ناتجة عن أسباب تاريخية كما وضحت سابقا , لن أتوسع في هذا الشق , كل ما أريد أن أقوله هو أن السبب في ظهور هكذا ضجيج كون الـ Net. لم يدعم منذ البداية الـ Generic وبالتالي كانت الواجهات السابقة تعيد Object فقط , تحديدا في الـ Current Property , ولا تعيد شيء ذو Type محدد مسبقا , وعند ظهور الـ generic قامو بتصميم واجهات جديدة ترث من الواجهات السابقة , فإذا تمعنت في الـ IEnumerable<T> Interface و الـ IEnumerator<T> Interface ستجدهما ترثان من IEnumerable و IEnumerator على الترتيب , فقط لحفظ الـ Backward Compatibility .

Yield Keyword :

لحسن الحظ , هناك كلمة مفتاحية ربما يجهل معظمنا عملها , لكن تخبئ الكثير من السحر في عملها , إنها : Yield , هذه الكلمة المحجوزة تقوم بإنشاء State Machine تحفظ لنا الكثير من العمل الروتيني الذي نقوم به عند تصميم الـ State Machine بأنفسنا , هل من أحد لم يلاحظ أننا كنا بالفعل نقوم بتصميم State Machine في الأمثلة السابقة ؟ أعد النظر في الكود لتتأكد من ذلك !

لنأخذ نظرة إلى طريقة حل مسألة الـ Iterator Pattern في الـ MyCollection عن طريق Yield :

public class MyCollection : IEnumerable<int>
 	{
        internal int[] array;

        public  MyCollection(params int[] items)
        {
            array =  items;
        }

        public IEnumerator<int>  GetEnumerator()
        {
            foreach (var item in array)
             	yield return item;
        }

     	IEnumerator IEnumerable.GetEnumerator()
        {
         	return (IEnumerator<int>)this;
        }
    }

ألم أقل أن هناك الكثير من السحر !

ما تفعله توليفة Yield Return هنا أنها تقوم بإنتاج عنصر جديد عندما يقوم الـ Consumer بطلب MoveNext() و Current على الترتيب , وتستمر هذه العملية إلى أن تنتهي السلسلة , أو أن يتوقف الـ Consumer بطلب عناصر جديدة ...

الاّن سنتكتشف من خلال عدة أمثلة مقدار الجمال البرمجي (إذا إتفقتم معي على وجوده !) و السهولة المقدمة بإستعمال Yield Keyword , فلنبدأ بتوسيع المثال السابق ,ولنقم بإضافة GetInReverse() Method التي تنتج السلسلة بشكل عكسي عند طلبها من الـ Consumer :

public class MyCollection : IEnumerable<int>
 	{
        internal int[] array;

        public  MyCollection(params int[] items)
        {
            array =  items;
        }

        public IEnumerator<int>  GetEnumerator()
        {
            foreach (var item in array)
             	yield return item;
        }
        public  IEnumerable<int> InReverse()
        {
            for (int i  = array.Length-1; i >= 0; i--)
                yield return  array;
        }


        IEnumerator  IEnumerable.GetEnumerator()
        {
            return  (IEnumerator<int>)this;
        }
    }

كما هو ملاحظ , فإستعمال كلمة Yield ممكن فقط وفقط عندما يكون الـ Return Type of Method كـ IEnumerable of Any Type أو IEnumerator أيضا ...

لنقم بتوسيع المثال أكثر و أكثر و لندخل سيناريوهات معقدة , فسنقوم بكتابة Method إسمها Till والتي تقوم بإنتاج سلسلة حتى تصادف عنصر أكبر من من رقم معطى مسبقا أو عند إنتهاء تلك السلسة ( في حال لم يُصادف عنصر يحمل الشروط السابقة ) : و تكون كالتالي :

.
.
.

     	public IEnumerable<int> Till(int num)
        {
         	foreach (var item in array)
            {
             	if (item > num)
                    yield break;
             	else
                    yield return item;
            }
     	}
.
.
.

وهنا كما تلاحظون , إستعملنا تركيب جديد هو : Yield break الذي سيقوم بإنهاء إنتاج السلسلة حال تنفيذه , وإستُعمل هنا لكي يتم إيقاف إنتاج السلسة عند مصادفة رقم أكبر من Num المزود من قبل الـ Consumer , و إلا تابع بإنتاج السلسلة بشكل طبيعي حتى نهايتها أو مصدافته لشيء معاكس للشرط ...

مثال , يقوم بتجريب ما تم إنجازه للاّن :

MyCollection collection = new MyCollection(1, 2, 3,  4, 5, 8, 8, 8, 8, 6, 4, 3);
            foreach (int num in  collection)
                Console.WriteLine(num);
         	foreach (int num in collection.InReverse())
             	Console.WriteLine(num);
            foreach (int num in  collection.Till(7))
                Console.WriteLine(num);

Lazy Evaluation :

الكسل الذي تعاني منه عمليه الإنتاج واضحة وجلية ربما للبعض , ولكن لمن لم يلاحظ ذلك , فإنتبه إلى طريقة عمل الـ Producer في إنتاج العناصر , فهي تنتج عند الطلب (عند طلب MoveNext) ,وبالتالي السلسلة الكاملة لا تحسب دفعة واحدة , بل يحسب عنصر واحد فقط عند الطلب , وهذا الإسلوب مفيد للغاية ,في عدم حساب أو إنتاج ما قد يكون غير مفيد (غير مفيد تعني في إحتمال عدم إستهلاك السلسلة بالكامل ) , فإذا كان حساب عناصر السلسلة مكلف (حوسبيا) فالـ Lazy Evaluation (التقييم الكسول) سيفتح اّفاف كبيرة في كونه مبدأ أساسي لتأجيل الحوسبة إلى اّخر لحظة ممكنة (أي البدء بإنتاج العنصر المراد فعلا إنتاجه فقط عند طلب العنصر التالي , عن طريق Move Next ) .

مثال , سنطرح فكرة مولد لأارقام عشوائة لا ينتهي أبدا , فنستطيع توليد أرقام عشوائية إلى الا نهاية , لكن الـ Consumer هو من سيقوم بالتوقف عن الطلب من الـ Producer في إنتاج أرقام عشوائية جديدة :

public static class RandomNumberGenerator
    {
     	public static IEnumerable<int> Generator
        {
         	get 
            {
                Random rand = new  Random();

                while (true)
                 	yield return rand.Next();
            }
        }
    }

والـ Consumer :

foreach (var num in  RandomNumberGenerator.Generator)
            {
             	Console.WriteLine(num);
                if (num % 9 == 0)
                 	break;
            }

هذا المستهلك سيقوم بطباعة كل الأرقام المولدة من الـ Generator عند الطلب , إلى أن يلقى رقما من قواسم الـ 9 , عندها يتوقف ...

سيتم التطرق لهذا الموضوع لاحقا في مقالات أخرى عند طرح تقنية الـ Linq ...

أخيرا , أود القول هنا , أن فكرة عودة الـ Method من حيث إنتهت في المرة السابقة لتنفيذها لشيء كـ Yield Return Something تبدو خارجة عن المألوف , وأنا أوافقك الرأي , فكلنا نتوقع إنتهاء حياة الـ Method وما بداخلها عن الإنتهاء من تنفيذها , لكن بما أن الـ Yield تخلق تحت الكواليس State Machine ,فحتى بعد الإنتهاء من تنفيذ الـ Method هناك الكثير من الأشياء المختبئة التي ستتكفل بحفظ حالة الـ Method ,لكي يتم التنفيذ من حيث توقفت عند الطلب المقبل من الـ Consumer في إنتاج عنصر جديد ...فلماذا سميت State Machine ؟

هناك كثير من الأفكار البناءة و المفيدة التي قد تخطر على بالك , وأود القول هنا إن أجمل التقنيات مبنية فوق هاتين الواجهتين كـ Linq .

مثال للإستئناس , كيفية عمل LinkedList تدعم IEnumerable (هذه الـ Linked List مختصرة للغاية لا تحوي إلا Add فقط ) :

class  LinkedList<T>:IEnumerable<T>
    {
        private  Node<T> root , tail;
        public void Add(T data)
     	{
            if (this.root == null)
            {
             	this.root = this.tail = new Node<T>(data, null);
         	}
            else
            {
             	this.tail.Next = new Node<T>(data, null);
             	this.tail = this.tail.Next;
            }
        }


     	public IEnumerator<T> GetEnumerator()
        {
         	Node<T> temp = root;
            while (temp != null)
         	{
                yield return temp.Data;
             	temp = temp.Next;
            }
        }

     	System.Collections.IEnumerator  System.Collections.IEnumerable.GetEnumerator()
        {
         	return (System.Collections.Generic.IEnumerator<T>)this;
     	}
    }
    internal class Node<T>
    {
     	internal Node(T data, Node<T> next)
        {
         	Data = data;
            Next = next; 
        }

     	public T Data { get; private set; }

        public Node<T>  Next { get; set; }
    }

أتمنى أن تكون قد وصلت الفكرة , والـ Design Pattern (أنماط التصميم) هي مجال ممتع فعلا لكل من يحب التجريد ...

ألقاكم في شرح الأنماط المتبقية ...

تم تعديل هذه المشاركة بواسطة AliBazzi في 18 سبتمبر 2010 في 16:39

2

مواضيع مشابهة