Est-ce une mauvaise pratique de passer des instances à travers plusieurs couches?

60

Dans la conception de mon programme, j'arrive souvent au point où je dois passer des instances d'objet à travers plusieurs classes. Par exemple, si j’ai un contrôleur qui charge un fichier audio, puis le passe à un lecteur, et le joueur le transmet au lecteurRunnable, qui le repasse à nouveau ailleurs, etc. savoir comment l'éviter. Ou est-ce bien de faire ça?

EDIT: Peut-être que l'exemple du lecteur n'est pas le meilleur car je pourrais charger le fichier plus tard, mais dans d'autres cas cela ne fonctionne pas.

Puckl
la source

Réponses:

54

Comme d'autres l'ont mentionné, ce n'est pas nécessairement une mauvaise pratique, mais vous devez faire attention à ne pas rompre la séparation des préoccupations entre les couches et à ne pas passer d'instances spécifiques à une couche entre les couches. Par exemple:

  • Les objets de base de données ne doivent jamais être transmis aux couches supérieures. J'ai vu des programmes utiliser la classe DataAdapter .NET , une classe d'accès à la base de données, et la transmettre à la couche d'interface utilisateur, plutôt que d'utiliser DataAdapter dans le DAL, de créer un DTO ou un jeu de données et de le transmettre. L'accès à la base de données est le domaine du DAL.
  • Les objets d'interface utilisateur doivent bien entendu être limités à la couche d'interface utilisateur. Encore une fois, j’ai vu cela violé, à la fois avec des ListBoxes remplies de données utilisateur transmises à la couche BL, au lieu d’un tableau / DTO de son contenu et (un de mes favoris en particulier), une classe DAL qui récupère des données hiérarchiques à partir de. la base de données et plutôt que de renvoyer une structure de données hiérarchique, elle vient de créer et de renseigner un objet TreeView, puis de le renvoyer à l'interface utilisateur pour l'ajouter dynamiquement à un formulaire.

Cependant, si les instances que vous passez sont les DTO ou les entités elles-mêmes, c'est probablement correct.

Avner Shahar-Kashtan
la source
1
Cela peut paraître choquant, mais dans les premiers jours sombres de .NET, c'était la pratique généralement recommandée et était probablement meilleure que ce que font la plupart des autres piles.
Wyatt Barnett
1
Je ne suis pas d'accord. Il est vrai que Microsoft a approuvé la pratique des applications à couche unique dans lesquelles le client Winforms a également accédé à la base de données. Le DataAdapter a été ajouté directement au formulaire en tant que contrôle invisible, mais il ne s'agit que d'une architecture spécifique, différente de la configuration à plusieurs niveaux de l'OP. . Mais dans une architecture multicouche, et cela était vrai pour VB6 / DNA même avant .NET, les objets de base de données restaient dans la couche de base de données.
Avner Shahar-Kashtan
Pour clarifier: avez-vous vu des personnes accéder directement à l'interface utilisateur (dans votre exemple, les listes de sélection) à partir de leur "couche de données"? Je ne pense pas avoir rencontré une telle violation dans le code de production .. wow ..
Simon Whitehead
7
@SimonWhitehead Exactement. Les gens qui avaient des doutes sur la distinction entre un ListBox et un tableau et utilisaient des ListBox comme DTO. C’est un moment qui m’a aidé à comprendre combien d’hypothèses invisibles que je pose ne sont pas intuitives pour les autres.
Avner Shahar-Kashtan
1
@SimonWhitehead - Oui, j'ai déjà vu cela dans les programmes VB6 et VB.NET Framework 1.1 et 2.0 et j'ai été chargé de maintenir de tels monstres. Ça devient très moche, très vite.
jfrankcarr le
15

Intéressant que personne n'ait encore parlé d' objets immuables . Je dirais que le fait de passer d'un objet immuable à travers toutes les couches est une bonne chose plutôt que de créer beaucoup d'objets de courte durée pour chaque couche.

Eric Lippert discute de l'immutabilité sur son blog

D'autre part, je dirais que passer des objets mutables entre les couches est une mauvaise conception. Vous construisez essentiellement un calque avec la promesse que les calques environnants ne le modifieront pas de manière à casser votre code.

M Afifi
la source
13

Faire passer des instances d'objet est une chose normale à faire. Cela réduit la nécessité de conserver l'état (c'est-à-dire les variables d'instance) et dissocie le code de son contexte d'exécution.

