Quand dois-je utiliser le modèle de conception de visiteur? [fermé]

315

Je continue de voir des références au profil des visiteurs dans les blogs mais je dois admettre que je ne comprends pas. J'ai lu l' article wikipedia pour le motif et je comprends sa mécanique, mais je ne sais toujours pas quand je l'utiliserai.

En tant que quelqu'un qui a récemment obtenu le motif de décoration et qui en voit maintenant les utilisations absolument partout, j'aimerais pouvoir comprendre vraiment intuitivement ce motif apparemment pratique.

George Mauer
la source
7
Je l' ai finalement obtenu après avoir lu cet article de Jermey Miller sur ma mûre tout en restant coincé dans un hall pendant deux heures. Il est long mais donne une merveilleuse explication de la double expédition, du visiteur et du composite, et de ce que vous pouvez en faire.
George Mauer
1
voici un bel article: codeproject.com/Articles/186185/Visitor-Design-Pattern
Seyed Morteza Mousavi
3
Modèle de visiteur? Laquelle? Le fait est qu'il y a beaucoup de malentendus et de pure confusion autour de ce modèle de conception. J'ai écrit et un article qui, je l'espère, met un peu d'ordre dans ce chaos: rgomes-info.blogspot.co.uk/2013/01/…
Richard Gomes
Lorsque vous souhaitez avoir des objets fonction sur les types de données d'union, vous aurez besoin d'un modèle de visiteur. Vous vous demandez peut-être quels sont les objets fonction et les types de données d'union, alors cela vaut la peine de lire ccs.neu.edu/home/matthias/htdc.html
Wei Qiu
Des exemples ici et ici .
jaco0646

Réponses:

315

Je ne connais pas très bien le modèle des visiteurs. 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.)

Maintenant, nous voulons ajouter une nouvelle opération à la hiérarchie, à savoir que nous voulons que chaque animal émette son son. Dans la mesure où la hiérarchie est aussi simple, vous pouvez le faire avec un polymorphisme 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 ajouter une opération, vous devez modifier l'interface pour chaque classe unique de la hiérarchie. Supposons maintenant que vous soyez satisfait de l'interface d'origine et que vous souhaitiez y apporter le moins de modifications possibles.

Le modèle de visiteur vous permet de déplacer chaque nouvelle opération dans une classe appropriée, et vous devez étendre l'interface de la hiérarchie 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 afin d'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 Chat ni Chien :

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 un moyen d'ajouter des opérations sans modifier la hiérarchie. Voici comment cela fonctionne:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}
Federico A. Ramponi
la source
19
S.Lott, marcher sur un arbre n'est pas vraiment le modèle du visiteur. (C'est le "modèle de visiteur hiérarchique", qui est complètement différent de manière confuse.) Il n'y a aucun moyen d'afficher le modèle de visiteur GoF sans utiliser l'héritage ou l'implémentation d'interface.
munificence
14
@Knownasilya - Ce n'est pas vrai. Le & -Operator donne l'adresse du Sound-Object, qui est nécessaire à l'interface. letsDo(Operation *v) a besoin d'un pointeur.
AquilaRapax
3
par souci de clarté, cet exemple de modèle de conception de visiteur est-il correct?
godzilla
4
Après beaucoup de réflexion, je me demande pourquoi vous avez appelé deux méthodes hereIsADog et hereIsACat alors que vous passez déjà le chien et le chat aux méthodes. Je préfère une simple performTask (Object * obj) et vous transformez cet objet dans la classe Operation. (et dans un langage prenant en charge la priorité, pas besoin de casting)
Abdalrahman Shatou
6
Dans votre exemple «principal» à la fin: theSound.hereIsACat(c)aurait fait le travail, comment justifiez-vous tous les frais généraux introduits par le modèle? la double répartition est la justification.
franssu
131

La raison de votre confusion est probablement que le visiteur est un terme impropre fatal. De nombreux programmeurs ( 1 éminent !) Sont tombés sur ce problème. Ce qu'il fait, c'est implémenter la double répartition 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 a appelé celui-ci l'un de ses plus importants C ++ aha! moments jamais .

