الفرق بين المراجعتين لصفحة: «Design Patterns/visitor»

من موسوعة حسوب
لا ملخص تعديل
طلا ملخص تعديل
سطر 1: سطر 1:
<noinclude>{{DISPLAYTITLE:نمط الزائر Visitor}}</noinclude>
<noinclude>{{DISPLAYTITLE:نمط الزائر Visitor}}</noinclude>
نمط الزائر Visitor هو نمط تصميم سلوكي يسمح لك بفصل الخوارزميات من الكائنات التي تشغّلها.
نمط الزائر (Visitor) هو نمط تصميم سلوكي يسمح لك بفصل الخوارزميات من الكائنات التي تشغّلها.


== المشكلة ==
== المشكلة ==

مراجعة 13:47، 4 مارس 2020

نمط الزائر (Visitor) هو نمط تصميم سلوكي يسمح لك بفصل الخوارزميات من الكائنات التي تشغّلها.

المشكلة

(ش.1) تصدير المخطط إلى ملف XML

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

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

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

الحل

(ش.2) اضطررنا لإضافة أسلوب تصدير XML إلى جميع فئات العقد، مما جعلنا نخاطر بتعطيل البرنامج بالكامل إن تسربت عثة أثناء إجراء التغيير

يقترح نمط الزائر أن تضع السلوك الجديد داخل فئة منفصلة تسمى الزائر (Visitor)، بدلًا من محاولة دمجها داخل فئات أخرى، ويُمرر الكائن الأصلي الذي اضطر لتنفيذ السلوك إلى أحد أساليب الزائر كوسيط (argument)، مزودًا الأسلوب بالوصول إل كل البيانات الضرورية المخزنة داخل الكائن. والآن، في حالة إن كان ذلك السلوك يمكن تنفيذه على كائنات من فئات مختلفة، كما في حالة تصدير 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)
    // ...
}

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

// شيفرة العميل
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)
    // ...

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

مثال واقعي

(ش.3) وكيل التأمينات الماهر مستعد دومًا لتقديم سياسات مختلفة للأنواع المختلفة من المنشآت

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

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

البنية

  1. يصرح الزائر Visitor عن مجموعة من أساليب الزيارة التي تأخذ عناصر حقيقية من بنية الكائن كوسائط، وقد يكون لتلك الأساليب نفس الأسماء إن كان البرنامج مكتوبًا بلغة تدعم التحميل الزائد، لكن يجب أن يكون نوع معامِلاتها مختلفًا.
  2. يستخدم كل زائر حقيقي Concrete Visitor عدة إصدارات من نفس السلوكيات، مخصصة لتناسب فئات العناصر الحقيقية المختلفة.
  3. تصرح واجهة العنصر Element عن أسلوب لقبول الزائرين، يجب أن يكون لذلك الأسلوب معامِل واحد مصرَّح عنه مع نوع واجهة الزائر.
  4. يجب أن يطبق كل عنصر حقيقي Concrete Element أسلوب القبول، والعرض من هذا الأسلوب هو إعادة توجيه الاستدعاء إلى أسلوب الزائر المناسب الموافق لفئة العنصر الحالية. انتبه إلى أنه حتى لو استخدمت فئة العنصر الأساسي هذا الأسلوب فإن كل الفئات الفرعية يجب أن تتخطاه في فئاتها وتستدعي الأسلوب المناسب لها على كائن الزائر.
  5. يمثل العميل Client عادة مجموعة أو كائنًا معقدًا (مثل شجرة من نمط المركّب Composite)، ولا تدرك العملاء عادة بوجود جميع فئات العناصر الحقيقية لأنها تعمل مع كائنات من تلك المجموعة من خلال واجهة مجردة.

مثال توضيحي

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

(ش.5) تصدير أنواع متعددة من الكائنات إلى صيغة XML من خلال كائن زائر.
// يأخذ واجهة الزائر الأساسي كوسيط (accept) تصرح واجهة العنصر عن أسلوب قبول
interface Shape is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

// بطريقة تستدعي أسلوب الزائر الموافق accept يجب أن تستخدم كل فئة عنصر حقيقي أسلوب 
// لفئة العنصر.
class Dot extends Shape is
    // ...

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

class Circle extends Dot is
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle extends 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 tree شجرة المركّب
// حالة وسيطة للخوارزمية أثناء تنفيذ أساليب الزائر على كائنات تلك البنية.
class XMLExportVisitor implements Visitor is
    method visitDot(d: Dot) is
        // وإحداثيات المركز (Dot's ID) صدِّر معرف النقطة.

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

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

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


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

    method export() is
        exportVisitor = new XMLExportVisitor()

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

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

  • استخدم نمط الزائر حين تريد إجراء عملية على كل العناصر في بنية كائن معقد (كشجرة كائنات مثلًا)

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

  • استخدم الزائر لتنقية منطق العمل من السلوكيات الإضافية

يسمح لك النمط بجعل الفئات الرئيسية (primary classes) في برنامجك تركز أكثر على وظائفها الرئيسية من خلال استخراج كل السلوكيات الأخرى إلى مجموعة من فئات الزائر.

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

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

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

  1. صرح عن واجهة الزائر بجموعة من أساليب الزيارة، واحد لكل فئة عنصر حقيقي موجودة داخل البرنامج.
  2. صرح عن واجهة العنصر، وإن كنت تعمل مع هرمية فئات عناصر موجودة فعلًا فأضف أسلوب acceptance المجرد إلى الفئة الأساسية من الهرمية، يجب أن يقبل هذا الأسلوب كائن الزائر كوسيط.
  3. طبق أساليب acceptance في كل فئات العناصر الحقيقية، يجب أن تعيد هذه الأساليب توجيه الاستدعاء إلى أسلوب زيارة على كائن الزائر القادم الذي يوافق فئة العنصر الحالي.
  4. يجب أن تعمل فئات العناصر مع الزوار فقط من خلال واجهة الزائر. لكن الزوار يجب أن يكونوا على علم بجميع فئات العنصر الحقيقي المشار إليها كأنواع معامِل لأساليب الزيارة.
  5. أنشئ فئة زائر حقيقي لكل سلوك لا يمكن تطبيقه داخل هرمية العناصر، وطبق جميع أساليب الزيارة. قد تواجه موقفًا يحتاج الزائر فيه إلى وصول إلى بعض الأعضاء الخصوصيين (private members) داخل فئة العنصر، ويمكنك في تلك الحالة أن تجعل تلك الحقول أو الأساليب عامة (public) خارقًا بذلك تغليف العنصر، أو تدخل فئة الزائر في فئة العنصر. والخيار الأخير غير ممكن إلا إن استطعت العمل مع لغة برمجة تدعم الفئات المتداخلة.
  6. يجب أن ينشئ العميل كائنات زائر ويمررها إلى العناصر من خلال أساليب قبول (acceptance methods).

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

