الفرق بين المراجعتين ل"Design Patterns/iterator"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
(2.0 محتوى)
(2.1 محتوى)
سطر 6: سطر 6:
  
 
ضع الصورة. أنواع مختلفة من المجموعات.
 
ضع الصورة. أنواع مختلفة من المجموعات.
 +
 +
تخزن أغلب المجموعات عناصرها في قوائم بسيطة، لكن تبنى بعضها على مكدسات أو أشجار أو مخططات أو هياكل بيانات معقدة أخرى. لكن بغض النظر عن كيفية هيكلة المجموعة فيجب أن توفر طريقة للوصول إلى عناصرها كي تستطيع الشيفرات الأخرى استخدام تلك العناصر، ويجب أن توجد طريقة للمرور على كل عنصر من المجموعة دون الوصول إلى نفس العناصر أكثر من مرة.
 +
 +
قد يبدو هذا الأمر سهلًا إن كانت مجموعتك مبنية على قائمة، فما عليك سوى المرور على العناصر بالترتيب مرة بعد مرة، لكن كيف تتخطى عناصر تركيب بيانات معقد مثل الشجرة بشكل متسلسل؟ فإن ناسبك تجاوز العمق الأول (depth-first traversal) لشجرة بيانات في يوم ما، فقد لا يناسبك في اليوم التالي إذ قد تحتاج إلى إجراء تجاوز العرض الأول (Breadth-first traversal)، وهكذا بنفس المنطق قد تحتاج شيئًا آخر تمامًا بعد أسبوع مثلًا، كأن تحتاج عناصر بشكل عشوائي داخل الشجرة.
 +
 +
ضع الصورة. يمكن تجاوز نفس المجموعة بطرق متعددة.
 +
 +
ومع الوقت فإن إضافة خورازميات تجاوز إلى المجموعة يشوش وظيفتها الأساسية، والتي هي تخزين البيانات بكفاءة، كذلك فإن بعض الخوارزميات قد تكون مصممة لتطبيق معين، فلا يكون مناسبًا أن تضاف إلى فئة مجموعة عامة.
 +
 +
ومن الناحية الأخرى فإن شيفرة العميل المفترض أن تعمل مع مجموعات متعددة قد لا تهتم أصلًا بالكيفية التي تخزن بها بياناتها، لكن بما أن المجموعات توفر طرقًا مختلفة من الوصول إلى عناصرها، فقد لا يكون لديك خيار سوى ربط شيفرتها بفئات مجموعات محددة.
 +
 +
== الحل ==
 +
الوظيفة الأساسية لنمط المكرر هي استخراج سلوك التجاوز للمجموعة إلى كائن منفصل يسمى المكرِّر (Iterator).
 +
 +
ضع الصورة (تستخدم المكرِّرات خوارزميات تجاوز مختلفة، وتستطيع عدة مكررات أن تتجاوز نفس المجموعة في نفس الوقت).
 +
 +
إضافة إلى استخدام الخوارزمية نفسها، فإن كائن المكرر يختزل كل تفاصيل التجاوز مثل الموضع الحالي وعدد العناصر المتبقية حتى النهاية، ولهذا فتستطيع عدة مكررات أن تمر على مجموعة واحدة في نفس الوقت بشكل مستقل عن بعضها.
 +
 +
وتوفر المكررات عادة أسلوبًا أساسيًا واحدًا للحصول على عناصر مجموعة ما، ويستطيع العميل أن يبقي هذا الأسلوب عاملًا إلى أن لا يعيد أي شيء، ما يعني أن المكرر قد تجاوز كل العناصر.
 +
 +
ويجب أن تستخدم كل المكررات نفس الواجهة، هذا يجعل شيفرة العميل متوافقة مع أي نوع مجموعة أو أي خوارزمية تجاوز طالما أن هناك مكررًا مناسبًا، وإن احتجت أسلوبًا خاصًا لتجاوز مجموعة فأنشئ فئة مكرر جديد ببساطة، دون الحاجة إلى تغيير المجموعة أو العميل.
 +
 +
== مثال من الواقع ==
 +
ضع الصورة. عدة طرق للسير في روما.
 +
 +
لنقل أنك تخطط للذهاب إلى روما في رحلة لزيارة المواقع الأثرية والسياحية، لكنك تكتشف بمجرد وصولك أنك قد تضيع ساعات كثيرة في السير دون هدى، غير قادر على إيجاد أي شيء حتى ساحة الكولوسيوم -أشهر معلم أثري في روما-. لكن من الناحية الأخرى، يمكنك شراء تطبيق للإرشاد السياحي على هاتفك واستخدامه للتنقل، خيار ذكي وغير مكلف، وتستطيع به البقاء في الأماكن الأثرية كما تشاء.
 +
 +
