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





التصميم انماط (16)


I really like the description and the example from http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html .

The assumption is that you have a primary class hierarchy that is fixed; perhaps it's from another vendor and you can't make changes to that hierarchy. However, your intent is that you'd like to add new polymorphic methods to that hierarchy, which means that normally you'd have to add something to the base class interface. So the dilemma is that you need to add methods to the base class, but you can't touch the base class. How do you get around this?

The design pattern that solves this kind of problem is called a “visitor” (the final one in the Design Patterns book), and it builds on the double dispatching scheme shown in the last section.

The visitor pattern allows you to extend the interface of the primary type by creating a separate class hierarchy of type Visitor to virtualize the operations performed upon the primary type. The objects of the primary type simply “accept” the visitor, then call the visitor's dynamically-bound member function.

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

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




إرسال مزدوج هو سبب واحد فقط من بين الآخرين لاستخدام هذا النمط .
ولكن لاحظ أنه الطريقة الوحيدة لتنفيذ الإرسال المزدوج أو أكثر في اللغات التي تستخدم نموذج إرسال مفرد.

فيما يلي أسباب استخدام النمط:

1) نريد تحديد عمليات جديدة دون تغيير النموذج في كل مرة لأن النموذج لا يتغير غالبًا ما تتغير عمليات wile بشكل متكرر.

2) We don't want to couple model and behavior because we want to have a reusable model in multiple applications or we want to have an extensible model that allow client classes to define their behaviors with their own classes.

3) We have common operations that depend on the concrete type of the model but we don't want to implement the logic in each subclass as that would explode common logic in multiple classes and so in multiple places .

4) We are using a domain model design and model classes of the same hierarchy perform too many distinct things that could be gathered somewhere else .

5) We need a double dispatch .
We have variables declared with interface types and we want to be able to process them according their runtime type … of course without using if (myObj instanceof Foo) {} or any trick.
The idea is for example to pass these variables to methods that declares a concrete type of the interface as parameter to apply a specific processing. This way of doing is not possible out of the box with languages relies on a single-dispatch because the chosen invoked at runtime depends only on the runtime type of the receiver.
Note that in Java, the method (signature) to call is chosen at compile time and it depends on the declared type of the parameters, not their runtime type.

The last point that is a reason to use the visitor is also a consequence because as you implement the visitor (of course for languages that doesn't support multiple dispatch), you necessarily need to introduce a double dispatch implementation.

Note that the traversal of elements (iteration) to apply the visitor on each one is not a reason to use the pattern.
You use the pattern because you split model and processing.
And by using the pattern, you benefit in addition from an iterator ability.
This ability is very powerful and goes beyond iteration on common type with a specific method as accept() is a generic method.
It is a special use case. So I will put that to one side.

Example in Java

I will illustrate the added value of the pattern with a chess example where we would like to define processing as player requests a piece moving.

Without the visitor pattern use, we could define piece moving behaviors directly in the pieces subclasses.
We could have for example a Piece interface such as :

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Each Piece subclass would implement it such as :

public class Pawn implements Piece{

    @Override
    public boolean checkMoveValidity(Coordinates coord) {
        ...
    }

    @Override
    public void performMove(Coordinates coord) {
        ...
    }

    @Override
    public Piece computeIfKingCheck() {
        ...
    }

}

And the same thing for all Piece subclasses.
Here is a diagram class that illustrates this design :

This approach presents three important drawbacks :

– behaviors such as performMove() or computeIfKingCheck() will very probably use common logic.
For example whatever the concrete Piece , performMove() will finally set the current piece to a specific location and potentially takes the opponent piece.
Splitting related behaviors in multiple classes instead of gathering them defeats in a some way the single responsibility pattern. Making their maintainability harder.

– processing as checkMoveValidity() should not be something that the Piece subclasses may see or change.
It is check that goes beyond human or computer actions. This check is performed at each action requested by a player to ensure that the requested piece move is valid.
So we even don't want to provide that in the Piece interface.

– In chess games challenging for bot developers, generally the application provides a standard API ( Piece interfaces, subclasses, Board, common behaviors, etc…) and let developers enrich their bot strategy.
To be able to do that, we have to propose a model where data and behaviors are not tightly coupled in the Piece implementations.

So let's go to use the visitor pattern !

We have two kinds of structure :

– the model classes that accept to be visited (the pieces)

– the visitors that visit them (moving operations)

Here is a class diagram that illustrates the pattern :

In the upper part we have the visitors and in the lower part we have the model classes.

Here is the PieceMovingVisitor interface (behavior specified for each kind of Piece ) :

public interface PieceMovingVisitor {

    void visitPawn(Pawn pawn);

    void visitKing(King king);

    void visitQueen(Queen queen);

    void visitKnight(Knight knight);

    void visitRook(Rook rook);

    void visitBishop(Bishop bishop);

}

The Piece is defined now :

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Its key method is :

void accept(PieceMovingVisitor pieceVisitor);

It provides the first dispatch : a invocation based on the Piece receiver.
At compile time, the method is bound to the accept() method of the Piece interface and at runtime, the bounded method will be invoked on the runtime Piece class.
And it is the accept() method implementation that will perform a second dispatch.

Indeed, each Piece subclass that wants to be visited by a PieceMovingVisitor object invokes the PieceMovingVisitor.visit() method by passing as argument itself.
In this way, the compiler bounds as soon as the compile time, the type of the declared parameter with the concrete type.
There is the second dispatch.
Here is the Bishop subclass that illustrates that :

public class Bishop implements Piece {

    private Coordinates coord;

    public Bishop(Coordinates coord) {
        super(coord);
    }

    @Override
    public void accept(PieceMovingVisitor pieceVisitor) {
        pieceVisitor.visitBishop(this);
    }

    @Override
    public Coordinates getCoordinates() {
        return coordinates;
    }

   @Override
    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
   }

}