Un problème auquel vous pouvez être confronté est le refactoring, lorsque vous devez modifier les signatures de plusieurs méthodes le long de la chaîne d’appel en réponse à la modification des exigences de paramètre d’une méthode située au bas de cette chaîne. Cependant, cela peut être atténué avec l'utilisation d'outils de développement de logiciels modernes qui facilitent le refactoring.

dasblinkenlight
la source
6
Je dirais qu'un autre problème que vous pourriez rencontrer concerne l'immutabilité. Je me souviens d’un bogue très déroutant dans un projet sur lequel j’ai travaillé: un développeur a modifié un DTO sans penser au fait que sa classe en particulier n’était pas la seule à faire référence à cet objet.
Phil
8

Peut-être mineure, mais il y a le risque d'affecter cette référence quelque part dans l'une des couches, ce qui pourrait éventuellement causer une fuite de mémoire ou une référence en suspens.

techfoobar
la source
Votre remarque est correcte, mais d'après la terminologie d'OP ("instances d'objet passantes"), j'ai l'impression qu'il transmet des valeurs (et non des pointeurs) ou qu'il parlait d'un environnement mal rempli (Java, C #, Python, Go, ...). ..).
Mohammad Dehghan
7

Si vous transmettez des objets simplement parce que vous en avez besoin dans une zone distante de votre code, l'utilisation de modèles de conception d'injection de contrôle et de dépendance ainsi que, éventuellement, d'un conteneur IoC approprié peut résoudre de manière satisfaisante les problèmes liés au transport d'instances d'objet. Je l'ai utilisé sur un projet de taille moyenne et je ne songerais plus jamais à écrire un gros morceau de code serveur sans l'utiliser.

Steven Schlansker
la source
Cela semble intéressant, j’utilise déjà l’injection de constructeur et je pense que mes composants de haut niveau contrôlent les composants de bas niveau. Comment utilisez-vous un conteneur IOC pour éviter de transporter des instances?
Puckl
Je pense avoir trouvé la réponse ici: martinfowler.com/articles/injection.html
Puckl
1
Remarque secondaire, si vous travaillez en Java, Guice est vraiment sympa. Vous pouvez également affecter vos liaisons à des requêtes, de sorte que le composant de haut niveau crée la portée et lie les instances aux classes appropriées à ce moment-là.
Dave
4

Passer des données dans un ensemble de couches n'est pas une mauvaise chose, c'est vraiment la seule façon pour un système en couches de fonctionner sans violer la structure en couches. Le problème, c’est que vous transmettez vos données à plusieurs objets du même calque pour atteindre votre objectif.

Ryathal
la source
3

Réponse rapide: Il n’ya rien de mal à passer des instances d’objets. Comme mentionné également, il convient de ne pas attribuer cette référence à toutes les couches, ce qui pourrait entraîner une perte de référence ou des fuites de mémoire.

Dans nos projets, nous utilisons cette pratique pour faire passer des objets DTO (objet de transfert de données) entre les couches et cette pratique est très utile. Nous réutilisons également nos objets dto pour construire une fois plus complexe, comme pour un résumé.

EL Yusubov
la source
3

Je suis avant tout un développeur d'interface utilisateur Web, mais il me semble que votre malaise intuitif réside peut-être moins dans la transmission d'instance que dans le fait que vous utilisez un peu de procédure avec ce contrôleur. Votre contrôleur doit-il transpirer tous ces détails? Pourquoi fait-il même référence au nom de plus d'un autre objet pour obtenir de l'audio lu?

Dans la conception POO, j'ai tendance à penser en termes de ce qui est toujours vert et de ce qui est plus susceptible d'être sujet à changement. Le sujet du changement est ce que vous allez vouloir mettre dans vos boîtes à objets plus grandes afin que vous puissiez maintenir des interfaces cohérentes même lorsque les joueurs changent ou que de nouvelles options sont ajoutées. Ou vous vous trouvez vouloir échanger des objets audio ou des composants en gros.