لديك خيار ثالث بديل هنا، وهو أن تنفق مزيدًا من ميزانية الرحلة لتوظف مرشدًا محليًا يعرف المدينة عن ظهر قلب، وسيستطيع ذلك المرشد أن يخصص الرحلة وفقًا لذوقك، ويريك كل موقع جذب سياحي ويخبرك الكثير من القصص المدهشة عن المدينة. لا شك أن هذا سيكون أكثر متعة لكنه مكلف أيضًا.
 +
 +
تتصرف كل تلك الخيارات السابقة، سواء الإرشادات العشوائية أو تطبيق الهاتف أو المرشد المحلي، تتصرف جميعها كمكرِّرات على مجموعة المشاهد ومواقع الجذب السياحي في روما.
 +
 +
== البنية ==
 +
ضع الصورة.
 +
# تصرح واجهة المكرر (Iterator) عن العمليات المطلوبة لتخطي مجموعة ما: جلب العنصر التالي، أو استرجاع الخيار الحالي، أو إعادة تشغيل التكرار، إلخ.
 +
# تستخدم المكرِّرات الحقيقية (Concrete Iterators) خوارزميات محددة لتخطي مجموعة ما، وينبغي أن يتتبع كائن المكرر تقدُّم عملية التخطي بنفسه، ذلك يسمح لعدة مكررات أن تتخطى نفس المجموعة بشكل مستقل عن بعضها البعض.
 +
# تصرح واجهة المجموعة (Collection) عن أسلوب واحد أو أكثر لجعل المكرِّرات تتوافق مع المجموعة، لاحظ أن نوع العودة (return type) للأساليب يجب أن يصرَّح عنه على أنه واجهة المكرِّر (iterator interface) كي تتمكن المجموعات الحقيقية من إعادة أنواع المكرِّرات المختلفة.
 +
# تعيد المجموعات الحقيقية (Concrete Collections) نسخًا جديدة من فئة مكرر حقيقي بعينه في كل مرة يطلب العميل واحدًا، وينبغي أن تكون بقية شيفرة المجموعة في نفس الفئة، لكننا نهملها لأن تلك التفاصيل ليست ضرورية للنمط الفعلي.
 +
# يعمل العميل (Client) مع المجموعات والمكرِّرات من خلال واجهاتها، وهكذا لا يُربط العميل بالفئات الحقيقية مما يسمح لك باستخدام مجموعات ومكررات متنوعة بنفس شيفرة العميل. ولا تنشئ العملاء عادة مكرِّرات بأنفسها، وإنما تحصل عليها من المجموعات، لكن يستطيع العميل إنشاء مكرر مباشرة في بعض الحالات، كأن يعرِّف العميل المكرِّر الخاص به.
 +
 +
== مثال وهمي ==
 +
في هذا المثال يُستخدم نمط المكرِّر للمرور على نوع خاص من المجموعات تغلف الدخول إلى مخطط اجتماعي لفيس بوك، وتوفر المجموعة عدة مكرِّرات يمكنها تجاوز الحسابات بطرق مختلفة.
 +
 +
الصورة. مثال للتكرار على الحسابات الاجتماعية.
 +
 +
يمكن استخدام مكرر "friends" للمرور على الأصدقاء من حساب بعينه، وكذلك مكرر "colleagues" إلا أن الأخير يهمل الأصدقاء الذين لا يعملون في نفس الشركة التي يعمل فيها الشخص الهدف. ويستخدم كلا المكرِّران واجهة مشتركة تسمح للعملاء بجلب الحسابات دون الغوص في تفاصيل الاستخدام كالتصديق (Authentication) وإرسال طلبات REST.
 +
 +
لا تكون شيفرة العميل مرتبطة بالفئات الحقيقية لأنها تعمل مع المجموعات والمكرِّرات من خلال الواجهات فقط، فإن قررت ربط تطبيقك بشبكة اجتماعية جديدة، فلا تحتاج إلا إلى إضافة فئة مجموعة جديدة وفئة مكرِّر دون تغيير الشيفرة الحالية.<syntaxhighlight lang="java">
 +
// The collection interface must declare a factory method for
 +
// producing iterators. You can declare several methods if there
 +
// are different kinds of iteration available in your program.
 +
