[design-patterns] متى يجب علي استخدام نموذج تصميم الزائر؟



Answers

ربما يكون سبب ارتباكك هو أن الزائر هو تسمية خاطئة قاتلة. تعثر العديد من المبرمجين (بارزين 1 !) على هذه المشكلة. ما يفعله في الواقع هو تنفيذ الإرسال المزدوج في اللغات التي لا تدعمها أصلاً (معظمها لا تفعل ذلك).

1) المثال المفضل لدي هو Scott Meyers ، مؤلف كتاب "Effective C ++" ، الذي أطلق على هذه واحدة من أهم C ++ aha! لحظات من أي وقت مضى .

Question

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

كشخص حصل مؤخرا فقط على نمط الديكور و هو يرى الآن استخدامات له في كل مكان على الإطلاق ، أود أن أكون قادرا على فهم هذا المفهوم البديهي.




While I have understood the how and when, I have never understood the why. In case it helps anyone with a background in a language like C++, you want to read this very carefully.

For the lazy, we use the visitor pattern because "while virtual functions are dispatched dynamically in C++, function overloading is done statically" .

Or, put another way, to make sure that CollideWith(ApolloSpacecraft&) is called when you pass in a SpaceShip reference that is actually bound to an ApolloSpacecraft object.

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}



يعمل نمط تصميم الزائر بشكل جيد للبنى "العودية" مثل أشجار الدليل ، أو هياكل XML ، أو مخططات المستند.

يقوم كائن زائر بزيارة كل عقدة في البنية العودية: كل دليل ، كل علامة XML ، أيا كان. لا يتم تنفيذ كائن الزائر من خلال الهيكل. بدلاً من ذلك يتم تطبيق أساليب الزائر على كل عقدة في البنية.

وهنا هيكل عقدة متكررة نموذجي. يمكن أن يكون دليل أو علامة XML. [إذا كان لديك شخص جافا ، تخيل الكثير من الطرق الإضافية لبناء قائمة الأطفال والحفاظ عليها.]

class TreeNode( object ):
    def __init__( self, name, *children ):
        self.name= name
        self.children= children
    def visit( self, someVisitor ):
        someVisitor.arrivedAt( self )
        someVisitor.down()
        for c in self.children:
            c.visit( someVisitor )
        someVisitor.up()

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

وإليك فئة متميزة للزائرين. يتم استخدامه بواسطة طريقة visit . انها "تصل" كل عقدة في الهيكل. نظرًا لأن طريقة visit تستدعي up down ، يمكن للزائر تتبع العمق.

class Visitor( object ):
    def __init__( self ):
        self.depth= 0
    def down( self ):
        self.depth += 1
    def up( self ):
        self.depth -= 1
    def arrivedAt( self, aTreeNode ):
        print self.depth, aTreeNode.name

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

هنا تطبيق. يبني هيكل شجرة ، بعض someTree . يخلق Visitor ، dumpNodes .

ثم يتم تطبيق dumpNodes على الشجرة. سيؤدي "كائن dumpNode " إلى "زيارة" كل عقدة في الشجرة.

someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )

ستضمن خوارزمية visit TreeNode استخدام كل TreeNode كوسيطة لطريقة arrivedAt الخاصة arrivedAt .




بناء على إجابة ممتازة منFederico A. Ramponi.

فقط تخيل أن لديك هذا التسلسل الهرمي:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

ماذا يحدث إذا كنت بحاجة إلى إضافة طريقة "السير" هنا؟ سيكون ذلك مؤلماً للتصميم بأكمله.

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

لذا ، تحقق من هذا المثال C # وقم بتشغيله:

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}



Visitor

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

هيكل الزوار:

استخدم نمط الزائر إذا:

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

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

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

مقتطف الشفرة:

import java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

تفسير:

  1. Visitable ( Element ) هو واجهة ويجب إضافة طريقة الواجهة هذه إلى مجموعة من الفئات.
  2. Visitor عبارة عن واجهة ، تحتوي على طرق لتنفيذ عملية على العناصر Visitable .
  3. GameVisitor هي فئة ، والتي تنفذ واجهة Visitor ( ConcreteVisitor ).
  4. يقبل كل عنصر Visitable Visitor Visitable إلى طريقة ملائمة من واجهة Visitor .
  5. يمكنك معاملة Game as Element والألعاب الخرسانية مثل Chess,Checkers and Ludo كـ ConcreteElements .

في المثال أعلاه ، Chess, Checkers and Ludo ثلاث ألعاب مختلفة (ودروس Visitable ). في يوم واحد جيد ، واجهت سيناريو لتسجيل إحصائيات كل لعبة. لذا بدون تعديل الصف الفردي لتطبيق وظيفة الإحصائيات ، يمكنك تركيز هذه المسؤولية في فئة GameVisitor ، والتي تعمل على خداعك دون تعديل هيكل كل لعبة.

