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
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 !
2 votes
"toutes les classes de visiteurs doivent être modifiées pour ajouter une méthode permettant de gérer le nouveau type dérivé de IShape" : Je ne dirais pas que c'est un "problème". Je dirais que c'est une très belle sécurité dans votre conception. Elle garantit que la classe nouvellement ajoutée est prise en compte à chaque endroit où vous effectuez des opérations spécifiques par type (c'est-à-dire : à chaque endroit où nous avons défini un visiteur.... si vous ne prenez pas en compte le nouveau type ajouté, le compilateur ne vous laissera pas partir...).