96 votes

L'objectif du modèle de visiteur avec des exemples

Je ne comprends pas très bien le modèle du visiteur et son utilisation. Je n'arrive pas à visualiser les avantages de l'utilisation de ce modèle ou son objectif. Si quelqu'un pouvait m'expliquer avec des exemples si possible, ce serait génial.

6 votes

Avez-vous consulté Wikipédia sur le sujet ? fr.wikipedia.org/wiki/Visitor_pattern Quelles sont les parties qui ne sont pas claires ? Ou cherchez-vous des exemples concrets ?

216voto

Juliet Points 40758

Vous avez probablement lu un milliard d'explications différentes sur le schéma de visite, et vous vous dites probablement encore "mais quand l'utiliseriez-vous ?".

Traditionnellement, les visiteurs sont utilisés pour mettre en œuvre le test de type sans sacrifier la sécurité de type, à condition que les types soient bien définis et connus à l'avance. Supposons que nous ayons quelques classes comme suit :

abstract class Fruit { }
class Orange : Fruit { }
class Apple : Fruit { }
class Banana : Fruit { }

Et disons que nous créons un Fruit[] :

var fruits = new Fruit[]
    { new Orange(), new Apple(), new Banana(),
      new Banana(), new Banana(), new Orange() };

Je veux diviser la liste en trois listes, chacune contenant des oranges, des pommes ou des bananes. Comment faire ? Eh bien, la liste facile serait un test de type :

List<Orange> oranges = new List<Orange>();
List<Apple> apples = new List<Apple>();
List<Banana> bananas = new List<Banana>();
foreach (Fruit fruit in fruits)
{
    if (fruit is Orange)
        oranges.Add((Orange)fruit);
    else if (fruit is Apple)
        apples.Add((Apple)fruit);
    else if (fruit is Banana)
        bananas.Add((Banana)fruit);
}

Cela fonctionne, mais ce code pose de nombreux problèmes :

  • Tout d'abord, il est laid.
  • Il n'est pas sûr du point de vue du type, nous ne détectons pas les erreurs de type jusqu'à l'exécution.
  • Il n'est pas possible de le maintenir. Si nous ajoutons une nouvelle instance dérivée de Fruit, nous devons effectuer une recherche globale pour chaque endroit qui effectue un test de type de fruit, sinon nous risquons de manquer des types.

Le modèle du visiteur résout le problème de manière élégante. Commencez par modifier notre classe de base Fruit :

interface IFruitVisitor
{
    void Visit(Orange fruit);
    void Visit(Apple fruit);
    void Visit(Banana fruit);
}

abstract class Fruit { public abstract void Accept(IFruitVisitor visitor); }
class Orange : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Apple : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Banana : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }

On a l'impression de copier-coller du code, mais il faut noter que les classes dérivées appellent toutes des surcharges différentes (la fonction Apple appels Visit(Apple) , le Banana appels Visit(Banana) et ainsi de suite).

Mettre en œuvre le visiteur :

class FruitPartitioner : IFruitVisitor
{
    public List<Orange> Oranges { get; private set; }
    public List<Apple> Apples { get; private set; }
    public List<Banana> Bananas { get; private set; }

    public FruitPartitioner()
    {
        Oranges = new List<Orange>();
        Apples = new List<Apple>();
        Bananas = new List<Banana>();
    }

    public void Visit(Orange fruit) { Oranges.Add(fruit); }
    public void Visit(Apple fruit) { Apples.Add(fruit); }
    public void Visit(Banana fruit) { Bananas.Add(fruit); }
}

Vous pouvez désormais répartir vos fruits sans test de type :

FruitPartitioner partitioner = new FruitPartitioner();
foreach (Fruit fruit in fruits)
{
    fruit.Accept(partitioner);
}
Console.WriteLine("Oranges.Count: {0}", partitioner.Oranges.Count);
Console.WriteLine("Apples.Count: {0}", partitioner.Apples.Count);
Console.WriteLine("Bananas.Count: {0}", partitioner.Bananas.Count);

Cela présente les avantages suivants

  • Un code relativement propre et facile à lire.
  • Sécurité de type, les erreurs de type sont détectées au moment de la compilation.
  • La maintenabilité. Si j'ajoute ou supprime une classe Fruit concrète, je peux modifier mon interface IFruitVisitor pour gérer le type en conséquence, et le compilateur trouvera immédiatement tous les endroits où nous implémentons l'interface afin que nous puissions apporter les modifications appropriées.

Cela dit, les visiteurs sont généralement exagérés et ont tendance à compliquer considérablement les API, et il peut être très fastidieux de définir un nouveau visiteur pour chaque nouveau type de comportement.

En général, des modèles plus simples comme l'héritage devraient être utilisés à la place des visiteurs. Par exemple, en principe, je pourrais écrire une classe comme :

class FruitPricer : IFruitVisitor
{
    public double Price { get; private set; }
    public void Visit(Orange fruit) { Price = 0.69; }
    public void Visit(Apple fruit) { Price = 0.89; }
    public void Visit(Banana fruit) { Price = 1.11; }
}

