Modèle de conception | Du modèle de visiteur à la correspondance de modèle

Préface


Dans le domaine du développement de logiciels, les problèmes que nous rencontrons peuvent être différents à chaque fois: certains sont liés au commerce électronique, certains sont liés à la structure de données sous-jacente et certains peuvent se concentrer sur l'optimisation des performances. Cependant, notre approche pour résoudre les problèmes au niveau du code présente certains points communs. Quelqu'un a-t-il résumé ces points communs?

Bien sûr, il y en a. En 1994, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides ont publié conjointement un livre d'une grande importance dans l'industrie: Design Patterns: Elements of Reusable Object-Oriented Software. Ce livre amène les gens au domaine du développement. Une série d'abstractions a été faite sur la similitude de divers problèmes, et 23 modèles de conception très classiques ont été formés. De nombreux problèmes peuvent être résumés dans un ou plusieurs de ces 23 modèles de conception. Les modèles de conception étant très polyvalents, ils sont également devenus un langage universel utilisé par les développeurs, et le code résumé dans les modèles de conception est plus facile à comprendre et à gérer.

Dans l'ensemble, les modèles de conception sont divisés en trois catégories:


1. Modèles de création: modèles de conception liés à la création et à la réutilisation d'objets
2. Modèles de structure: modèles de conception liés à la combinaison et à la construction d'objets
3. Modèles de comportement: modèles de conception liés au comportement entre les objets

Le modèle de conception décrit dans cet article est le modèle de visiteur, qui est une sorte de modèle de comportement, utilisé pour résoudre le problème de la combinaison et du développement d'objets ayant des comportements similaires. Plus précisément, cet article présente les scénarios d'utilisation, les avantages et les inconvénients du modèle de visiteur et de la technologie Double Dispatch liés au modèle de visiteur. Et à la fin de l'article, il explique comment utiliser Pattern Matching, qui vient d'être lancé dans Java 14, pour résoudre les problèmes résolus par le précédent modèle de visiteur.

problème


En supposant qu'il existe un programme de carte, il y a de nombreux nœuds sur la carte, tels que bâtiment (bâtiment), usine (usine), école (école), comme indiqué ci-dessous:

interface Node {
    String getName();
    String getDescription();
    // 其余的方法这里忽略......
}


class Building implements Node {
    ...
}


class Factory implements Node {
    ...
}


class School implements Node {
    ...
}

Voici une nouvelle exigence: vous devez ajouter la fonction de dessin Node. Si vous y réfléchissez, c'est très simple. Ajoutons une méthode draw () dans Node, puis le reste des classes d'implémentation implémentera cette méthode séparément. Mais il y a un problème avec cela. Nous avons ajouté la méthode draw () cette fois. Et si vous ajoutiez une méthode d'exportation la prochaine fois? De plus, l'interface doit être à nouveau modifiée. En tant que pont reliant les composants, les interfaces doivent être aussi stables que possible et ne doivent pas être changées fréquemment. Par conséquent, vous voulez être en mesure de rendre l'évolutivité de l'interface aussi élevée que possible et de maximiser la portée fonctionnelle de l'interface sans changer fréquemment l'interface. Après quelques pesées, vous avez trouvé la solution suivante.

Solution initiale


Nous définissons une nouvelle classe DrawService et y écrivons toute la logique de dessin. Le code est le suivant:

public class DrawService {
    public void draw(Building building) {
        System.out.println("draw building");
    }
    public void draw(Factory factory) {
        System.out.println("draw factory");
    }
    public void draw(School school) {
        System.out.println("draw school");
    }
    public void draw(Node node) {
        System.out.println("draw node");
    }
}

Voici le diagramme de classes:

Vous pensez que le problème est résolu maintenant, vous allez donc rentrer du travail après un petit test:

public class App {
    private void draw(Node node) {
        DrawService drawService = new DrawService();
        drawService.draw(node);
    }


    public static void main(String[] args) {
        App app = new App();
        app.draw(new Factory());
    }
}

Cliquez pour exécuter, sortie:

draw node

Comment ça va? Vous regardez de plus près votre code à nouveau: "J'ai passé un objet Factory, il devrait sortir draw factory". Sérieusement, vous êtes allé vérifier certaines informations, puis vous en avez trouvé la raison.

expliquer la raison