المزايا

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

العيوب

  • تحتاج أن تحدث جميع كائنات الزائر في كل مرة تضاف فيها فئة إلى هرمية العناصر أو تُحذف منها.
  • قد يفتقر الزوار إلى الوصول المطلوب للعناصر الخاصة (private fields) وأساليب العناصر التي يفترض أن تعمل معها.

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

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

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

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

تصدير الأشكال إلى XML

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

الأشكال shapes

shapes/Shape.java: واجهة الشكل المشتركة
package refactoring_guru.visitor.example.shapes;

import refactoring_guru.visitor.example.visitor.Visitor;

public interface Shape {
    void move(int x, int y);
    void draw();
    String accept(Visitor visitor);
}
shapes/Dot.java: نقطة (Dot)
package refactoring_guru.visitor.example.shapes;

import refactoring_guru.visitor.example.visitor.Visitor;

public class Dot implements Shape {
    private int id;
    private int x;
    private int y;

    public Dot() {
    }

    public Dot(int id, int x, int y) {
        this.id = id;
        this.x = x;
        this.y = y;
    }

    @Override
    public void move(int x, int y) {
        // حرِّك الشكل
    }

    @Override
    public void draw() {
        // ارسم الشكل.
    }

    public String accept(Visitor visitor) {
        return visitor.visitDot(this);
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getId() {
        return id;
    }
}
shapes/Circle.java:دائرة
package refactoring_guru.visitor.example.shapes;

import refactoring_guru.visitor.example.visitor.Visitor;

public class Circle extends Dot {
    private int radius;

    public Circle(int id, int x, int y, int radius) {
        super(id, x, y);
        this.radius = radius;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitCircle(this);
    }

    public int getRadius() {
        return radius;
    }
}
shapes/Rectangle.java: مستطيل
package refactoring_guru.visitor.example.shapes;

import refactoring_guru.visitor.example.visitor.Visitor;

public class Rectangle implements Shape {
    private int id;
    private int x;
    private int y;
    private int width;
    private int height;

    public Rectangle(int id, int x, int y, int width, int height) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitRectangle(this);
    }

    @Override
    public void move(int x, int y) {
        // حرك الشكل.
    }

    @Override
    public void draw() {
        // ارسم الشكل.
    }

    public int getId() {
        return id;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}
shapes/CompoundShape.java:شكل مجمَّع
package refactoring_guru.visitor.example.shapes;

import refactoring_guru.visitor.example.visitor.Visitor;

import java.util.ArrayList;
import java.util.List;

public class CompoundShape implements Shape {
    public int id;
    public List<Shape> children = new ArrayList<>();

    public CompoundShape(int id) {
        this.id = id;
    }

    @Override
    public void move(int x, int y) {
        // حرك الشكل.
    }

    @Override
    public void draw() {
        // ارسم الشكل.
    }

    public int getId() {
        return id;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitCompoundGraphic(this);
    }

    public void add(Shape shape) {
        children.add(shape);
    }
}

الزائر Visitor

visitor/Visitor.java: واجهة الزائر المشتركة
package refactoring_guru.visitor.example.visitor;

import refactoring_guru.visitor.example.shapes.Circle;
import refactoring_guru.visitor.example.shapes.CompoundShape;
import refactoring_guru.visitor.example.shapes.Dot;
import refactoring_guru.visitor.example.shapes.Rectangle;

public interface Visitor {
    String visitDot(Dot dot);

    String visitCircle(Circle circle);

    String visitRectangle(Rectangle rectangle);

    String visitCompoundGraphic(CompoundShape cg);
}
visitor/XMLExportVisitor.java: الزائر الحقيقي يصدر جميع الأشكال إلى XML
package refactoring_guru.visitor.example.visitor;

import refactoring_guru.visitor.example.shapes.*;

public class XMLExportVisitor implements Visitor {

    public String export(Shape... args) {
        StringBuilder sb = new StringBuilder();
        for (Shape shape : args) {
            sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "\n");
            sb.append(shape.accept(this)).append("\n");
            System.out.println(sb.toString());
            sb.setLength(0);
        }
        return sb.toString();
    }

    public String visitDot(Dot d) {
        return "<dot>" + "\n" +
                "    <id>" + d.getId() + "</id>" + "\n" +
                "    <x>" + d.getX() + "</x>" + "\n" +
                "    <y>" + d.getY() + "</y>" + "\n" +
                "</dot>";
    }

    public String visitCircle(Circle c) {
        return "<circle>" + "\n" +
                "    <id>" + c.getId() + "</id>" + "\n" +
                "    <x>" + c.getX() + "</x>" + "\n" +
                "    <y>" + c.getY() + "</y>" + "\n" +
                "    <radius>" + c.getRadius() + "</radius>" + "\n" +
                "</circle>";
    }

    public String visitRectangle(Rectangle r) {
        return "<rectangle>" + "\n" +
                "    <id>" + r.getId() + "</id>" + "\n" +
                "    <x>" + r.getX() + "</x>" + "\n" +
                "    <y>" + r.getY() + "</y>" + "\n" +
                "    <width>" + r.getWidth() + "</width>" + "\n" +
                "    <height>" + r.getHeight() + "</height>" + "\n" +
                "</rectangle>";
    }

    public String visitCompoundGraphic(CompoundShape cg) {
        return "<compound_graphic>" + "\n" +
                "   <id>" + cg.getId() + "</id>" + "\n" +
                _visitCompoundGraphic(cg) +
                "</compound_graphic>";
    }

