Je continue à voir des références au type de visiteurs dans les blogs mais je dois admettre que je ne les comprends pas. Je lis l'article de wikipedia pour le motif et je comprends ses mécanismes, mais je ne comprends toujours pas quand je l'utiliserai.
En tant que personne qui a récemment obtenu le motif de décorateur et qui en voit actuellement les utilisations absolument partout, j'aimerais pouvoir aussi comprendre intuitivement ce motif en apparence pratique.
Je ne connais pas très bien le modèle Visiteur. 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 que ce soit une hiérarchie complexe avec une interface bien établie.)
Nous souhaitons maintenant ajouter une nouvelle opération à la hiérarchie, à savoir que chaque animal produise son son. Dans la mesure où la hiérarchie est aussi simple, vous pouvez le faire avec un polymorphisme direct:
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 de cette manière, chaque fois que vous souhaitez ajouter 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 possible.
Le modèle de visiteur vous permet de déplacer chaque nouvelle opération dans une classe appropriée et vous ne devez étendre l'interface qu'une seule fois de la hiérarchie. 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, on modifie 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 réelle, 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"; }
Vous avez maintenant le moyen d’ajouter 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 terme impropre et fatal. Beaucoup (éminents1!) les programmeurs sont tombés sur ce problème. En réalité, il implémente double dispatching dans des langues qui ne le supportent pas nativement (la plupart d’entre elles ne le font pas).
1) Mon exemple préféré est Scott Meyers, auteur acclamé de «Effective C++», qui l’a appelé l’un de ses moments les plus importants en C++ aha! Ever .
Tout le monde ici a raison, mais je pense que cela ne règle pas le "quand". Tout d'abord, à partir de modèles de conception:
Visiteur vous permet de définir un nouveau fichier opération sans changer les classes des éléments sur lesquels il opère.
Maintenant, pensons à une simple hiérarchie de classes. 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 des colonnes.
Désormais, la conception orientée objet suppose que vous êtes plus susceptible de développer de nouvelles classes que de nouvelles méthodes. Il est donc plus facile d'ajouter plus de lignes, pour ainsi dire. Vous ajoutez simplement une nouvelle classe, spécifiez ce qui est différent dans cette classe et héritez du reste.
Parfois, cependant, les classes sont relativement statiques, mais vous devez ajouter plusieurs méthodes fréquemment - en ajoutant des colonnes. La méthode standard dans une conception OO serait d’ajouter de telles méthodes à toutes les classes, ce qui peut être coûteux. Le modèle de visiteur rend cela facile.
À propos, c’est le problème que le modèle de Scala correspond à son intention.
Le modèle Visitor design fonctionne très bien pour les structures "récursives" telles que les arborescences de répertoires, les structures XML ou les contours de documents.
Un objet Visiteur visite chaque noeud de la structure récursive: chaque répertoire, chaque balise XML, peu importe. L'objet Visiteur ne parcourt pas la structure. À la place, les méthodes de visiteur sont appliquées à chaque noeud de la structure.
Voici une structure de nœud récursive typique. Peut-être un répertoire ou une balise XML . [Si vous êtes une personne de Java, imaginez beaucoup de 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 visit
applique un objet Visiteur à chaque nœud de la structure. Dans ce cas, c'est un visiteur descendant. Vous pouvez modifier la structure de la méthode visit
pour effectuer un ordre ascendant ou autre.
Voici une super classe pour les visiteurs. Il est utilisé par la méthode visit
. Il "arrive à" chaque noeud de la structure. Étant donné que la méthode visit
appelle up
et down
, le visiteur peut garder une trace de 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 peut, par exemple, compter les nœuds à chaque niveau et accumuler une liste de nœuds, générant ainsi un numéro de section hiérarchique du chemin Nice.
Voici une application. Il construit une arborescence, someTree
. Cela crée une Visitor
, dumpNodes
.
Ensuite, il applique la dumpNodes
à l’arbre. L'objet dumpNode
"visitera" chaque noeud de l'arborescence.
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
L'algorithme TreeNode visit
garantit que chaque TreeNode est utilisé comme argument de la méthode arrivedAt
du visiteur.
Une façon de voir les choses est que le modèle de visiteur est un moyen de laisser vos clients ajouter 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 assez stable, mais que vous devez modifier les exigences de ce qu'il faut faire avec cette hiérarchie.
L'exemple classique concerne les compilateurs et autres. Un arbre de syntaxe abstraite (AST) peut définir avec précision la structure du langage de programmation, mais les opérations à effectuer sur AST changeront à mesure que votre projet avance: générateurs de code, jolies imprimantes, débogueurs , analyse des métriques de complexité.
Sans le modèle de visiteur, chaque développeur souhaitant ajouter une nouvelle fonctionnalité aurait besoin d'ajouter cette méthode à toutes les fonctionnalités 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 de visiteur est en conflit avec de bonnes pratiques OO), car il éloigne les données des opérations. Le modèle de visiteur est utile dans la situation où la normale OO les pratiques échouent.)
Il y a au moins trois très bonnes raisons d'utiliser le modèle de visiteur:
Réduisez la prolifération de code qui n’est que légèrement différent lorsque les structures de données changent.
Appliquez le même calcul à plusieurs structures de données, sans changer le code qui implémente le calcul.
Ajoutez des informations aux bibliothèques héritées sans changer le code hérité.
Veuillez regarder un article que j'ai écrit à ce sujet .
Comme Konrad Rudolph l'a déjà souligné, il convient aux cas où nous avons besoin de double dépêche
Voici un exemple pour montrer une situation dans laquelle nous avons besoin d'une double répartition et de la façon dont les visiteurs nous aident à le faire.
Exemple :
Disons que j'ai 3 types d'appareils mobiles - iPhone, Android, Windows Mobile.
Une radio Bluetooth est installée sur ces trois appareils.
Supposons que la radio Blue tooth puisse provenir de deux constructeurs distincts - Intel et Broadcom.
Juste pour rendre l’exemple pertinent pour notre discussion, supposons également que les API exposées par Intel Radio sont différentes de celles exposées par Broadcom radio.
Voici à quoi ressemblent mes cours -
Maintenant, je voudrais introduire une opération - Activer le Bluetooth sur un appareil mobile.
Sa signature de fonction devrait ressembler à quelque chose comme ça -
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
Donc, selon le type d'appareil approprié et , selon le type d'appareil radio Bluetooth approprié, il peut être activé en appelant les étapes appropriées ou l'algorithme.
En principe, cela devient une matrice 3 x 2, dans laquelle j'essaie de vectoriser la bonne opération en fonction du type d'objets impliqué.
Un comportement polymorphe dépendant du type des deux arguments.
Maintenant, le modèle de visiteur peut être appliqué à ce problème. L'inspiration vient de la page Wikipedia qui indique - «En gros, le visiteur permet d'ajouter de nouvelles fonctions virtuelles à une famille de classes sans modifier les classes elles-mêmes. au lieu de cela, on crée une classe de visiteur qui implémente toutes les spécialisations appropriées de la fonction virtuelle. Le visiteur prend en entrée la référence à l'instance et implémente l'objectif par double dispatch. ”
La double distribution est une nécessité ici en raison de la matrice 3x2
Voici à quoi ressemblera la configuration -
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:
In http://www.remondo.net/visitor-pattern-example-csharp/ J'ai trouvé un exemple qui montre un exemple factice qui montre les avantages du modèle de visiteurs. Ici vous avez 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. Vous remarquerez peut-être que Bottle
utilise unit
, qui est un type de données différent et doit être converti.
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 enfreint 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 en sorte que l'interrupteur dure plus longtemps 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 déplacé la responsabilité de compter le nombre de Pill
s vers la classe appelée PillCountVisitor
(et nous avons supprimé l'instruction de changement de cas). Cela signifie que chaque fois que vous devez ajouter un nouveau type de contenant pour pilules, vous ne devez changer que la classe PillCountVisitor
. De plus, notez que l'interface IVisitor
est d'utilisation générale dans un autre scénario.
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 autorisons les visiteurs à visiter les cours de piluliers.
À 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: chaque boîte à pilules permet au visiteur PillCountVisitor
de voir ses pilules compter. Il sait compter vos pilules.
Au visitor.Count
a la valeur de pilules.
In http://butunclebob.com/ArticleS.UncleBob.IuseVisitor vous voyez un scénario réel dans lequel vous ne pouvez pas utiliser polymorphisme (la réponse) pour suivre le principe de responsabilité unique. En fait dans:
public class HourlyEmployee extends Employee {
public String reportQtdHoursAndPay() {
//generate the line for this hourly employee
}
}
la méthode reportQtdHoursAndPay
sert à la génération de rapports et à la représentation, ce qui constitue une violation du principe de responsabilité unique. Il est donc préférable d'utiliser le modèle de visiteurs pour résoudre le problème.
Cay Horstmann a un excellent exemple d’endroit où appliquer Visiteur dans son OO livre de conception et de patrons . Il résume le problème:
Les objets 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'ajouter de nouvelles opérations à une telle conception.
La raison pour laquelle ce n'est pas facile, c'est parce que des opérations sont ajoutées dans les classes de structure elles-mêmes. Par exemple, imaginons que vous ayez un système de fichiers:
Voici quelques opérations (fonctionnalités) que nous pourrions vouloir implémenter avec cette structure:
Vous pouvez ajouter des fonctions à chaque classe du système de fichiers pour implémenter les opérations (et des personnes l'ont déjà fait par le passé car il est très évident de savoir comment le faire). Le problème est que chaque fois que vous ajoutez une nouvelle fonctionnalité (la ligne "etc." ci-dessus), vous devrez peut-être ajouter 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 une FileNode
qui a une méthode calculateFileColorForFunctionABC()
afin d'implémenter la dernière fonctionnalité de visualisation sur le système de fichiers.
Le modèle de visiteur (comme beaucoup de modèles de conception) est né de la {douleur et souffrance de développeurs qui savaient qu'il existait un meilleur moyen de permettre à leur code de changer sans nécessiter beaucoup de modifications partout et en respectant également les principes de bonne conception. (forte cohésion, faible couplage). À mon avis, il est difficile de comprendre l'utilité de nombreux schémas jusqu'à ce que vous ressentiez cette douleur. Expliquer la douleur (comme nous essayons de le faire ci-dessus avec les fonctionnalités "etc." qui sont ajoutées) prend de la place dans l'explication et constitue 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 au design 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 des implémentations Visitor
. Cela s'effectue via double-dispatching (qui est la partie compliquée du modèle): en utilisant les méthodes accept()
dans les classes de structure et les méthodes visitX()
dans les classes Visitor (la fonctionnalité):
Cette structure nous permet d’ajouter de nouvelles fonctionnalités qui fonctionnent sur la structure en tant que visiteurs concrets (sans changer les classes de structure).
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` générant les données au format XML, ou un autre visiteur le générant 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.
Note finale: La complexité de Visitor avec sa double dépêche signifie qu'il est plus difficile à comprendre, à coder et à déboguer. En bref, il a un facteur geek élevé et va contre le principe KISS. Dans une enquête réalisée par des chercheurs, il apparaissait que Visitor était un motif controversé (il n’ya pas eu de consensus quant à son utilité). Certaines expériences ont même montré que cela ne facilitait pas la maintenance du code.
À mon avis, la quantité de travail nécessaire pour ajouter une nouvelle opération est plus ou moins la même en utilisant Visitor Pattern
ou une modification directe de la structure de chaque élément. De plus, si je devais ajouter 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 donc une recompilation de toutes les classes d'éléments. Alors, quel est le point?
Le modèle de visiteur correspond à la même implémentation souterraine que la programmation Aspect Object.
Par exemple, si vous définissez une nouvelle opération sans changer les classes des éléments sur lesquels elle opère
Description rapide du modèle de visiteur. Les classes nécessitant une modification doivent toutes implémenter la méthode 'accept'. Les clients appellent cette méthode accept pour effectuer de nouvelles actions sur cette famille de classes, étendant ainsi leurs fonctionnalités. Les clients peuvent utiliser cette méthode d'acceptation unique pour effectuer un large éventail de nouvelles actions en transmettant une classe de visiteur différente pour chaque action spécifique. Une classe de visiteurs contient plusieurs méthodes de visite substituées définissant comment réaliser cette même action spécifique pour chaque classe de la famille. Ces méthodes de visite reçoivent une instance sur laquelle travailler.
Quand vous pourriez envisager de l'utiliser
Visiteur permet d'ajouter de nouvelles fonctions virtuelles à une famille de classes sans modifier les classes elles-mêmes. à la place, on crée une classe de visiteur qui implémente toutes les spécialisations appropriées de la fonction virtuelle
Structure du visiteur:
Utilisez le motif Visiteur si:
Bien que le motif Visitor offre la possibilité d'ajouter de nouvelles opérations sans modifier le code existant dans Object, cette flexibilité présente un inconvénient.
Si un nouvel objet Visitable a été ajouté, des modifications de code sont nécessaires dans les classes Visitor & ConcreteVisitor. Il existe une solution de contournement pour résoudre ce problème: Utilisez une réflexion 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 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);
}
}
}
Explication:
Visitable
(Element
) est une interface et cette méthode d'interface doit être ajoutée à un ensemble de classes. Visitor
est une interface, qui contient des méthodes pour effectuer une opération sur les éléments Visitable
.GameVisitor
est une classe qui implémente l'interface Visitor
(ConcreteVisitor
).Visitable
accepte Visitor
et appelle une méthode pertinente de l'interface Visitor
.Game
comme Element
et des jeux concrets comme Chess,Checkers and Ludo
comme ConcreteElements
.Dans l'exemple ci-dessus, Chess, Checkers and Ludo
correspond à trois jeux différents (et à Visitable
classes). Un beau jour, j’ai rencontré un scénario pour enregistrer les statistiques de chaque match. Ainsi, 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 à
fabrication de la source article
pour plus de détails
pattern permet d'ajouter le comportement à un objet individuel, de manière statique ou dynamique, sans affecter le comportement des autres objets de la même classe
Articles Similaires:
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 ajouter une méthode "Walk" ici? Ce sera pénible pour toute la conception.
Dans le même temps, l'ajout de la méthode "Walk" génère de nouvelles questions. Qu'en est-il de "manger" ou "dormir"? Faut-il vraiment ajouter une nouvelle méthode à la hiérarchie Animal pour chaque nouvelle action ou opération à ajouter? C'est moche et le plus important, nous ne pourrons jamais fermer l'interface Animal. Ainsi, avec le modèle de visiteur, nous pouvons ajouter une nouvelle méthode à la hiérarchie sans modifier la hiérarchie!
Alors, vérifiez et lancez cet exemple 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);
}
}
}
J'aime beaucoup la description et l'exemple de http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html .
L'hypothèse est que vous avez une hiérarchie de classes primaire qui est fixe; peut-être provient-il d’un autre fournisseur et vous ne pouvez pas modifier cette hiérarchie. Toutefois, vous souhaitez ajouter de nouvelles méthodes polymorphes à cette hiérarchie, ce qui signifie que vous devez normalement ajouter quelque chose à l’interface de classe de base. Le dilemme est donc que vous devez ajouter des méthodes à la classe de base, mais que vous ne pouvez pas toucher à la classe de base. Comment vous en sortez-vous?
Le modèle de conception qui résout ce type de problème s'appelle un «visiteur» (le dernier dans le livre Modèles de conception) et s'appuie sur le schéma de double répartition présenté dans la dernière section.
Le modèle de visiteur vous permet d'étendre l'interface du type principal en créant une hiérarchie de classe distincte du type Visiteur afin de virtualiser les opérations effectuées sur le type principal. Les objets de type primaire «acceptent» simplement le visiteur, puis appellent la fonction membre liée de manière dynamique du visiteur.
Je ne comprenais pas ce modèle avant de tomber sur uncle bob article et de lire les commentaires . Considérons le code suivant:
public class Employee
{
}
public class SalariedEmployee : Employee
{
}
public class HourlyEmployee : Employee
{
}
public class QtdHoursAndPayReport
{
public void PrintReport()
{
var employees = new List<Employee>
{
new SalariedEmployee(),
new HourlyEmployee()
};
foreach (Employee e in employees)
{
if (e is HourlyEmployee he)
PrintReportLine(he);
if (e is SalariedEmployee se)
PrintReportLine(se);
}
}
public void PrintReportLine(HourlyEmployee he)
{
System.Diagnostics.Debug.WriteLine("hours");
}
public void PrintReportLine(SalariedEmployee se)
{
System.Diagnostics.Debug.WriteLine("fix");
}
}
class Program
{
static void Main(string[] args)
{
new QtdHoursAndPayReport().PrintReport();
}
}
Bien que cela puisse sembler bon car cela confirme Single Responsibility , il viole le principe Open/Closed . Chaque fois que vous avez un nouveau type d'employé, vous devrez ajouter si avec le type check. Et si vous ne le faites pas, vous ne le saurez jamais au moment de la compilation.
Avec un modèle de visiteur, vous pouvez rendre votre code plus propre car il ne viole pas le principe ouvert/fermé et ne viole pas la responsabilité simple. Et si vous oubliez de mettre en œuvre visit, il ne compilera pas:
public abstract class Employee
{
public abstract void Accept(EmployeeVisitor v);
}
public class SalariedEmployee : Employee
{
public override void Accept(EmployeeVisitor v)
{
v.Visit(this);
}
}
public class HourlyEmployee:Employee
{
public override void Accept(EmployeeVisitor v)
{
v.Visit(this);
}
}
public interface EmployeeVisitor
{
void Visit(HourlyEmployee he);
void Visit(SalariedEmployee se);
}
public class QtdHoursAndPayReport : EmployeeVisitor
{
public void Visit(HourlyEmployee he)
{
System.Diagnostics.Debug.WriteLine("hourly");
// generate the line of the report.
}
public void Visit(SalariedEmployee se)
{
System.Diagnostics.Debug.WriteLine("fix");
} // do nothing
public void PrintReport()
{
var employees = new List<Employee>
{
new SalariedEmployee(),
new HourlyEmployee()
};
QtdHoursAndPayReport v = new QtdHoursAndPayReport();
foreach (var emp in employees)
{
emp.Accept(v);
}
}
}
class Program
{
public static void Main(string[] args)
{
new QtdHoursAndPayReport().PrintReport();
}
}
}
La magie est que, même si v.Visit(this)
a le même aspect, il est en fait différent car il appelle différentes surcharges de visiteur.
Bien que j'ai compris le comment et quand, je n'ai jamais compris le pourquoi. Au cas où cela aiderait des personnes ayant une formation dans un langage comme le C++, vous voulez lisez ceci très attentivement.
Pour les paresseux, nous utilisons le modèle de visiteur car "alors que les fonctions virtuelles sont distribuées dynamiquement en C++, la surcharge de fonctions est effectuée de manière statique".
Autrement dit, assurez-vous que CollideWith (ApolloSpacecraft &) est appelé lorsque vous transmettez une référence SpaceShip liée à un objet ApolloSpacecraft.
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;
}
}
Lorsque vous souhaitez avoir des objets de fonction sur des types de données d'union, vous avez besoin d'un modèle de visiteur.
Vous vous demandez peut-être ce que sont les objets de fonction et les types de données d'union, alors il vaut la peine de lire http://www.ccs.neu.edu/home/matthias/htdc.html
Merci pour l'explication géniale de @ Federico A. Ramponi , je viens de faire ceci dans la version Java. J'espère que cela pourrait être utile.
De même que @ Konrad Rudolph a fait remarquer, il s’agit en fait de double dispatch utilisant deux instances concrètes ensemble pour déterminer les méthodes d’exécution.
Donc, en réalité, il n'est pas nécessaire de créer une interface common pour l'exécuteur opération tant que l'interface opération est correctement définie.
import static Java.lang.System.out;
public class Visitor_2 {
public static void main(String...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 {
String name = "Hearen";
void showTheHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine {
String 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");
}
}
Comme vous vous en doutez, une interface common nous apportera plus de clarté, bien que ce ne soit en fait pas la partie essentielle de ce modèle.
import static Java.lang.System.out;
public class Visitor_2 {
public static void main(String...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 {
String name;
protected Person(String 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");
}
}
votre question est de savoir quand:
je ne code pas d'abord avec le modèle de visiteur. Je code standard et attend que le besoin se produise et refactor ensuite. alors disons que vous avez plusieurs systèmes de paiement que vous avez installés un à la fois. Au moment du paiement, vous pouvez avoir plusieurs conditions if (ou instanceOf), par exemple:
//psuedo code
if(Paypal)
do Paypal checkout
if(stripe)
do strip stuff checkout
if(payoneer)
do payoneer checkout
maintenant, imaginez que j'avais 10 modes de paiement, ça devient un peu moche. Ainsi, lorsque vous voyez ce type de motif se produire, un visiteur s’efforce de séparer tout cela et vous appelez ensuite quelque chose comme ceci:
new PaymentCheckoutVistor(paymentType).visit()
Vous pouvez voir comment l'implémenter à partir du nombre d'exemples ci-dessous, en vous montrant simplement un cas d'utilisation.