Konrad Rudolph
la source
3
+1 "il n'y a pas de modèle" - la réponse parfaite. la réponse la plus appréciée prouve que de nombreux programmeurs c ++ ne sont pas encore conscients des limitations des fonctions virtuelles par rapport au polymorphisme "adhoc" en utilisant une énumération de type et un cas de commutateur (la manière c). Il peut être plus net et invisible d'utiliser le virtuel, mais il est toujours limité à une seule expédition. À mon avis, c'est le plus gros défaut de c ++.
user3125280
@ user3125280 J'ai lu 4/5 articles et le chapitre Design Patterns sur le modèle Visitor maintenant, et aucun d'entre eux n'explique l'avantage d'utiliser ce modèle obscur sur un stmt de cas, ou quand vous pourriez en utiliser un sur l'autre. Merci d'avoir au moins soulevé la question!
spinkus
4
@ Sam Je suis sûr qu'ils ne l' expliquent - c'est le même avantage que vous toujours obtenir du polymorphisme sous - classement / exécution sur switch: switchdisques codes de la prise de décision sur le côté client (duplication de code) et ne propose pas de vérification de type statique ( vérifier l'exhaustivité et la distinction des cas, etc.). Un modèle de visiteur est vérifié par le vérificateur de type et rend généralement le code client plus simple.
Konrad Rudolph
@KonradRudolph merci pour cela. Notant cependant, ce n'est pas abordé explicitement dans Patterns ou dans l'article wikipedia par exemple. Je ne suis pas en désaccord avec vous, mais vous pourriez dire qu'il y a aussi des avantages à utiliser un cas stmt donc son étrange n'est pas généralement contrasté: 1. vous n'avez pas besoin d'une méthode accept () sur les objets de votre collection. 2. Le ~ visiteur peut manipuler des objets de type inconnu. Ainsi, le cas stmt semble mieux adapté pour opérer sur des structures d'objets avec une collection modifiable de types impliqués. Les modèles concèdent que le modèle de visiteur n'est pas bien adapté à un tel scénario (p333).
spinkus
1
@SamPinkus konrad's spot on - c'est pourquoi des virtualfonctionnalités similaires sont si utiles dans les langages de programmation modernes - elles sont le bloc de construction de base des programmes extensibles - à mon avis, le chemin c (commutateur imbriqué ou correspondance de modèle, etc. selon la langue de votre choix) est beaucoup plus propre dans le code qui n'a pas besoin d'être extensible et j'ai été agréablement surpris de voir ce style dans un logiciel compliqué comme le prouveur 9. Plus important encore, tout langage qui veut fournir une extensibilité devrait probablement s'adapter à de meilleurs schémas de distribution que la distribution récursive simple (c'est-à-dire visiteur).
user3125280
84

Tout le monde ici a raison, mais je pense que cela ne tient pas compte du "quand". Tout d'abord, à partir des modèles de conception:

Visitor vous permet de définir une nouvelle opération sans changer les classes des éléments sur lesquels elle 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 une feuille de calcul: les classes sont des lignes et les méthodes sont des colonnes.

Maintenant, la conception orientée objet suppose que vous êtes plus susceptible de développer de nouvelles classes que de nouvelles méthodes, donc ajouter plus de lignes, pour ainsi dire, est plus facile. 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 plus de méthodes fréquemment - en ajoutant des colonnes. La manière standard dans une conception OO serait d'ajouter de telles méthodes à toutes les classes, ce qui peut être coûteux. Le modèle Visitor rend cela facile.

À propos, c'est le problème que les correspondances de motifs de Scala ont l'intention de résoudre.

Daniel C. Sobral
la source
Pourquoi devrais-je utiliser le modèle de visiteur sur juste une classe utlity. je peux appeler ma classe utilitaire comme ceci: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Quelle est la différence ? ils font tous les deux une séparation de leurs préoccupations, n'est-ce pas? j'espère que vous pouvez aider.
j2emanue
@ j2emanue Parce que le modèle de visiteur utilise la surcharge correcte du visiteur au moment de l'exécution. Alors que votre code a besoin d'une conversion de type pour appeler une surcharge correcte.
Accès refusé
y a-t-il un gain d'efficacité avec cela? je suppose que cela évite de jeter son une bonne idée
j2emanue
@ j2emanue l'idée est d'écrire du code conforme au principe ouvert / fermé et non à des raisons de performances. Voir ouvert fermé chez oncle Bob butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Accès refusé
22

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 documents.

Un objet Visitor visite chaque nœud de la structure récursive: chaque répertoire, chaque balise XML, peu importe. L'objet Visitor ne boucle pas à travers la structure. Au lieu de cela, les méthodes Visitor sont appliquées à chaque nœud de la structure.

Voici une structure de nœud récursive typique. Il peut s'agir d'un répertoire ou d'une balise XML. [Si vous êtes une personne 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 visitméthode applique un objet Visitor à chaque nœud de la structure. Dans ce cas, c'est un visiteur descendant. Vous pouvez modifier la structure de la visitméthode pour effectuer un ordre ascendant ou un autre ordre.

Voici une superclasse pour les visiteurs. Il est utilisé par la visitméthode. Il "arrive" à chaque nœud de la structure. Puisque la visitméthode appelle upet 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 numéro de section hiérarchique de chemin.

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

Ensuite, il applique le dumpNodesà l'arbre. L' dumpNodeobjet "visitera" chaque nœud de l'arborescence.

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

L' visitalgorithme TreeNode garantit que chaque TreeNode est utilisé comme argument de la arrivedAtméthode du visiteur .

S.Lott
la source
8
Comme d'autres l'ont déclaré, il s'agit du "modèle hiérarchique de visiteurs".
PPC-Coder
1
@ PPC-Coder Quelle est la différence entre «modèle de visiteur hiérarchique» et modèle de visiteur?
Tim Lovell-Smith
3
Le modèle de visiteur hiérarchique est plus flexible que le modèle de visiteur classique. Par exemple, avec le modèle hiérarchique, vous pouvez suivre la profondeur de la traversée et décider quelle branche parcourir ou arrêter de traverser ensemble. Le visiteur classique n'a pas ce concept et visitera tous les nœuds.
PPC-Coder
18

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.

Il est utile lorsque vous avez une hiérarchie de classes assez stable, mais que vous avez des exigences changeantes quant à ce qui doit être fait avec cette hiérarchie.

L'exemple classique concerne les compilateurs et similaires. Un arbre de syntaxe abstraite (AST) peut définir avec précision la structure du langage de programmation, mais les opérations que vous voudrez peut-être effectuer sur l'AST changeront au fur et à mesure de l'avancement de votre projet: générateurs de code, jolies imprimantes, débogueurs, analyse des métriques de complexité.

Sans le modèle de visiteur, chaque fois qu'un développeur souhaite ajouter une nouvelle fonctionnalité, il doit ajouter cette méthode à chaque fonctionnalité 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 les bonnes pratiques d'OO, car il éloigne les opérations des données des données. Le modèle de visiteur est utile précisément dans la situation où les pratiques normales d'OO échouent.)

Pensée étrange
la source
J'aimerais également connaître votre opinion sur les points suivants: pourquoi utiliser le modèle de visiteur sur une classe d'utilité uniquement. je peux appeler ma classe utilitaire comme ceci: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Quelle est la différence ? ils font tous les deux une séparation de leurs préoccupations, n'est-ce pas? j'espère que vous pouvez aider.
j2emanue
@ j2emanue: Je ne comprends pas la question. Je vous suggère de l'étoffer et de le poster en tant que question complète à laquelle tout le monde peut répondre.
Oddthinking
1
j'ai posté une nouvelle question ici: stackoverflow.com/questions/52068876/…
j2emanue
14

Il y a au moins trois très bonnes raisons d'utiliser le modèle de visiteur:

  1. Réduisez la prolifération de code qui n'est que légèrement différent lorsque les structures de données changent.

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

  3. Ajoutez des informations aux bibliothèques héritées sans modifier le code hérité.

Veuillez consulter un article que j'ai écrit à ce sujet .

Richard Gomes
la source
1
J'ai commenté votre article avec la plus grande utilisation que j'ai vue pour le visiteur. Pensées?
George Mauer
13

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 répartition et comment le visiteur nous aide à le faire.

Exemple :

Disons que j'ai 3 types d'appareils mobiles - iPhone, Android, Windows Mobile.

Ces trois appareils sont équipés d'une radio Bluetooth.

Supposons que la radio Blue tooth peut provenir de 2 OEM distincts - 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 à quoi ressemblent mes cours -

entrez la description de l'image ici entrez 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 aimer quelque chose comme ça -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Ainsi, selon le bon type d'appareil et selon le bon type de radio Bluetooth , il peut être allumé en appelant les étapes ou l'algorithme appropriés .

En principe, il devient une matrice 3 x 2, où j'essaie de vectoriser la bonne opération en fonction du bon type d'objets impliqués.

Un comportement polymorphe en fonction du type des deux arguments.

entrez la description de l'image ici

Maintenant, le modèle de visiteur peut être appliqué à ce problème. L'inspiration vient de la page Wikipédia déclarant - «En substance, le 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 visiteur qui implémente toutes les spécialisations appropriées de la fonction virtuelle. Le visiteur prend la référence d'instance comme entrée et met en œuvre l'objectif via une double répartition. »

La double répartition est une nécessité ici en raison de la matrice 3x2

Voici à quoi ressemblera la configuration - entrez 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 .

Kapoor
la source
9

Je l'ai trouvé plus facile dans les liens suivants:

Dans http://www.remondo.net/visitor-pattern-example-csharp/, j'ai trouvé un exemple qui montre un exemple simulé qui montre les avantages du modèle de visiteur. 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 BilsterPackcontenez des paires de pilules, vous devez donc multiplier le nombre de paires par 2. Vous pouvez également remarquer cette Bottleutilisation unitqui est d'un type de données différent et doit être castée.

Donc, dans la méthode principale, vous pouvez calculer le nombre de comprimés 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 Single Responsibility Principle. Cela signifie que vous devez modifier le code de la méthode principale si vous ajoutez un nouveau type de conteneur. Prolonger également le passage 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 Pills à la classe appelée PillCountVisitor(et nous avons supprimé l'instruction switch case). Cela signifie que chaque fois que vous devez ajouter un nouveau type de pilulier, vous ne devez modifier que la PillCountVisitorclasse. Notez également que l' IVisitorinterface est générale à utiliser dans d'autres scénarios.

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

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

    #region IAcceptor Members

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

    #endregion
}

nous permettons au visiteur de visiter les classes de conteneurs de pilules.

À la fin, nous calculons le nombre de comprimés à l'aide du code suivant:

var visitor = new PillCountVisitor();

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

Cela signifie que: chaque boîte à pilules permet au PillCountVisitorvisiteur de voir le nombre de ses comprimés. Il sait compter vos comprimés.

A la visitor.Countvaleur des pilules.

Dans http://butunclebob.com/ArticleS.UncleBob.IuseVisitor, vous voyez un scénario réel dans lequel vous ne pouvez pas utiliser le 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 reportQtdHoursAndPayméthode est pour le signalement et la représentation et cela viole le principe de responsabilité unique. Il est donc préférable d'utiliser le modèle de visiteur pour surmonter le problème.

Seyed Morteza Mousavi
la source
2
Salut Sayed, pouvez-vous modifier votre réponse pour ajouter les parties que vous avez trouvées les plus éclairantes. SO décourage généralement les réponses liées uniquement aux liens car l'objectif est d'être une base de données de connaissances et les liens descendent.
George Mauer
8

La double expédition n'est qu'une raison parmi d'autres d'utiliser ce modèle .
Mais notez que c'est la seule façon d'implémenter une répartition double ou plus dans les langues qui utilise un paradigme de répartition unique.

Voici les raisons d'utiliser le modèle:

1) Nous voulons définir de nouvelles opérations sans changer le modèle à chaque fois car le modèle ne change pas souvent alors que les opérations changent fréquemment.