انتاج:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

تشير إلى

المادة أوديسيجن

المادة sourcemaking

لمزيد من التفاصيل

Decorator

يسمح النمط بإضافة السلوك إلى كائن فردي ، إما بشكل ثابت أو ديناميكي ، دون التأثير على سلوك كائنات أخرى من نفس الفئة

الوظائف ذات الصلة:

نمط الديكور ل IO

متى تستخدم نمط الديكور؟




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

  1. تقليل تكاثر الكود الذي يختلف اختلافًا طفيفًا عند تغيير بنية البيانات.

  2. تطبيق نفس الحساب على عدة هياكل البيانات ، دون تغيير التعليمات البرمجية التي تطبق الحساب.

  3. أضف معلومات إلى المكتبات القديمة دون تغيير الشفرة القديمة.

يرجى إلقاء نظرة على مقال كتبته عن هذا .




When you want to have function objects on union data types, you will need visitor pattern.

قد تتسائل عن كائنات الدوال وأنواع بيانات الاتحادات ، ثم الأمر يستحق القراءة http://www.ccs.neu.edu/home/matthias/htdc.html




لقد وجدتها أسهل في الروابط التالية:

في http://www.remondo.net/visitor-pattern-example-csharp/ لقد عثرت على مثال يوضح مثالًا وهميًا يوضح مدى فائدة نمط الزائر. هنا لديك فئات مختلفة للحاويات Pill :

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

كما ترون في الأعلى ، تحتوي أنت BilsterPack على أزواج من حبوب منع الحمل بحيث تحتاج إلى مضاعفة عدد الأزواج بمقدار 2. كما قد تلاحظ أن unit استخدام Bottle هي نوع بيانات مختلف وتحتاج إلى الإدلاء بها.

لذلك في الطريقة الرئيسية يمكنك حساب عدد حبوب منع الحمل باستخدام التعليمات البرمجية التالية:

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

لاحظ أن الشفرة أعلاه تنتهك Single Responsibility Principle . هذا يعني أنه يجب عليك تغيير رمز الطريقة الرئيسي إذا قمت بإضافة نوع جديد من الحاوية. أيضا جعل التحول يعد ممارسة سيئة.

لذلك من خلال تقديم الكود التالي:

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

نقلت مسؤولية عد عدد Pill إلى فئة تسمى PillCountVisitor (وأزلنا بيان حالة التبديل). هذا يعني كلما كنت بحاجة إلى إضافة نوع جديد من الحاويات حبوب منع الحمل يجب تغيير فئة PillCountVisitor فقط. لاحظ أيضًا أن واجهة IVisitor عامة للاستخدام في سيناريوهات أخرى.

بإضافة طريقة القبول إلى فئة حاويات الحبة:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

نحن نسمح للزائر بزيارة فصول حاوية حبوب منع الحمل.

في النهاية نقوم بحساب عدد حبوب منع الحمل باستخدام الكود التالي:

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

هذا يعني: كل حاوية حبوب منع الحمل تسمح لزائر PillCountVisitor . يعرف كيف يحسب حبوب منع الحمل الخاصة بك.

في visitor.Count عدد قيمة حبوب منع الحمل.

في http://butunclebob.com/ArticleS.UncleBob.IuseVisitor ترى السيناريو الحقيقي الذي لا يمكنك استخدام polymorphism (الجواب) لمتابعة مبدأ المسؤولية الواحدة. في الواقع في:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

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




يقدم كاي هورسمان مثالاً رائعاً على مكان تطبيق الزائر في كتابه الخاص بالتصميم وأنماط التصميم . يلخص المشكلة:

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

السبب في أنه ليس من السهل أن تتم إضافة العمليات داخل فئات البنية نفسها. على سبيل المثال ، تخيل أن لديك نظام ملفات:

فيما يلي بعض العمليات (الوظائف) التي قد نرغب في تنفيذها مع هذه البنية:

  • عرض أسماء عناصر العقدة (قائمة ملف)
  • عرض الحجم المحسوب لعناصر العقدة (حيث يتضمن حجم الدليل حجم جميع عناصره الفرعية)
  • إلخ

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

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

يسمح لنا الزائر بفصل الوظائف في بنية البيانات (على سبيل المثال ، FileSystemNodes ) من هياكل البيانات نفسها. ويسمح النمط بأن يحترم التصميم التماسك - فكلمات بنية البيانات أبسط (لديها طرق أقل) ، كما يتم تضمين الوظائف في تطبيقات Visitor . ويتم ذلك عبر الإرسال المزدوج (وهو الجزء المعقد للنمط): باستخدام أساليب accept() في فئات البنية و visitX() في فصول الزائر (الوظائف):

تسمح لنا هذه البنية بإضافة وظائف جديدة تعمل على الهيكل كزائرين ملموسين (دون تغيير فئات البنية).

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

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




Related