Dans ce cas, votre contrôleur doit déterminer qu’il est nécessaire de lire un fichier audio, puis de le faire de manière cohérente / permanente. Le lecteur audio, par contre, pourrait facilement changer à mesure que la technologie et les plates-formes sont modifiées ou que de nouveaux choix sont ajoutés. Tous ces détails doivent figurer sous l'interface d'un objet composite plus grand, IMO, et vous ne devriez pas avoir à réécrire votre contrôleur lorsque les détails de la manière dont l'audio est lu changent. Ensuite, lorsque vous transmettez une instance d'objet avec les détails tels que l'emplacement du fichier dans l'objet plus grand, tout échange est effectué à l'intérieur d'un contexte approprié dans lequel une personne est moins susceptible de faire quelque chose de stupide avec elle.

Donc, dans ce cas, je ne pense pas que ce soit cette instance d'objet renvoyée qui pourrait vous déranger. C’est que le capitaine Picard se rend dans la salle des machines pour allumer le noyau de chaîne, puis remonte sur le pont pour tracer les coordonnées, puis appuie sur le bouton «Coup de poing» après avoir activé les boucliers au lieu de nous à la planète X à la chaîne 9. Faites-le ainsi. " et laisser son équipe régler les détails. Parce que, lorsqu'il le gère de cette manière, il peut commander n'importe quel navire de la flotte sans connaître la configuration de chaque navire et son fonctionnement. Et c’est finalement la plus grande victoire de conception POO pour laquelle tirer, IMO.

Erik Reppen
la source
2

C'est une conception assez commune qui finit par se produire, même si vous pouvez avoir des problèmes de latence si votre application est sensible à ce genre de chose.

James
la source
2

Ce problème peut être résolu avec des variables à portée dynamique, si votre langue les utilise, ou un stockage local au thread. Ces mécanismes nous permettent d'associer des variables personnalisées à une chaîne d'activation ou à un thread de contrôle, de sorte que nous ne soyons pas obligés de transférer ces valeurs dans un code qui n'a rien à voir avec elles, simplement pour pouvoir les communiquer avec un autre code. qui en a besoin.

Kaz
la source
2

Comme les autres réponses l'ont souligné, ce n'est pas une conception intrinsèquement médiocre. Il peut créer un couplage étroit entre les classes imbriquées et celles qui les imbriquent, mais le desserrage du couplage peut ne pas être une option valide si l'imbrication des références fournit une valeur à la conception.

Une solution possible consiste à "aplatir" les références imbriquées dans la classe du contrôleur.

Au lieu de transmettre plusieurs fois un paramètre via des objets imbriqués, vous pouvez conserver dans le contrôleur des références à tous les objets imbriqués.

La façon dont cela est mis en œuvre (ou même s’il s’agit même d’une solution valable) dépend de la conception actuelle du système, telle que:

  • Pouvez-vous conserver une sorte de carte des objets imbriqués dans le contrôleur sans que cela devienne trop compliqué?
  • Lorsque vous transmettez le paramètre à l'objet imbriqué approprié, celui-ci peut-il reconnaître immédiatement le paramètre ou existait-il une fonctionnalité supplémentaire lors de sa transmission dans les objets imbriqués?
  • etc.

C'est un problème que j'ai rencontré dans un modèle de conception MVC pour un client GXT. Nos composants d'interface graphique contenaient des composants d'interface graphique imbriqués pour plusieurs couches. Lorsque les données du modèle ont été mises à jour, nous avons fini par les faire passer à travers plusieurs couches jusqu'à atteindre le ou les composants appropriés. Cela créait un couplage indésirable entre les composants de l'interface graphique, car si nous souhaitions qu'une nouvelle classe de composant d'interface graphique accepte les données de modèle, nous devions créer des méthodes pour mettre à jour les données de modèle dans tous les composants d'interface graphique contenant la nouvelle classe.

Pour résoudre ce problème, nous avons conservé dans la classe View une mappe de références à tous les composants d'interface graphique imbriqués afin que, chaque fois que les données du modèle étaient mises à jour, la vue puisse envoyer les données de modèle mises à jour directement aux composants d'interface graphique qui en avaient besoin, fin du récit. . Cela a bien fonctionné car il n'y avait qu'une seule instance de chaque composant d'interface graphique. Je pouvais voir que cela ne fonctionnait pas très bien s'il y avait plusieurs instances de certains composants de l'interface graphique, ce qui rendait difficile l'identification de la copie à mettre à jour.

David Kaczynski
la source
0

Ce que vous décrivez est un modèle de conception appelé chaîne de responsabilité . Apple utilise ce modèle pour son système de gestion des événements, à sa juste valeur.

utilisateur8865
la source