web-dev-qa-db-fra.com

But du modèle de visiteur avec des exemples

Je suis vraiment confus au sujet du modèle de visiteur et de ses utilisations. Je n'arrive pas vraiment à visualiser les avantages d'utiliser ce modèle ou son objectif. Si quelqu'un pouvait expliquer avec des exemples si possible, ce serait formidable.

80
Victor

Il était une fois...

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

Ensuite, vous réalisez que vous souhaitez pouvoir filtrer la collection de la bibliothèque par d'autres genres. Vous pouvez continuer à ajouter de nouvelles méthodes getter. Ou vous pouvez utiliser des visiteurs.

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 plus visiteurs, au lieu de constamment modifier (et gonfler) la classe qui contient les données.

64
Noel Ang

Vous avez donc probablement lu un bajillion d'explications différentes sur le modèle de visiteur, et vous dites probablement toujours "mais quand l'utiliseriez-vous!"

Traditionnellement, les visiteurs sont habitués à mettre en œuvre des tests de type sans sacrifier la sécurité des types, tant que vos types sont bien définis à l'avance et connus à l'avance. Disons que nous avons 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 souhaite partitionner la liste en trois listes, chacune contenant des oranges, des pommes ou des bananes. Comment feriez-vous? Eh bien, la solution 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 il y a beaucoup de problèmes avec ce code:

  • Pour commencer, c'est moche.
  • Ce n'est pas sûr pour le type, nous n'attraperons pas les erreurs de type avant l'exécution.
  • Ce n'est pas maintenable. 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 pourrions manquer des types.

Le modèle de visiteur résout le problème avec élégance. Commencez par modifier notre classe de fruits de base:

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

Il semble que nous copions du code de collage, mais notez que les classes dérivées appellent toutes des surcharges différentes (les Apple appels Visit(Apple), les Banana appels Visit(Banana), etc).

Implémenter 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 maintenant partitionner 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 de:

  • Être un code relativement propre et facile à lire.
  • Sécurité de type, les erreurs de type sont détectées lors de la compilation.
  • Maintenabilité. Si j'ajoute ou supprime une classe concrète Fruit, je pourrais 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 ils ont tendance à compliquer considérablement les API, et il peut être très difficile de définir un nouveau visiteur pour chaque nouveau type de comportement.

Habituellement, 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 sur cette modification triviale:

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

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

  • Vous disposez d'un ensemble de classes bien défini et connu qui sera visité.

  • 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 souhaitez donner aux consommateurs un moyen d'ajouter de nouvelles fonctionnalités ad hoc aux objets. Ils sont é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 au moment de l'exécution. C'est généralement le cas lorsque vous parcourez une hiérarchie d'objets disparates ayant des propriétés différentes.

N'utilisez pas de visiteurs lorsque:

  • Vous prenez en charge les 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 qui ont les mêmes propriétés ou interface.

  • Vous voulez une API relativement simple.

186
Juliet

Il fournit une autre couche d'abstraction. Réduit la complexité d'un objet et le rend plus modulaire. Sorta aime utiliser une interface (l'implémentation étant complètement indépendante et personne ne se soucie de la façon dont cela se fait juste que cela se fait.)

Maintenant, je ne l'ai jamais utilisé, mais il serait utile pour: implémenter une fonction particulière qui doit être effectuée dans différentes sous-classes, car 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 a une assez bonne explication: http://en.wikipedia.org/wiki/Visitor_pattern Et leur exemple aide à expliquer ce que j'essaie de dire.

J'espère que cela clarifiera un peu les choses.

EDIT ** Désolé, j'ai lié à wikipedia pour votre réponse, mais ils ont vraiment un bon exemple :) N'essayant pas d'être ce gars qui dit allez le trouver vous-même.

6
Kaili

Exemple de modèle de visiteur. Livre, fruits et légumes sont des éléments de base de type "Visible" et il y en a deux "Visiteurs", BillingVisitor & OfferVisitor chacun des visiteurs a son propre but .Algo pour calculer la facture et algo pour calculer les offres sur ces éléments est encapsulé dans le visiteur respectif et les visitables (éléments) 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);            
        }
    }


}
4
Shakti

Je pense que l'objectif principal du modèle de visiteur est qu'il a une grande extensibilité. L'intuition est que vous avez acheté un robot. Le robot a déjà pleinement implémenté des fonctionnalités élémentaires comme aller de l'avant, tourner à gauche, tourner à droite, revenir en arrière, choisir quelque chose, parler une phase,…

Un jour, vous voulez que votre robot puisse aller au bureau de poste pour vous. Avec toutes ces fonctionnalités élémentaires, cela peut le faire, mais vous devez apporter votre robot à la boutique et "mettre à jour" votre robot. Le vendeur de la boutique n'a pas besoin de modifier le robot, mais simplement de mettre 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 apporter votre robot à la boutique et mettre à jour cette fonctionnalité "avancée". Pas besoin de modifier le robot lui-même.

etc …

L'idée du modèle de visiteur est donc, étant donné toutes les fonctionnalités élémentaires implémentées, vous pouvez utiliser le modèle de visiteur pour ajouter un nombre infini de fonctionnalités sophistiquées. Dans l'exemple, le robot correspond à vos classes de travailleurs et la "puce de mise à jour" aux visiteurs. Chaque fois que vous avez besoin d'une nouvelle "mise à jour" des fonctionnalités, vous ne modifiez pas votre classe de travailleur, mais vous ajoutez un visiteur.

3
ctNGUYEN

Il s'agit de séparer la manipulation des données des données réelles. En prime, vous pouvez réutiliser la même classe de visiteurs pour toute la hiérarchie de vos classes, ce qui vous évite à nouveau de transporter les algorithmes de manipulation de données qui ne sont pas pertinents pour vos objets réels.

1
Egor Pavlikhin