نمط المكرِّر Iterator

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

نمط المكرِّر هو نمط تصميم سلوكي يسمح لك بتخطي عناصر من مجموعة (Collection) دون كشف التمثيل التحتي (underlying representation) لها (قائمة، مكدَّس، شجرة، إلخ).

المشكلة

المجموعات هي إحدى أكثر أنواع البيانات استخدامًا في البرمجة، لكن رغم ذلك فهي لا تعدو كونها مجرد حاوية لمجموعة من الكائنات.

ضع الصورة. أنواع مختلفة من المجموعات.

تخزن أغلب المجموعات عناصرها في قوائم بسيطة، لكن تبنى بعضها على مكدسات أو أشجار أو مخططات أو هياكل بيانات معقدة أخرى. لكن بغض النظر عن كيفية هيكلة المجموعة فيجب أن توفر طريقة للوصول إلى عناصرها كي تستطيع الشيفرات الأخرى استخدام تلك العناصر، ويجب أن توجد طريقة للمرور على كل عنصر من المجموعة دون الوصول إلى نفس العناصر أكثر من مرة.

قد يبدو هذا الأمر سهلًا إن كانت مجموعتك مبنية على قائمة، فما عليك سوى المرور على العناصر بالترتيب مرة بعد مرة، لكن كيف تتخطى عناصر تركيب بيانات معقد مثل الشجرة بشكل متسلسل؟ فإن ناسبك تجاوز العمق الأول (depth-first traversal) لشجرة بيانات في يوم ما، فقد لا يناسبك في اليوم التالي إذ قد تحتاج إلى إجراء تجاوز العرض الأول (Breadth-first traversal)، وهكذا بنفس المنطق قد تحتاج شيئًا آخر تمامًا بعد أسبوع مثلًا، كأن تحتاج عناصر بشكل عشوائي داخل الشجرة.

ضع الصورة. يمكن تجاوز نفس المجموعة بطرق متعددة.

ومع الوقت فإن إضافة خورازميات تجاوز إلى المجموعة يشوش وظيفتها الأساسية، والتي هي تخزين البيانات بكفاءة، كذلك فإن بعض الخوارزميات قد تكون مصممة لتطبيق معين، فلا يكون مناسبًا أن تضاف إلى فئة مجموعة عامة.

ومن الناحية الأخرى فإن شيفرة العميل المفترض أن تعمل مع مجموعات متعددة قد لا تهتم أصلًا بالكيفية التي تخزن بها بياناتها، لكن بما أن المجموعات توفر طرقًا مختلفة من الوصول إلى عناصرها، فقد لا يكون لديك خيار سوى ربط شيفرتها بفئات مجموعات محددة.

الحل

الوظيفة الأساسية لنمط المكرر هي استخراج سلوك التجاوز للمجموعة إلى كائن منفصل يسمى المكرِّر (Iterator).

ضع الصورة (تستخدم المكرِّرات خوارزميات تجاوز مختلفة، وتستطيع عدة مكررات أن تتجاوز نفس المجموعة في نفس الوقت).

إضافة إلى استخدام الخوارزمية نفسها، فإن كائن المكرر يختزل كل تفاصيل التجاوز مثل الموضع الحالي وعدد العناصر المتبقية حتى النهاية، ولهذا فتستطيع عدة مكررات أن تمر على مجموعة واحدة في نفس الوقت بشكل مستقل عن بعضها.

وتوفر المكررات عادة أسلوبًا أساسيًا واحدًا للحصول على عناصر مجموعة ما، ويستطيع العميل أن يبقي هذا الأسلوب عاملًا إلى أن لا يعيد أي شيء، ما يعني أن المكرر قد تجاوز كل العناصر.

ويجب أن تستخدم كل المكررات نفس الواجهة، هذا يجعل شيفرة العميل متوافقة مع أي نوع مجموعة أو أي خوارزمية تجاوز طالما أن هناك مكررًا مناسبًا، وإن احتجت أسلوبًا خاصًا لتجاوز مجموعة فأنشئ فئة مكرر جديد ببساطة، دون الحاجة إلى تغيير المجموعة أو العميل.

مثال من الواقع

ضع الصورة. عدة طرق للسير في روما.