    private String _visitCompoundGraphic(CompoundShape cg) {
        StringBuilder sb = new StringBuilder();
        for (Shape shape : cg.children) {
            String obj = shape.accept(this);
            // Proper indentation for sub-objects.
            obj = "    " + obj.replace("\n", "\n    ") + "\n";
            sb.append(obj);
        }
        return sb.toString();
    }

}
Demo.java: شيفرة العميل
package refactoring_guru.visitor.example;

import refactoring_guru.visitor.example.shapes.*;
import refactoring_guru.visitor.example.visitor.XMLExportVisitor;

public class Demo {
    public static void main(String[] args) {
        Dot dot = new Dot(1, 10, 55);
        Circle circle = new Circle(2, 23, 15, 10);
        Rectangle rectangle = new Rectangle(3, 10, 17, 20, 30);

        CompoundShape compoundShape = new CompoundShape(4);
        compoundShape.add(dot);
        compoundShape.add(circle);
        compoundShape.add(rectangle);

        CompoundShape c = new CompoundShape(5);
        c.add(dot);
        compoundShape.add(c);

        export(circle, compoundShape);
    }

    private static void export(Shape... shapes) {
        XMLExportVisitor exportVisitor = new XMLExportVisitor();
        System.out.println(exportVisitor.export(shapes));
    }
}
OutputDemo.txt: نتائج التنفيذ
<?xml version="1.0" encoding="utf-8"?>
<circle>
    <id>2</id>
    <x>23</x>
    <y>15</y>
    <radius>10</radius>
</circle>

<?xml version="1.0" encoding="utf-8"?>
<compound_graphic>
   <id>4</id>
    <dot>
        <id>1</id>
        <x>10</x>
        <y>55</y>
    </dot>
    <circle>
        <id>2</id>
        <x>23</x>
        <y>15</y>
        <radius>10</radius>
    </circle>
    <rectangle>
        <id>3</id>
        <x>10</x>
        <y>17</y>
        <width>20</width>
        <height>30</height>
    </rectangle>
    <compound_graphic>
       <id>5</id>
        <dot>
            <id>1</id>
            <x>10</x>
            <y>55</y>
        </dot>
    </compound_graphic>
</compound_graphic>

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

أمثلة الاستخدام: لا يكثر استخدام نمط الزائر في لغة جافا بسبب تعقيده والمجال الضيق لتطبيقه.

مثال تصوري

يوضح هذا المثال بنية نمط الزائر (Visitor)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

Program.cs: مثال تصوري

using System;
using System.Collections.Generic;

namespace RefactoringGuru.DesignPatterns.Visitor.Conceptual
{
    // يأخذ واجهة accept تصرح واجهة العنصر عن أسلوب 
    // الزائر الأساسية كوسيط.
    public interface IComponent
    {
        void Accept(IVisitor visitor);
    }

     // بطريقة تستدعي أسلوب الزائر الموافق لفئة العنصر Accept يجب أن يطبق كل عنصر حقيقي أسلوب.
    public class ConcreteComponentA : IComponent
    {
        // المطابق لاسم الفئة الحالية VisitConcreteComponentA لاحظ أننا نستدعي
        // وهكذا نسمح للزائر بمعرفة فئة العنصر الذي يعمل معه.
        public void Accept(IVisitor visitor)
        {
            visitor.VisitConcreteComponentA(this);
        }

        // قد يكون للعناصر الحقيقية أساليب خاصة لا توجد داخل فئتها الأساسية أو واجهتها، وسيستطيع
        // الزائر استخدام تلك الأساليب بما أنه مدرك لفئة العنصر الحقيقية.
        public string ExclusiveMethodOfConcreteComponentA()
        {
            return "A";
        }
    }

    public class ConcreteComponentB : IComponent
    {
        // نفس الشيء هنا: VisitConcreteComponentB => ConcreteComponentB
        public void Accept(IVisitor visitor)
        {
            visitor.VisitConcreteComponentB(this);
        }

        public string SpecialMethodOfConcreteComponentB()
        {
            return "B";
        }
    }

    // تصرح واجهة الزائر عن مجموعة من أساليب الزيارة التي تتوافق مع فئات العنصر.
    // ويسمح توقيع أسلوب الزيارة للزائر بتحديد فئة العنصر الذي يتعامل معه.
    public interface IVisitor
    {
        void VisitConcreteComponentA(ConcreteComponentA element);

        void VisitConcreteComponentB(ConcreteComponentB element);
    }

    // يستخدم الزائرون الحقيقيون عدة إصدارات من نفس الخوارزمية، وتستطيع تلك الإصدارات
    // أن تعمل مع جميع فئات العنصر الحقيقية.
    // 
    // تستطيع تجربة أكبر فائدة لنمط الزائر عند استخدامه مع بنية كائن معقد مثل شجرة المركب، وفي تلك الحالة فقد
    // يكون من المفيد تخزين حالة وسيطة للخوارزمية أثناء تنفيذ أساليب الزائر على عدة كائنات من البنية.
    class ConcreteVisitor1 : IVisitor
    {
        public void VisitConcreteComponentA(ConcreteComponentA element)
        {
            Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor1");
        }

        public void VisitConcreteComponentB(ConcreteComponentB element)
        {
            Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor1");
        }
    }

    class ConcreteVisitor2 : IVisitor
    {
        public void VisitConcreteComponentA(ConcreteComponentA element)
        {
            Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor2");
        }

        public void VisitConcreteComponentB(ConcreteComponentB element)
        {
            Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor2");
        }
    }

