Le livre du Dr Yan Hong « JAVA and Patterns » commence par une description du modèle de visiteur :
Le modèle de visiteur est le modèle de comportement des objets. Le but du modèle de visiteur est d'encapsuler certaines opérations appliquées à certains éléments de la structure de données. Une fois que ces opérations doivent être modifiées, la structure de données acceptant cette opération peut rester inchangée.
Le concept d'expédition
Le type lorsqu'une variable est déclarée est appelé le type statique de la variable (Type statique), et certaines personnes appellent le type statique le type apparent (Type apparent) et le type réel de l'objet référencé par la variable est également appelé le type ; type réel de la variable (Actual Type). Par exemple:
Copiez le code comme suit :
Liste liste = null ;
liste = new ArrayList();
Une liste de variables est déclarée, son type statique (également appelé type évident) est List et son type réel est ArrayList.
La sélection des méthodes en fonction du type d'objet est la répartition. La répartition est divisée en deux types, à savoir la répartition statique et la répartition dynamique.
La répartition statique se produit au moment de la compilation et la répartition se produit en fonction des informations de type statique. La répartition statique ne nous est pas étrangère. La surcharge de méthodes est une répartition statique.
La répartition dynamique se produit pendant l'exécution et la répartition dynamique remplace dynamiquement une méthode.
répartition statique
Java prend en charge la répartition statique via la surcharge de méthodes. En utilisant l'histoire de Mozi montant à cheval comme exemple, Mozi pourrait monter un cheval blanc ou un cheval noir. Le diagramme de classe de Mozi et du cheval blanc, du cheval noir et du cheval est le suivant :
Dans ce système, Mozi est représenté par la classe Mozi. Le code est le suivant :
classe publique Mozi {
balade dans le vide public (Cheval h) {
System.out.println("équitation");
}
balade dans le vide public (WhiteHorse wh) {
System.out.println("monter un cheval blanc");
}
balade dans le vide public (BlackHorse bh) {
System.out.println("Montez sur le cheval noir");
}
public static void main (String[] arguments) {
Cheval wh = new WhiteHorse();
Cheval bh = new BlackHorse();
Mozi mozi = nouveau Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
Évidemment, la méthode ride() de la classe Mozi est surchargée à partir de trois méthodes. Ces trois méthodes acceptent respectivement les paramètres de Horse, WhiteHorse, BlackHorse et d’autres types.
Alors, quels résultats le programme imprimera-t-il lors de son exécution ? Le résultat est que le programme imprime les deux mêmes lignes de "cheval". En d’autres termes, Mozi a découvert qu’il ne montait que des chevaux.
Pourquoi? Les deux appels à la méthode ride() passent des paramètres différents, à savoir wh et bh. Bien qu’ils aient des types réels différents, leurs types statiques sont tous les mêmes, qui sont des types Chevaux.
La répartition des méthodes surchargées est basée sur des types statiques et ce processus de répartition est terminé au moment de la compilation.
répartition dynamique
Java prend en charge la répartition dynamique via le remplacement de méthode. En prenant comme exemple l’histoire d’un cheval mangeant de l’herbe, le code est le suivant :
Copiez le code comme suit :
Cheval de classe publique {
public void manger(){
System.out.println("Cheval mangeant de l'herbe");
}
}
Copiez le code comme suit :
la classe publique BlackHorse étend Horse {
@Outrepasser
public void manger() {
System.out.println("Cheval noir mangeant de l'herbe");
}
}
Copiez le code comme suit :
Client de classe publique {
public static void main (String[] arguments) {
Cheval h = new BlackHorse();
chaleur();
}
}
Le type statique de la variable h est Horse et le type réel est BlackHorse. Si la méthode eat() de la dernière ligne ci-dessus appelle la méthode eat() de la classe BlackHorse, alors ce qui est imprimé ci-dessus est "Black Horse Eating Grass" au contraire, si la méthode eat() ci-dessus appelle la méthode eat(); ) méthode de la classe Horse , alors ce qui est imprimé est "le cheval mange de l'herbe".
Par conséquent, le cœur du problème est que le compilateur Java ne sait pas toujours quel code sera exécuté lors de la compilation, car le compilateur ne connaît que le type statique de l'objet, mais ne connaît pas le type réel de l'objet ni la méthode ; l'appel est basé sur les types réels de l'objet, et non sur les types statiques. De cette façon, la méthode eat() dans la dernière ligne ci-dessus appelle la méthode eat() de la classe BlackHorse et affiche « un cheval noir mangeant de l'herbe ».
type d'envoi
L'objet auquel appartient une méthode est appelé le récepteur de la méthode. Le récepteur de la méthode et les paramètres de la méthode sont collectivement appelés le volume de la méthode. Par exemple, le code de copie de la classe Test dans l'exemple ci-dessous est le suivant :
Test de classe publique {
public void print(String str){
System.out.println(str);
}
}
Dans la classe ci-dessus, la méthode print() appartient à l'objet Test, son récepteur est donc également l'objet Test. La méthode print() a un paramètre appelé str et son type est String.
En fonction du nombre de types de quantités sur lesquelles l'expédition peut être basée, les langages orientés objet peuvent être divisés en langages à expédition unique (Uni-Dispatch) et en langages à expédition multiple (Multi-Dispatch). Les langages à répartition unique sélectionnent des méthodes en fonction du type d'une instance, tandis que les langages à répartition multiple sélectionnent des méthodes en fonction du type de plusieurs instances.
C++ et Java sont tous deux des langages à répartition unique, et des exemples de langages à répartition multiple incluent CLOS et Cecil. Selon cette distinction, Java est un langage dynamique à répartition unique, car la répartition dynamique de ce langage ne prend en compte que le type du récepteur de méthode, et c'est un langage statique à répartition multiple, car ce langage distribue des méthodes surchargées. le type du récepteur de la méthode et les types de tous les paramètres de la méthode sont pris en compte.
Dans un langage qui prend en charge la répartition unique dynamique, deux conditions déterminent quelle opération une requête appellera : l'une est le nom de la requête et le type réel du destinataire. La répartition unique limite le processus de sélection de la méthode de sorte qu'une seule instance puisse être prise en compte, qui est généralement le récepteur de la méthode. Dans le langage Java, si une opération est effectuée sur un objet de type inconnu, alors le test de type réel de l'objet n'aura lieu qu'une seule fois. C'est la caractéristique du dispatch unique dynamique.
double expédition
Une méthode décide d'exécuter du code différent en fonction des types de deux variables. C'est la « double répartition ». Le langage Java ne prend pas en charge la répartition multiple dynamique, ce qui signifie que Java ne prend pas en charge la répartition double dynamique. Mais en utilisant des modèles de conception, la double répartition dynamique peut également être implémentée dans le langage Java.
En Java, deux répartitions peuvent être réalisées via deux appels de méthode. Le diagramme de classes est le suivant :
Il y a deux objets sur l’image, celui de gauche s’appelle Ouest et celui de droite s’appelle Est. Désormais, l'objet West appelle d'abord la méthode goEast() de l'objet East, en se transmettant. Lorsque l'objet East est appelé, il sait immédiatement qui est l'appelant en fonction des paramètres transmis, donc la méthode goWest() de l'objet "appelant" est appelée à son tour. Grâce à deux appels, le contrôle du programme est transféré tour à tour à deux objets. Le diagramme de séquence est le suivant :
De cette façon, il y a deux appels de méthode : le contrôle du programme est transmis entre les deux objets. Tout d'abord, il est transmis de l'objet Ouest à l'objet Est, puis il est retransmis à l'objet Ouest.
Mais le simple fait de renvoyer le ballon ne résout pas le problème de la double distribution. La clé est de savoir comment utiliser ces deux appels et la fonction de répartition dynamique unique du langage Java pour déclencher deux répartitions uniques au cours de ce processus de passage.
La répartition dynamique unique dans le langage Java se produit lorsqu'une sous-classe remplace une méthode d'une classe parent. En d’autres termes, West et East doivent être placés dans leur propre hiérarchie de types, comme indiqué ci-dessous :
code source
Le code de copie de la classe Ouest est le suivant :
classe abstraite publique Ouest {
public abstract void goWest1(SubEast1 east);
public abstract void goWest2(SubEast2 east);
}
Le code de copie de la classe SubWest1 est le suivant :
la classe publique SubWest1 étend West{
@Outrepasser
public void goWest1(SubEast1 est) {
System.out.println("SubWest1 + " + east.myName1());
}
@Outrepasser
public void goWest2(SubEast2 est) {
System.out.println("SubWest1 + " + east.myName2());
}
}
SubWest Classe 2
Copiez le code comme suit :
la classe publique SubWest2 étend l'ouest{
@Outrepasser
public void goWest1(SubEast1 est) {
System.out.println("SubWest2 + " + east.myName1());
}
@Outrepasser
public void goWest2(SubEast2 est) {
System.out.println("SubWest2 + " + east.myName2());
}
}
Le code de copie de la classe Est est le suivant :
classe abstraite publique Est {
public abstract void goEast(West west);
}
Le code de copie de la classe SubEast1 est le suivant :
la classe publique SubEast1 s'étend vers l'est{
@Outrepasser
public void goEast (Ouest ouest) {
west.goWest1(this);
}
chaîne publique monNom1(){
renvoie "SubEst1" ;
}
}
Le code de copie de la classe SubEast2 est le suivant :
la classe publique SubEast2 s'étend vers l'est{
@Outrepasser
public void goEast (Ouest ouest) {
west.goWest2(ce);
}
chaîne publique monNom2(){
renvoyer "SubEast2" ;
}
}
Le code de copie de la classe client est le suivant :
Client de classe publique {
public static void main (String[] arguments) {
//combinaison 1
Est est = nouveau SubEast1();
Ouest ouest = new SubWest1();
east.goEast(ouest);
//combinaison 2
est = nouveau SubEast1();
ouest = nouveau SubWest2();
east.goEast(ouest);
}
}
Les résultats d'exécution sont les suivants. Copiez le code.
Sous-Ouest1 + Sous-Est1
Sous-Ouest2 + Sous-Est1
Lorsque le système est en cours d'exécution, les objets SubWest1 et SubEast1 sont d'abord créés, puis le client appelle la méthode goEast() de SubEast1 et transmet l'objet SubWest1. Puisque l'objet SubEast1 remplace la méthode goEast() de sa superclasse East, une répartition dynamique unique se produit à ce moment-là. Lorsque l'objet SubEast1 reçoit l'appel, il obtiendra l'objet SubWest1 du paramètre, il appelle donc immédiatement la méthode goWest1() de cet objet et se transmet. Puisque l'objet SubEast1 a le droit de choisir quel objet appeler, une autre répartition de méthode dynamique est effectuée à ce moment.
A ce moment, l'objet SubWest1 a obtenu l'objet SubEast1. En appelant la méthode myName1() de cet objet, vous pouvez imprimer votre propre nom et le nom de l'objet SubEast. Le diagramme de séquence est le suivant :
Puisque l’un de ces deux noms vient de la hiérarchie Est et l’autre de la hiérarchie Ouest, leur combinaison est déterminée dynamiquement. Il s'agit du mécanisme de mise en œuvre de la double répartition dynamique.
La structure du modèle de visiteur
Le modèle visiteur convient aux systèmes avec des structures de données relativement indéterminées. Il découple le couplage entre la structure de données et les opérations qui agissent sur la structure, permettant à l'ensemble des opérations d'évoluer relativement librement. Un diagramme simplifié du modèle de visiteur est présenté ci-dessous :
Chaque nœud de la structure de données peut accepter un appel d'un visiteur. Ce nœud transmet l'objet nœud à l'objet visiteur, et l'objet visiteur effectue à son tour les opérations de l'objet nœud. Ce processus est appelé « double expédition ». Le nœud appelle le visiteur, se transmet, et le visiteur exécute un algorithme sur ce nœud. Un diagramme de classes schématique pour le modèle Visiteur est présenté ci-dessous :
Les rôles impliqués dans le mode visiteur sont les suivants :
● Rôle de visiteur abstrait (Visiteur) : déclare une ou plusieurs opérations de méthode pour former l'interface que tous les rôles de visiteur spécifiques doivent implémenter.
● Rôle Concrete Visitor (ConcreteVisitor) : implémente l'interface déclarée par le visiteur abstrait, c'est-à-dire chaque opération d'accès déclarée par le visiteur abstrait.
● Rôle de nœud abstrait (Node) : déclare une opération d'acceptation et accepte un objet visiteur en paramètre.
● Rôle ConcreteNode : implémente l'opération d'acceptation spécifiée par le nœud abstrait.
● Rôle d'objet de structure (ObjectStructure) : a les responsabilités suivantes, peut parcourir tous les éléments de la structure si nécessaire, fournir une interface de haut niveau pour que les objets visiteurs puissent accéder à chaque élément si nécessaire, peut être conçu comme un objet composite ou ; Une collection, telle que List ou Set.
code source
Comme vous pouvez le voir, le rôle de visiteur abstrait prépare une opération d'accès pour chaque nœud spécifique. Puisqu’il y a deux nœuds, il y a deux opérations d’accès correspondantes.
Copiez le code comme suit :
Interface publique Visiteur {
/**
* Correspond à l'opération d'accès de NodeA
*/
visite publique vide (nœud NodeA);
/**
* Correspond à l'opération d'accès de NodeB
*/
visite publique vide (nœud NodeB);
}
Le code de copie spécifique de la classe visiteurA est le suivant :
classe publique VisitorA implémente Visitor {
/**
* Correspond à l'opération d'accès de NodeA
*/
@Outrepasser
visite publique vide (nœud NodeA) {
System.out.println(node.opérationA());
}
/**
* Correspond à l'opération d'accès de NodeB
*/
@Outrepasser
visite publique vide (nœud NodeB) {
System.out.println(node.opérationB());
}
}
Le code de copie de classe VisitorB spécifique du visiteur est le suivant :
classe publique VisitorB implémente Visitor {
/**
* Correspond à l'opération d'accès de NodeA
*/
@Outrepasser
visite publique vide (nœud NodeA) {
System.out.println(node.opérationA());
}
/**
* Correspond à l'opération d'accès de NodeB
*/
@Outrepasser
visite publique vide (nœud NodeB) {
System.out.println(node.opérationB());
}
}
Le code de copie du code de classe de nœud abstrait est le suivant :
classe abstraite publique Node {
/**
* Accepter l'opération
*/
public abstract void accept (Visiteur visiteur);
}
Classe de nœud spécifique NodeA
Copiez le code comme suit :
la classe publique NodeA étend Node{
/**
* Accepter l'opération
*/
@Outrepasser
public void accept (Visiteur visiteur) {
visiteur.visit(this);
}
/**
*Méthode spécifique à NodeA
*/
opération de chaîne publiqueA(){
renvoyer "NoeudA" ;
}
}
Classe de nœud spécifique NodeB
Copiez le code comme suit :
la classe publique NodeB étend Node{
/**
*Accepter la méthode
*/
@Outrepasser
public void accept (Visiteur visiteur) {
visiteur.visit(this);
}
/**
*Méthodes spécifiques au nœud B
*/
opération de chaîne publiqueB(){
renvoyer "NoeudB" ;
}
}
Classe de rôle d'objet structurel. Ce rôle d'objet structurel contient une collection et fournit la méthode add() au monde extérieur en tant qu'opération de gestion de la collection. En appelant cette méthode, un nouveau nœud peut être ajouté dynamiquement.
Copiez le code comme suit :
classe publique ObjectStructure {
private List<Node> nodes = new ArrayList<Node>();
/**
* Exécuter l'opération de la méthode
*/
action de vide public (visiteur visiteur) {
pour (nœud nœud : nœuds)
{
node.accept(visiteur);
}
}
/**
* Ajouter un nouvel élément
*/
public void add (nœud de nœud) {
nodes.add(nœud);
}
}
Le code de copie de la classe client est le suivant :
Client de classe publique {
public static void main (String[] arguments) {
//Créer un objet structure
ObjectStructure os = new ObjectStructure();
//Ajouter un nœud à la structure
os.add(nouveau NodeA());
//Ajouter un nœud à la structure
os.add(nouveau NodeB());
//Créer un visiteur
Visiteur visiteur = new VisitorA();
os.action (visiteur);
}
}
Bien qu'une structure arborescente d'objets complexe avec plusieurs nœuds de branche n'apparaisse pas dans cette implémentation schématique, dans les systèmes réels, le modèle de visiteur est généralement utilisé pour gérer des structures arborescentes d'objets complexes, et le modèle de visiteur peut être utilisé pour traiter les problèmes de structure arborescente qui s'étendent sur plusieurs hiérarchies. . C’est là que le modèle de visiteur est si puissant.
Diagramme de séquence du processus de préparation
Tout d’abord, ce client illustratif crée un objet de structure, puis transmet un nouvel objet NodeA et un nouvel objet NodeB.
Deuxièmement, le client crée un objet VisitorA et transmet cet objet à l'objet structure.
Ensuite, le client appelle la méthode de gestion d'agrégation d'objets de structure pour ajouter les nœuds NodeA et NodeB à l'objet de structure.
Enfin, le client appelle la méthode d'action action() de l'objet structure pour démarrer le processus d'accès.
Diagramme de séquence de processus d'accès
L'objet structure traversera tous les nœuds de la collection qu'il enregistre, qui dans ce système sont les nœuds NodeA et NodeB. Tout d'abord, NodeA sera accédé. Cet accès comprend les opérations suivantes :
(1) La méthode accept() de l'objet NodeA est appelée et l'objet VisitorA lui-même est transmis ;
(2) L'objet NodeA appelle à son tour la méthode d'accès de l'objet VisitorA et transmet l'objet NodeA lui-même ;
(3) L'objet VisitorA appelle la méthode unique operationA() de l'objet NodeA.
Ainsi, le processus de double répartition est terminé. Ensuite, NodeB sera accédé. Le processus d'accès est le même que le processus d'accès de NodeA, qui ne sera pas décrit ici.
Avantages du modèle de visiteur
● Une bonne extensibilité peut ajouter de nouvelles fonctions aux éléments de la structure de l'objet sans modifier les éléments de la structure de l'objet.
● Une bonne réutilisabilité permet aux visiteurs de définir des fonctions communes à l'ensemble de la structure de l'objet, améliorant ainsi le degré de réutilisabilité.
● Séparation des comportements non pertinents Vous pouvez utiliser des visiteurs pour séparer les comportements non pertinents et encapsuler les comportements associés pour former un visiteur, de sorte que la fonction de chaque visiteur soit relativement unique.
Inconvénients du modèle de visiteur
● Il est difficile de modifier la structure de l'objet. Cela ne convient pas aux situations dans lesquelles les classes de la structure de l'objet changent fréquemment. Parce que la structure de l'objet change, l'interface du visiteur et l'implémentation du visiteur doivent changer en conséquence, ce qui est trop coûteux.
● Rompre le modèle d'encapsulation des visiteurs nécessite généralement que la structure de l'objet ouvre les données internes aux visiteurs et à ObjectStructrue, ce qui rompt l'encapsulation de l'objet.