ConstructiCat Logo
CodeBust.
Browse section ▾

الزائر.

الزائر هو نمط تصميم سلوكي يتيح لك فصل الخوارزميات عن الكائنات التي تعمل عليها.

##Intent

الزائر هو نمط تصميم سلوكي يتيح لك فصل الخوارزميات عن الكائنات التي تعمل عليها.

نمط تصميم الزائر

##Problem

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

تصدير الرسم البياني إلى XML

تصدير الرسم البياني إلى XML.

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

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

أسلوب تصدير XML كان لا بد من إضافته إلى جميع فئات العقد

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

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

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

##Solution

يقترح نمط الزائر وضع السلوك الجديد في فئة منفصلة تُسمى visitor، بدلاً من محاولة دمجه في الفئات الموجودة. يُمرَّر الكائن الأصلي الذي كان عليه تنفيذ السلوك الآن إلى أحد أساليب الزائر كوسيطة، مما يتيح للأسلوب الوصول إلى جميع البيانات اللازمة الموجودة في الكائن.

الآن، ماذا لو كان يمكن تنفيذ هذا السلوك على كائنات من فئات مختلفة؟ على سبيل المثال، في حالتنا مع تصدير XML، سيكون التنفيذ الفعلي مختلفاً قليلاً عبر فئات العقد المختلفة. وبالتالي، قد تُعرِّف فئة الزائر ليس أسلوباً واحداً، بل مجموعة من الأساليب، يمكن لكل منها أن يأخذ وسيطات من أنواع مختلفة، كما يلي:

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

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

foreach (Node node in graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node)
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node)
    // ...
}

قد تسأل، لماذا لا نستخدم التحميل الزائد للأساليب؟ هذا عندما تعطي جميع الأساليب نفس الاسم، حتى لو كانت تدعم مجموعات مختلفة من المعاملات. لسوء الحظ، حتى لو افترضنا أن لغة البرمجة تدعمه أصلاً (كما تفعل Java و C#)، فلن يفيدنا ذلك. نظراً لأن الفئة الدقيقة لكائن العقدة غير معروفة مسبقاً، لن تتمكن آلية التحميل الزائد من تحديد الأسلوب الصحيح للتنفيذ. ستعود بشكل افتراضي إلى الأسلوب الذي يأخذ كائناً من الفئة الأساسية Node.

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

// كود العميل
foreach (Node node in graph)
    node.accept(exportVisitor)

// المدينة
class City is
    method accept(Visitor v) is
        v.doForCity(this)
    // ...

// الصناعة
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this)
    // ...

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

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

##Structure

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

  2. يُنفِّذ كل زائر ملموس عدة إصدارات من نفس السلوكيات، مُخصَّصة لفئات عنصر ملموسة مختلفة.

  3. تُعلن واجهة العنصر عن أسلوب "لقبول" الزوار. يجب أن يحتوي هذا الأسلوب على معامل واحد مُعلَن بنوع واجهة الزائر.

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

  5. يمثّل العميل عادةً مجموعة أو أي كائن معقد آخر (على سبيل المثال، شجرة مركّب). عادةً لا يكون العملاء على دراية بجميع فئات العنصر الملموسة لأنهم يعملون مع الكائنات من تلك المجموعة عبر واجهة مجردة.

##Pseudocode

في هذا المثال، يُضيف نمط الزائر دعم تصدير XML إلى التسلسل الهرمي لفئات الأشكال الهندسية.

بنية مثال نمط الزائر

تصدير أنواع مختلفة من الكائنات إلى تنسيق XML عبر كائن زائر.

// تُعلن واجهة العنصر عن أسلوب `accept` يأخذ
// واجهة الزائر الأساسية كوسيطة.
interface Shape is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

// يجب أن تُنفِّذ كل فئة عنصر ملموسة أسلوب `accept`
// بطريقة تستدعي أسلوب الزائر المقابل لفئة العنصر.
class Dot implements Shape is
    // ...

    // لاحظ أننا ندعو `visitDot`، الذي يطابق
    // اسم الفئة الحالية. بهذه الطريقة نُعلم الزائر بفئة
    // العنصر الذي يعمل معه.
    method accept(v: Visitor) is
        v.visitDot(this)

class Circle implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitRectangle(this)

class CompoundShape implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCompoundShape(this)