    public class Client
    {
        // تستطيع شيفرة العميل أن تشغِّل عمليات الزائر على أي مجموعة عناصر دون معرفة
        // استدعاءً إلى العملية المناسبة (accept) فئاتها الحقيقية، وتوجِّه عملية 
        // في كائن الزائر.
        public static void ClientCode(List<IComponent> components, IVisitor visitor)
        {
            foreach (var component in components)
            {
                component.Accept(visitor);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<IComponent> components = new List<IComponent>
            {
                new ConcreteComponentA(),
                new ConcreteComponentB()
            };

            Console.WriteLine("The client code works with all visitors via the base Visitor interface:");
            var visitor1 = new ConcreteVisitor1();
            Client.ClientCode(components,visitor1);

            Console.WriteLine();

            Console.WriteLine("It allows the same client code to work with different types of visitors:");
            var visitor2 = new ConcreteVisitor2();
            Client.ClientCode(components, visitor2);
        }
    }
}
Output.txt: نتائج التنفيذ
The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1

It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

أمثلة الاستخدام: لا يكثر استخدام نمط الزائر في لغة جافا بسبب تعقيده والمجال الضيق لتطبيقه.

مثال تصوري

يوضح هذا المثال بنية نمط الزائر (Visitor)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP.

index.php: مثال تصوري

<?php

namespace RefactoringGuru\Visitor\Conceptual;

/**
 * يأخذ واجهة accept تصرح واجهة العنصر عن أسلوب 
 * الزائر الأساسية كوسيط.
 */
interface Component
{
    public function accept(Visitor $visitor): void;
}

/**
 * بطريقة تستدعي أسلوب الزائر الموافق لفئة العنصر Accept يجب أن يطبق كل عنصر حقيقي أسلوب.
 */
class ConcreteComponentA implements Component
{
    /**
     * المطابق لاسم الفئة الحالية VisitConcreteComponentA لاحظ أننا نستدعي
     * وهكذا نسمح للزائر بمعرفة فئة العنصر الذي يعمل معه.
     */
    public function accept(Visitor $visitor): void
    {
        $visitor->visitConcreteComponentA($this);
    }

    /**
     * قد يكون للعناصر الحقيقية أساليب خاصة لا توجد داخل فئتها الأساسية أو واجهتها، وسيستطيع
     * الزائر استخدام تلك الأساليب بما أنه مدرك لفئة العنصر الحقيقية.
     */
    public function exclusiveMethodOfConcreteComponentA(): string
    {
        return "A";
    }
}

class ConcreteComponentB implements Component
{
    /**
     * نفس الشيء هنا: VisitConcreteComponentB => ConcreteComponentB
     */
    public function accept(Visitor $visitor): void
    {
        $visitor->visitConcreteComponentB($this);
    }

    public function specialMethodOfConcreteComponentB(): string
    {
        return "B";
    }
}

/**
 * تصرح واجهة الزائر عن مجموعة من أساليب الزيارة التي تتوافق مع فئات العنصر.
 * ويسمح توقيع أسلوب الزيارة للزائر بتحديد فئة العنصر الذي يتعامل معه.
 */
interface Visitor
{
    public function visitConcreteComponentA(ConcreteComponentA $element): void;

    public function visitConcreteComponentB(ConcreteComponentB $element): void;
}

/**
 * يستخدم الزائرون الحقيقيون عدة إصدارات من نفس الخوارزمية، وتستطيع تلك الإصدارات
 * أن تعمل مع جميع فئات العنصر الحقيقية.
 *
 * تستطيع تجربة أكبر فائدة لنمط الزائر عند استخدامه مع بنية كائن معقد مثل شجرة المركب، وفي تلك الحالة فقد
 * يكون من المفيد تخزين حالة وسيطة للخوارزمية أثناء تنفيذ أساليب الزائر على عدة كائنات من البنية.
 */
class ConcreteVisitor1 implements Visitor
{
    public function visitConcreteComponentA(ConcreteComponentA $element): void
    {
        echo $element->exclusiveMethodOfConcreteComponentA() . " + ConcreteVisitor1\n";
    }

    public function visitConcreteComponentB(ConcreteComponentB $element): void
    {
        echo $element->specialMethodOfConcreteComponentB() . " + ConcreteVisitor1\n";
    }
}

class ConcreteVisitor2 implements Visitor
{
    public function visitConcreteComponentA(ConcreteComponentA $element): void
    {
        echo $element->exclusiveMethodOfConcreteComponentA() . " + ConcreteVisitor2\n";
    }

    public function visitConcreteComponentB(ConcreteComponentB $element): void
    {
        echo $element->specialMethodOfConcreteComponentB() . " + ConcreteVisitor2\n";
    }
}

/**
 * تستطيع شيفرة العميل أن تشغِّل عمليات الزائر على أي مجموعة عناصر دون معرفة
 * استدعاءً إلى العملية المناسبة (accept) فئاتها الحقيقية، وتوجِّه عملية 
 * في كائن الزائر.
 */
function clientCode(array $components, Visitor $visitor)
{
    // ...
    foreach ($components as $component) {
        $component->accept($visitor);
    }
    // ...
}

$components = [
    new ConcreteComponentA,
    new ConcreteComponentB,
];

echo "The client code works with all visitors via the base Visitor interface:\n";
$visitor1 = new ConcreteVisitor1;
clientCode($components, $visitor1);
echo "\n";

echo "It allows the same client code to work with different types of visitors:\n";
$visitor2 = new ConcreteVisitor2;
clientCode($components, $visitor2);

Output.txt: نتائج التنفيذ

The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1

It allows the same client code to work with different types of visitors:
A + ConcreteVisitor1
B + ConcreteVisitor2

مثال واقعي

يساعد نمط الزائر في هذا المثال على إدخال ميزة التقرير (reporting) إلى هرمية فئات موجودة فعلًا: Company > Department > Employee وبمجرد أن تضاف بنية الزائر التحتية إلى البرنامج ستستطيع إضافة سلوكيات مشابهة بسهولة إليه دون تغيير الفئات الحالية.

index.php: مثال واقعي

<?php

namespace RefactoringGuru\Visitor\RealWorld;

/**
 * تصرح واجهة العنصر عن أسلوب لقبول كائنات الزائر.
 *
 * في هذا الأسلوب، يجب أن يستدعي عنصر حقيقيٌ أسلوب زائر محدد لديه نفس نوع المعامِل
 * الذي للعنصر.
 */
interface Entity
{
    public function accept(Visitor $visitor): string;
}

/**
 * عنصر الشركة الحقيقي.
 */
class Company implements Entity
{
    private $name;

    /**
     * @var Department[]
     */
    private $departments;

    public function __construct(string $name, array $departments)
    {
        $this->name = $name;
        $this->departments = $departments;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getDepartments(): array
    {
        return $this->departments;
    }

    // ...

    public function accept(Visitor $visitor): string
    {
        //  visitCopmany يجب أن يستدعي عنصر الشركة أسلوب
        // ونفس المبدأ ينطبق على جميع العناصر.
        return $visitor->visitCompany($this);
    }
}

/**
 * عنصر القسم الحقيقي
 */
class Department implements Entity
{
    private $name;