Afin de comprendre la raison, nous comprenons d'abord les deux modes de liaison de type variable de l'éditeur.

★   Reliure dynamique / tardive

Jetons un œil à ce code

class NodeService {
    public String getName(Node node) {
        return node.getName();
    }
}

Lorsque le programme exécute NodeService :: getName, il doit déterminer le type du paramètre Node, qu'il s'agisse d'une usine, d'une école ou d'un bâtiment, afin qu'il puisse appeler la méthode getName de la classe d'implémentation correspondante. Le programme peut-il obtenir ces informations pendant la phase de compilation? Evidemment non, car le type de Node peut changer en fonction de l'environnement d'exploitation, et il peut même être transmis depuis un autre système.Nous ne pouvons pas obtenir cette information pendant la phase de compilation. Ce que le programme peut faire est de le démarrer en premier et, lorsqu'il s'exécute sur la méthode getName, examinez le type de Node, puis appelez l'implémentation getName () du type correspondant pour obtenir le résultat. Le choix de la méthode à appeler au moment de l'exécution (et non au moment de la compilation) est appelé liaison dynamique / tardive.

★   Reliure statique / précoce

Regardons un autre morceau de code

public void drawNode(Node node) {
    DrawService drawService = new DrawService();
    drawService.draw(node);
}

Lorsque nous exécutons drawService.draw (node), le compilateur connaît-il le type de nœud? Il doit être connu au moment de l'exécution, alors pourquoi passons-nous une Factory, mais un nœud de dessin en sortie au lieu de draw factory? On peut penser à ce problème du point de vue du programme. Il n'y a que 4 méthodes de dessin dans DrawService et les types de paramètres sont Factory, Building, School et Node. Que faire si l'appelant passe dans une ville? Après tout, l'appelant peut implémenter une classe City pour passer. Quelle méthode le programme doit-il appeler dans ce cas? Nous n'avons pas de méthode draw (City) Pour éviter que cela ne se produise, le programme choisit directement d'utiliser la méthode DrawService :: draw (Node) pendant la phase de compilation. Quelle que soit l'implémentation passée par l'appelant, nous utiliserons la méthode DrawService :: draw (Node) pour garantir le bon fonctionnement du programme. Décider quelle méthode appeler au moment de la compilation (pas au moment de l'exécution) s'appelle Static / Early Binding. Cela explique également pourquoi nous sortons draw node.

La solution finale


Il s'avère que c'est parce que le compilateur ne connaît pas le type de variable.Dans ce cas, nous pouvons dire au compilateur de quel type il s'agit. Cela peut-il être fait? Cela peut bien sûr être fait, nous vérifions le type de variable à l'avance.

if (node instanceof Building) {
    Building building = (Building) node;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) node;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) node;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

Ce code est faisable, mais il est très lourd à écrire. Nous devons laisser l'appelant déterminer le type de nœud et choisir la méthode à appeler. Existe-t-il une meilleure solution? Oui, c'est le modèle de visiteur. Le modèle de visiteur utilise une méthode appelée Double Dispatch, qui peut transférer le travail de routage de l'appelant vers la classe d'implémentation respective, afin que le client n'ait pas besoin d'écrire cette logique de jugement fastidieuse. Voyons d'abord à quoi ressemble le code implémenté.

interface Visitor {
    void visit(Node node);
    void visit(Factory factory);
    void visit(Building building);
    void visit(School school);
}


class DrawVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("draw node");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("draw factory");
    }


    @Override
    public void visit(Building building) {
        System.out.println("draw building");
    }


    @Override
    public void visit(School school) {
        System.out.println("draw school");
    }
}


interface Node {
    ...
    void accpet(Visitor v);
}


class Factory implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Factory类型的,并且知道Visitor::visit(Factory)方法确实存在,
         * 因此会直接调用Visitor::visit(Factory)方法
         */
        v.visit(this);
    }
}


class Building implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Building类型的,并且知道Visitor::visit(Building)方法确实存在,
         * 因此会直接调用Visitor::visit(Building)方法
         */
        v.visit(this);
    }
}


class School implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是School类型的,并且知道Visitor::visit(School)方法确实存在,
         * 因此会直接调用Visitor::visit(School)方法
         */
        v.visit(this);
    }
}

L'appelant peut l'utiliser comme ça

