41
votes

Confus sur le modèle de conception des visiteurs

donc, je lisais juste sur le modèle du visiteur et j'ai trouvé les allers-retours entre le visiteur et les éléments très étranges!

Fondamentalement, nous appelons l'élément, nous le passons un visiteur, puis l'élément se passe le visiteur. Et puis le visiteur exploite l'élément. Quoi? Pourquoi? C'est si inutile. Je l'appelle la "folie de va-et-vient".

Ainsi, l'intention du visiteur est de découpler les éléments de leurs actions lorsque les mêmes actions doivent être mises en œuvre sur tous les éléments. Cela se fait au cas où nous devons étendre nos éléments avec de nouvelles actions, nous ne voulons pas entrer dans toutes ces classes et modifier le code déjà stable. Nous suivons donc le principe ouvert / fermé ici.

Pourquoi y a-t-il tout ce va-et-vient et que perdons-nous si nous n'avons pas cela?

Par exemple, j'ai fait ce code qui garde ce but à l'esprit mais saute la folie d'interaction du modèle des visiteurs. Fondamentalement, j'ai des animaux qui sautent et mangent. Je voulais découpler ces actions des objets, alors je déplace les actions aux visiteurs. Manger et sauter augmente la santé animale (je sais, c'est un exemple très idiot ...)

public interface AnimalAction { // Abstract Visitor
    public void visit(Dog dog);
    public void visit(Cat cat);
}

public class EatVisitor implements AnimalAction { // ConcreteVisitor
    @Override
    public void visit(Dog dog) {
        // Eating increases the dog health by 100
        dog.increaseHealth(100);
    }

    @Override
    public void visit(Cat cat) {
        // Eating increases the cat health by 50
        cat.increaseHealth(50);
    }
}

public class JumpVisitor implements AnimalAction { // ConcreteVisitor
    public void visit(Dog dog) {
        // Jumping increases the dog health by 10
        dog.increaseHealth(10);
    }

    public void visit(Cat cat) {
        // Jumping increases the cat health by 20
        cat.increaseHealth(20);
    }
}

public class Cat { // ConcreteElement
    private int health;

    public Cat() {
        this.health = 50;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

public class Dog { // ConcreteElement

    private int health;

    public Dog() {
        this.health = 10;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

public class Main {

    public static void main(String[] args) {
        AnimalAction jumpAction = new JumpVisitor();
        AnimalAction eatAction = new EatVisitor();

        Dog dog = new Dog();
        Cat cat = new Cat();

        jumpAction.visit(dog); // NOTE HERE. NOT DOING THE BACK AND FORTH MADNESS.
        eatAction.visit(dog);
        System.out.println(dog.getHealth());

        jumpAction.visit(cat);
        eatAction.visit(cat);
        System.out.println(cat.getHealth());
    }
}


6 commentaires

IMHO Lors du raisonnement avec le visiteur, j'utilise généralement un modèle d'arborescence où vous avez nœud de différents types (par exemple, pensez à un AST où certains nœuds représentent les opérateurs et d'autres opérandes, etc. afin que vous représentez 1++ 1 = 2 as est égal (plus (littéral (1), littéral (1)), littéral (2)) par exemple égal / Plus etc sont génériques car les expressions n'ont pas besoin d'être des littéraux simples et réfléchis maintenant à la façon dont vous allez écrire un visiteur qui évalue cette expression ... puis écrivez un code qui analyse le texte d'une expression et l'évalue , maintenant vous avez 0 cours de béton connus au moment de la compilation


@ Gacy20 Vous avez raison. La seule raison pour laquelle ce code fonctionne, c'est parce que je sais déjà que le chien est un chien et que le chat est un chat. Merci pour cet exemple, cela donne beaucoup plus de sens au visiteur.


Le code fonctionnant avec chien et le code fonctionnant avec cat regardez identique, donc que se passe-t-il si vous essayez de les mettre dans une liste et utiliser une boucle ?


Je pense que Shvets rationalise bien le modèle des visiteurs; Vous l'utilisez lorsque vous avez besoin de étendre les classes existantes pour implémenter un comportement commun - à cause de cela, vous devez utiliser "Double expédition" - sourcemaking.com/design_patterns/visitor Lorsque vous n'avez pas cette contrainte à gérer, il existe généralement des méthodes plus simples pour atteindre le même objectif.


Voir aussi ma réponse ici , cela pourrait vous apporter un aperçu supplémentaire.


Personnellement, je n'obtiens pas non plus le bénéfice de ce modèle. Je ne vois aucun avantage sur une seule méthode qui vérifie les types de béton et délégue en conséquence. Vous devez connaître à l'avance toutes les classes de béton prises en charge (qui doivent implémenter une interface dénuée de sens) à l'avance, ce qui rend le modèle 0 flexible. Et l'indirection de la méthode d'acceptation le rend très intuitif. De plus, c'est une mauvaise pratique d'ajouter du code comportemental aux classes de données - à mon humble avis, même si c'est du code générique. Le seul avantage que vous obtenez est qu'en obtenant toute la mise en œuvre du visiteur, vous obtenez ce genre d'utilisations de vos cours.


4 Réponses :


38
votes

Le code dans l'OP ressemble à une variation bien connue du modèle de conception des visiteurs connue sous le nom de visiteur interne (voir par exemple extensibilité pour les masses. Extensibilité pratique avec des algèbres d'objet Em > Par Bruno C. d. S. Oliveira et William R. Cook). Cette variation, cependant, utilise des génériques et des valeurs de retour (au lieu de void ) pour résoudre certains des problèmes que le modèle du visiteur aborde.

quel problème est cela, et pourquoi la variation OP est-elle probablement insuffisante ?

Le principal problème résolu par le modèle du visiteur est lorsque vous avez des objets hétérogènes que vous devez traiter la même chose. Comme le gang de quatre , (les auteurs de motifs de conception ), vous utilisez le modèle lorsque

"Une structure d'objet contient de nombreuses classes d'objets avec des interfaces différentes, et vous souhaitez effectuer des opérations sur ces objets qui dépendent de leurs classes de béton."

Ce qui manque à cette phrase, c'est que même si vous souhaitez "effectuer des opérations sur ces objets qui dépendent de leurs classes concrètes", vous voulez traiter ces classes concrètes comme si elles avaient un seul type polymorphe.

Un exemple de période

en utilisant le domaine Animal est rarement illustratif (j'y reviendrai plus tard), donc voici un autre exemple plus réaliste. Des exemples sont en C # - J'espère qu'ils vous sont toujours utiles.

Imaginez que vous développez un système de réservation de restaurants en ligne. Dans le cadre de ce système, vous devez être en mesure d'afficher un calendrier aux utilisateurs. Ce calendrier pourrait afficher le nombre de sièges restants disponibles un jour donné, ou énumérer toutes les réservations le jour.

Parfois, vous voulez afficher une seule journée, mais à d'autres moments, vous souhaitez afficher un mois entier en tant qu'objet calendrier unique. Jetez une année entière pour faire bonne mesure. Cela signifie que vous avez trois périodes: année , mois , et jour . Chacun a des interfaces différentes:

public class Animal {
    private int health;

    public Animal(int health) {
        this.health = health;
    }

    public void increaseHealth(int healthIncrement) {
        this.health += healthIncrement;
    }

    public int getHealth() {
        return health;
    }
}

Pour Brevity, ce ne sont que les constructeurs de trois classes distinctes. Beaucoup de gens pourraient simplement modéliser cela en une seule classe avec des champs nullables, mais cela vous oblige alors à faire face aux champs nuls, ou en énumér, ou à d'autres types de méchanceté.

Les trois classes ci-dessus ont une structure différente car elles contiennent différentes données, mais vous souhaitez les traiter comme un seul concept - une période .

Pour ce faire, définissez une interface iperiod :

var previous = period.Accept(new PreviousPeriodVisitor());
var next = period.Accept(new NextPeriodVisitor());

dto.Links = new[]
{
    url.LinkToPeriod(previous, "previous"),
    url.LinkToPeriod(next, "next")
};

et faites implémenter chaque classe l'interface. Voici mois :

private class PreviousPeriodVisitor : IPeriodVisitor<IPeriod>
{
    public IPeriod VisitYear(int year)
    {
        var date = new DateTime(year, 1, 1);
        var previous = date.AddYears(-1);
        return Period.Year(previous.Year);
    }

    public IPeriod VisitMonth(int year, int month)
    {
        var date = new DateTime(year, month, 1);
        var previous = date.AddMonths(-1);
        return Period.Month(previous.Year, previous.Month);
    }

    public IPeriod VisitDay(int year, int month, int day)
    {
        var date = new DateTime(year, month, day);
        var previous = date.AddDays(-1);
        return Period.Day(previous.Year, previous.Month, previous.Day);
    }
}

Cela vous permet de traiter les trois classes hétérogènes comme un seul type et de définir les opérations sur ce type unique sans avoir à changer L'interface.

Ici, par exemple, est une implémentation qui calcule la période précédente :

internal sealed class Month : IPeriod
{
    private readonly int year;
    private readonly int month;

    public Month(int year, int month)
    {
        this.year = year;
        this.month = month;
    }

    public T Accept<T>(IPeriodVisitor<T> visitor)
    {
        return visitor.VisitMonth(year, month);
    }
}

si vous avez un jour , vous obtiendrez le jour précédent, mais si vous avez un mois , vous obtiendrez le mois précédent Code>, etc. 08/24 / Adding-Rest-links-as-a-a-cross-cutting-concern "rel =" noreferrer "> cet article , mais voici les quelques lignes de code où elles sont utilisées: p >

internal interface IPeriod
{
    T Accept<T>(IPeriodVisitor<T> visitor);
}

Ici, Période est un objet iperiod , mais le code ne sait pas s'il s'agit d'un jour Code>, et mois , ou un an .

Pour être clair, l'exemple ci-dessus utilise la visi interne Tor Variation, qui est isomorphique à un codage d'église A >.

Animaux

L'utilisation d'animaux pour comprendre la programmation orientée objet est rarement éclairante. Je pense que les écoles devraient cesser d'utiliser cet exemple, car il est plus susceptible de confondre que de l'aide.

L'exemple de code op ne souffre pas du problème que le modèle des visiteurs résout, donc dans ce contexte, ce n'est pas surprenant si vous ne voyez pas l'avantage.

Les classes cat et dog sont pas hétérogènes. Ils ont les mêmes champ de classe et le même comportement. La seule différence est dans le constructeur. Vous pouvez refacter ces deux classes à une seule classe Animal :

public Year(int year)

public Month(int year, int month)

public Day(int year, int month, int day)
.

Puisque vous avez maintenant une seule classe, aucun visiteur n'est justifié.


10 commentaires

Je pense qu'il serait instructif d'ajouter un exemple de code client, c'est-à-dire en itérant dans une collection de période , pour indiquer clairement pourquoi vous ne pouvez pas simplement avoir par exemple daycommand comme suggéré par OP.


@daniu Comme je l'ai écrit, il y a un exemple avec un contexte complet dans l'article lié.


@daniu mais vous avez raison, j'ai donc modifié la réponse pour inclure un peu de code de cet article. Cela améliore la réponse. Merci pour la suggestion.


Qu'est-ce qu'un gof , et y a-t-il une référence pour savoir où il indique les choses


@Burnsba gof = le gang de quatre , les auteurs de motifs de conception . Il est également souvent utilisé comme métonyme pour le livre lui-même.


Merci! J'ai édité dans la clarification pour quelqu'un d'autre


J'ai une question. Ce va-et-vient est nécessaire car Java n'a pas de double-diss. Si Java avait une double dépêche, puis-je utiliser le même code que j'ai (pas d'avant en arrière) et traiter le chien et le chat de mon exemple comme un animal au lieu de leurs classes en béton?


@ AFP_555 C'est spéculatif: Si votre langue avait une fonctionnalité de langue qu'elle n'a pas, une qui résout un problème résolu par un modèle de conception, auriez-vous alors besoin du modèle? Tautologiquement: non. Il s'agit d'une critique typique (et entièrement raisonnable) soulevée (en particulier par la communauté FP) contre DP: qu'elle résout des problèmes que les «meilleures» langues n'ont pas.


@Markseemann Vous parlez ici d'objets hétérogènes et cela m'a vraiment fait réfléchir à un problème conceptuel que j'ai avec le modèle de stratégie. Je ne sais pas si demander que cela soit "illégal", mais pourriez-vous également vérifier cette question? stackoverflow.com/questions/6783661/…


@ AFP_555 qui n'est pas du tout illégal 😀 Je pense, cependant, que vous pouvez mal comprendre mon utilisation du terme hétérogène . Cela peut très bien être de ma faute. Ce que j'entends par hétérogénéité, ce sont des types d'objets qui ne partagent aucune similitude perceptible. Vos exemples de liste dans cette question ne sont pas comme ça. À tout le moins, je pense qu'ils peuvent tous être itérables (mais, surprise, surprise! Je ne suis pas sûr, car je ne suis en fait pas un programmeur Java).



8
votes

avec va-et-vient, voulez-vous dire cela?

public class Main {

    public static void main(String[] args) {
        AnimalAction jumpAction = new JumpVisitor();
        AnimalAction eatAction = new EatVisitor();


        Animal animal = aFunctionThatCouldReturnAnyAnimal();
        animal.accept(jumpAction);
        animal.accept(eatAction);
    }

    private static Animal aFunctionThatCouldReturnAnyAnimal() {
        return new Dog();
    }
}

Le but de ce code est que vous pouvez expédier sur le type sans connaître le type de béton, comme ici: p>

public class Dog implements Animal {

    //...

    @Override
    public void accept(AnimalAction action) {
        action.visit(this);
    }
}

Donc ce que vous obtenez est: vous pouvez appeler la bonne action individuelle sur un animal avec seulement ce que c'est un animal.

C'est particulièrement utile si Vous parcourez un motif composite, où les nœuds de feuilles sont animaux et les nœuds intérieurs sont des agrégations (par exemple A list ) des Animals . Une list ne peut pas être traitée avec votre conception.


7 commentaires

Le principe montré ici qui manque à l'OP est, Programme à une interface, pas une implémentation. L'OP est en train de programmer directement à dog et cat Il n'y a donc pas de polymorphisme, et donc les modèles de conception OO ne sont pas très utiles.


Yeha, c'est vrai, je ne peux pas traiter le chien de chien comme un chien animal, le code ne compilera pas de cette façon. C'est un truc Java très particulier. Maintenant, je comprends que le va-et-vient est complètement nécessaire, même s'il est extrêmement gênant. Merci.


Oui pour lister . Je regarde juste la question du PO, il était évident qu'ils n'avaient jamais vu la norme "pour (animal a: myanimals) {a.dothing ();}" Exemple sur une liste de chats, de chiens et un furet de classe préalable pour comprendre dynamique expédition.


@ jaco0646 attendez ... Je lis juste le livre GoF et ils disent ceci: "Un itérateur ne peut pas travailler sur des structures d'objets avec différents types d'éléments. (...) Le visiteur n'a pas cette restriction. Il peut visiter des objets qui n'ont pas de classe de parent commun. Vous pouvez ajouter n'importe quel type d'objet à une interface de visiteur. MyType et YourType n'ont pas à être lié à l'héritage du tout "...... donc ... nous ne Il a vraiment besoin de la classe des parents animaux, c'est alors le point du visiteur, pouvoir traverser des objets non liés qui n'appartiennent pas à une même hiérarchie. Alors pourquoi devrais-je avoir la classe d'animaux?


La classe parent n'est pas nécessaire, mais l'interface doit fournir un accepter (visiteur du visiteur) -method, et chacun des objets traversés doit l'implémenter.


@ AFP_555, dans la phrase, " Visitez des objets qui n'ont pas de classe de parent commun ", le mot clé est commun (pas parent). Plutôt que de relier tous les éléments via Animal , un autre exemple pourrait être canine dog et Feline Cat sans relation entre les deux classes parentales. Au sein de la POO, vous programmez toujours à une interface. Le visiteur dit que vous n'avez pas toujours à programmer à la même interface . Si vous ne programmez à aucune interface, alors vous ne faites pas de POO, et deuxièmement, le modèle de visiteur est inutile, comme le montre l'OP.


@Corona Yeha, tu as raison. Ils ont besoin d'une interface commune avec la méthode d'acceptation au moins. Ainsi, les objets peuvent en fait être des choses totalement différentes et avoir simplement cette interface "visitable" et elles pourraient être traversées par un visiteur. Agréable. Je pense que je comprends maintenant, le visiteur nous donne un moyen de traverser des objets complètement indépendants en leur donnant une interface "visitable".



23
votes

L'arrière-et-north du visiteur consiste à imiter une sorte de Double répartition Mécanisme , où vous sélectionnez une implémentation de méthode basée sur le type d'exécution des objets deux .

Ceci est utile si le type de votre animal et visiteur sont abstraits (ou polymorphes). Dans ce cas, vous avez un potentiel de 2 x 2 = 4 implémentations de méthode à choisir, en fonction de a) quel type d'action (visitez) vous souhaitez faire, et b) quel type d'animal vous souhaitez que cette action s'applique.

 Entrez la description de l'image ici Entrez la description de l'image ici

Si vous utilisez des types de béton et non polymorphes, alors une partie de ce va-et-vient est en effet superflue.


3 commentaires

Cette. Double Dispatch est le concept clé ici, et son importance pour le modèle des visiteurs serait soulignée en modifiant le nom de la méthode conventionnelle " accepter " en quelque chose de plus comme revelyourclasstothisGuy .


@JohnBollinger Votre commentaire m'a fait comprendre le point. J'ai essayé d'utiliser une interface animale au lieu d'animaux en béton (chien, chat) lors de la création des objets animaux, et bien sûr, le code ne se compilait pas. Donc ... le but de la méthode d'acceptation est que l'animal en béton dise au visiteur "Hé, je suis un chat parce que j'appelle la méthode VisitCat sur vous. Maintenant, vous pouvez me traiter comme un chat.". Je ne comprends toujours pas le concept de double-dispatch


@ AFP_555, il pourrait être utile de jeter un œil à Qu'est-ce que la méthode expédiée? En Java, c'est une activité avec à la fois compile -Time et les pièces d'exécution. Double Dispatch signifie simplement que vous enchaînez deux expédiés de méthode pour réaliser ce que vous recherchez au lieu d'un seul. C'est exactement la "folie de va-et-vient" que vous critiquez dans la question, et c'est le mécanisme qui fait fonctionner le modèle des visiteurs.



3
votes

Le modèle du visiteur résout le problème de l'application d'une fonction aux éléments d'une structure graphique.

Plus précisément, il résout le problème de la visite de chaque nœud N dans une structure de graphique, dans le contexte d'un objet V, et Pour chaque N, invoquant une fonction générique F (V, N). La mise en œuvre de la méthode de F est choisie en fonction du type de V et de n.

Dans les langages de programmation qui ont plusieurs expéditions, le modèle de visiteur disparaît presque. Il se réduit à une promenade de l'objet graphique (par exemple, une descente de l'arbre récursive), ce qui fait un simple appel f (v, n) pour chaque nœud n. Terminé!

Par exemple dans LISP commun. Pour Brivity, ne définissons même pas les classes: entiers et chaînes sont des classes, alors utilisons-nous.

Tout d'abord, écrivons les quatre méthodes des Fonction générique, pour chaque combinaison d'un entier ou d'une chaîne visitant un entier ou une chaîne. Les méthodes produisent simplement la sortie. Nous ne définissons pas la fonction générique avec defGeneric ; Lisp déduit ceci et le fait implicitement pour nous:

class VisitorBase {
  virtual void Visit(VisitedBase);
}

class IntegerVisitor;
class StringVisitor;

class VisitedBase {
  virtual void Accept(IntegerVisitor);
  virtual void Accept(StringVisitor);
}

class IntegerVisitor : inherit VisitorBase {
  Integer value;
  void Visit(VisitedBase);
}

class StringVisitor: inherit VisitorBase {
  String value;
  void Visit(VisitedBase);
}

class IntegerNode : inherit VisitedBase {
  Integer value;
  void Accept(IntegerVisitor);
  void Accept(StringVisitor);
}

class StringNode : inherit VisitedBase {
  String value;
  void Accept(IntegerVisitor);
  void Accept(StringVisitor);
}

Utilisons maintenant une liste comme notre structure pour être itérée par le visiteur, et écrivons une fonction wrapper pour cela: p>

IntegerNode::visit(StringVisitor v)
{
   print(`integer @{self.value} visits string @{v.value}`)
}

IntegerNode::visit(IntegerVisitor v)
{
   print(`integer @{self.value} visits string @{v.value}`)
}

Tester de manière interactive:

StringVisitor::visit(Visited obj)
{
  obj.Accept(self)
}

IntegerVisitor::visit(Visited obj)
{
  obj.Accept(self)
}

ok, donc c'est le motif du visiteur: une traversée de chaque élément d'une structure, avec un Double expédition d'une méthode avec un objet de contexte en visite.

La "folie de va-et-vient" a à voir avec le code de la plaque de chaudière de simulation de double répartition dans un système OOP qui n'a que des expéditions uniques et dans Quelles méthodes appartiennent à des classes plutôt que d'être des spécialisations des fonctions génériques.

Parce que dans le système OOP unique à dissal, les méthodes sont encapsulées dans les classes, le premier problème que nous avons est où le Visite / Code> Méthode en direct? Est-ce sur le visiteur ou le nœud?

La réponse s'avère qu'il doit être les deux. Nous devrons expédier quelque chose sur les deux types.

Vient ensuite le problème que dans la pratique OOP, nous avons besoin de bons noms. Nous ne pouvons pas avoir une méthode visiter sur l'objet Visitor et l'objet Visité . Lorsqu'un objet visité est visité, le verbe "Visite" n'est pas utilisé pour décrire ce que fait cet objet. Il "accepte" un visiteur. Nous devons donc appeler cette moitié de l'action accepter .

Nous créons une structure par laquelle chaque nœud à visiter a une méthode accepter . Cette méthode est envoyée sur le type de nœud et prend un argument Visitor . En fait, le nœud a plusieurs méthodes accepter , qui sont statiquement spécialisées sur différents types de visiteurs: IntegerVisitor , StringVisitor , Foovisitor . Notez que nous ne pouvons pas simplement utiliser string , même si nous avons une telle classe dans la langue, car elle n'implémente pas l'interface Visitor avec la Visitez Méthode.

Alors ce qui se passe, c'est que nous marchons sur la structure, obtenez chaque nœud n, puis appelons v.visit (n) pour que le visiteur le visite pour le visiter . Nous ne connaissons pas le type exact de v ; C'est une référence de base. Chaque implémentation du visiteur doit implémenter visiter comme morceau de plaque de chaudière (en utilisant un pseudo-langage qui n'est pas Java ou C ++):

(visitor-pattern 42 '(1 "abc"))
integer 42 visits integer 1!
integer 42 visits string "abc"!

(visitor-pattern "foo" '(1 "abc"))
string "foo" visits integer 1!
string "foo" visits string "abc"!

la raison Est-ce que self doit être typé statique Différents types choisis au moment de la compilation:

(defun visitor-pattern (visitor list)
  ;; map over the list, doing the visitation
  (mapc (lambda (item) (visit visitor item)) list)
  ;; return  nothing
  (values))

Toutes ces classes et méthodes doivent être déclarées quelque part:

(defmethod visit ((visitor integer) (node string))
  (format t "integer ~s visits string ~s!~%" visitor node))

(defmethod visit ((visitor integer) (node integer))
  (format t "integer ~s visits integer ~s!~%" visitor node))

(defmethod visit ((visitor string) (node string))
  (format t "string ~s visits string ~s!~%" visitor node))

(defmethod visit ((visitor string) (node integer))
  (format t "string ~s visits integer ~s!~%" visitor node))

donc c'est donc Le modèle de visiteur de surcharge à disqueurs unique avec un seul tas: il y a un tas de plaque de chaudière, plus la limitation que l'une des classes, visiteur ou visitée, doit connaître les types statiques de tous les autres pris en charge, Il peut donc envoyer des expéditions statiquement dessus, et pour chaque type statique, il y aura également une méthode factice.


0 commentaires