57 votes

Une alternative au modèle visiteur ?

Je cherche une alternative au modèle visiteur. Permettez-moi de me concentrer sur quelques aspects pertinents du modèle, tout en laissant de côté les détails sans importance. Je vais utiliser un exemple de forme (désolé !):

  1. Vous avez une hiérarchie d'objets qui implémentent l'interface IShape.
  2. Vous avez un certain nombre d'opérations globales qui doivent être effectuées sur tous les objets de la hiérarchie, par exemple Draw, WriteToXml etc...
  3. Il est tentant de se jeter à l'eau et d'ajouter une méthode Draw() et WriteToXml() à l'interface IShape. Ce n'est pas forcément une bonne chose - chaque fois que vous souhaitez ajouter une nouvelle opération qui doit être exécutée sur toutes les formes, chaque classe dérivée de IShape doit être modifiée
  4. L'implémentation d'un visiteur pour chaque opération, par exemple un visiteur Draw ou un visiteur WirteToXml, encapsule tout le code de cette opération dans une seule classe. L'ajout d'une nouvelle opération consiste alors à créer une nouvelle classe de visiteur qui exécute l'opération sur tous les types d'IShape.
  5. Lorsque vous devez ajouter une nouvelle classe dérivée d'IShape, vous rencontrez essentiellement le même problème qu'en 3 - toutes les classes de visiteurs doivent être modifiées pour ajouter une méthode permettant de gérer le nouveau type dérivé d'IShape.

La plupart des sites où l'on trouve des informations sur le modèle de visiteur indiquent que le point 5 est le critère principal pour que le modèle fonctionne et je suis tout à fait d'accord. Si le nombre de classes dérivées d'IShape est fixe, alors cette approche peut être assez élégante.

Le problème est donc que lorsqu'une nouvelle classe dérivée d'IShape est ajoutée, chaque implémentation visiteur doit ajouter une nouvelle méthode pour gérer cette classe. Cela est, au mieux, désagréable et, au pire, impossible et montre que ce modèle n'est pas vraiment conçu pour faire face à de tels changements.

La question est donc la suivante : quelqu'un a-t-il trouvé des approches alternatives pour gérer cette situation ?

0 votes

Juste une remarque en passant, car il est peu probable que vous puissiez changer de langue : Il existe des langages qui prennent directement en charge les fonctions génériques de dispatching multiple.

9 votes

Excellente question. Je voulais juste apporter un contrepoint. Parfois, votre problème avec (5) peut être une bonne chose. J'utilise le modèle visiteur lorsque j'ai une fonctionnalité qui doit être mise à jour lorsqu'un nouveau sous-type IShape est défini. J'ai une interface IShapeVisitor qui définit les méthodes nécessaires. Tant que cette interface est mise à jour avec le nouveau sous-type, mon code n'est pas construit tant que la fonctionnalité critique n'est pas mise à jour. Dans certaines situations, cela peut s'avérer très utile.

1 votes

Je suis d'accord avec @oillio, mais vous pourriez aussi l'imposer comme une méthode abstraite sur IShape. Ce que le modèle Visiteur vous apporte dans un langage OO pur est la localisation de la fonction (par opposition à la localisation de la classe) et donc une séparation des préoccupations. Quoi qu'il en soit, l'utilisation du modèle Visiteur devrait être explicitement interrompue au moment de la compilation lorsque vous voulez forcer l'ajout de nouveaux types à être examinés attentivement !

16voto

0xA3 Points 73439

Vous pourriez vouloir jeter un coup d'œil à la Modèle de stratégie . Cela vous donne toujours une séparation des préoccupations tout en étant capable d'ajouter de nouvelles fonctionnalités sans avoir à modifier chaque classe de votre hiérarchie.

class AbstractShape
{
    IXmlWriter _xmlWriter = null;
    IShapeDrawer _shapeDrawer = null;