2) Nous ne voulons pas coupler modèle et comportement car nous voulons avoir un modèle réutilisable dans plusieurs applications ou nous voulons avoir un modèle extensible qui permet aux classes clientes de définir leurs comportements avec leurs propres classes.

3) Nous avons des opérations communes qui dépendent du type concret du modèle mais nous ne voulons pas implémenter la logique dans chaque sous-classe car cela exploserait la logique commune dans plusieurs classes et donc à plusieurs endroits .

4) Nous utilisons une conception de modèle de domaine et les classes de modèle de la même hiérarchie effectuent trop de choses distinctes qui pourraient être rassemblées ailleurs .

5) Nous avons besoin d'une double expédition .
Nous avons des variables déclarées avec des types d'interface et nous voulons pouvoir les traiter en fonction de leur type d'exécution… bien sûr sans utiliser if (myObj instanceof Foo) {}ni truc.
L'idée est par exemple de passer ces variables à des méthodes qui déclarent un type concret de l'interface comme paramètre pour appliquer un traitement spécifique. Cette façon de faire n'est pas possible à la sortie de la boîte avec les langues repose sur une seule expédition car le choix invoqué au moment de l'exécution dépend uniquement du type d'exécution du récepteur.
Notez qu'en Java, la méthode (signature) à appeler est choisie au moment de la compilation et dépend du type déclaré des paramètres, pas de leur type d'exécution.