لنقل أنك تخطط للذهاب إلى روما في رحلة لزيارة المواقع الأثرية والسياحية، لكنك تكتشف بمجرد وصولك أنك قد تضيع ساعات كثيرة في السير دون هدى، غير قادر على إيجاد أي شيء حتى ساحة الكولوسيوم -أشهر معلم أثري في روما-. لكن من الناحية الأخرى، يمكنك شراء تطبيق للإرشاد السياحي على هاتفك واستخدامه للتنقل، خيار ذكي وغير مكلف، وتستطيع به البقاء في الأماكن الأثرية كما تشاء.

لديك خيار ثالث بديل هنا، وهو أن تنفق مزيدًا من ميزانية الرحلة لتوظف مرشدًا محليًا يعرف المدينة عن ظهر قلب، وسيستطيع ذلك المرشد أن يخصص الرحلة وفقًا لذوقك، ويريك كل موقع جذب سياحي ويخبرك الكثير من القصص المدهشة عن المدينة. لا شك أن هذا سيكون أكثر متعة لكنه مكلف أيضًا.

تتصرف كل تلك الخيارات السابقة، سواء الإرشادات العشوائية أو تطبيق الهاتف أو المرشد المحلي، تتصرف جميعها كمكرِّرات على مجموعة المشاهد ومواقع الجذب السياحي في روما.

البنية

ضع الصورة.

  1. تصرح واجهة المكرر (Iterator) عن العمليات المطلوبة لتخطي مجموعة ما: جلب العنصر التالي، أو استرجاع الخيار الحالي، أو إعادة تشغيل التكرار، إلخ.
  2. تستخدم المكرِّرات الحقيقية (Concrete Iterators) خوارزميات محددة لتخطي مجموعة ما، وينبغي أن يتتبع كائن المكرر تقدُّم عملية التخطي بنفسه، ذلك يسمح لعدة مكررات أن تتخطى نفس المجموعة بشكل مستقل عن بعضها البعض.
  3. تصرح واجهة المجموعة (Collection) عن أسلوب واحد أو أكثر لجعل المكرِّرات تتوافق مع المجموعة، لاحظ أن نوع العودة (return type) للأساليب يجب أن يصرَّح عنه على أنه واجهة المكرِّر (iterator interface) كي تتمكن المجموعات الحقيقية من إعادة أنواع المكرِّرات المختلفة.
  4. تعيد المجموعات الحقيقية (Concrete Collections) نسخًا جديدة من فئة مكرر حقيقي بعينه في كل مرة يطلب العميل واحدًا، وينبغي أن تكون بقية شيفرة المجموعة في نفس الفئة، لكننا نهملها لأن تلك التفاصيل ليست ضرورية للنمط الفعلي.
  5. يعمل العميل (Client) مع المجموعات والمكرِّرات من خلال واجهاتها، وهكذا لا يُربط العميل بالفئات الحقيقية مما يسمح لك باستخدام مجموعات ومكررات متنوعة بنفس شيفرة العميل. ولا تنشئ العملاء عادة مكرِّرات بأنفسها، وإنما تحصل عليها من المجموعات، لكن يستطيع العميل إنشاء مكرر مباشرة في بعض الحالات، كأن يعرِّف العميل المكرِّر الخاص به.

مثال وهمي

في هذا المثال يُستخدم نمط المكرِّر للمرور على نوع خاص من المجموعات تغلف الدخول إلى مخطط اجتماعي لفيس بوك، وتوفر المجموعة عدة مكرِّرات يمكنها تجاوز الحسابات بطرق مختلفة.

الصورة. مثال للتكرار على الحسابات الاجتماعية.

يمكن استخدام مكرر "friends" للمرور على الأصدقاء من حساب بعينه، وكذلك مكرر "colleagues" إلا أن الأخير يهمل الأصدقاء الذين لا يعملون في نفس الشركة التي يعمل فيها الشخص الهدف. ويستخدم كلا المكرِّران واجهة مشتركة تسمح للعملاء بجلب الحسابات دون الغوص في تفاصيل الاستخدام كالتصديق (Authentication) وإرسال طلبات REST.

لا تكون شيفرة العميل مرتبطة بالفئات الحقيقية لأنها تعمل مع المجموعات والمكرِّرات من خلال الواجهات فقط، فإن قررت ربط تطبيقك بشبكة اجتماعية جديدة، فلا تحتاج إلا إلى إضافة فئة مجموعة جديدة وفئة مكرِّر دون تغيير الشيفرة الحالية.