And here an usage example :

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();

// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);

// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
    piece.accept(new MovePerformingVisitor(coord));
}

Visitor drawbacks

The Visitor pattern is a very powerful pattern but it also has some important limitations that you should consider before using it.

1) Risk to reduce/break the encapsulation

In some kinds of operation, the visitor pattern may reduce or break the encapsulation of domain objects.

For example, as the MovePerformingVisitor class needs to set the coordinates of the actual piece, the Piece interface has to provide a way to do that :

void setCoordinates(Coordinates coordinates);

The responsibility of Piece coordinates changes is now open to other classes than Piece subclasses.
Moving the processing performed by the visitor in the Piece subclasses is not an option either.
It will indeed create another issue as the Piece.accept() accepts any visitor implementation. It doesn't know what the visitor performs and so no idea about whether and how to change the Piece state.
A way to identify the visitor would be to perform a post processing in Piece.accept() according to the visitor implementation. It would be a very bad idea as it would create a high coupling between Visitor implementations and Piece subclasses and besides it would probably require to use trick as getClass() , instanceof or any marker identifying the Visitor implementation.

2) Requirement to change the model

Contrary to some other behavioral design patterns as Decorator for example, the visitor pattern is intrusive.
We indeed need to modify the initial receiver class to provide an accept() method to accept to be visited.
We didn't have any issue for Piece and its subclasses as these are our classes .
In built-in or third party classes, things are not so easy.
We need to wrap or inherit (if we can) them to add the accept() method.

3) Indirections

The pattern creates multiples indirections.
The double dispatch means two invocations instead of a single one :

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)

And we could have additional indirections as the visitor changes the visited object state.
It may look like a cycle :

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)



وكما أشار كونراد رودولف بالفعل ، فهو مناسب للحالات التي نحتاج فيها إلى إرسال مزدوج

في ما يلي مثال لإظهار موقف نحتاج فيه إلى إرسال مزدوج وكيف يساعدنا الزائر في القيام بذلك.

مثال:

لنفترض أن لدي 3 أنواع من أجهزة الجوال - iPhone و Android و Windows Mobile.

تحتوي كل هذه الأجهزة الثلاثة على راديو Bluetooth مثبت عليها.

لنفترض أن جهاز راديو الأسنان الأزرق يمكن أن يكون من مصنعين صانعين منفصلين - Intel و Broadcom.

فقط لجعل المثال ملائم لمناقشتنا ، دعنا نفترض أيضًا أن واجهات برمجة التطبيقات التي يكشف عنها راديو إنتل تختلف عن تلك التي يعرضها راديو Broadcom.

هذه هي الطريقة التي تبدو بها فصولي -

الآن ، أود تقديم عملية - التبديل على Bluetooth على جهاز محمول.

يجب أن يشبه توقيع وظيفته شيئًا كهذا -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

في المبدأ ، يصبح مصفوفة 3 × 2 ، حيث أحاول أن أقوم بتوجيه العملية الصحيحة اعتمادًا على النوع الصحيح من الكائنات المتضمنة.

سلوك متعدد الأشكال يعتمد على نوع كلتا الحجج.

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

إرسال مزدوج هو ضرورة هنا بسبب مصفوفة 3X2

هنا كيف ستبدو الإعدادات

كتبت المثال للإجابة على سؤال آخر ، وقد تم ذكر الكود وشرحه here .




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

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




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

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

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

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

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

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

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

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

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

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

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




بناء على إجابة ممتازة من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);
        }
    }
}



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

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

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

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

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




يعمل نمط تصميم الزائر بشكل جيد للبنى "العودية" مثل أشجار الدليل ، أو هياكل 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 .




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

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(لنفترض أنها عبارة عن تسلسل هرمي معقد بواجهة راسخة).

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

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

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

يتيح لك نمط الزائر نقل كل عملية جديدة في فئة مناسبة ، وتحتاج إلى توسيع واجهة التسلسل الهرمي مرة واحدة فقط. دعنا نقوم به. أولاً ، نحدد عملية مجردة (فئة "الزائر" في GoF) والتي تحتوي على طريقة لكل صف في التسلسل الهرمي:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

ثم نقوم بتعديل التسلسل الهرمي لقبول العمليات الجديدة:

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

وأخيرًا ، نقوم بتنفيذ العملية الفعلية ، دون تعديل Cat أو Dog :

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

الآن لديك طريقة لإضافة العمليات دون تعديل التسلسل الهرمي بعد الآن. هنا كيف يعمل:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}



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




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

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




الجميع هنا هو الصحيح ، ولكن أعتقد أنه فشل في معالجة "متى". أولا ، من أنماط التصميم:

يتيح لك الزائر تحديد عملية جديدة دون تغيير فئات العناصر التي تعمل عليها.

الآن ، دعونا نفكر في التسلسل الهرمي الطبقة بسيط. لدي فصول 1 و 2 و 3 و 4 وأساليب A و B و C و D. ضعها في جدول بيانات: فالفصول هي خطوط وأساليب أعمدة.

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

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

بالمناسبة ، هذه هي المشكلة التي تعتزم حلها نمط سكالا.




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;
  }
}



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




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

عندما قد تفكر في استخدامه

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



يستخدم نمط مصنع مجردة في أماكن مختلفة. على سبيل المثال ، DatagramSocketImplFactory ، PreferencesFactory . هناك الكثير أكثر --- البحث في Javadoc للواجهات التي تحتوي على الكلمة "Factory" في أسمائهم.

أيضا هناك حالات قليلة جدا من نمط المصنع ، أيضا.







design-patterns visitor-pattern