    public AbstractShape(IXmlWriter xmlWriter, 
                IShapeDrawer drawer)
    {
        _xmlWriter = xmlWriter;
        _shapeDrawer = drawer;
    }

    //...
    public void WriteToXml(IStream stream)
    {
        _xmlWriter.Write(this, stream);

    }

    public void Draw()
    {
        _drawer.Draw(this);
    }

    // any operation could easily be injected and executed 
    // on this object at run-time
    public void Execute(IGeneralStrategy generalOperation)
    {
        generalOperation.Execute(this);
    }
}

Vous trouverez plus d'informations dans cette discussion connexe :

Un objet doit-il s'écrire lui-même dans un fichier, ou un autre objet doit-il agir sur lui pour effectuer des E/S ?

0 votes

J'ai marqué cette réponse comme étant la réponse à ma question, car je pense que cette réponse, ou une variation mineure de celle-ci, correspond probablement à ce que je veux faire. Pour ceux que cela intéresse, j'ai ajouté une "réponse" qui décrit certaines de mes réflexions sur le problème.

0 votes

Ok - j'ai changé d'avis à propos de la réponse - je vais essayer de la condenser dans un commentaire (suivant)

4 votes

Je pense qu'il y a un conflit fondamental ici - si vous avez un tas de choses et un tas d'actions qui peuvent être effectuées sur ces choses, alors ajouter une nouvelle chose signifie que vous devez définir l'effet de toutes les actions sur elle et vice versa - il n'y a pas d'échappatoire. Le visiteur fournit une manière très simple et élégante d'ajouter de nouvelles actions au prix de rendre difficile l'ajout de nouvelles choses. Si cette contrainte doit être assouplie, il faut payer. J'espérais qu'il pourrait y avoir une solution qui a l'élégance et la simplicité du visiteur, mais comme je le soupçonnais, je ne pense pas qu'il en existe une...suite...

13voto

Daniel Martin Points 9148

Il y a le "modèle visiteur avec défaut", dans lequel vous faites le modèle visiteur comme d'habitude mais définissez ensuite une classe abstraite qui implémente votre IShapeVisitor en déléguant tout à une méthode abstraite avec la signature visitDefault(IShape) .

Ensuite, lorsque vous définissez un visiteur, étendez cette classe abstraite au lieu d'implémenter directement l'interface. Vous pouvez surcharger la classe visit * les méthodes que vous connaissez à ce moment-là, et prévoyez une valeur par défaut raisonnable. Cependant, s'il n'y a vraiment aucun moyen de déterminer à l'avance un comportement par défaut raisonnable, vous devriez simplement implémenter l'interface directement.

Lorsque vous ajoutez une nouvelle IShape alors, vous fixez la classe abstraite pour qu'elle délègue à ses visitDefault et chaque visiteur qui a spécifié un comportement par défaut obtient ce comportement pour la nouvelle méthode IShape .

Une variation de ceci si votre IShape La façon dont les classes s'intègrent naturellement dans une hiérarchie consiste à faire en sorte que la classe abstraite délègue plusieurs méthodes différentes. DefaultAnimalVisitor pourrait le faire :

public abstract class DefaultAnimalVisitor implements IAnimalVisitor {
  // The concrete animal classes we have so far: Lion, Tiger, Bear, Snake
  public void visitLion(Lion l)   { visitFeline(l); }
  public void visitTiger(Tiger t) { visitFeline(t); }
  public void visitBear(Bear b)   { visitMammal(b); }
  public void visitSnake(Snake s) { visitDefault(s); }

  // Up the class hierarchy
  public void visitFeline(Feline f) { visitMammal(f); }
  public void visitMammal(Mammal m) { visitDefault(m); }

  public abstract void visitDefault(Animal a);
}

Cela vous permet de définir des visiteurs qui précisent leur comportement au niveau de spécificité que vous souhaitez.