Visitor drawVisitor = new DrawVisitor();
Factory factory = new Factory();
factory.accept(drawVisitor);

On peut voir que le modèle de visiteur implémente en fait avec élégance notre if instanceof ci-dessus, de sorte que le code de l'appelant soit beaucoup plus propre, le diagramme de classe global est le suivant

Pourquoi s'appelle-t-il Double Dispatch?


Après avoir compris comment le modèle de visiteur résout ce problème, certains élèves peuvent être curieux. Pourquoi la technologie utilisée par le modèle de visiteur est-elle appelée double répartition? Qu'est-ce que Double Dispatch exactement? Avant de comprendre le double envoi, voyons d'abord ce que l'on appelle l'envoi simple

★   Expédition unique

Choisissez différentes méthodes d'appel en fonction de différentes implémentations de classe d'exécution, appelées Single Dispatch, telles que

String name = node.getName();

Appelons-nous Factory :: getName, School :: getName ou Building :: getName? Cela dépend principalement de la classe d'implémentation du nœud, qui est Single Dispatch: une couche de routage

★   Double envoi

Passez en revue le code de modèle de visiteur que nous avons tout à l'heure

node.accept(drawVisitor);

Il existe deux couches de routage:

  • Choisissez la méthode d'implémentation spécifique d'accept (Factory :: accept, School :: accept ou Building :: accept)

  • Sélectionnez la méthode de visite spécifique (dans cet exemple, il n'y a qu'un seul DrawVisit :: visit)

Après avoir effectué deux routes, la logique correspondante est exécutée, qui s'appelle Double Dispatch



Avantages du modèle de visiteur


1. Le modèle de visiteur peut augmenter autant que possible l'évolutivité de l'interface sans changer fréquemment l'interface (il suffit de changer une fois: ajout d'une méthode d'acceptation)    

Toujours dans l'exemple de dessin ci-dessus, supposons que nous ayons maintenant une nouvelle exigence et que nous devions ajouter la fonction d'affichage des informations de nœud. Bien sûr, la méthode traditionnelle consiste à ajouter une nouvelle méthode showDetails () dans Node, mais maintenant nous n'avons pas besoin de changer l'interface, nous avons seulement besoin d'ajouter un nouveau visiteur.

class ShowDetailsVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("node details");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("factory details");
    }


    @Override
    public void visit(Building building) {
        System.out.println("building details");
    }


    @Override
    public void visit(School school) {
        System.out.println("school details");
    }
}


// 调用方这么使用
Visitor showDetailsVisitor = new ShowDetailsVisitor();
Factory factory = new Factory();
factory.accept(showDetailsVisitor); // factory details

À partir de cet exemple, nous pouvons voir un scénario d'utilisation typique de Visitor Pattern: il est très approprié pour une utilisation dans des scénarios où des méthodes d'interface doivent être ajoutées fréquemment. Par exemple, nous avons maintenant 4 classes A, B, C, D, trois méthodes x, y, z, méthode de dessin horizontal, classe de dessin vertical, nous pouvons obtenir l'image suivante:

               x      y      z
    A       A::x   A::y   A::z
    B       B::x   B::y   B::z
    C       C::x   C::y   C::z

Dans des circonstances normales, notre table est développée verticalement, c'est-à-dire que nous sommes habitués à ajouter des classes d'implémentation plutôt que des méthodes d'implémentation. Le modèle de visiteur convient à un autre scénario: l'expansion horizontale. Nous devons fréquemment ajouter des méthodes d'interface, plutôt que d'ajouter des classes d'implémentation. Le modèle de visiteur nous permet d'atteindre cet objectif sans modifier fréquemment l'interface.

2. Le modèle de visiteur peut facilement faire en sorte que plusieurs classes d'implémentation partagent une logique

Puisque toutes les méthodes d'implémentation sont écrites dans une classe (comme DrawVisitor), nous pouvons facilement faire en sorte que chaque type (comme Factory / Building / School) utilise la même logique au lieu d'écrire cette logique à plusieurs reprises dans chaque implémentation d'interface Classe.

Inconvénients du modèle de visiteur


  • Le modèle de visiteur rompt l'encapsulation du modèle de domaine