Le dernier point qui est une raison d'utiliser le visiteur est également une conséquence car lorsque vous implémentez le visiteur (bien sûr pour les langues qui ne prennent pas en charge la répartition multiple), vous devez nécessairement introduire une implémentation à double répartition.

Notez que la traversée d'éléments (itération) pour appliquer le visiteur sur chacun d'eux n'est pas une raison pour utiliser le motif.
Vous utilisez le modèle car vous divisez le modèle et le traitement.
Et en utilisant le modèle, vous bénéficiez en plus d'une capacité d'itérateur.
Cette capacité est très puissante et va au-delà de l'itération sur un type commun avec une méthode spécifique comme accept()c'est une méthode générique.
Il s'agit d'un cas d'utilisation spécial. Je vais donc mettre cela de côté.


Exemple en Java

Je vais illustrer la valeur ajoutée du motif avec un exemple d'échecs où nous aimerions définir le traitement lorsque le joueur demande le déplacement d'une pièce.

Sans l'utilisation du modèle de visiteur, nous pourrions définir des comportements de déplacement de pièces directement dans les sous-classes de pièces.
On pourrait avoir par exemple une Pieceinterface telle que:

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Chaque sous-classe Piece le mettrait en œuvre, par exemple:

public class Pawn implements Piece{

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

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

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

}