Cela fonctionne, mais quel est l'avantage de cette modification triviale ?

abstract class Fruit
{
    public abstract void Accept(IFruitVisitor visitor);
    public abstract double Price { get; }
}

Vous devez donc utiliser les visiteurs lorsque les conditions suivantes sont réunies :

  • Vous disposez d'un ensemble de classes bien définies et connues qui seront visitées.

  • Les opérations sur ces classes ne sont pas bien définies ou connues à l'avance. Par exemple, si quelqu'un consomme votre API et que vous voulez donner aux consommateurs un moyen d'ajouter de nouvelles fonctionnalités ad hoc aux objets. Ils constituent également un moyen pratique d'étendre les classes scellées avec des fonctionnalités ad hoc.

  • Vous effectuez des opérations sur une classe d'objets et souhaitez éviter les tests de type à l'exécution. C'est généralement le cas lorsque vous traversez une hiérarchie d'objets disparates ayant des propriétés différentes.

N'utilisez pas les visiteurs lorsque :

  • Vous prenez en charge des opérations sur une classe d'objets dont les types dérivés ne sont pas connus à l'avance.

  • Les opérations sur les objets sont bien définies à l'avance, en particulier si elles peuvent être héritées d'une classe de base ou définies dans une interface.

  • Il est plus facile pour les clients d'ajouter de nouvelles fonctionnalités aux classes en utilisant l'héritage.

  • Vous parcourez une hiérarchie d'objets ayant les mêmes propriétés ou la même interface.

  • Vous souhaitez une API relativement simple.

6 votes

Sauf que le visiteur n'est pas fermé à la modification (principe ouvert/fermé). Tout nouveau fruit doit faire l'objet d'une nouvelle méthode. Il est préférable que le visiteur n'ait qu'une méthode Visit(Fruit fruit) et une mise en œuvre concrète qui associe chaque fruit à une méthode spécifique. (Pour que d'autres classes de visiteurs puissent étendre la base concrète).

9 votes

@jgauffin l'une des conséquences formelles du pattern Visitor est que chaque fois que vous ajoutez un nouvel objet qui peut être visité, une nouvelle méthode est créée, donc une violation de Open/Closed est impliquée dans les conséquences de ce pattern. Avoir une méthode Visit() qui fait la résolution de type offre des tonnes d'inconvénients, y compris le fait de ne pas permettre aux IDE, compilateurs, RTE, etc. de valider les implémentations.

0 votes

Pourriez-vous refrapper le code en java, s'il vous plaît. En outre, la question comporte une étiquette java, de sorte que "nous" préférerions l'exemple en java. Je ne comprends pas le code, il ressemble à du pseudo-code.

79voto

Noel Ang Points 1991

Il était une fois...

class MusicLibrary {
    private Set<Music> collection ...
    public Set<Music> getPopMusic() { ... }
    public Set<Music> getRockMusic() { ... }
    public Set<Music> getElectronicaMusic() { ... }
}

Puis vous vous rendez compte que vous aimeriez pouvoir filtrer la collection de la bibliothèque par d'autres genres. Vous pouvez continuer à ajouter de nouvelles méthodes d'obtention. Ou vous pouvez utiliser Visitors.

interface Visitor<T> {
    visit(Set<T> items);
}

interface MusicVisitor extends Visitor<Music>;

class MusicLibrary {
    private Set<Music> collection ...
    public void accept(MusicVisitor visitor) {
       visitor.visit( this.collection );
    }
}

class RockMusicVisitor implements MusicVisitor {
    private final Set<Music> picks = ...
    public visit(Set<Music> items) { ... }
    public Set<Music> getRockMusic() { return this.picks; }
}
class AmbientMusicVisitor implements MusicVisitor {
    private final Set<Music> picks = ...
    public visit(Set<Music> items) { ... }
    public Set<Music> getAmbientMusic() { return this.picks; }
}

Vous séparez les données de l'algorithme. Vous déchargez l'algorithme sur les implémentations des visiteurs. Vous ajoutez des fonctionnalités en créant des más au lieu de modifier constamment (et de gonfler) la classe qui contient les données.

43 votes

Désolé, ce n'est pas vraiment un bon exemple pour le modèle Visiteur, c'est trop simpliste. L'un des principaux mécanismes du modèle du visiteur, la sélection de la fonctionnalité par le biais du type (double dispatch) de l'élément visité, n'est pas illustré -1

0 votes

Après avoir suivi un cours sur les compilateurs, je me suis également rendu compte de l'inutilité de cet exemple.

3 votes

@HaraldScheirich Les visiteurs peuvent choisir ou non de sélectionner les fonctionnalités par type. J'ai trouvé les visiteurs extrêmement utiles même sans cela.

5voto

Kaili Points 724

Il fournit une autre couche d'abstraction. Il réduit la complexité d'un objet et le rend plus modulaire. C'est un peu comme l'utilisation d'une interface (l'implémentation est complètement indépendante et personne ne se soucie de la façon dont elle est réalisée, mais seulement du fait qu'elle est réalisée).