Dans des circonstances normales, nous écrirons la logique de la fabrique dans la classe Factory, mais le modèle de visiteur nous oblige à déplacer une partie de la logique de la fabrique (comme draw) vers une autre classe (DrawVisitor). La logique d'un modèle de domaine est dispersée en deux Cela entraîne des inconvénients pour la compréhension et la maintenance du modèle de domaine.

  • Le modèle de visiteur a dans une certaine mesure provoqué la réalisation du couplage logique de classe

Toutes les méthodes (draw) de la classe d'implémentation (Factory / School / Building) sont toutes écrites dans une classe (DrawVisitor), ce qui est un couplage logique dans une certaine mesure et n'est pas propice à la maintenance du code.

  • Le modèle de visiteur rend la relation entre les classes compliquée et difficile à comprendre

Comme le montre le nom Double Dispatch, nous avons besoin de deux dépêches pour appeler avec succès la logique correspondante: la première étape est d'appeler la méthode accpet, la seconde est d'appeler la méthode visit, la relation d'appel devient plus compliquée, le code derrière Le mainteneur peut facilement gâcher le code.

Correspondance de motif


Voici un autre épisode. Java 14 a introduit la fonction Pattern Matching.Bien que cette fonctionnalité existe dans le domaine Scala / Haskel depuis de nombreuses années, de nombreux étudiants ne savent toujours pas ce que c'est parce que Java vient d'être introduit. Par conséquent, avant d'expliquer la relation entre la correspondance de modèle et le modèle de visiteur, présentons brièvement ce qu'est la correspondance de modèle. Tu te souviens que nous avons écrit ce code?

if (node instanceof Building) {
    Building building = (Building) building;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) factory;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) school;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

Avec Pattern Matching, nous pouvons simplifier ce code:

if (node instanceof Building building) {
    drawService.draw(building);
} else if (node instanceof Factory factory) {
    drawService.draw(factory);
} else if (node instanceof School school) {
    drawService.draw(school);
} else {
    drawService.draw(node);
}

Cependant, la correspondance des motifs de Java est toujours un peu lourde, tandis que celle de Scala peut être meilleure:

node match {
  case node: Factory => drawService.draw(node)
  case node: Building => drawService.draw(node)
  case node: School => drawService.draw(node)
  case _ => drawService.draw(node)
}

Parce qu'il est plus concis, de nombreuses personnes préconisent le Pattern Matching comme un substitut au Pattern Visiteur. Personnellement, je pense que le Pattern Matching semble beaucoup plus simple. Beaucoup de gens pensent que la correspondance de motifs est la version avancée du boîtier de commutation. En fait, ce n'est pas le cas. Pour plus de détails, reportez-vous à TOUR OF SCALA-PATTERN MATCHING (https://docs.scala-lang.org/tour/pattern-matching.html), à propos du modèle de visiteur La relation avec Pattern Matching peut être vue dans Pattern Matching = Pattern Visiteur de Scala sur les stéroïdes, cet article ne le répétera pas.

Matériel de référence:

  • Correspondance de modèle de Scala = modèle de visiteur sur les stéroïdes

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • Quand dois-je utiliser le modèle de conception de visiteur?

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • Modèle de conception - Modèles de comportement - Visiteur

    https://refactoring.guru/design-patterns/visitor 

  • Correspondance de modèle par exemple de Java 14

    https://refactoring.guru/design-patterns/visitor 

Tao Department Technology Department-Industry and Intelligent Operation-Recruiting Talents

Nous sommes l'équipe d'analyse des données d'Alibaba Operation Workbench. Il existe d'énormes quantités de données, des moteurs de calcul en temps réel hautes performances et des scénarios commerciaux difficiles. Du 618 au Double 11, de Taobao à Tmall, de l'analyse des données à la précipitation commerciale, nous répandrons la volonté et l'atmosphère de recherche de la perfection à chaque coin du cercle technologique. Dans l'attente de votre adhésion à la poursuite technique et à la profondeur technique!

Poste de recrutement: expert en technologie Java, ingénieur de données,
si vous êtes intéressé, veuillez envoyer votre CV à [email protected], bienvenue pour venir chercher ~

✿ En outre   la lecture

Auteur | Yu Haining (Jing Fan)

Modifier | Orange

Produit | La nouvelle technologie de vente au détail d'Alibaba

Je suppose que tu aimes

Origine blog.csdn.net/Taobaojishu/article/details/111503210
conseillé
Classement