    /**
     * @var Employee[]
     */
    private $employees;

    public function __construct(string $name, array $employees)
    {
        $this->name = $name;
        $this->employees = $employees;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmployees(): array
    {
        return $this->employees;
    }

    public function getCost(): int
    {
        $cost = 0;
        foreach ($this->employees as $employee) {
            $cost += $employee->getSalary();
        }

        return $cost;
    }

    // ...

    public function accept(Visitor $visitor): string
    {
        return $visitor->visitDepartment($this);
    }
}

/**
 * عنصر الموظف الحقيقي.
 */
class Employee implements Entity
{
    private $name;

    private $position;

    private $salary;

    public function __construct(string $name, string $position, int $salary)
    {
        $this->name = $name;
        $this->position = $position;
        $this->salary = $salary;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPosition(): string
    {
        return $this->position;
    }

    public function getSalary(): int
    {
        return $this->salary;
    }

    // ...

    public function accept(Visitor $visitor): string
    {
        return $visitor->visitEmployee($this);
    }
}

/**
 * تصرح واجهة الزائر عن مجموعة أساليب زيارة لكل فئة من فئات العنصر الحقيقي.
 */
interface Visitor
{
    public function visitCompany(Company $company): string;

    public function visitDepartment(Department $department): string;

    public function visitEmployee(Employee $employee): string;
}

/**
 * يجب أن يوفر الزائر الحقيقي تطبيقات لكل فئة من العناصر الحقيقية.
 */
class SalaryReport implements Visitor
{
    public function visitCompany(Company $company): string
    {
        $output = "";
        $total = 0;

        foreach ($company->getDepartments() as $department) {
            $total += $department->getCost();
            $output .= "\n--" . $this->visitDepartment($department);
        }

        $output = $company->getName() .
            " (" . money_format("%i", $total) . ")\n" . $output;

        return $output;
    }

    public function visitDepartment(Department $department): string
    {
        $output = "";

        foreach ($department->getEmployees() as $employee) {
            $output .= "   " . $this->visitEmployee($employee);
        }

        $output = $department->getName() .
            " (" . money_format("%i", $department->getCost()) . ")\n\n" .
            $output;

        return $output;
    }

    public function visitEmployee(Employee $employee): string
    {
        return money_format("%#6n", $employee->getSalary()) .
            " " . $employee->getName() .
            " (" . $employee->getPosition() . ")\n";
    }
}

/**
 * شيفرة العميل.
 */

$mobileDev = new Department("Mobile Development", [
    new Employee("Albert Falmore", "designer", 100000),
    new Employee("Ali Halabay", "programmer", 100000),
    new Employee("Sarah Konor", "programmer", 90000),
    new Employee("Monica Ronaldino", "QA engineer", 31000),
    new Employee("James Smith", "QA engineer", 30000),
]);
$techSupport = new Department("Tech Support", [
    new Employee("Larry Ulbrecht", "supervisor", 70000),
    new Employee("Elton Pale", "operator", 30000),
    new Employee("Rajeet Kumar", "operator", 30000),
    new Employee("John Burnovsky", "operator", 34000),
    new Employee("Sergey Korolev", "operator", 35000),
]);
$company = new Company("SuperStarDevelopment", [$mobileDev, $techSupport]);

setlocale(LC_MONETARY, 'en_US');
$report = new SalaryReport;

echo "Client: I can print a report for a whole company:\n\n";
echo $company->accept($report);

echo "\nClient: ...or just for a single department:\n\n";
echo $techSupport->accept($report);

// $export = new JSONExport; 
// echo $company->accept($export);

Output.txt: نتائج التنفيذ

Client: I can print a report for a whole company:

SuperStarDevelopment (USD550,000.00)

--Mobile Development (USD351,000.00)

    $100,000.00 Albert Falmore (designer)
    $100,000.00 Ali Halabay (programmer)
    $ 90,000.00 Sarah Konor (programmer)
    $ 31,000.00 Monica Ronaldino (QA engineer)
    $ 30,000.00 James Smith (QA engineer)

--Tech Support (USD199,000.00)

    $ 70,000.00 Larry Ulbrecht (supervisor)
    $ 30,000.00 Elton Pale (operator)
    $ 30,000.00 Rajeet Kumar (operator)
    $ 34,000.00 John Burnovsky (operator)
    $ 35,000.00 Sergey Korolev (operator)

Client: ...or just for a single department:

Tech Support (USD199,000.00)

    $ 70,000.00 Larry Ulbrecht (supervisor)
    $ 30,000.00 Elton Pale (operator)
    $ 30,000.00 Rajeet Kumar (operator)
    $ 34,000.00 John Burnovsky (operator)
    $ 35,000.00 Sergey Korolev (operator)

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

أمثلة الاستخدام: لا يكثر استخدام نمط الزائر في لغة جافا بسبب تعقيده والمجال الضيق لتطبيقه.

مثال تصوري

يوضح هذا المثال بنية نمط الزائر (Visitor)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

main.py: مثال تصوري

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Component(ABC):
    """
    يأخذ واجهة accept تصرح واجهة العنصر عن أسلوب 
    الزائر الأساسية كوسيط.
    """

    @abstractmethod
    def accept(self, visitor: Visitor) -> None:
        pass


class ConcreteComponentA(Component):
    """
    بطريقة تستدعي أسلوب الزائر الموافق لفئة العنصر Accept يجب أن يطبق كل عنصر حقيقي أسلوب.
    """

    def accept(self, visitor: Visitor) -> None:
        """
        المطابق لاسم الفئة الحالية VisitConcreteComponentA لاحظ أننا نستدعي
        وهكذا نسمح للزائر بمعرفة فئة العنصر الذي يعمل معه.
        """

        visitor.visit_concrete_component_a(self)

    def exclusive_method_of_concrete_component_a(self) -> str:
        """
        قد يكون للعناصر الحقيقية أساليب خاصة لا توجد داخل فئتها الأساسية أو واجهتها، وسيستطيع
        الزائر استخدام تلك الأساليب بما أنه مدرك لفئة العنصر الحقيقية.
        """

        return "A"


class ConcreteComponentB(Component):
    """
    نفس الشيء هنا: VisitConcreteComponentB => ConcreteComponentB
    """