// يجب أن تصرِّح واجهة المجموعة عن أسلوب مصنع لإنتاج المكرِّرات.
// تستطيع التصريح عن عدة أساليب إن كان لديك أنواعًا مختلفة من التكرار متاحةً في برنامجك.
// are different kinds of iteration available in your program.
interface SocialNetwork is
    method createFriendsIterator(profileId):ProfileIterator
    method createCoworkersIterator(profileId):ProfileIterator


// تُربَط كل مجموعة حقيقية بمجموعة من فئات المكرِّر الحقيقية التي تعيدها.
// هذه الأساليب يعيد واجهات المكرِّر. (signature) لكن لا ينطبق ذلك على العميل لأن توقيع 
class Facebook implements SocialNetwork is
    // ... توضع شيفرة المجموعة هنا ...

    // شيفرة إنشاء المكرِّر.
    method createFriendsIterator(profileId) is
        return new FacebookIterator(this, profileId, "friends")
    method createCoworkersIterator(profileId) is
        return new FacebookIterator(this, profileId, "coworkers")


// الواجهة المشتركة لكل المكرِّرات.
interface ProfileIterator is
    method getNext():Profile
    method hasMore():bool


// فئة المكرِّر الحقيقي.
class FacebookIterator implements ProfileIterator is
    // يحتاج المكرِّر إلى مرجع إلى المجموعة التي يتخطاها
    private field facebook: Facebook
    private field profileId, type: string

    // يتخطى مكرِّر ما المجموعةَ بشكل مستقل عن باقي المكرِّرات.
    // لذا يجب أن يخزن حالة التكرار.
    private field currentPosition
    private field cache: array of Profile

    constructor FacebookIterator(facebook, profileId, type) is
        this.facebook = facebook
        this.profileId = profileId
        this.type = type

    private method lazyInit() is
        if (cache == null)
            cache = facebook.socialGraphRequest(profileId, type)

    // الخاص بها من واجهة المكرِّر المشتركة (implementation) كل فئة مكرر حقيقي لها تطبيقها.
    method getNext() is
        if (hasMore())
            currentPosition++
            return cache[currentPosition]

    method hasMore() is
        lazyInit()
        return cache.length < currentPosition


// تستطيع تمرير مكرِّرٍ إلى فئة عميل بدلًا من إعطائه وصولًا إلى المجموعة كلها.
// وهكذا تتجنب كشف المجموعة للعميل.
//
// إحدى الفوائد الأخرى كذلك هي أنك تستطيع تغيير الطريقة التي يعمل بها العميل مع المجموعة
// أثناء وقت التشغيل من خلال تمرير مكرر مختلف إليه.
//وهذا ممكن التنفيذ بسبب أن شيفرة العميل ليست مرتبطة بفئات مكرر حقيقية.
class SocialSpammer is
    method send(iterator: ProfileIterator, message: string) is
        while (iterator.hasNext())
            profile = iterator.getNext()
            System.sendEmail(profile.getEmail(), message)


// تهيئ فئة التطبيق المجموعات والمكرِّرات ثم تمررهم إلى شيفرة العميل.
class Application is
    field network: SocialNetwork
    field spammer: SocialSpammer

    method config() is
        if working with Facebook
            this.network = new Facebook()
        if working with LinkedIn
            this.network = new LinkedIn()
        this.spammer = new SocialSpammer()

    method sendSpamToFriends(profile) is
        iterator = network.createFriendsIterator(profile.getId())
        spammer.send(iterator, "Very important message")

    method sendSpamToCoworkers(profile) is
        iterator = network.createCoworkersIterator(profile.getId())
        spammer.send(iterator, "Very important message")

قابلية التطبيق

  • استخدم نمط المكرِّر عندما تكون مجموعتك بها هيكل بيانات معقد وتريد إخفاء ذلك التعقيد عن العملاء (من أجل تسهيل تجربة الاستخدام أو لأسباب أمنية).

يختزل المكرر تفاصيل العمل مع هيكل البيانات المعقد موفرًا عدة أساليب بسيطة للعميل للوصول إلى عناصر المجموعة. يسهل هذا المنظور تجربة الاستخدام للعميل كما يحمي المجموعة من الإهمال أو الأفعال الضارة التي قد ينفذها العميل إن كان بتعامل مباشرة مع المجموعة.

  • استخدم النمط لتقليل تكرار شيفرات التخطي في برنامجك.