Je ne l'ai jamais utilisé, mais il serait utile : Implémenter une fonction particulière qui doit être réalisée dans différentes sous-classes, puisque chacune des sous-classes doit l'implémenter de différentes manières, une autre classe implémenterait toutes les fonctions. Un peu comme un module mais seulement pour une collection de classes. Wikipedia donne une assez bonne explication : http://en.wikipedia.org/wiki/Visitor_pattern Et leur exemple permet d'expliquer ce que j'essaie de dire.

J'espère que cela vous aidera à y voir plus clair.

EDIT**Sorry I linked to wikipedia for your answer but they really do have a decent example :) Je n'essaie pas d'être le gars qui dit d'aller trouver la réponse par soi-même.

0 votes

Cette explication ressemble à un modèle de stratégie

4voto

Markus Zeller Points 111

Exemple de modèle de visiteur. Le livre, les fruits et les légumes sont les éléments de base de la typographie. "Visible et il y a deux "Visiteurs , Visiteur de facturation et Visiteur d'offre L'algorithme de calcul de la facture et l'algorithme de calcul des offres sur ces éléments sont encapsulés dans le visiteur respectif et les éléments visitables restent les mêmes.

import java.util.ArrayList;
import java.util.List;

public class VisitorPattern {

    public static void main(String[] args) {
        List<Visitable> visitableElements = new ArrayList<Visitable>();
        visitableElements.add(new Book("I123",10,2.0));
        visitableElements.add(new Fruit(5,7.0));
        visitableElements.add(new Vegetable(25,8.0));
        BillingVisitor billingVisitor = new BillingVisitor();
        for(Visitable visitableElement : visitableElements){
            visitableElement.accept(billingVisitor);
        }

        OfferVisitor offerVisitor = new OfferVisitor();
        for(Visitable visitableElement : visitableElements){
            visitableElement.accept(offerVisitor);
        }
        System.out.println("Total bill " + billingVisitor.totalPrice);
        System.out.println("Offer  " + offerVisitor.offer);

    }

    interface Visitor {
        void visit(Book book);
        void visit(Vegetable vegetable);
        void visit(Fruit fruit);
    }

    //Element
    interface Visitable{
        public void accept(Visitor visitor);
    }

    static class OfferVisitor implements Visitor{
        StringBuilder offer = new StringBuilder();

        @Override
        public void visit(Book book) {
            offer.append("Book " +  book.isbn +  " discount 10 %" + " \n");
        }

        @Override
        public void visit(Vegetable vegetable) {
            offer.append("Vegetable  No discount \n");
        }

        @Override
        public void visit(Fruit fruit) {
            offer.append("Fruits  No discount \n");
        }

    }

    static class BillingVisitor implements Visitor{
        double totalPrice = 0.0;

        @Override
        public void visit(Book book) {
            totalPrice += (book.quantity * book.price);
        }

        @Override
        public void visit(Vegetable vegetable) {
            totalPrice += (vegetable.weight * vegetable.price);
        }

        @Override
        public void visit(Fruit fruit) {
            totalPrice += (fruit.quantity * fruit.price);
        }

    }

    static class Book implements Visitable{
        private String isbn;
        private double quantity;
        private double price;

        public Book(String isbn, double quantity, double price) {
            this.isbn = isbn;
            this.quantity = quantity;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    }

    static class Fruit implements Visitable{
        private double quantity;
        private double price;

        public Fruit(double quantity, double price) {
            this.quantity = quantity;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    }

    static class Vegetable implements Visitable{
        private double weight;
        private double price;

        public Vegetable(double weight, double price) {
            this.weight = weight;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);            
        }
    }

}

4voto

ctNGUYEN Points 66

Je pense que l'objectif principal du modèle de visiteur est sa grande extensibilité. L'intuition est que vous avez acheté un robot. Le robot possède déjà des fonctionnalités élémentaires entièrement implémentées telles que avancer, tourner à gauche, tourner à droite, revenir en arrière, prendre quelque chose, parler une phase,

Un jour, vous voudrez que votre robot puisse aller à la poste pour vous. Avec toutes ces fonctionnalités élémentaires, il peut le faire, mais vous devez amener votre robot au magasin et le "mettre à jour". Le vendeur du magasin n'a pas besoin de modifier le robot, mais simplement de placer une nouvelle puce de mise à jour sur votre robot et il peut faire ce que vous voulez.

Un autre jour, vous voulez que votre robot aille au supermarché. Même processus, vous devez amener votre robot au magasin et mettre à jour cette fonctionnalité "avancée". Il n'est pas nécessaire de modifier le robot lui-même.

et ainsi de suite

L'idée de Visitor pattern est donc que, compte tenu de toutes les fonctionnalités élémentaires mises en œuvre, vous pouvez utiliser Visitor pattern pour ajouter un nombre infini de fonctionnalités sophistiquées. Dans l'exemple, le robot est votre classe de travailleur, et la "puce de mise à jour" est le visiteur. Chaque fois que vous avez besoin d'une nouvelle "mise à jour" de fonctionnalité, vous ne modifiez pas votre classe de travailleur, mais vous ajoutez un visiteur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X