interface SocialNetwork is
 +
    method createFriendsIterator(profileId):ProfileIterator
 +
    method createCoworkersIterator(profileId):ProfileIterator
 +
 +
 +
// Each concrete collection is coupled to a set of concrete
 +
// iterator classes it returns. But the client isn't, since the
 +
// signature of these methods returns iterator interfaces.
 +
class Facebook implements SocialNetwork is
 +
    // ... The bulk of the collection's code should go here ...
 +
 +
    // Iterator creation code.
 +
    method createFriendsIterator(profileId) is
 +
        return new FacebookIterator(this, profileId, "friends")
 +
    method createCoworkersIterator(profileId) is
 +
        return new FacebookIterator(this, profileId, "coworkers")
 +
 +
 +
// The common interface for all iterators.
 +
interface ProfileIterator is
 +
    method getNext():Profile
 +
    method hasMore():bool
 +
 +
 +
// The concrete iterator class.
 +
class FacebookIterator implements ProfileIterator is
 +
    // The iterator needs a reference to the collection that it
 +
    // traverses.
 +
    private field facebook: Facebook
 +
    private field profileId, type: string
 +
 +
    // An iterator object traverses the collection independently
 +
    // from other iterators. Therefore it has to store the
 +
    // iteration state.
 +
    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)
 +
 +
    // Each concrete iterator class has its own implementation
 +
    // of the common iterator interface.
 +
    method getNext() is
 +
        if (hasMore())
 +
            currentPosition++
 +
            return cache[currentPosition]
 +
 +
    method hasMore() is
 +
        lazyInit()
 +
        return cache.length < currentPosition
 +
 +
 +
// Here is another useful trick: you can pass an iterator to a
 +
// client class instead of giving it access to a whole
 +
// collection. This way, you don't expose the collection to the
 +
// client.
 +
//
 +
// And there's another benefit: you can change the way the
 +
// client works with the collection at runtime by passing it a
 +
// different iterator. This is possible because the client code
 +
// isn't coupled to concrete iterator classes.
 +
class SocialSpammer is
 +
    method send(iterator: ProfileIterator, message: string) is
 +
        while (iterator.hasNext())
 +
            profile = iterator.getNext()
 +
            System.sendEmail(profile.getEmail(), message)
 +
 +
 +
// The application class configures collections and iterators
 +
// and then passes them to the client code.
 +
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")
 +
</syntaxhighlight>

مراجعة 06:13، 13 يوليو 2019

نمط المكرِّر هو نمط تصميم سلوكي يسمح لك بتخطي عناصر من مجموعة (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.

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

// The collection interface must declare a factory method for
// producing iterators. You can declare several methods if there
// are different kinds of iteration available in your program.
interface SocialNetwork is
    method createFriendsIterator(profileId):ProfileIterator
    method createCoworkersIterator(profileId):ProfileIterator


// Each concrete collection is coupled to a set of concrete
// iterator classes it returns. But the client isn't, since the
// signature of these methods returns iterator interfaces.
class Facebook implements SocialNetwork is
    // ... The bulk of the collection's code should go here ...

    // Iterator creation code.
    method createFriendsIterator(profileId) is
        return new FacebookIterator(this, profileId, "friends")
    method createCoworkersIterator(profileId) is
        return new FacebookIterator(this, profileId, "coworkers")


// The common interface for all iterators.
interface ProfileIterator is
    method getNext():Profile
    method hasMore():bool


// The concrete iterator class.
class FacebookIterator implements ProfileIterator is
    // The iterator needs a reference to the collection that it
    // traverses.
    private field facebook: Facebook
    private field profileId, type: string

    // An iterator object traverses the collection independently
    // from other iterators. Therefore it has to store the
    // iteration state.
    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)

    // Each concrete iterator class has its own implementation
    // of the common iterator interface.
    method getNext() is
        if (hasMore())
            currentPosition++
            return cache[currentPosition]

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


// Here is another useful trick: you can pass an iterator to a
// client class instead of giving it access to a whole
// collection. This way, you don't expose the collection to the
// client.
//
// And there's another benefit: you can change the way the
// client works with the collection at runtime by passing it a
// different iterator. This is possible because the client code
// isn't coupled to concrete iterator classes.
class SocialSpammer is
    method send(iterator: ProfileIterator, message: string) is
        while (iterator.hasNext())
            profile = iterator.getNext()
            System.sendEmail(profile.getEmail(), message)


// The application class configures collections and iterators
// and then passes them to the client code.
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")