Malheureusement, il n'y a aucun moyen d'éviter de faire quelque chose pour spécifier comment les visiteurs se comporteront avec une nouvelle classe - soit vous pouvez définir une valeur par défaut à l'avance, soit vous ne le pouvez pas. (Voir aussi le deuxième panneau de ce dessin animé )

6voto

RS Conley Points 6268

Je maintiens un logiciel de CAO/FAO pour machine de découpe de métaux. J'ai donc une certaine expérience de ces questions.

Lorsque nous avons converti notre logiciel (il a été publié pour la première fois en 1985 !) en un logiciel orienté objet, j'ai fait exactement ce que vous n'aimez pas. Les objets et les interfaces avaient Draw, WriteToFile, etc. La découverte et la lecture des Design Patterns à mi-chemin de la conversion m'ont beaucoup aidé mais il y avait encore beaucoup de mauvaises odeurs de code.

J'ai fini par comprendre qu'aucune de ces opérations ne concernait vraiment l'objet. Mais plutôt des divers sous-systèmes qui avaient besoin de faire les diverses opérations. J'ai géré cela en utilisant ce que l'on appelle maintenant un Vue passive Objet de commande, et interface bien définie entre les couches du logiciel.

Notre logiciel est structuré de la manière suivante

  • Les formulaires mettant en œuvre divers formulaires Interface. Ces formulaires sont une coquille qui transmet des événements à la couche d'interface utilisateur.
  • Couche de l'interface utilisateur qui reçoit les événements et manipule les formulaires par le biais de l'interface Form.
  • La couche UI exécutera les commandes qui implémentent toutes l'interface Command.
  • Les objets de l'interface utilisateur possèdent leurs propres interfaces avec lesquelles la commande peut interagir.
  • Les commandes obtiennent les informations dont elles ont besoin, les traitent, manipulent le modèle et font ensuite un rapport aux objets de l'interface utilisateur qui font ensuite tout ce qui est nécessaire avec les formulaires.
  • Enfin les modèles qui contiennent les différents objets de notre système. Comme les programmes de forme, les chemins de coupe, la table de coupe et les feuilles de métal.

Le dessin est donc géré dans la couche UI. Nous avons différents logiciels pour différentes machines. Ainsi, bien que tous nos logiciels partagent le même modèle et réutilisent la plupart des mêmes commandes. Ils gèrent des choses comme le dessin de manière très différente. Par exemple, une table de découpe est dessinée différemment pour une défonceuse et pour une machine utilisant une torche à plasma, bien qu'elles soient toutes deux essentiellement une table plane géante X-Y. Ceci parce que, comme les voitures, les deux machines sont différentes. Ceci parce que, comme pour les voitures, les deux machines sont construites de manière suffisamment différente pour que le client perçoive une différence visuelle.

En ce qui concerne les formes, ce que nous faisons est le suivant

Nous disposons de programmes de forme qui produisent des trajectoires de coupe à partir des paramètres saisis. La trajectoire de coupe sait quel programme de forme a produit. Cependant, une trajectoire de découpe n'est pas une forme. C'est juste l'information nécessaire pour dessiner sur l'écran et couper la forme. L'une des raisons de cette conception est que les trajectoires de découpe peuvent être créées sans programme de forme lorsqu'elles sont importées d'une application externe.

Cette conception nous permet de séparer la conception du chemin de coupe de la conception de la forme qui ne sont pas toujours la même chose. Dans votre cas, il est probable que tout ce dont vous avez besoin pour emballer est l'information nécessaire pour dessiner la forme.

Chaque programme de forme possède un certain nombre de vues mettant en œuvre une interface IShapeView. Grâce à l'interface IShapeView, le programme de mise en forme peut indiquer à la forme de mise en forme générique dont nous disposons comment se configurer pour afficher les paramètres de cette forme. Le formulaire de forme générique met en œuvre une interface IShapeForm et s'enregistre auprès de l'objet ShapeScreen. L'objet ShapeScreen s'enregistre auprès de notre objet d'application. Les vues de forme utilisent l'écran de forme qui s'enregistre avec l'application.

