Quand dois-je utiliser le modèle de conception de visiteur?

Je ne cesse de voir des références au modèle de visiteur dans les blogs mais je dois admettre que je ne comprends tout simplement pas. Je lis l’ article de Wikipédia pour le modèle et je comprends sa mécanique, mais je suis toujours confus quant à savoir quand l’utiliser.

En tant que personne qui a eu récemment le modèle de décorateur et qui en fait maintenant des utilisations absolument partout, j’aimerais pouvoir comprendre intuitivement ce motif apparemment pratique.

    Je ne connais pas très bien le pattern Visitor. Voyons si j’ai bien compris. Supposons que vous ayez une hiérarchie d’animaux

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

    (Supposons qu’il s’agisse d’une hiérarchie complexe avec une interface bien établie.)

    Maintenant, nous voulons append une nouvelle opération à la hiérarchie, à savoir que chaque animal doit faire son bruit. Dans la mesure où la hiérarchie est aussi simple, vous pouvez le faire avec un polymorphism simple:

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

    Mais en procédant ainsi, chaque fois que vous souhaitez append une opération, vous devez modifier l'interface pour chaque classe de la hiérarchie. Supposons maintenant que vous êtes satisfait de l'interface d'origine et que vous souhaitiez y apporter le moins de modifications possibles.

    Le modèle Visiteur vous permet de déplacer chaque nouvelle opération dans une classe appropriée, et vous ne devez étendre l'interface de la hiérarchie qu'une seule fois. Faisons le. Tout d'abord, nous définissons une opération abstraite (la classe "Visitor" dans GoF) qui a une méthode pour chaque classe de la hiérarchie:

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

    Ensuite, nous modifions la hiérarchie pour accepter de nouvelles opérations:

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

    Enfin, nous implémentons l'opération proprement dite, sans modifier ni Cat ni 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"; } 

    Maintenant, vous avez un moyen d'append des opérations sans modifier la hiérarchie. Voici comment cela fonctionne:

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

    La raison de votre confusion est probablement que le Visiteur est un abus de langage fatal. Beaucoup de programmeurs (éminents 1 !) Ont trébuché sur ce problème. Ce qu’il fait en réalité, c’est implémenter la double dissortingbution dans des langues qui ne le supportent pas nativement (la plupart ne le font pas).


    1) Mon exemple préféré est Scott Meyers, auteur acclamé de «Effective C ++», qui a appelé cela l’un de ses plus importants aha C ++! moments jamais .

    Tout le monde ici est correct, mais je pense que cela ne résout pas le “quand”. Tout d’abord, à partir des modèles de conception:

    Visiteur vous permet de définir une nouvelle opération sans changer les classes des éléments sur lesquels il opère.

    Maintenant, pensons à une hiérarchie de classes simple. J’ai les classes 1, 2, 3 et 4 et les méthodes A, B, C et D. Disposez-les comme dans un tableur: les classes sont des lignes et les méthodes sont des colonnes.

    Désormais, la conception orientée object suppose que vous êtes plus susceptible de développer de nouvelles classes que de nouvelles méthodes. Il est donc plus facile d’append des lignes, pour ainsi dire. Vous ajoutez simplement une nouvelle classe, spécifiez ce qui est différent dans cette classe et héritez du rest.

    Parfois, les classes sont relativement statiques, mais vous devez append plus de méthodes fréquemment – en ajoutant des colonnes. La méthode standard dans une conception OO consisterait à append de telles méthodes à toutes les classes, ce qui peut être coûteux. Le motif de visiteur rend cela facile.

    Soit dit en passant, c’est le problème que Scala cherche à résoudre.

    Le modèle de conception de Visiteur fonctionne très bien pour les structures “récursives” comme les arborescences de répertoires, les structures XML ou les contours de document.

    Un object Visiteur visite chaque nœud de la structure récursive: chaque répertoire, chaque balise XML, etc. L’object Visitor ne traverse pas la structure. Au lieu de cela, les méthodes Visiteurs sont appliquées à chaque nœud de la structure.

    Voici une structure de nœud récursive typique. Peut être un répertoire ou une balise XML. [Si vous êtes un utilisateur Java, imaginez de nombreuses méthodes supplémentaires pour créer et gérer la liste des enfants.]

     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() 

    La méthode de visit applique un object Visiteur à chaque nœud de la structure. Dans ce cas, c’est un visiteur de haut en bas. Vous pouvez modifier la structure de la méthode de visit pour effectuer un ordre ascendant ou inférieur.

    Voici une super classe pour les visiteurs. Il est utilisé par la méthode de visit . Il “arrive” à chaque nœud de la structure. Comme la méthode de visit appelle de up down , le visiteur peut suivre la profondeur.

     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 

    Une sous-classe pourrait faire des choses comme compter les nœuds à chaque niveau et accumuler une liste de nœuds, générant un joli chemin des numéros de section hiérarchique.

    Voici une application. Il construit une structure arborescente, en someTree . Il crée un Visitor , dumpNodes .

    Ensuite, il applique les dumpNodes à l’arborescence. L’object dumpNode “visitera” chaque noeud de l’arborescence.

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

    L’algorithme de visit TreeNode garantira que chaque TreeNode est utilisé comme argument pour la méthode arrivedAt de arrivedAt .

    Une façon de voir les choses est que le modèle de visiteur permet à vos clients d’append des méthodes supplémentaires à toutes vos classes dans une hiérarchie de classes particulière.

    C’est utile lorsque vous avez une hiérarchie de classes relativement stable, mais vous avez des exigences changeantes concernant ce qu’il faut faire avec cette hiérarchie.

    L’exemple classique concerne les compilateurs et autres. Un arbre de syntaxe abstraite (AST, Abstract Syntax Tree) peut définir avec précision la structure du langage de programmation, mais les opérations que vous pouvez effectuer sur l’AST vont évoluer au fur et à mesure de votre projet: générateurs de code, jolies imprimantes, débogueurs

    Sans le modèle de visiteur, chaque fois qu’un développeur souhaite append une nouvelle fonctionnalité, il doit append cette méthode à chaque entité de la classe de base. Cela est particulièrement difficile lorsque les classes de base apparaissent dans une bibliothèque distincte ou sont produites par une équipe distincte.

    (J’ai entendu dire que le modèle Visitor est en conflit avec les bonnes pratiques OO, car il éloigne les opérations des données. Le modèle Visitor est utile dans la situation où les pratiques OO normales échouent.)

    Il y a au moins trois très bonnes raisons d’utiliser le Pattern visiteur:

    1. Réduire la prolifération du code qui n’est que légèrement différente lorsque les structures de données changent.

    2. Appliquez le même calcul à plusieurs structures de données, sans changer le code qui implémente le calcul.

    3. Ajoutez des informations aux anciennes bibliothèques sans modifier le code existant.

    S’il vous plaît jeter un oeil à un article que j’ai écrit à ce sujet .

    Comme Konrad Rudolph l’a déjà souligné, il convient aux cas où nous avons besoin d’une double expédition

    Voici un exemple pour montrer une situation où nous avons besoin d’une double expédition et de la manière dont le visiteur nous aide à le faire.

    Exemple :

    Disons que je dispose de 3 types d’appareils mobiles – iPhone, Android, Windows Mobile.

    Une radio Bluetooth est installée sur ces trois appareils.

    Supposons que la radio à dents bleues puisse provenir de deux OEM différents – Intel et Broadcom.

    Juste pour rendre l’exemple pertinent pour notre discussion, supposons également que les API exposées par la radio Intel sont différentes de celles exposées par la radio Broadcom.

    Voici comment mes cours se présentent –

    entrer la description de l'image ici entrer la description de l'image ici

    Maintenant, je voudrais introduire une opération – Allumer le Bluetooth sur un appareil mobile.

    Sa signature de fonction devrait ressembler à ceci –

      void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio) 

    Ainsi, selon le bon type d’appareil et selon le bon type de radio Bluetooth , il est possible de l’activer en appelant les étapes ou l’algorithme appropriés .

    En principe, il devient une masortingce 3 x 2, où j’essaie de trouver la bonne opération en fonction du bon type d’objects impliqués.

    Un comportement polymorphe dépendant du type des deux arguments.

    entrer la description de l'image ici

    Maintenant, le modèle de visiteur peut être appliqué à ce problème. L’inspiration vient de la page Wikipedia en déclarant – «En substance, le visiteur permet d’append de nouvelles fonctions virtuelles à une famille de classes sans modifier les classes elles-mêmes; au lieu de cela, on crée une classe visiteur qui implémente toutes les spécialisations appropriées de la fonction virtuelle. Le visiteur prend la référence de l’instance en entrée et implémente l’objective par double envoi.

    Double envoi est une nécessité ici en raison de la masortingce 3×2

    Voici à quoi ressemblera la configuration – entrer la description de l'image ici

    J’ai écrit l’exemple pour répondre à une autre question, le code et son explication sont mentionnés ici .

    J’ai trouvé cela plus facile dans les liens suivants:

    Dans http://www.remondo.net/visitor-pattern-example-csharp/, j’ai trouvé un exemple montrant un exemple fictif montrant ce qui est bénéfique pour les visiteurs. Vous avez ici différentes classes de conteneurs pour 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; } } } 

    Comme vous le voyez ci-dessus, vous BilsterPack contient des paires de pilules, vous devez donc multiplier le nombre de paires par 2. Aussi, vous remarquerez peut-être que l’ unit utilise un type de données différent et doit être lancé.

    Donc, dans la méthode principale, vous pouvez calculer le nombre de pilules en utilisant le code suivant:

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

    Notez que le code ci-dessus viole le Single Responsibility Principle . Cela signifie que vous devez changer le code de la méthode principale si vous ajoutez un nouveau type de conteneur. Faire plus long changement est une mauvaise pratique.

    Donc, en introduisant le code suivant:

     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 } 

    Vous avez transféré la responsabilité de compter le nombre de Pill vers la classe appelée PillCountVisitor (et nous avons supprimé la déclaration de casse). Cela signifie que chaque fois que vous devez append un nouveau type de contenant de pilule, vous ne devez modifier que la classe PillCountVisitor . Notez également que l’interface IVisitor est générale pour l’utilisation dans d’autres scénarios.

    En ajoutant la méthode Accept à la classe de conteneur de pilule:

     public class BlisterPack : IAcceptor { public int TabletPairs { get; set; } #region IAcceptor Members public void Accept(IVisitor visitor) { visitor.Visit(this); } #endregion } 

    Nous permettons aux visiteurs de visiter des classes de conteneurs de pilules.

    À la fin, nous calculons le nombre de pilules en utilisant le code suivant:

     var visitor = new PillCountVisitor(); foreach (IAcceptor item in packageList) { item.Accept(visitor); } 

    Cela signifie que: Chaque contenant de pilule permet au visiteur PillCountVisitor de voir son nombre de pilules. Il sait compter vos pilules.

    Au visitor.Count a la valeur des pilules.

    Dans http://butunclebob.com/ArticleS.UncleBob.IuseVisitor, vous voyez un scénario réel dans lequel vous ne pouvez pas utiliser le polymorphism (la réponse) pour suivre le principe de la responsabilité unique. En fait dans:

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

    reportQtdHoursAndPay méthode reportQtdHoursAndPay sert à la génération de rapports et à la représentation, ce qui viole le principe de la responsabilité unique. Il est donc préférable d’utiliser le modèle de visiteur pour résoudre le problème.

    À mon avis, la quantité de travail nécessaire pour append une nouvelle opération est plus ou moins la même en utilisant Visitor Pattern ou en modifiant directement chaque structure d’élément. En outre, si je devais append une nouvelle classe d’élément, par exemple Cow , l’interface d’opération serait affectée et cela se propagerait à toutes les classes d’éléments existantes, nécessitant par conséquent la recompilation de toutes les classes d’éléments. Alors, quel est le point?

    Visitor Pattern comme la même implémentation souterraine de la programmation Aspect Object.

    Par exemple, si vous définissez une nouvelle opération sans modifier les classes des éléments sur lesquels elle opère

    Cay Horstmann a un excellent exemple de l’application de Visitor dans son livre OO Design et son livre de motifs . Il résume le problème:

    Les objects composés ont souvent une structure complexe, composée d’éléments individuels. Certains éléments peuvent à nouveau avoir des éléments enfants. … Une opération sur un élément visite ses éléments enfants, leur applique l’opération et combine les résultats. … Cependant, il n’est pas facile d’append de nouvelles opérations à une telle conception.

    La raison en est que les opérations sont ajoutées dans les classes de structure elles-mêmes. Par exemple, imaginez que vous ayez un système de fichiers:

    Diagramme de classe FileSystem

    Voici quelques opérations (fonctionnalités) que nous pourrions vouloir implémenter avec cette structure:

    • Affiche les noms des éléments de noeud (une liste de fichiers)
    • Affiche la taille calculée des éléments du noeud (où la taille d’un répertoire inclut la taille de tous ses éléments enfants)
    • etc.

    Vous pouvez append des fonctions à chaque classe du FileSystem pour implémenter les opérations (et les utilisateurs l’ont déjà fait, car c’est très évident de le faire). Le problème est que chaque fois que vous ajoutez une nouvelle fonctionnalité (la ligne “etc.” ci-dessus), vous devrez peut-être append de plus en plus de méthodes aux classes de structure. À un moment donné, après un certain nombre d’opérations que vous avez ajoutées à votre logiciel, les méthodes de ces classes n’ont plus de sens en termes de cohésion fonctionnelle des classes. Par exemple, vous avez un FileNode qui a une méthode calculateFileColorForFunctionABC() pour implémenter la dernière fonctionnalité de visualisation sur le système de fichiers.

    Le patron de visiteurs (comme beaucoup de modèles de conception) est né de la douleur et de la souffrance des développeurs qui savaient qu’il existait un meilleur moyen de modifier leur code sans nécessiter beaucoup de changements et de respecter les principes de conception (grande cohésion, faible couplage). ). À mon avis, il est difficile de comprendre l’utilité de nombreux modèles jusqu’à ce que vous ressentiez cette douleur. Expliquer la douleur (comme nous essayons de le faire avec les fonctionnalités “etc.” ajoutées) prend de la place dans l’explication et est une distraction. Comprendre les schémas est difficile pour cette raison.

    Visiteur nous permet de découpler les fonctionnalités de la structure de données (par exemple, FileSystemNodes ) des structures de données elles-mêmes. Le modèle permet à la conception de respecter la cohésion: les classes de structure de données sont plus simples (elles ont moins de méthodes) et les fonctionnalités sont également encapsulées dans les implémentations de Visitor . Cela se fait par double-dispatching (qui est la partie compliquée du pattern): utiliser accept() méthodes accept() dans les classes de structure et les méthodes visitX() dans les classes Visitor (la fonctionnalité):

    Diagramme de classe FileSystem avec Visitor appliqué

    Cette structure nous permet d’append de nouvelles fonctionnalités qui fonctionnent sur la structure en tant que visiteurs concrets (sans changer les classes de structure).

    Diagramme de classe FileSystem avec Visitor appliqué

    Par exemple, un PrintNameVisitor qui implémente la fonctionnalité de liste de répertoires et un PrintSizeVisitor qui implémente la version avec la taille. On pourrait imaginer un jour avoir un ‘ExportXMLVisitor` qui génère les données en XML, ou un autre visiteur qui le génère en JSON, etc. Nous pourrions même avoir un visiteur qui affiche mon arborescence de répertoires en utilisant un langage graphique tel que DOT , à visualiser avec un autre programme.

    Enfin, la complexité de Visitor avec sa double répartition signifie qu’il est plus difficile de comprendre, de coder et de déboguer. En bref, il a un facteur geek élevé et maintient le principe KISS. Dans une enquête réalisée par des chercheurs, il a été montré que Visitor était un modèle controversé (il n’y avait pas de consensus sur son utilité). Certaines expériences ont même montré que cela ne facilitait pas la maintenance du code.

    Description rapide du modèle de visiteur. Les classes à modifier doivent toutes implémenter la méthode ‘accept’. Les clients appellent cette méthode d’acceptation pour effectuer de nouvelles actions sur cette famille de classes, étendant ainsi leurs fonctionnalités. Les clients peuvent utiliser cette méthode d’acceptation pour effectuer un large éventail de nouvelles actions en transmettant une classe de visiteur différente pour chaque action spécifique. Une classe de visiteur contient plusieurs méthodes de visite remplacées définissant comment réaliser la même action spécifique pour chaque classe de la famille. Ces méthodes de visite passent une instance sur laquelle travailler.

    Lorsque vous pourriez envisager de l’utiliser

    1. Lorsque vous avez une famille de classes, vous devez les append toutes, mais pour une raison quelconque, vous ne pourrez pas modifier ou recomstackr la famille de classes dans le futur.
    2. Lorsque vous souhaitez append une nouvelle action et que cette nouvelle action est entièrement définie dans la classe du visiteur plutôt que répartie sur plusieurs classes.
    3. Quand votre patron dit que vous devez produire une série de classes qui doivent faire quelque chose en ce moment ! … mais personne ne sait exactement ce que cela signifie déjà.

    Basé sur l’excellente réponse de @Federico A. Ramponi.

    Imaginez que vous ayez cette hiérarchie:

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

    Que se passe-t-il si vous devez append une méthode “Walk” ici? Cela sera pénible pour tout le design.

    En même temps, l’ajout de la méthode “Walk” génère de nouvelles questions. Qu’en est-il de “manger” ou “dormir”? Doit-on vraiment append une nouvelle méthode à la hiérarchie Animal pour chaque nouvelle action ou opération que nous voulons append? C’est moche et le plus important, nous ne pourrons jamais fermer l’interface Animal. Ainsi, avec le modèle de visiteur, nous pouvons append une nouvelle méthode à la hiérarchie sans modifier la hiérarchie!

    Donc, vérifiez et exécutez cet exemple C #:

     using System; using System.Collections.Generic; namespace VisitorPattern { class Program { static void Main(ssortingng[] args) { var animals = new List { 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); } } } 

    Visiteur

    Le visiteur permet d’append de nouvelles fonctions virtuelles à une famille de classes sans modifier les classes elles-mêmes; au lieu de cela, on crée une classe visiteur qui implémente toutes les spécialisations appropriées de la fonction virtuelle

    Structure du visiteur:

    entrer la description de l'image ici

    Utilisez le motif Visiteur si:

    1. Des opérations similaires doivent être effectuées sur des objects de différents types regroupés dans une structure
    2. Vous devez exécuter de nombreuses opérations distinctes et non liées. Il sépare l’opération de la structure des objects
    3. De nouvelles opérations doivent être ajoutées sans modification de la structure de l’object
    4. Rassemblez les opérations liées dans une classe unique plutôt que de vous forcer à changer ou à dériver des classes
    5. Ajoutez des fonctions aux bibliothèques de classes pour lesquelles vous n’avez pas la source ou ne pouvez pas changer la source

    Même si le modèle Visitor offre la possibilité d’append de nouvelles opérations sans modifier le code existant dans Object, cette flexibilité présente un inconvénient.

    Si un nouvel object Visitable a été ajouté, il nécessite des modifications de code dans les classes Visitor et ConcreteVisitor . Il existe une solution de contournement pour résoudre ce problème: utiliser la reflection, qui aura un impact sur les performances.

    Extrait de code:

     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 atsortingbutes and methods here public Game(){ } public void getNextMove(){}; public void makeNextMove(){} public abstract Ssortingng getName(); } class Chess extends Game implements Visitable{ public Ssortingng getName(){ return Chess.class.getName(); } public void accept(Visitor visitor){ visitor.logGameStatistics(this); } } class Checkers extends Game implements Visitable{ public Ssortingng getName(){ return Checkers.class.getName(); } public void accept(Visitor visitor){ visitor.logGameStatistics(this); } } class Ludo extends Game implements Visitable{ public Ssortingng getName(){ return Ludo.class.getName(); } public void accept(Visitor visitor){ visitor.logGameStatistics(this); } } public class VisitorPattern{ public static void main(Ssortingng args[]){ Visitor visitor = new GameVisitor(); Visitable games[] = { new Chess(),new Checkers(), new Ludo()}; for (Visitable v : games){ v.accept(visitor); } } } 

    Explication:

    1. Visitable ( Element ) est une interface et cette méthode d’interface doit être ajoutée à un ensemble de classes.
    2. Visitor est une interface qui contient des méthodes permettant d’effectuer une opération sur les éléments Visitable .
    3. GameVisitor est une classe qui implémente l’interface de Visitor ( ConcreteVisitor ).
    4. Chaque élément Visitable accepte les Visitor et appelle une méthode appropriée de l’interface du Visitor .
    5. Vous pouvez traiter Game as Element et des jeux concrets tels que Chess,Checkers and Ludo comme ConcreteElements .

    Dans l’exemple ci-dessus, Chess, Checkers and Ludo sont trois jeux différents (et des classes Visitable ). Un beau jour, j’ai rencontré un scénario pour enregistrer les statistiques de chaque jeu. Donc, sans modifier la classe individuelle pour implémenter la fonctionnalité de statistiques, vous pouvez centraliser cette responsabilité dans la classe GameVisitor , ce qui fait l’affaire pour vous sans modifier la structure de chaque jeu.

    sortie:

     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 

    Faire référence à

    article oodesign

    article de fabrication

    pour plus de détails

    Décorateur

    pattern allows behaviour to be added to an individual object, either statically or dynamically, without affecting the behaviour of other objects from the same class

    Articles Similaires:

    Decorator Pattern for IO

    Quand utiliser le motif de décorateur?

    Double dispatch is just one reason among others to use this pattern .
    But note that it is the single way to implement double or more dispatch in languages that uses a single dispatch paradigm.

    Here are reasons to use the pattern :

    1) We want to define new operations without changing the model at each time because the model doesn’t change often wile operations change frequently.

    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 sortingck.
    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 comstack 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 :

    [model class diagram

    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 :

    entrer la description de l'image ici

    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 comstack 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 comstackr bounds as soon as the comstack 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 sortingck 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) 

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

    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.

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

    You might wonder what function objects and union data types are, then it’s worth reading http://www.ccs.neu.edu/home/matthias/htdc.html

    Thanks for the awesome explanation of @Federico A. Ramponi , I just made this in java version. Hope it might be helpful.

    Also just as @Konrad Rudolph pointed out, it’s actually a double dispatch using two concrete instances together to determine the run-time methods.

    So actually there is no need to create a common interface for the operation executor as long as we have the operation interface properly defined.

     import static java.lang.System.out; public class Visitor_2 { public static void main(Ssortingng...args) { Hearen hearen = new Hearen(); FoodImpl food = new FoodImpl(); hearen.showTheHobby(food); Katherine katherine = new Katherine(); katherine.presentHobby(food); } } interface Hobby { void insert(Hearen hearen); void embed(Katherine katherine); } class Hearen { Ssortingng name = "Hearen"; void showTheHobby(Hobby hobby) { hobby.insert(this); } } class Katherine { Ssortingng name = "Katherine"; void presentHobby(Hobby hobby) { hobby.embed(this); } } class FoodImpl implements Hobby { public void insert(Hearen hearen) { out.println(hearen.name + " start to eat bread"); } public void embed(Katherine katherine) { out.println(katherine.name + " start to eat mango"); } } 

    As you expect, a common interface will bring us more clarity though it’s actually not the essential part in this pattern.

     import static java.lang.System.out; public class Visitor_2 { public static void main(Ssortingng...args) { Hearen hearen = new Hearen(); FoodImpl food = new FoodImpl(); hearen.showHobby(food); Katherine katherine = new Katherine(); katherine.showHobby(food); } } interface Hobby { void insert(Hearen hearen); void insert(Katherine katherine); } abstract class Person { Ssortingng name; protected Person(Ssortingng n) { this.name = n; } abstract void showHobby(Hobby hobby); } class Hearen extends Person { public Hearen() { super("Hearen"); } @Override void showHobby(Hobby hobby) { hobby.insert(this); } } class Katherine extends Person { public Katherine() { super("Katherine"); } @Override void showHobby(Hobby hobby) { hobby.insert(this); } } class FoodImpl implements Hobby { public void insert(Hearen hearen) { out.println(hearen.name + " start to eat bread"); } public void insert(Katherine katherine) { out.println(katherine.name + " start to eat mango"); } } 

    your question is when to know:

    i do not first code with visitor pattern. i code standard and wait for the need to occur & then refactor. so lets say you have multiple payment systems that you installed one at a time. At checkout time you could have many if conditions (or instanceOf) , for example :

     //psuedo code if(payPal) do paypal checkout if(ssortingpe) do ssortingp stuff checkout if(payoneer) do payoneer checkout 

    now imagine i had 10 payment methods, it gets kind of ugly. So when you see that kind of pattern occuring visitor comes in handly to seperate all that out and you end up calling something like this afterwards:

     new PaymentCheckoutVistor(paymentType).visit() 

    You can see how to implement it from the number of examples here, im just showing you a usecase.