    def accept(self, visitor: Visitor):
        visitor.visit_concrete_component_b(self)

    def special_method_of_concrete_component_b(self) -> str:
        return "B"


class Visitor(ABC):
    """
    تصرح واجهة الزائر عن مجموعة من أساليب الزيارة التي تتوافق مع فئات العنصر.
    ويسمح توقيع أسلوب الزيارة للزائر بتحديد فئة العنصر الذي يتعامل معه.
    """

    @abstractmethod
    def visit_concrete_component_a(self, element: ConcreteComponentA) -> None:
        pass

    @abstractmethod
    def visit_concrete_component_b(self, element: ConcreteComponentB) -> None:
        pass


"""
يستخدم الزائرون الحقيقيون عدة إصدارات من نفس الخوارزمية، وتستطيع تلك الإصدارات
أن تعمل مع جميع فئات العنصر الحقيقية.

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


class ConcreteVisitor1(Visitor):
    def visit_concrete_component_a(self, element) -> None:
        print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor1")

    def visit_concrete_component_b(self, element) -> None:
        print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor1")


class ConcreteVisitor2(Visitor):
    def visit_concrete_component_a(self, element) -> None:
        print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor2")

    def visit_concrete_component_b(self, element) -> None:
        print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor2")


def client_code(components: List[Component], visitor: Visitor) -> None:
    """
    تستطيع شيفرة العميل أن تشغِّل عمليات الزائر على أي مجموعة عناصر دون معرفة
    استدعاءً إلى العملية المناسبة (accept) فئاتها الحقيقية، وتوجِّه عملية 
    في كائن الزائر.
    """

    # ...
    for component in components:
        component.accept(visitor)
    # ...


if __name__ == "__main__":
    components = [ConcreteComponentA(), ConcreteComponentB()]

    print("The client code works with all visitors via the base Visitor interface:")
    visitor1 = ConcreteVisitor1()
    client_code(components, visitor1)

    print("It allows the same client code to work with different types of visitors:")
    visitor2 = ConcreteVisitor2()
    client_code(components, visitor2)

Output.txt: نتائج التنفيذ

The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

أمثلة الاستخدام: لا يكثر استخدام نمط الزائر في لغة جافا بسبب تعقيده والمجال الضيق لتطبيقه.

مثال تصوري

يوضح هذا المثال بنية نمط الزائر (Visitor)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟
# يأخذ واجهة accept تصرح واجهة العنصر عن أسلوب
# الزائر الأساسية كوسيط.
class Component
  # @abstract
  #
  # @param [Visitor] visitor
  def accept(_visitor)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# بطريقة تستدعي أسلوب الزائر الموافق لفئة العنصر Accept يجب أن يطبق كل عنصر حقيقي أسلوب.
class ConcreteComponentA < Component
  # المطابق لاسم الفئة الحالية VisitConcreteComponentA لاحظ أننا نستدعي
  # وهكذا نسمح للزائر بمعرفة فئة العنصر الذي يعمل معه.
  def accept(visitor)
    visitor.visit_concrete_component_a(self)
  end

  # قد يكون للعناصر الحقيقية أساليب خاصة لا توجد داخل فئتها الأساسية أو واجهتها، وسيستطيع
  # الزائر استخدام تلك الأساليب بما أنه مدرك لفئة العنصر الحقيقية.
  def exclusive_method_of_concrete_component_a
    'A'
  end
end

# نفس الشيء هنا: VisitConcreteComponentB => ConcreteComponentB
class ConcreteComponentB < Component
  # @param [Visitor] visitor
  def accept(visitor)
    visitor.visit_concrete_component_b(self)
  end

  def special_method_of_concrete_component_b
    'B'
  end
end

# تصرح واجهة الزائر عن مجموعة من أساليب الزيارة التي تتوافق مع فئات العنصر.
# ويسمح توقيع أسلوب الزيارة للزائر بتحديد فئة العنصر الذي يتعامل معه.
class Visitor
  # @abstract
  #
  # @param [ConcreteComponentA] element
  def visit_concrete_component_a(_element)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # @abstract
  #
  # @param [ConcreteComponentB] element
  def visit_concrete_component_b(_element)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# يستخدم الزائرون الحقيقيون عدة إصدارات من نفس الخوارزمية، وتستطيع تلك الإصدارات
# أن تعمل مع جميع فئات العنصر الحقيقية.
#
# تستطيع تجربة أكبر فائدة لنمط الزائر عند استخدامه مع بنية كائن معقد مثل شجرة المركب، وفي تلك الحالة فقد
# يكون من المفيد تخزين حالة وسيطة للخوارزمية أثناء تنفيذ أساليب الزائر على عدة كائنات من البنية.
class ConcreteVisitor1 < Visitor
  def visit_concrete_component_a(element)
    puts "#{element.exclusive_method_of_concrete_component_a} + #{self.class}"
  end

  def visit_concrete_component_b(element)
    puts "#{element.special_method_of_concrete_component_b} + #{self.class}"
  end
end

class ConcreteVisitor2 < Visitor
  def visit_concrete_component_a(element)
    puts "#{element.exclusive_method_of_concrete_component_a} + #{self.class}"
  end

  def visit_concrete_component_b(element)
    puts "#{element.special_method_of_concrete_component_b} + #{self.class}"
  end
end

# تستطيع شيفرة العميل أن تشغِّل عمليات الزائر على أي مجموعة عناصر دون معرفة
# استدعاءً إلى العملية المناسبة (accept) فئاتها الحقيقية، وتوجِّه عملية 
# في كائن الزائر.
def client_code(components, visitor)
  # ...
  components.each do |component|
    component.accept(visitor)
  end
  # ...
end

components = [ConcreteComponentA.new, ConcreteComponentB.new]

puts 'The client code works with all visitors via the base Visitor interface:'
visitor1 = ConcreteVisitor1.new
client_code(components, visitor1)

puts 'It allows the same client code to work with different types of visitors:'
visitor2 = ConcreteVisitor2.new
client_code(components, visitor2)

output.txt: نتائج التنفيذ

The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

أمثلة الاستخدام: لا يكثر استخدام نمط الزائر في لغة جافا بسبب تعقيده والمجال الضيق لتطبيقه.

مثال تصوري

يوضح هذا المثال بنية نمط الزائر (Visitor)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

بعد تعلم بنية النمط سيكون من السهل عليك استيعاب المثال التالي المبني على حالة واقعية في لغة Swift.

Example.swift: مثال تصوري

import XCTest

/// يأخذ واجهة accept تصرح واجهة العنصر عن أسلوب
/// الزائر الأساسية كوسيط.
protocol Component {

    func accept(_ visitor: Visitor)
}

/// بطريقة تستدعي أسلوب الزائر الموافق لفئة العنصر Accept يجب أن يطبق كل عنصر حقيقي أسلوب.
class ConcreteComponentA: Component {

    /// المطابق لاسم الفئة الحالية VisitConcreteComponentA لاحظ أننا نستدعي
    /// وهكذا نسمح للزائر بمعرفة فئة العنصر الذي يعمل معه.
    func accept(_ visitor: Visitor) {
        visitor.visitConcreteComponentA(element: self)
    }

    /// قد يكون للعناصر الحقيقية أساليب خاصة لا توجد داخل فئتها الأساسية أو واجهتها، وسيستطيع
    /// الزائر استخدام تلك الأساليب بما أنه مدرك لفئة العنصر الحقيقية.
    func exclusiveMethodOfConcreteComponentA() -> String {
        return "A"
    }
}

class ConcreteComponentB: Component {

    /// نفس الشيء هنا: VisitConcreteComponentB => ConcreteComponentB
    func accept(_ visitor: Visitor) {
        visitor.visitConcreteComponentB(element: self)
    }

    func specialMethodOfConcreteComponentB() -> String {
        return "B"
    }
}

/// تصرح واجهة الزائر عن مجموعة من أساليب الزيارة التي تتوافق مع فئات العنصر.
/// ويسمح توقيع أسلوب الزيارة للزائر بتحديد فئة العنصر الذي يتعامل معه.
protocol Visitor {

    func visitConcreteComponentA(element: ConcreteComponentA)
    func visitConcreteComponentB(element: ConcreteComponentB)
}

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

    func visitConcreteComponentA(element: ConcreteComponentA) {
        print(element.exclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor1\n")
    }

    func visitConcreteComponentB(element: ConcreteComponentB) {
        print(element.specialMethodOfConcreteComponentB() + " + ConcreteVisitor1\n")
    }
}

class ConcreteVisitor2: Visitor {

    func visitConcreteComponentA(element: ConcreteComponentA) {
        print(element.exclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor2\n")
    }

    func visitConcreteComponentB(element: ConcreteComponentB) {
        print(element.specialMethodOfConcreteComponentB() + " + ConcreteVisitor2\n")
    }
}

/// تستطيع شيفرة العميل أن تشغِّل عمليات الزائر على أي مجموعة عناصر دون معرفة
/// استدعاءً إلى العملية المناسبة (accept) فئاتها الحقيقية، وتوجِّه عملية 
/// في كائن الزائر.
class Client {
    // ...
    static func clientCode(components: [Component], visitor: Visitor) {
        // ...
        components.forEach({ $0.accept(visitor) })
        // ...
    }
    // ...
}

/// لنرى كيف سيعمل كل ذلك معًا...
class VisitorConceptual: XCTestCase {

    func test() {
        let components: [Component] = [ConcreteComponentA(), ConcreteComponentB()]

        print("The client code works with all visitors via the base Visitor interface:\n")
        let visitor1 = ConcreteVisitor1()
        Client.clientCode(components: components, visitor: visitor1)

        print("\nIt allows the same client code to work with different types of visitors:\n")
        let visitor2 = ConcreteVisitor2()
        Client.clientCode(components: components, visitor: visitor2)
    }
}

Output.txt: نتائج التنفيذ

The client code works with all visitors via the base Visitor interface:

A + ConcreteVisitor1

B + ConcreteVisitor1


It allows the same client code to work with different types of visitors:

A + ConcreteVisitor2

B + ConcreteVisitor2

مثال واقعي

Example.swift: شيفرة العميل

import Foundation
import XCTest


protocol Notification: CustomStringConvertible {

    func accept(visitor: NotificationPolicy) -> Bool
}

struct Email {

    let emailOfSender: String

    var description: String { return "Email" }
}

struct SMS {

    let phoneNumberOfSender: String

    var description: String { return "SMS" }
}

struct Push {

    let usernameOfSender: String

    var description: String { return "Push" }
}

extension Email: Notification {

    func accept(visitor: NotificationPolicy) -> Bool {
        return visitor.isTurnedOn(for: self)
    }
}

extension SMS: Notification {

    func accept(visitor: NotificationPolicy) -> Bool {
        return visitor.isTurnedOn(for: self)
    }
}

extension Push: Notification {

    func accept(visitor: NotificationPolicy) -> Bool {
        return visitor.isTurnedOn(for: self)
    }
}


protocol NotificationPolicy: CustomStringConvertible {

    func isTurnedOn(for email: Email) -> Bool

    func isTurnedOn(for sms: SMS) -> Bool

    func isTurnedOn(for push: Push) -> Bool
}

class NightPolicyVisitor: NotificationPolicy {

    func isTurnedOn(for email: Email) -> Bool {
        return false
    }

    func isTurnedOn(for sms: SMS) -> Bool {
        return true
    }

    func isTurnedOn(for push: Push) -> Bool {
        return false
    }

    var description: String { return "Night Policy Visitor" }
}

class DefaultPolicyVisitor: NotificationPolicy {

    func isTurnedOn(for email: Email) -> Bool {
        return true
    }

    func isTurnedOn(for sms: SMS) -> Bool {
        return true
    }

    func isTurnedOn(for push: Push) -> Bool {
        return true
    }

    var description: String { return "Default Policy Visitor" }
}

class BlackListVisitor: NotificationPolicy {

    private var bannedEmails = [String]()
    private var bannedPhones = [String]()
    private var bannedUsernames = [String]()

    init(emails: [String], phones: [String], usernames: [String]) {
        self.bannedEmails = emails
        self.bannedPhones = phones
        self.bannedUsernames = usernames
    }

    func isTurnedOn(for email: Email) -> Bool {
        return bannedEmails.contains(email.emailOfSender)
    }

    func isTurnedOn(for sms: SMS) -> Bool {
        return bannedPhones.contains(sms.phoneNumberOfSender)
    }

    func isTurnedOn(for push: Push) -> Bool {
        return bannedUsernames.contains(push.usernameOfSender)
    }

    var description: String { return "Black List Visitor" }
}



class VisitorRealWorld: XCTestCase {

    func testVisitorRealWorld() {

        let email = Email(emailOfSender: "some@email.com")
        let sms = SMS(phoneNumberOfSender: "+3806700000")
        let push = Push(usernameOfSender: "Spammer")

        let notifications: [Notification] = [email, sms, push]

        clientCode(handle: notifications, with: DefaultPolicyVisitor())

        clientCode(handle: notifications, with: NightPolicyVisitor())
    }
}

extension VisitorRealWorld {

    /// تمرر شيفرة العميل الإشعارات إلى الزوار وتتفقد إن كان الإشعار في قائمة سوداء أم لا
    /// SilencePolicy وكذلك إن كان يجب إظهاره بالتوافق مع سياسة الكتم الحالية.

    func clientCode(handle notifications: [Notification], with policy: NotificationPolicy) {

        let blackList = createBlackList()

        print("\nClient: Using \(policy.description) and \(blackList.description)")

        notifications.forEach { item in

            guard !item.accept(visitor: blackList) else {
                print("\tWARNING: " + item.description + " is in a black list")
                return
            }

            if item.accept(visitor: policy) {
                print("\t" + item.description + " notification will be shown")
            } else {
                print("\t" + item.description + " notification will be silenced")
            }
        }
    }

    private func createBlackList() -> BlackListVisitor {
        return BlackListVisitor(emails: ["banned@email.com"],
                                phones: ["000000000", "1234325232"],
                                usernames: ["Spammer"])
    }
}

Output.txt: نتائج التنفيذ

Client: Using Default Policy Visitor and Black List Visitor
    Email notification will be shown
    SMS notification will be shown
    WARNING: Push is in a black list

Client: Using Night Policy Visitor and Black List Visitor
    Email notification will be silenced
    SMS notification will be shown
    WARNING: Push is in a black list

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

المستوى: ★ ★ ★

الانتشار:  ★ ☆ ☆

أمثلة الاستخدام: لا يكثر استخدام نمط الزائر في لغة جافا بسبب تعقيده والمجال الضيق لتطبيقه.

مثال تصوري

يوضح هذا المثال بنية نمط الزائر (Visitor)، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

index.ts: مثال تصوري

/**
 * يأخذ واجهة accept تصرح واجهة العنصر عن أسلوب
 * الزائر الأساسية كوسيط.
 */
interface Component {
    accept(visitor: Visitor): void;
}

/**
 * بطريقة تستدعي أسلوب الزائر الموافق لفئة العنصر Accept يجب أن يطبق كل عنصر حقيقي أسلوب.
 */
class ConcreteComponentA implements Component {
    /**
     * المطابق لاسم الفئة الحالية VisitConcreteComponentA لاحظ أننا نستدعي
     * وهكذا نسمح للزائر بمعرفة فئة العنصر الذي يعمل معه.
     */
    public accept(visitor: Visitor): void {
        visitor.visitConcreteComponentA(this);
    }

    /**
     * قد يكون للعناصر الحقيقية أساليب خاصة لا توجد داخل فئتها الأساسية أو واجهتها، وسيستطيع
     * الزائر استخدام تلك الأساليب بما أنه مدرك لفئة العنصر الحقيقية.
     */
    public exclusiveMethodOfConcreteComponentA(): string {
        return 'A';
    }
}

class ConcreteComponentB implements Component {
    /**
     * نفس الشيء هنا: VisitConcreteComponentB => ConcreteComponentB
     */
    public accept(visitor: Visitor): void {
        visitor.visitConcreteComponentB(this);
    }

    public specialMethodOfConcreteComponentB(): string {
        return 'B';
    }
}

/**
 * تصرح واجهة الزائر عن مجموعة من أساليب الزيارة التي تتوافق مع فئات العنصر.
 * ويسمح توقيع أسلوب الزيارة للزائر بتحديد فئة العنصر الذي يتعامل معه.
 */
interface Visitor {
    visitConcreteComponentA(element: ConcreteComponentA): void;

    visitConcreteComponentB(element: ConcreteComponentB): void;
}

/**
 * يستخدم الزائرون الحقيقيون عدة إصدارات من نفس الخوارزمية، وتستطيع تلك الإصدارات
 * أن تعمل مع جميع فئات العنصر الحقيقية.
 *
 * تستطيع تجربة أكبر فائدة لنمط الزائر عند استخدامه مع بنية كائن معقد مثل شجرة المركب، وفي تلك الحالة فقد
 * يكون من المفيد تخزين حالة وسيطة للخوارزمية أثناء تنفيذ أساليب الزائر على عدة كائنات من البنية.
 */
class ConcreteVisitor1 implements Visitor {
    public visitConcreteComponentA(element: ConcreteComponentA): void {
        console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor1`);
    }

    public visitConcreteComponentB(element: ConcreteComponentB): void {
        console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor1`);
    }
}

class ConcreteVisitor2 implements Visitor {
    public visitConcreteComponentA(element: ConcreteComponentA): void {
        console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor2`);
    }

    public visitConcreteComponentB(element: ConcreteComponentB): void {
        console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor2`);
    }
}

/**
 * تستطيع شيفرة العميل أن تشغِّل عمليات الزائر على أي مجموعة عناصر دون معرفة
 * استدعاءً إلى العملية المناسبة (accept) فئاتها الحقيقية، وتوجِّه عملية 
 * في كائن الزائر.
 */
function clientCode(components: Component[], visitor: Visitor) {
    // ...
    for (const component of components) {
        component.accept(visitor);
    }
    // ...
}

const components = [
    new ConcreteComponentA(),
    new ConcreteComponentB(),
];

console.log('The client code works with all visitors via the base Visitor interface:');
const visitor1 = new ConcreteVisitor1();
clientCode(components, visitor1);
console.log('');

console.log('It allows the same client code to work with different types of visitors:');
const visitor2 = new ConcreteVisitor2();
clientCode(components, visitor2);

Output.txt: نتائج التنفيذ

The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1

It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2

انظر أيضًا

مصادر