La raison de ces vues multiples est que nous avons des clients qui aiment saisir les formes de différentes manières. Notre clientèle est divisée en deux : ceux qui aiment saisir les paramètres des formes sous forme de tableau et ceux qui préfèrent saisir avec une représentation graphique de la forme devant eux. Nous devons aussi parfois accéder aux paramètres par le biais d'un dialogue minimal plutôt que par l'écran complet de saisie des formes. D'où les vues multiples.

Les commandes qui manipulent les formes peuvent être classées en deux catégories. Soit elles manipulent le chemin de coupe, soit elles manipulent les paramètres de la forme. Pour manipuler les paramètres de la forme, nous les renvoyons généralement dans l'écran de saisie de la forme ou nous affichons le dialogue minimal. Recalculer la forme, et l'afficher au même endroit.

Pour le chemin de coupe, nous avons regroupé chaque opération dans un objet de commande distinct. Par exemple, nous avons des objets de commande

ResizePath RotatePath MovePath SplitPath et ainsi de suite.

Lorsque nous devons ajouter une nouvelle fonctionnalité, nous ajoutons un autre objet de commande, nous trouvons un menu, un raccourci clavier ou un bouton de barre d'outils dans l'écran d'interface utilisateur approprié et nous configurons l'objet d'interface utilisateur pour exécuter cette commande.

Par exemple

   CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath

o

   CuttingTableScreen.Toolbar("Edit Path").AddButton Application.Icons("MirrorPath"),"Mirror Path", New MirrorPath

Dans les deux cas, l'objet de commande MirrorPath est associé à un élément d'interface utilisateur souhaité. Dans la méthode execute de MirrorPath se trouve tout le code nécessaire pour refléter le chemin dans un axe particulier. Il est probable que la commande disposera de sa propre boîte de dialogue ou utilisera l'un des éléments de l'interface utilisateur pour demander à l'utilisateur quel axe refléter. Il ne s'agit pas de créer un visiteur, ni d'ajouter une méthode au chemin.

Vous constaterez que beaucoup de choses peuvent être gérées en regroupant les actions en commandes. Cependant, je vous préviens que ce n'est pas une situation noire ou blanche. Vous trouverez toujours que certaines choses fonctionnent mieux en tant que méthodes sur l'objet original. Dans mon expérience, j'ai trouvé que peut-être 80% de ce que j'avais l'habitude de faire dans les méthodes pouvaient être déplacés dans la commande. Les derniers 20% fonctionnent tout simplement mieux sur l'objet.

Certains n'aiment pas cela car cela semble violer les encapsulations. Pour avoir maintenu notre logiciel comme un système orienté objet pendant la dernière décennie, je dois dire que la chose la plus importante à long terme que vous pouvez faire est de documenter clairement les interactions entre les différentes couches de votre logiciel et entre les différents objets.

Le regroupement des actions en objets de commande permet d'atteindre cet objectif bien mieux qu'une dévotion servile aux idéaux de l'encapsulation. Tout ce qui doit être fait pour mettre en miroir un chemin est regroupé dans l'objet de commande Mirror Path.

0 votes

La solution semble intéressante, mais j'apprécierais que vous puissiez me renvoyer à un exemple de code pour une telle solution afin de mieux comprendre le concept.

2voto

Adrian Regan Points 1574

Quelle que soit la voie choisie, l'implémentation de la fonctionnalité alternative actuellement fournie par le modèle Visitor devra "savoir" quelque chose sur l'implémentation concrète de l'interface sur laquelle elle travaille. Il n'y a donc aucun moyen de contourner le fait que vous allez devoir écrire une fonctionnalité "visiteur" supplémentaire pour chaque implémentation supplémentaire. Cela dit, ce que vous recherchez, c'est une approche plus souple et plus structurée pour créer cette fonctionnalité.

Vous devez séparer la fonctionnalité du visiteur de l'interface de la forme.