Et la même chose pour toutes les sous-classes Piece.
Voici une classe de diagramme qui illustre cette conception:

[diagramme de classe modèle

Cette approche présente trois inconvénients importants:

- des comportements tels que performMove()ou computeIfKingCheck()utiliseront très probablement une logique commune.
Par exemple, quel que soit le béton Piece, performMove()définira finalement la pièce actuelle à un emplacement spécifique et prendra éventuellement la pièce adverse.
Diviser les comportements associés en plusieurs classes au lieu de les rassembler détruit en quelque sorte le modèle de responsabilité unique. Rendre leur maintenabilité plus difficile.

- le traitement checkMoveValidity()ne devrait pas être quelque chose que les Piecesous-classes peuvent voir ou changer.
C'est un contrôle qui va au-delà des actions humaines ou informatiques. Cette vérification est effectuée à chaque action demandée par un joueur pour s'assurer que le mouvement de pièce demandé est valide.
Donc, nous ne voulons même pas fournir cela dans l' Pieceinterface.

- Dans les jeux d'échecs difficiles pour les développeurs de robots, l'application fournit généralement une API standard ( Pieceinterfaces, sous-classes, Board, comportements communs, etc.) et permet aux développeurs d'enrichir leur stratégie de robots.
Pour ce faire, nous devons proposer un modèle où les données et les comportements ne sont pas étroitement couplés dans les Pieceimplémentations.

Alors allons-y pour utiliser le modèle de visiteur!

Nous avons deux types de structure:

- les classes modèles qui acceptent d'être visitées (les pièces)

- les visiteurs qui les visitent (opérations de déménagement)

Voici un diagramme de classe qui illustre le modèle:

entrez la description de l'image ici

Dans la partie supérieure, nous avons les visiteurs et dans la partie inférieure, nous avons les classes modèles.

Voici l' PieceMovingVisitorinterface (comportement spécifié pour chaque type de 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);

}

La pièce est définie maintenant:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Sa méthode clé est:

void accept(PieceMovingVisitor pieceVisitor);

Il fournit le premier envoi: une invocation basée sur le Piecerécepteur.
Au moment de la compilation, la méthode est liée à la accept()méthode de l'interface Piece et au moment de l'exécution, la méthode limitée sera invoquée sur la Piececlasse d' exécution .
Et c'est l' accept()implémentation de la méthode qui effectuera une deuxième répartition.

En effet, chaque Piecesous-classe qui veut être visitée par un PieceMovingVisitorobjet invoque la PieceMovingVisitor.visit()méthode en passant comme argument lui-même.
De cette façon, le compilateur délimite dès la compilation, le type du paramètre déclaré avec le type concret.
Il y a la deuxième dépêche.
Voici la Bishopsous-classe qui illustre cela:

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

}

Et voici un exemple d'utilisation:

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

Inconvénients pour les visiteurs

Le modèle de visiteur est un modèle très puissant, mais il a également quelques limitations importantes que vous devez considérer avant de l'utiliser.

1) Risque de réduire / casser l'encapsulation

Dans certains types d'opérations, le modèle de visiteur peut réduire ou interrompre l'encapsulation des objets de domaine.

Par exemple, comme la MovePerformingVisitor classe doit définir les coordonnées de la pièce réelle, l' Pieceinterface doit fournir un moyen de le faire:

void setCoordinates(Coordinates coordinates);