// تُعلن واجهة الزائر عن مجموعة من أساليب الزيارة التي
// تتوافق مع فئات العنصر. يتيح توقيع أسلوب الزيارة
// للزائر تحديد الفئة الدقيقة للعنصر الذي يتعامل معه.
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundShape(cs: CompoundShape)

// تُنفِّذ الزوار الملموسون عدة إصدارات من نفس
// الخوارزمية، التي يمكنها العمل مع جميع فئات العنصر الملموسة.
//
// يمكنك الاستفادة من أكبر ميزة لنمط الزائر
// عند استخدامه مع هيكل كائن معقد مثل
// شجرة Composite. في هذه الحالة، قد يكون من المفيد تخزين
// بعض الحالة الوسيطة للخوارزمية أثناء تنفيذ
// أساليب الزائر على كائنات مختلفة من الهيكل.
class XMLExportVisitor implements Visitor is
    method visitDot(d: Dot) is
        // تصدير معرّف النقطة وإحداثيات المركز.

    method visitCircle(c: Circle) is
        // تصدير معرّف الدائرة وإحداثيات المركز ونصف القطر.

    method visitRectangle(r: Rectangle) is
        // تصدير معرّف المستطيل وإحداثيات الزاوية العلوية اليسرى،
        // والعرض والارتفاع.

    method visitCompoundShape(cs: CompoundShape) is
        // تصدير معرّف الشكل بالإضافة إلى قائمة معرّفات عناصره الفرعية.


// يمكن لكود العميل تشغيل عمليات الزائر على أي مجموعة من
// العناصر دون معرفة فئاتها الملموسة. تُوجِّه عملية
// القبول الاستدعاء إلى العملية المناسبة
// في كائن الزائر.
class Application is
    field allShapes: array of Shapes

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach (shape in allShapes) do
            shape.accept(exportVisitor)

إذا تساءلت لماذا نحتاج أسلوب accept في هذا المثال، فإن مقالتي الزائر والإرسال المزدوج تتناول هذا السؤال بالتفصيل.

##Applicability

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

يتيح لك نمط الزائر تنفيذ عملية على مجموعة من الكائنات ذات فئات مختلفة، من خلال جعل كائن الزائر ينفّذ عدة متغيرات من نفس العملية تتوافق مع جميع الفئات المستهدفة.

استخدم الزائر لتنظيف منطق الأعمال من السلوكيات المساعدة.

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

استخدم النمط عندما يكون للسلوك معنى في بعض فئات التسلسل الهرمي للفئات فقط، دون غيرها.

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

##How to Implement

  1. أعلن عن واجهة الزائر بمجموعة من أساليب "الزيارة"، أسلوب واحد لكل فئة عنصر ملموسة موجودة في البرنامج.

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

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

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

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

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

  6. يجب على العميل إنشاء كائنات الزائر وتمريرها إلى العناصر عبر أساليب "القبول".

##Pros & Cons

Pros
  • مبدأ المفتوح/المغلق. يمكنك إدخال سلوك جديد يعمل مع كائنات من فئات مختلفة دون تغيير هذه الفئات.
  • مبدأ المسؤولية الواحدة. يمكنك نقل إصدارات متعددة من نفس السلوك إلى نفس الفئة.
  • يمكن لكائن الزائر تجميع بعض المعلومات المفيدة أثناء عمله مع كائنات مختلفة. قد يكون هذا مفيداً عندما تريد اجتياز هيكل كائن معقد، مثل شجرة كائنات، وتطبيق الزائر على كل كائن من هذا الهيكل.
Cons
  • تحتاج إلى تحديث جميع الزوار في كل مرة تُضاف فيها فئة إلى التسلسل الهرمي للعناصر أو تُزال منه.
  • قد يفتقر الزوار إلى الصلاحيات اللازمة للوصول إلى الحقول والأساليب الخاصة بالعناصر التي من المفترض أن يعملوا معها.

##Relations with Other Patterns

  • يمكنك التعامل مع الزائر باعتباره إصداراً قوياً من نمط الأمر. يمكن لكائناته تنفيذ عمليات على كائنات مختلفة من فئات متنوعة.

  • يمكنك استخدام الزائر لتنفيذ عملية على شجرة المركّب بأكملها.

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

##Extra Content

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