Quelle est la meilleure pratique quand il s'agit d'écrire des classes qui pourraient avoir besoin de connaître l'interface utilisateur. Une classe sachant comment se dessiner ne briserait-elle pas certaines des meilleures pratiques car cela dépend de ce qu'est l'interface utilisateur (console, GUI, etc.)?
Dans de nombreux livres de programmation, je suis tombé sur l'exemple "Shape" qui montre l'héritage. La forme de classe de base a une méthode draw () que chaque forme telle qu'un cercle et un carré remplace. Cela permet le polymorphisme. Mais la méthode draw () ne dépend-elle pas beaucoup de l'interface utilisateur? Si nous écrivons cette classe pour, disons, Win Forms, nous ne pouvons pas la réutiliser pour une application console ou une application Web. Est-ce correct?
La raison de la question est que je me retrouve toujours coincé et que je raccroche à la façon de généraliser les classes afin qu'elles soient les plus utiles. Cela fonctionne en fait contre moi et je me demande si j'essaye trop fort.
Shape
classe, vous écrivez probablement la pile graphique elle-même, et non pas un client dans la pile graphique.Réponses:
Cela dépend de la classe et du cas d'utilisation. Un élément visuel sachant se dessiner n'est pas nécessairement une violation du principe de responsabilité unique.
Encore une fois, pas nécessairement. Si vous pouvez créer une interface (drawPoint, drawLine, définir la couleur, etc.), vous pouvez à peu près passer n'importe quel contexte pour dessiner des choses sur quelque chose à la forme, par exemple dans le constructeur de la forme. Cela permettrait aux formes de se dessiner sur une console ou une toile donnée.
Eh bien, c'est vrai. Si vous écrivez un UserControl (pas une classe en général) pour Windows Forms, vous ne pourrez pas l'utiliser avec une console. Mais ce n'est pas un problème. Pourquoi vous attendriez-vous à ce qu'un UserControl pour Windows Forms fonctionne avec tout type de présentation? UserControl doit faire une chose et bien le faire. C'est lié à une certaine forme de présentation par définition. Au final, l'utilisateur a besoin de quelque chose de concret et non d'une abstraction. Cela n'est peut-être que partiellement vrai pour les frameworks, mais pour les applications d'utilisateur final, c'est le cas.
Cependant, la logique sous-jacente doit être découplée, afin que vous puissiez l'utiliser à nouveau avec d'autres technologies de présentation. Introduisez des interfaces si nécessaire pour maintenir l'orthogonalité de votre application. La règle générale est la suivante: les choses concrètes doivent être échangeables avec d'autres choses concrètes.
Vous savez, les programmeurs extrêmes aiment leur attitude YAGNI . N'essayez pas de tout écrire de manière générique et n'essayez pas trop en essayant de tout faire à usage général. Cela s'appelle une ingénierie excessive et conduira finalement à un code totalement alambiqué. Donnez à chaque composant exactement une tâche et assurez-vous qu'il le fait bien. Mettez des abstractions si nécessaire, là où vous vous attendez à ce que les choses changent (par exemple, interface pour dessiner le contexte, comme indiqué ci-dessus).
En général, lors de l'écriture d'applications métier, vous devez toujours essayer de découpler les choses. MVC et MVVM sont parfaits pour découpler la logique de la présentation, vous pouvez donc la réutiliser pour une présentation Web ou une application console. N'oubliez pas qu'en fin de compte, certaines choses doivent être concrètes. Vos utilisateurs ne peuvent pas travailler avec une abstraction, ils ont besoin de quelque chose de concret. Les abstractions ne sont que des aides pour vous, le programmeur, pour garder le code extensible et maintenable. Vous devez savoir où vous avez besoin de votre code pour être flexible. Finalement, toutes les abstractions doivent donner naissance à quelque chose de concret.
Edit: Si vous voulez en savoir plus sur les techniques d'architecture et de conception qui peuvent fournir les meilleures pratiques, je vous suggère de lire la réponse @Catchops et de lire sur les pratiques SOLID sur wikipedia.
De plus, pour commencer, je recommande toujours le livre suivant: Head First Design Patterns . Il vous aidera à comprendre les techniques d'abstraction / les pratiques de conception OOP, plus encore que le livre GoF (qui est excellent, il ne convient tout simplement pas aux débutants).
la source
Vous avez certainement raison. Et non, vous "essayez très bien" :)
En savoir plus sur le principe de responsabilité unique
Votre fonctionnement interne de la classe et la façon dont ces informations doivent être présentées à l'utilisateur sont deux responsabilités.
N'ayez pas peur de découpler les cours. Rarement le problème est trop d'abstraction et de découplage :)
Deux modèles très pertinents sont Model – view – controller pour les applications Web et Model View ViewModel pour Silverlight / WPF.
la source
J'utilise beaucoup MVVM et à mon avis, une classe d'objets métier ne devrait jamais avoir besoin de connaître quoi que ce soit sur l'interface utilisateur. Bien sûr, ils pourraient avoir besoin de connaître le
SelectedItem
, ouIsChecked
, ouIsVisible
, etc., mais ces valeurs n'ont pas besoin d'être liées à une interface utilisateur particulière et peuvent être des propriétés génériques de la classe.Si vous devez faire quelque chose à l'interface dans le code derrière, comme définir le focus, exécuter une animation, gérer les raccourcis clavier, etc., le code doit faire partie du code derrière de l'interface utilisateur, pas vos classes de logique métier.
Je dirais donc, n'arrêtez pas d'essayer de séparer votre interface utilisateur et vos classes. Plus ils sont découplés, plus ils sont faciles à entretenir et à tester.
la source
Il existe un certain nombre de modèles de conception éprouvés qui ont été développés au fil des ans pour répondre exactement à ce dont vous parlez. D'autres réponses à votre question ont fait référence au principe de responsabilité unique - qui est absolument valable - et à ce qui semble motiver votre question. Ce principe stipule simplement qu'une classe doit bien faire une chose. En d'autres termes, augmenter la cohésion et abaisser le couplage, c'est ce qu'est une bonne conception orientée objet - une classe fait-elle bien une chose, et n'a pas beaucoup de dépendances sur les autres.
Eh bien ... vous avez raison de remarquer que si vous voulez dessiner un cercle sur un iPhone, ce sera différent que d'en dessiner un sur un PC sous Windows. Vous DEVEZ avoir (dans ce cas) une classe concrète qui dessine un puits sur l'iPhone, et une autre qui dessine un puits sur un PC. C'est là que le contenu OO de l'héritage de base que tous ces exemples de formes décompose. Vous ne pouvez tout simplement pas le faire avec l'héritage seul.
C'est là qu'interviennent les interfaces - comme l' indique le livre Gang of Four (DOIT LIRE) - Toujours privilégier l'implémentation plutôt que l'héritage. En d'autres termes, utilisez des interfaces pour reconstituer une architecture qui peut exécuter diverses fonctions de nombreuses manières sans compter sur des dépendances codées en dur.
J'ai vu se référer aux principes SOLIDES . C’est génial. Le «S» est le principe de responsabilité unique. MAIS, le «D» signifie Dependency Inversion. Le modèle d'inversion de contrôle (injection de dépendance) peut être utilisé ici. Il est très puissant et peut être utilisé pour répondre à la question de savoir comment concevoir un système capable de dessiner un cercle pour un iPhone ainsi qu'un cercle pour le PC.
Il est possible de créer une architecture qui contient des règles métier et un accès aux données communs, mais avec différentes implémentations d'interfaces utilisateur utilisant ces constructions. Cela aide vraiment, cependant, d'avoir réellement fait partie d'une équipe qui l'a mis en œuvre et de le voir en action pour vraiment le comprendre.
Ceci est juste une réponse rapide de haut niveau à une question qui mérite une réponse plus détaillée. Je vous encourage à approfondir ces modèles. Une implémentation plus concrète de ces patters peut être trouvée sous les noms bien connus de MVC et MVVM.
Bonne chance!
la source
Dans ce cas, vous pouvez toujours utiliser MVC / MVVM et injecter différentes implémentations d'interface utilisateur à l'aide d'une interface commune:
De cette façon, vous pourrez réutiliser votre logique de contrôleur et de modèle tout en étant capable d'ajouter de nouveaux types de GUI.
la source
Il existe différents modèles pour ce faire: MVP, MVC, MVVM, etc ...
Un bel article de Martin Fowler (grand nom) à lire est Architectures GUI: http://www.martinfowler.com/eaaDev/uiArchs.html
MVP n'a pas encore été mentionné mais il mérite certainement d'être cité: jetez-y un œil.
C'est le modèle suggéré par les développeurs de Google Web Toolkit, c'est vraiment bien.
Vous pouvez trouver du vrai code, des exemples réels et des raisons pour lesquelles cette approche est utile ici:
http://code.google.com/webtoolkit/articles/mvp-architecture.html
http://code.google.com/webtoolkit/articles/mvp-architecture-2.html
L'un des avantages de suivre cette approche ou des approches similaires qui n'a pas été suffisamment souligné ici est la testabilité! Dans de nombreux cas, je dirais que c'est le principal avantage!
la source
C'est l'un des endroits où la POO ne parvient pas à faire un bon travail d'abstraction. Le polymorphisme OOP utilise la répartition dynamique sur une seule variable («ceci»). Si le polymorphisme était enraciné dans Shape, vous ne pouvez pas répartir polymorphiquement sur le moteur de rendu (console, interface graphique, etc.).
Considérons un système de programmation qui pourrait répartir sur deux ou plusieurs variables:
et supposons également que le système pourrait vous donner un moyen d'exprimer poly_draw pour diverses combinaisons de types Shape et de types Renderer. Il serait alors facile de trouver une classification des formes et des rendus, non? Le vérificateur de type vous aiderait en quelque sorte à déterminer s'il y avait des combinaisons de formes et de rendus que vous avez peut-être manqué d'implémenter.
La plupart des langages POO ne prennent en charge rien de tel que ci-dessus (certains le font, mais ils ne sont pas courants). Je vous suggère de jeter un œil au modèle de visiteur pour une solution de contournement.
la source
Les sons ci-dessus me conviennent. À ma connaissance, on peut dire que cela signifie un couplage relativement étroit entre le contrôleur et la vue en termes de modèle de conception MVC. Cela signifie également que pour basculer entre desktop-console-webapp, il faudra en conséquence basculer à la fois Controller et View en tant que paire - seul le modèle reste inchangé.
Eh bien, mon point de vue actuel est que ce couplage View-Controller dont nous parlons est OK et encore plus, il est plutôt tendance dans un design moderne.
Cependant, il y a un an ou deux, je n'étais pas aussi sûr de cela. J'ai changé d'avis après avoir étudié les discussions du forum Sun sur les modèles et la conception OO.
Si vous êtes intéressé, essayez ce forum vous-même - il a migré vers Oracle maintenant ( lien ). Si vous y arrivez, essayez de cingler le gars Saish - à l'époque, ses explications sur ces questions délicates m'ont été très utiles. Je ne peux pas dire s'il participe toujours - moi-même, je n'y suis pas depuis longtemps
la source
D'un point de vue pragmatique, du code dans votre système doit savoir comment dessiner quelque chose comme un
Rectangle
si c'est une exigence de l'utilisateur. Et cela va se résumer à un moment donné à faire des choses de très bas niveau comme la pixellisation des pixels ou l'affichage de quelque chose dans une console.Du point de vue du couplage, la question est de savoir qui / quoi devrait dépendre de ce type d'informations et à quel degré de détail (dans quelle mesure, par exemple, abstrait)?
Résumé des capacités de dessin / rendu
Parce que si le code de dessin de niveau supérieur ne dépend que de quelque chose de très abstrait, cette abstraction pourrait fonctionner (par substitution d'implémentations concrètes) sur toutes les plates-formes que vous avez l'intention de cibler. À titre d'exemple artificiel, une
IDrawer
interface très abstraite peut être capable d'être implémentée dans la console et les API GUI pour faire des choses comme des formes de tracé (l'implémentation de la console peut traiter la console comme une "image" 80xN avec l'art ASCII). Bien sûr, c'est un exemple artificiel car ce n'est généralement pas ce que vous voulez faire, c'est traiter une sortie de console comme un tampon image / frame; généralement, la plupart des besoins des utilisateurs exigent davantage d'interactions textuelles dans les consoles.Une autre considération est à quel point est-il facile de concevoir une abstraction stable? Parce que cela pourrait être facile si tout ce que vous visez est des API GUI modernes pour résumer les capacités de base de dessin de formes comme les lignes de tracé, les rectangles, les chemins, le texte, les choses de ce genre (juste une rastérisation 2D simple d'un ensemble limité de primitives) , avec une interface abstraite qui peut être facilement implémentée pour eux tous à travers différents sous-types à peu de frais. Si vous pouvez concevoir une telle abstraction efficacement et l'implémenter sur toutes les plates-formes cibles, je dirais que c'est un mal bien moindre, voire un mal du tout, pour une forme ou un contrôle graphique ou quoi que ce soit pour savoir comment se dessiner en utilisant un tel abstraction.
Mais disons que vous essayez d'abstraire les détails sanglants qui varient entre une Playstation Portable, un iPhone, une XBox One et un PC de jeu puissant tandis que vos besoins sont d'utiliser les techniques de rendu / d'ombrage 3D en temps réel les plus avancées sur chacun . Dans ce cas, essayer de proposer une interface abstraite pour résumer les détails de rendu lorsque les capacités matérielles et les API sous-jacentes varient si énormément est presque certain de générer un temps énorme de conception et de reconception, une forte probabilité de modifications de conception récurrentes avec des imprévus découvertes, et également une solution de dénominateur commun le plus bas qui ne parvient pas à exploiter le caractère unique et la puissance du matériel sous-jacent.
Faire passer les dépendances vers des conceptions stables et "faciles"
Dans mon domaine, je suis dans ce dernier scénario. Nous ciblons de nombreux matériels différents avec des capacités et des API sous-jacentes radicalement différentes, et essayer de proposer une seule abstraction de rendu / dessin pour les gouverner tous est sans espoir (nous pourrions devenir mondialement célèbre en le faisant efficacement car ce serait un jeu). changeur dans l'industrie). Donc, la dernière chose que je veux dans mon cas est comme l'analogique
Shape
ouModel
ouParticle Emitter
qui sait se dessiner, même s'il exprime ce dessin de la manière la plus élevée et la plus abstraite possible ...... parce que ces abstractions sont trop difficiles à concevoir correctement, et quand un design est difficile à obtenir correctement, et que tout en dépend, c'est une recette pour les changements de conception centrale les plus coûteux qui ondulent et cassent tout en fonction. Donc, la dernière chose que vous voulez, c'est que les dépendances de vos systèmes se dirigent vers des conceptions abstraites trop difficiles à obtenir (trop difficiles à stabiliser sans changements intrusifs).
Difficile dépend de facile, pas facile dépend de difficile
Donc, ce que nous faisons à la place, c'est que les dépendances se dirigent vers des choses faciles à concevoir. Il est beaucoup plus facile de concevoir un "modèle" abstrait qui se concentre uniquement sur le stockage de choses comme les polygones et les matériaux et d'obtenir ce design correct que de concevoir un "rendu" abstrait qui peut être efficacement mis en œuvre (via des sous-types concrets substituables) pour dessiner le dessin. demande uniformément un matériel aussi disparate qu'une PSP à partir d'un PC.
Nous inversons donc les dépendances loin des choses difficiles à concevoir. Au lieu de faire en sorte que les modèles abstraits sachent se dessiner sur un design de rendu abstrait dont ils dépendent tous (et casser leurs implémentations si ce design change), nous avons plutôt un rendu abstrait qui sait comment dessiner chaque objet abstrait de notre scène ( modèles, émetteurs de particules, etc.), et ainsi nous pouvons ensuite implémenter un sous-type de rendu OpenGL pour les PC comme
RendererGl
, un autre pour les PSP commeRendererPsp
, un autre pour les téléphones mobiles, etc. Dans ce cas, les dépendances se dirigent vers des conceptions stables, faciles à corriger, du rendu à différents types d'entités (modèles, particules, textures, etc.) dans notre scène, et non l'inverse.Si vous essayez d'abstraire quelque chose au niveau central de votre base de code et que c'est trop difficile à concevoir, au lieu de vous battre obstinément la tête contre les murs et d'y apporter constamment des changements intrusifs chaque mois / année, ce qui nécessite la mise à jour de 8000 fichiers source car c'est briser tout ce qui en dépend, ma suggestion numéro un est d'envisager d'inverser les dépendances. Voyez si vous pouvez écrire le code d'une manière telle que la chose qui est si difficile à concevoir dépend de tout le reste qui est plus facile à concevoir, ne pas avoir les choses qui sont plus faciles à concevoir en fonction de la chose qui est si difficile à concevoir. Notez que je parle de conceptions (en particulier de conceptions d'interfaces) et non d'implémentations: parfois les choses sont faciles à concevoir et difficiles à implémenter, et parfois les choses sont difficiles à concevoir mais faciles à mettre en œuvre. Les dépendances se déplacent vers les conceptions, donc l'accent ne devrait être mis que sur la difficulté de concevoir quelque chose ici pour déterminer la direction dans laquelle les dépendances se déplacent.
Principe de responsabilité unique
Pour moi, SRP n'est pas si intéressant ici habituellement (bien que cela dépende du contexte). Je veux dire qu'il y a un acte d'équilibre sur la corde raide dans la conception de choses claires dans leur objectif et maintenables, mais vos
Shape
objets peuvent avoir à exposer des informations plus détaillées s'ils ne savent pas comment se dessiner, par exemple, et il peut ne pas y avoir beaucoup de choses significatives à faire avec une forme dans un contexte d'utilisation particulier que de la construire et de la dessiner. Il y a des compromis avec à peu près tout, et ce n'est pas lié à la SRP qui peut rendre les choses conscientes de la façon de se dessiner capables de devenir un tel cauchemar de maintenance dans mon expérience dans certains contextes.Cela a beaucoup plus à voir avec le couplage et la direction dans laquelle les dépendances circulent dans votre système. Si vous essayez de porter une interface de rendu abstraite dont tout dépend (car ils l'utilisent pour se dessiner) vers une nouvelle API / matériel cible et que vous réalisez que vous devez modifier considérablement sa conception pour qu'elle fonctionne efficacement là-bas, alors c'est un changement très coûteux à effectuer qui nécessite de remplacer les implémentations de tout ce qui sait se dessiner dans votre système. Et c'est le problème de maintenance le plus pratique que je rencontre avec des choses qui savent comment se dessiner si cela se traduit par une charge de dépendances qui coule vers des abstractions qui sont trop difficiles à concevoir correctement à l'avance.
Développeur Pride
Je mentionne ce point parce que, selon mon expérience, c'est souvent le plus grand obstacle à la direction des dépendances vers des choses plus faciles à concevoir. Il est très facile pour les développeurs de devenir un peu ambitieux ici et de dire: "Je vais concevoir l'abstraction de rendu multiplateforme pour les gouverner tous, je vais résoudre ce que les autres développeurs passent des mois à porter, et je vais obtenir il va bien et ça va fonctionner comme par magie sur chaque plate-forme unique que nous prenons en charge et utiliser les techniques de rendu de pointe sur chacun; je l'ai déjà imaginé dans ma tête. "Dans ce cas, ils résistent à la solution pratique qui consiste à éviter de faire cela et à inverser simplement la direction des dépendances et à traduire ce qui pourrait être extrêmement coûteux et les modifications récurrentes de la conception centrale en des changements récurrents simplement bon marché et locaux à la mise en œuvre. Il doit y avoir une sorte d'instinct de "drapeau blanc" chez les développeurs pour abandonner quand quelque chose est trop difficile à concevoir à un niveau aussi abstrait et reconsidérer toute leur stratégie, sinon ils sont dans une situation de chagrin et de douleur. Je suggérerais de transférer de telles ambitions et un esprit combatif vers des implémentations de pointe d'une chose plus facile à concevoir que de prendre de telles ambitions de conquête du monde au niveau de la conception d'interface.
la source
OpenGLBillboard
, vous feriez unOpenGLRenderer
qui sait dessiner n'importe quel type deIBillBoard
? Mais le ferait-il en déléguant la logique àIBillBoard
, ou aurait-il d'énormes commutateurs ouIBillBoard
types avec des conditions? C'est ce que je trouve difficile à comprendre, car cela ne semble pas du tout maintenable!RendererPsp
qui connaît les abstractions de haut niveau de votre scène de jeu, il peut alors faire toute la magie et les backflips dont il a besoin pour rendre ces choses d'une manière qui semble convaincante sur la PSP ....BillBoard
serait vraiment difficile de faire des objets de bas niveau tels que ceux qui en dépendent? Alors qu'unIRenderer
niveau est déjà élevé et pourrait dépendre de ces préoccupations avec beaucoup moins de difficultés?