La responsabilité des Piecechangements de coordonnées est désormais ouverte à d'autres classes que les Piecesous-classes.
Déplacer le traitement effectué par le visiteur dans les Piecesous-classes n'est pas non plus une option.
Cela créera en effet un autre problème car le Piece.accept()accepte toute implémentation de visiteur. Il ne sait pas ce que le visiteur effectue et donc aucune idée de savoir si et comment changer l'état de la pièce.
Un moyen d'identifier le visiteur serait d'effectuer un post-traitement en Piece.accept()fonction de l'implémentation du visiteur. Ce serait une très mauvaise idée car cela créerait un fort couplage entre les implémentations des visiteurs et des sous - classes Piece et d' ailleurs , il aurait probablement besoin d'utiliser comme truc getClass(), instanceofou tout autre marqueur identifiant la mise en œuvre des visiteurs.

2) Obligation de changer de modèle

Contrairement à certains autres modèles de conception comportementale comme Decoratorpar exemple, le modèle de visiteur est intrusif.
Il nous faut en effet modifier la classe réceptrice initiale pour fournir une accept()méthode à accepter d'être visitée.
Nous n'avons eu aucun problème pour Pieceet ses sous-classes car ce sont nos classes .
Dans les classes intégrées ou tierces, les choses ne sont pas si faciles.
Nous devons les envelopper ou les hériter (si nous le pouvons) pour ajouter la accept()méthode.

3) Indirections

Le motif crée de multiples indirections.
La double répartition signifie deux invocations au lieu d'une seule:

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

Et nous pourrions avoir des indirections supplémentaires lorsque le visiteur modifie l'état de l'objet visité.
Cela peut ressembler à un cycle:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)
davidxxx
la source
6

Cay Horstmann a un excellent exemple où appliquer Visitor dans son livre OO Design and patterns . 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 est que les opérations sont ajoutées dans les classes de structure elles-mêmes. Par exemple, imaginez que vous avez un système de fichiers:

Diagramme de classes FileSystem

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

  • Afficher les noms des éléments du nœud (une liste de fichiers)
  • Afficher la taille calculée des éléments du nœud (où la taille d'un répertoire inclut la taille de tous ses éléments enfants)
  • etc.

Vous pouvez ajouter des fonctions à chaque classe du FileSystem pour implémenter les opérations (et les gens l'ont fait dans le passé car il 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 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 disposez d'un FileNodequi 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 de nombreux modèles de conception) est né de la douleur et de la souffrance des développeurs qui savaient qu'il y avait une meilleure façon de permettre à leur code de changer sans nécessiter beaucoup de changements partout et en respectant également les bons principes de conception (haute cohésion, faible couplage ). Je pense qu'il est difficile de comprendre l'utilité de nombreux modèles tant que vous n'avez pas ressenti cette douleur. Expliquer la douleur (comme nous essayons de le faire ci-dessus avec les fonctionnalités "etc." qui s'ajoutent) prend de la place dans l'explication et est une distraction. Comprendre les schémas est difficile pour cette raison.

Visitor 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 Visitorimplémentations. Cela se fait via la double répartition (qui est la partie compliquée du modèle): en utilisant des accept()méthodes dans les classes de structure et des visitX()méthodes dans les classes Visitor (la fonctionnalité):

Diagramme de classes FileSystem avec Visitor appliqué

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

Diagramme de classes FileSystem avec Visitor appliqué

Par exemple, un PrintNameVisitorqui implémente la fonctionnalité de liste de répertoires et un PrintSizeVisitorqui implémente la version avec la taille. Nous pourrions imaginer un jour avoir un «ExportXMLVisitor» qui génère les données en XML, ou un autre visiteur qui les 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 à 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, Visitor s'est révélé être 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.

Fuhrmanator
la source
Je pense que la structure du répertoire est un bon modèle composite, mais je suis d'accord avec votre dernier paragraphe.
zar
5

À mon avis, la quantité de travail pour ajouter une nouvelle opération est plus ou moins la même en utilisant Visitor Patternou en modifiant directement la structure de chaque élément. De plus, si je devais ajouter une nouvelle classe d'éléments, disons Cow, l'interface Operation sera affectée et cela se propagera à toutes les classes d'éléments existantes, nécessitant donc une recompilation de toutes les classes d'éléments. Alors à quoi ça sert?

kaosad
la source
4
Presque chaque fois que j'utilise Visitor, c'est lorsque vous travaillez avec la traversée d'une hiérarchie d'objets. Considérez un menu arborescent imbriqué. Vous souhaitez réduire tous les nœuds. Si vous n'implémentez pas visiteur, vous devez écrire du code de traversée de graphe. Ou avec visiteur: rootElement.visit (node) -> node.collapse(). Avec Visitor, chaque nœud implémente le parcours du graphe pour tous ses enfants, vous avez donc terminé.
George Mauer
@GeorgeMauer, le concept de double expédition a éclairci la motivation pour moi: soit la logique dépendante du type est avec le type, soit le monde de la douleur. L'idée de distribuer la logique de traversée me fait encore réfléchir. Est-ce plus efficace? Est-il plus facile à maintenir? Que faire si "plier au niveau N" est ajouté comme exigence?
nik.shornikov
L'efficacité de @ nik.shornikov ne devrait vraiment pas être un problème ici. Dans presque toutes les langues, quelques appels de fonction représentent une surcharge négligeable. Au-delà, c'est de la micro-optimisation. Est-il plus facile à maintenir? En fait ça dépend. Je pense que la plupart du temps c'est le cas, parfois ce n'est pas le cas. Comme pour "plier au niveau N". Passe facile dans un levelsRemainingcompteur comme paramètre. Décrémentez-le avant d'appeler le prochain niveau d'enfants. À l'intérieur de votre visiteur if(levelsRemaining == 0) return.
George Mauer
1
@GeorgeMauer, totalement d'accord sur l'efficacité étant une préoccupation mineure. Mais la maintenabilité, par exemple les remplacements de la signature d'acceptation, sont exactement ce que je pense que la décision devrait se résumer.
nik.shornikov
5

Modèle de visiteur comme la même implémentation souterraine pour 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

mixturez
la source
à mentionner la programmation d'objets Aspect
milesma
5

Description rapide du modèle de visiteur. Les classes qui nécessitent une modification doivent toutes implémenter la méthode «accept». Les clients appellent cette méthode d'acceptation pour effectuer une nouvelle action 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 visiteurs différente pour chaque action spécifique. Une classe de visiteurs contient plusieurs méthodes de visite redéfinies définissant comment réaliser la même action spécifique pour chaque classe de la famille. Ces méthodes de visite reçoivent une instance sur laquelle travailler.

Quand pourriez-vous envisager de l'utiliser

  1. Lorsque vous avez une famille de classes, vous savez que vous devrez ajouter de nombreuses nouvelles actions, mais pour une raison quelconque, vous ne pourrez pas modifier ou recompiler la famille de classes à l'avenir.
  2. Lorsque vous souhaitez ajouter une nouvelle action et que cette nouvelle action soit entièrement définie au sein d'une classe de visiteurs plutôt que répartie sur plusieurs classes.
  3. Quand votre patron vous dit que vous devez produire une gamme de classes qui doivent faire quelque chose tout de suite ! ... mais personne ne sait exactement ce que c'est quelque chose.
Andrew Pate
la source
4

Je n'ai pas compris ce schéma jusqu'à ce que je tombe sur un article d' oncle bob et que je lise des commentaires. Considérez 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 qu'il puisse sembler bon car il confirme la responsabilité unique, il viole le principe ouvert / fermé . Chaque fois que vous avez un nouveau type d'employé, vous devrez l'ajouter avec la vérification de type. Et si vous ne le faites pas, vous ne le saurez jamais au moment de la compilation.

Avec le 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é unique. Et si vous oubliez d'implémenter visit, il ne sera pas compilé:

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 v.Visit(this)même si elle est identique, elle est en fait différente, car elle appelle différentes surcharges de visiteurs.

Accès refusé
la source
Oui, je trouve cela particulièrement utile lorsque vous travaillez avec des structures arborescentes, pas seulement des listes plates (les listes plates seraient un cas spécial d'arbre). Comme vous le constatez, ce n'est pas très compliqué uniquement sur les listes, mais le visiteur peut être un sauveur car la navigation entre les nœuds devient plus complexe
George Mauer
3

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

Imaginez simplement que vous avez 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 l'ensemble du design.

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 de «dormir»? Faut-il vraiment ajouter une nouvelle méthode à la hiérarchie animale pour chaque nouvelle action ou opération que nous voulons 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!

Donc, vérifiez et exécutez 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);
        }
    }
}
Tomás Escamez
la source
marcher, manger ne sont pas des exemples appropriés car ils sont communs à la fois Dogaussi bien que Cat. Vous auriez pu les faire dans la classe de base afin qu'ils soient hérités ou choisissez un exemple approprié.
Abhinav Gauniyal
les sons sont différents mais bon échantillon, mais je ne sais pas si cela a quelque chose à voir avec le modèle de visiteur
DAG
3

Visiteur