Ce que je propose, c'est une approche créationniste via une usine abstraite pour créer des implémentations de remplacement pour les fonctionnalités des visiteurs.

public interface IShape {
  // .. common shape interfaces
}

//
// This is an interface of a factory product that performs 'work' on the shape.
//
public interface IShapeWorker {
     void process(IShape shape);
}

//
// This is the abstract factory that caters for all implementations of
// shape.
//
public interface IShapeWorkerFactory {
    IShapeWorker build(IShape shape);
    ...
}

//
// In order to assemble a correct worker we need to create
// and implementation of the factory that links the Class of
// shape to an IShapeWorker implementation.
// To do this we implement an abstract class that implements IShapeWorkerFactory
//
public AbsractWorkerFactory implements IShapeWorkerFactory {

    protected Hashtable map_ = null;

    protected AbstractWorkerFactory() {
          map_ = new Hashtable();
          CreateWorkerMappings();
    }

    protected void AddMapping(Class c, IShapeWorker worker) {
           map_.put(c, worker);
    }

    //
    // Implement this method to add IShape implementations to IShapeWorker
    // implementations.
    //
    protected abstract void CreateWorkerMappings();

    public IShapeWorker build(IShape shape) {
         return (IShapeWorker)map_.get(shape.getClass())
    }
}

//
// An implementation that draws circles on graphics
//
public GraphicsCircleWorker implements IShapeWorker {

     Graphics graphics_ = null;

     public GraphicsCircleWorker(Graphics g) {
        graphics_ = g;
     }

     public void process(IShape s) {
       Circle circle = (Circle)s;
       if( circle != null) {
          // do something with it.
          graphics_.doSomething();
       }
     }

}

//
// To replace the previous graphics visitor you create
// a GraphicsWorkderFactory that implements AbstractShapeFactory 
// Adding mappings for those implementations of IShape that you are interested in.
//
public class GraphicsWorkerFactory implements AbstractShapeFactory {

   Graphics graphics_ = null;
   public GraphicsWorkerFactory(Graphics g) {
      graphics_ = g;
   }

   protected void CreateWorkerMappings() {
      AddMapping(Circle.class, new GraphicCircleWorker(graphics_)); 
   }
}

//
// Now in your code you could do the following.
//
IShapeWorkerFactory factory = SelectAppropriateFactory();

//
// for each IShape in the heirarchy
//
for(IShape shape : shapeTreeFlattened) {
    IShapeWorker worker = factory.build(shape);
    if(worker != null)
       worker.process(shape);
}

Cela signifie toujours que vous devez écrire des implémentations concrètes pour travailler sur de nouvelles versions de "shape", mais comme elle est complètement séparée de l'interface de shape, vous pouvez adapter cette solution sans casser l'interface originale et le logiciel qui interagit avec elle. Elle agit comme une sorte d'échafaudage autour des implémentations de IShape.

0 votes

Dans AbstractWorkerFactory vous devez toujours faire instanceof

1voto

Adrian Regan Points 1574

Qu'est-ce que vous essayez d'atteindre et qui vous a conduit vers le modèle de visiteur en premier lieu ?

Comme vous l'avez déjà souligné, ce serait une très mauvaise idée de modifier une interface existante, car il s'agit après tout d'un contrat. Et comme vous l'avez déjà souligné, le modèle visiteur est idéalement adapté aux structures d'héritage qui sont fixes.

Il me semble que vous n'avez pas une hiérarchie fixe et que vous ajouterez des implémentations au fil du temps.

Y a-t-il quelque chose que vous essayez de faire avec un visiteur et qui pourrait être incorporé dans la structure de l'interface ?

Le motif décorateur permet également d'étendre la fonctionnalité sans héritage. Cela vous permet de "fixer" en quelque sorte votre hiérarchie du point de vue du visiteur et de l'étendre là où l'extension est applicable.

Mais une image plus claire de vos intentions aiderait

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