تميل شيفرة خوارزميات التكرار غير العادي إلى أن تكون كبيرة الحجم، وقد تشوش على مسؤولية الشيفرة الأصلية لبرنامج إن وُضعت داخل منطق العمل الخاص به، وكذلك قد تجعل تعديلها وإصلاحها أصعب. لهذا فإن نقل شيفرة التخطي (traverse code) إلى مكرِّرات محددة يجعل شيفرة البرنامج أكثر رشاقة ونظامًا.

  • استخدم المكرِّر حين تريد لشيفرتك أن تكون قادرة على تخطي هياكل بيانات مختلفة أو عند عدم معرفة أنواع من تلك الهياكل مسبقًا.

يوفر النمط بضعة واجهات عامة لكل من المجموعات والمكرِّرات، وبما أن شيفرتك تستخدم الآن هذه الواجهات، فستعمل إن مررت إليها أنواعًا مختلفة من المجموعات والمكرِّرات التي تستخدم تلك الواجهات.

كيفية الاستخدام

  1. صرِّح عن واجهة المكرِّر، يجب أن يكون بها على أسلوبًا واحدًا على الأقل لجلب العنصر التالي من مجموعة، لكن يمكنك إضافة بعض الأساليب الأخرى من أجل التيسير على نفسك، مثل جلب العنصر السابق وتتبع الموضع الحالي وتفقد نهاية التكرار.
  2. صرِّح عن واجهة المجموعة وصِفْ أسلوبًا لجلب المكرِّرات، يجب أن يكون نوع الإعادة (return type) مكافئًا لنوع إعادة واجهة المكرِّر. قد تصرِّح عن أساليب مشابهة إن كنت تخطط أن يكون لديك عدة مجموعات مميزة (distinct) من المكرِّرات.
  3. استخدم فئات المكرر الحقيقي (concrete iterator) للمجموعات التي تريدها أن تكون قابلة للتخطي (traversable) بواسطة المكرِّرات، ويجب أن يكون كائن المكرر مرتبطًا بمجموعة واحدة فقط. عادة ما يُنشأ هذا الرابط من خلال منشئ المكرِّر (iterator's constructor).
  4. استخدم واجهة المجموعة في فئات مجموعتك، غرض هذه الاستخدام هو تزويد العميل باختصار لإنشاء المكرِّرات المصممة من أجل فئة مجموعة بعينها، يجب أن يمرر كائن المجموعة نفسه إلى منشئ المكرِّر من أجل إنشاء رابط بينهما.
  5. اذهب إلى شيفرة العميل لتغيير شيفرة تخطي المجموعة لإحلال المكرِّرات مكانها، سيجلب العميل كائن مكرِّر جديد في كل مرة يحتاج أن يمر فيها على عناصر المجموعة.

المزايا والعيوب

المزايا

مبدأ المسؤولية الواحدة. تستطيع تنفية شيفرة العميل والمجموعات باستخراج خوارزميات التخطي كبيرة الحجم إلى فئات منفصلة.

مبدأ المفتوح/المغلق. تستطيع استخدام أنواع جديدة من المجموعات والمكرِّرات وتمريرها إلى الشيفرة الحالية دون تعطيل أي شيء.

تستطيع المرور على نفس المجموعة بشكل متوازي، ذلك أن كل كائن مكرِّر يحتوي على حالة التكرار الخاصة به.

وأيضًا، لنفس السبب السابق، يمكنك تأخير تكرار واستكماله عند الحاجة.

العيوب

قد يكون من الإسراف استخدام النمط إن كان برنامجك لا يعمل إلا مع المجموعات البسيطة.

قد يكون استخدام النمط أقل كفاءة من المرور على عناصر بعض المجموعات المخصصة مباشرة.

العلاقات مع الأنماط الأخرى

تستطيع استخدام المكرِّرات لتجاوز أشجار نمط المركَّب.

تستطيع استخدام أسلوب المصنع مع المكرر من أجل السماح للفئات الفرعية لمجموعة ان تعيد أنواعًا مختلفة من المكررات المتوافقة معها.

تستطيع استخدام نمط التذكرة مع نمط المكرر لتسجيل حالة التكرار الحالية وإرجاعها لحالة سابقة عند الحاجة.

تستطيع استخدام نمط الزائر (Visitor) مع المكرِّر لتجاوز هيكل بيانات معقد وتنفيذ عملية ما على عناصره، حتى لو كانت عناصره كلها تحتوي على فئات مختلفة.

الاستخدام في لغة جافا

الاستخدام في لغة #C

الاستخدام في لغة PHP

الاستخدام في لغة بايثون

الاستخدام في لغة روبي

الاستخدام في لغة Swift

الاستخدام في لغة TypeScript