Visitor 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 visiteur qui implémente toutes les spécialisations appropriées de la fonction virtuelle

Structure des visiteurs:

entrez la description de l'image ici

Utilisez le modèle Visiteur si:

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

Même si le modèle de visiteur offre la flexibilité 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é, il nécessite des modifications de code dans les classes Visitor & ConcreteVisitor . Il existe une solution de contournement pour résoudre ce problème: utilisez la 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:

  1. Visitable( Element) est une interface et cette méthode d'interface doit être ajoutée à un ensemble de classes.
  2. Visitorest une interface, qui contient des méthodes pour effectuer une opération sur des Visitableéléments.
  3. GameVisitorest une classe qui implémente Visitorinterface ( ConcreteVisitor).
  4. Chaque Visitableélément accepte Visitoret appelle une méthode d' Visitorinterface pertinente .
  5. Vous pouvez traiter les jeux Gameas Elementet concrets comme Chess,Checkers and Ludoas ConcreteElements.

Dans l'exemple ci-dessus, il Chess, Checkers and Ludoy a trois jeux (et Visitableclasses) différents. Un beau jour, j'ai rencontré un scénario pour enregistrer les statistiques de chaque match. Ainsi, sans modifier une classe individuelle pour implémenter la fonctionnalité de statistiques, vous pouvez centraliser cette responsabilité en GameVisitorclasse, ce qui fait l'affaire pour vous sans modifier la structure de chaque jeu.

production:

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 sourcemaking

pour plus de détails

Décorateur

modèle permet d'ajouter un comportement à un objet individuel, soit statiquement soit dynamiquement, sans affecter le comportement d'autres objets de la même classe

Articles Similaires:

Modèle de décorateur pour IO

Quand utiliser le motif décorateur?

Ravindra babu
la source
2

J'aime vraiment 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 principale qui est fixe; il s'agit peut-être d'un autre fournisseur et vous ne pouvez pas modifier cette hiérarchie. Cependant, votre intention est que vous souhaitiez ajouter de nouvelles méthodes polymorphes à cette hiérarchie, ce qui signifie que normalement vous devez 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 vous ne pouvez pas toucher la classe de base. Comment contournez-vous cela?

Le modèle de conception qui résout ce type de problème est appelé «visiteur» (le dernier dans le livre Design Patterns), et il 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 classes distincte de type Visitor pour virtualiser les opérations effectuées sur le type principal. Les objets du type principal «acceptent» simplement le visiteur, puis appellent la fonction membre liée dynamiquement du visiteur.

wojcikstefan
la source
Bien que techniquement le modèle Visitor, ce soit vraiment une double répartition de base de leur exemple. Je dirais que l'utilité n'est pas particulièrement visible de ce seul fait.
George Mauer
1

Bien que j'aie compris le comment et le quand, je n'ai jamais compris le pourquoi. Dans le cas où cela aiderait toute personne ayant une formation dans un langage comme C ++, vous voulez lire cela très attentivement.

Pour les paresseux, nous utilisons le modèle de visiteur parce que "alors que les fonctions virtuelles sont distribuées dynamiquement en C ++, la surcharge des fonctions se fait statiquement" .

Ou, autrement dit, pour vous assurer que CollideWith (ApolloSpacecraft &) est appelé lorsque vous passez une référence SpaceShip qui est en fait 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;
  }
}
Carl
la source
2
L'utilisation de la répartition dynamique dans le modèle de visiteur me rend complètement perplexe. Les utilisations suggérées du modèle décrivent les branchements qui pourraient être effectués au moment de la compilation. Ces cas seraient apparemment mieux lotis avec un modèle de fonction.
Praxeolitic
0

Merci pour l'explication impressionnante de @Federico A. Ramponi , je viens de le faire en version java . J'espère que cela pourrait être utile.

Aussi, comme l'a souligné @Konrad Rudolph , il s'agit en fait d'une double répartition 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 commune pour l' exécuteur d' opération tant que l' interface d' 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 commune nous apportera plus de clarté même si ce n'est 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");
    }
}
Hearen
la source
0

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, puis refactorise. disons donc 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

Imaginez maintenant que j'avais 10 méthodes de paiement, ça devient plutôt moche. Donc, quand vous voyez ce type de modèle se produire, le visiteur intervient pour séparer tout cela et vous finissez par appeler quelque chose comme ça après:

new PaymentCheckoutVistor(paymentType).visit()

Vous pouvez voir comment l'implémenter à partir du nombre d'exemples ici, je vous montre juste un cas d'utilisation.

j2emanue
la source