Couplage lâche dans la conception orientée objet

16

J'essaie d'apprendre GRASP et j'ai trouvé cela expliqué ( ici à la page 3 ) sur le couplage bas et j'ai été très surpris quand j'ai trouvé ceci:

Considérez la méthode addTrackpour une Albumclasse, deux méthodes possibles sont:

addTrack( Track t )

et

addTrack( int no, String title, double duration )

Quelle méthode réduit le couplage? La seconde le fait, car la classe utilisant la classe Album n'a pas besoin de connaître une classe Track. En général, les paramètres des méthodes doivent utiliser les types de base (int, char ...) et les classes des packages java. *.

J'ai tendance à ne pas être d'accord avec cela; Je pense que addTrack(Track t)c'est mieux que addTrack(int no, String title, double duration)pour diverses raisons:

  1. Il est toujours préférable pour une méthode d'avoir le moins de paramètres possible (selon le code propre de l'oncle Bob, aucun ou un de préférence, 2 dans certains cas et 3 dans des cas spéciaux; plus de 3 doivent être refactorisés - ce sont bien sûr des recommandations et non des règles holistiques) .

  2. Si addTrackest une méthode d'une interface, et que les conditions requises Trackdoivent avoir plus d'informations (par exemple l'année ou le genre), l'interface doit être modifiée et la méthode doit donc prendre en charge un autre paramètre.

  3. L'encapsulation est rompue; si addTrackest dans une interface, alors il ne devrait pas connaître les internes du Track.

  4. Il est en fait plus couplé dans la deuxième manière, avec de nombreux paramètres. Supposons que le noparamètre doit être changé de intà longcar il y a plus de MAX_INTpistes (ou pour une raison quelconque); alors la Tracket la méthode doivent être changées tandis que si la méthode était la addTrack(Track track)seule Trackserait changée.

Tous les 4 arguments sont en fait liés les uns aux autres, et certains d'entre eux sont des conséquences d'autres.

Quelle approche est la meilleure?

m3th0dman
la source
2
S'agit-il d'un document élaboré par un professeur ou un formateur? Sur la base de l'URL du lien que vous avez fourni, il semble que c'était pour une classe, bien que je ne vois aucun crédit dans le document quant à qui l'a créé. Si cela faisait partie d'un cours, je vous suggère de poser ces questions à la personne qui a fourni le document. Soit dit en passant, je suis d'accord avec votre raisonnement - il me semble évident qu'une classe Album voudrait connaître intrinsèquement une classe Track.
Derek
Honnêtement, chaque fois que je lis sur les "meilleures pratiques", je les prends avec un grain de sel!
AraK
@Derek J'ai trouvé le document en recherchant sur Google "exemple de modèles de saisie"; Je ne sais pas qui l'a écrit, mais comme il provenait d'une université, je pense qu'il est fiable. Je cherche un exemple basé sur les informations fournies et en ignorant la source.
m3th0dman
4
@ m3th0dman "mais comme il provenait d'une université, je pense que c'est fiable." Pour moi parce que ça vient d'une université, je le considère peu fiable. Je ne fais pas confiance à quelqu'un qui n'a pas travaillé sur des projets pluriannuels et qui parle des meilleures pratiques en développement logiciel.
AraK
1
@AraK Reliable ne signifie pas incontestable; et c'est ce que je fais ici, le questionnant.
m3th0dman

Réponses:

15

Eh bien, vos trois premiers points concernent en fait d'autres principes que le couplage. Vous devez toujours trouver un équilibre entre des principes de conception souvent contradictoires.

Votre quatrième point est au sujet de couplage, et je suis d' accord avec vous fortement. Le couplage concerne le flux de données entre les modules. Le type de conteneur dans lequel les données circulent est largement immatériel. Passer une durée sous la forme d'un double au lieu d'un champ d'un Trackne supprime pas la nécessité de la passer. Les modules doivent toujours partager la même quantité de données et ont toujours la même quantité de couplage.

Il ne parvient pas non plus à considérer tous les couplages du système comme un agrégat. Bien que l'introduction d'une Trackclasse ajoute certes une autre dépendance entre deux modules individuels, elle peut réduire considérablement le couplage du système , ce qui est la mesure importante ici.

Par exemple, considérez un bouton "Ajouter à la liste de lecture" et un Playlistobjet. L'introduction d'un Trackobjet peut être envisagée pour augmenter le couplage si vous ne considérez que ces deux objets. Vous avez maintenant trois classes interdépendantes au lieu de deux. Cependant, ce n'est pas l'intégralité de votre système. Vous devez également importer la piste, lire la piste, afficher la piste, etc. L'ajout d'une classe de plus à ce mixage est négligeable.

Envisagez maintenant de devoir ajouter la prise en charge de la lecture des pistes sur le réseau plutôt que localement. Il vous suffit de créer un NetworkTrackobjet conforme à la même interface. Sans l' Trackobjet, il faudrait créer partout des fonctions comme:

addNetworkTrack(int no, string title, double duration, URL location)

Cela double efficacement votre couplage, nécessitant même des modules qui ne se soucient pas des éléments spécifiques au réseau pour néanmoins en garder la trace, afin de pouvoir le transmettre.

Votre test d'effet d'entraînement est un bon test pour déterminer votre véritable quantité de couplage. Ce qui nous préoccupe, c'est de limiter les endroits où un changement affecte.

Karl Bielefeldt
la source
1
+ Le couplage aux primitives est toujours le couplage, quelle que soit la façon dont il est découpé.
JustinC
+1 pour avoir mentionné l'option Ajouter une option d'URL / effet d'entraînement.
user949300
4
+1 Une lecture intéressante à ce sujet serait également la discussion du principe d'inversion de dépendance dans DIP dans la nature où l'utilisation de types primitifs est en fait considérée comme une "odeur" d' obsession primitive avec Value Object comme correctif. Pour moi, il semble qu'il serait préférable de passer un objet Track qu'un ensemble de types primitifs ... Et si vous voulez éviter la dépendance / le couplage avec des classes spécifiques, utilisez des interfaces.
Marjan Venema
Réponse acceptée en raison d'une belle explication sur la différence entre le couplage du système total et le couplage des modules.
m3th0dman
10

Ma recommandation est:

Utilisation

addTrack( ITrack t )

mais assurez-vous qu'il ITracks'agit d'une interface et non d'une classe concrète.

Album ne connaît pas les composants internes des ITrackimplémenteurs. Il est uniquement couplé au contrat défini par la ITrack.

Je pense que c'est la solution qui génère le moins de couplage.

Tulains Córdova
la source
1
Je crois que Track est juste un simple objet de transfert de données / bean, où il n'a que des champs et des getters / setters sur eux; une interface est-elle requise dans ce cas?
m3th0dman
6
Obligatoire? Probablement pas. Suggestif, oui. La signification concrète d'une piste peut et va évoluer, mais ce que la classe consommatrice en attend ne le sera probablement pas.
JustinC
2
@ m3th0dman Dépend toujours des abstractions, pas des concrétions. Cela s'applique indépendamment du Trackfait d' être stupide ou intelligent. Trackest une concrétion. ITrackl'interface est une abstraction. De cette façon, vous pourrez avoir différents types de pistes à l'avenir, à condition qu'elles soient conformes ITrack.
Tulains Córdova
4
Je suis d'accord avec l'idée, mais je perds le préfixe «I». Extrait de Clean Code, de Robert Martin, page 24: "Le précédent I, si courant dans les liasses héritées d'aujourd'hui, est au mieux une distraction et au pire trop d'informations. Je ne veux pas que mes utilisateurs sachent que je leur remets un interface."
Benjamin Brumfield
1
@BenjaminBrumfield Vous avez raison. Je n'aime pas non plus le préfixe, bien que je laisse dans la réponse pour plus de clarté.
Tulains Córdova
4

Je dirais que le deuxième exemple de méthode augmente très probablement le couplage, car il est très probable qu'il instancie un objet Track et le stocke dans l'objet Album actuel. (Comme suggéré dans mon commentaire ci-dessus, je suppose qu'il est inhérent qu'une classe Album ait le concept d'une classe Track quelque part à l'intérieur.)

Le premier exemple de méthode suppose qu'une piste est instanciée en dehors de la classe Album, donc à tout le moins, nous pouvons supposer que l' instanciation de la classe Track n'est pas couplée à la classe Album.

Si les meilleures pratiques suggéraient que nous n'avons jamais une référence de classe à une seconde classe, l'intégralité de la programmation orientée objet serait rejetée par la fenêtre.

Derek
la source
Je ne vois pas comment le fait d'avoir une référence implicite à une autre classe la rend plus couplée que d'avoir une référence explicite. Dans les deux cas, les deux classes sont couplées. Je pense qu'il vaut mieux que le couplage soit explicite, mais je ne pense pas qu'il soit "plus" couplé de toute façon.
TMN
1
@TMN, le couplage supplémentaire est dans la façon dont j'implique que le deuxième exemple finirait probablement par créer en interne un nouvel objet Track. L'instanciation de l'objet est couplée à une méthode qui, autrement, devrait simplement ajouter un objet Track à une sorte de liste dans l'objet Album (brisant le principe de responsabilité unique). Si la façon dont la piste est créée doit être modifiée, la méthode addTrack () devra également être modifiée. Ce n'est pas le cas dans le premier exemple.
Derek
3

Le couplage n'est qu'un des nombreux aspects à essayer d'obtenir dans votre code. En réduisant le couplage, vous n'améliorez pas nécessairement votre programme. En général, c'est une meilleure pratique, mais dans ce cas particulier, pourquoi ne devrait-on pas Trackle savoir?

En utilisant une Trackclasse à laquelle passer Album, vous rendez votre code plus facile à lire, mais plus important encore, comme vous l'avez mentionné, vous transformez une liste statique de paramètres en un objet dynamique. Cela rend finalement votre interface beaucoup plus dynamique.

Vous mentionnez que l'encapsulation est rompue, mais ce n'est pas le cas. Albumdoit connaître les éléments internes de Track, et si vous n'utilisez pas un objet, Albumdevrait connaître chaque élément d'information qui lui est transmis avant de pouvoir l'utiliser tout de même. L'appelant doit également connaître les éléments internes Track, car il doit construire un Trackobjet, mais l'appelant doit tout de même connaître ces informations si elles sont transmises directement à la méthode. En d'autres termes, si l'avantage de l'encapsulation n'est pas de connaître le contenu d'un objet, il ne pourrait pas être utilisé dans ce cas car il Albumdoit utiliser Trackles informations de la même façon.

Si vous ne souhaitez pas utiliser, Trackc'est si Trackcontient une logique interne à laquelle vous ne souhaitez pas que l'appelant ait accès. En d'autres termes, si Albumune classe devait utiliser un programmeur utilisant votre bibliothèque, vous ne voudriez pas qu'il l'utilise Tracksi vous l'utilisez pour dire, appelez une méthode pour la conserver dans la base de données. Le vrai problème avec cela réside dans le fait que l'interface est enchevêtrée avec le modèle.

Pour résoudre le problème, vous devez vous séparer Tracken ses composants d'interface et ses composants logiques, créant deux classes distinctes. Pour l'appelant, Trackdevient une classe légère qui est destinée à contenir des informations et à offrir des optimisations mineures (données calculées et / ou valeurs par défaut). À l'intérieur Album, vous utiliseriez une classe nommée TrackDAOpour effectuer le gros du travail associé à l'enregistrement des informations Trackdans la base de données.

Bien sûr, ce n'est qu'un exemple. Je suis sûr que ce n'est pas du tout votre cas, et n'hésitez donc pas à utiliser sans Trackculpabilité. N'oubliez pas de garder votre appelant à l'esprit lorsque vous construisez des classes et de créer des interfaces si nécessaire.

Neil
la source
3

Les deux sont corrects

addTrack( Track t ) 

est mieux (comme vous l'avez déjà fait valoir) tout en

addTrack( int no, String title, double duration ) 

est moins couplé car le code qui utilise addTrackn'a pas besoin de savoir qu'il existe une Trackclasse. La piste peut être renommée par exemple sans avoir à mettre à jour le code appelant.

Pendant que vous parlez de code plus lisible / maintenable, l'article parle de couplage . Un code moins couplé n'est pas nécessairement plus facile à implémenter et à comprendre.

k3b
la source
Voir l'argument 4; Je ne vois pas comment le second est moins couplé.
m3th0dman
3

Un faible couplage ne signifie pas aucun couplage. Quelque chose, quelque part, doit être connu sur les objets ailleurs dans la base de code, et plus vous réduisez la dépendance aux objets "personnalisés", plus vous donnez de raisons pour que le code change. Ce que l'auteur que vous citez promeut avec la deuxième fonction est moins couplé, mais aussi moins orienté objet, ce qui est contraire à l'idée de GRASP comme étant une méthodologie de conception orientée objet . L'essentiel est de savoir comment concevoir le système comme une collection d'objets et leurs interactions; les éviter, c'est comme vous apprendre à conduire une voiture en disant que vous devriez plutôt faire du vélo.

Au lieu de cela, la bonne voie consiste à réduire la dépendance à l'égard des objets concrets , qui est la théorie du «couplage lâche». Moins une méthode doit connaître de types concrets définis, mieux c'est. Juste par cette déclaration, la première option est en fait moins couplée, car la deuxième méthode prenant les types les plus simples doit connaître tous ces types plus simples. Bien sûr, ils sont intégrés et le code à l'intérieur de la méthode peut avoir à faire attention, mais la signature de la méthode et les appelants de la méthode ne le font certainement pas . La modification de l'un de ces paramètres relatifs à une piste audio conceptuelle va nécessiter plus de changements lorsqu'ils sont séparés par rapport à lorsqu'ils sont contenus dans un objet Track (qui est le point des objets; encapsulation).

En allant un peu plus loin, si Track devait être remplacé par quelque chose qui faisait mieux le même travail, une interface définissant les fonctionnalités requises serait peut-être en règle, un ITrack. Cela pourrait permettre différentes implémentations telles que "AnalogTrack", "CdTrack" et "Mp3Track" qui fournissaient des informations supplémentaires plus spécifiques à ces formats, tout en fournissant l'exposition de données de base d'ITrack qui représente conceptuellement une "piste"; un sous-morceau fini de l'audio. Track pourrait également être une classe de base abstraite, mais cela vous oblige à toujours vouloir utiliser l'implémentation inhérente à Track; réimplémentez-le comme BetterTrack et maintenant vous devez modifier les paramètres attendus.

Ainsi la règle d'or; les programmes et leurs composants de code auront toujours des raisons de changer. Vous ne pouvez pas écrire un programme qui ne nécessitera jamais de modifier le code que vous avez déjà écrit afin d'ajouter quelque chose de nouveau ou de modifier son comportement. Votre objectif, dans n'importe quelle méthodologie (GRASP, SOLID, tout autre acronyme ou mot à la mode auquel vous pouvez penser) est simplement d'identifier les choses qui devront changer au fil du temps, et de concevoir le système afin que ces changements soient aussi faciles à effectuer que possible (traduit; toucher le moins de lignes de code et affecter le moins possible d'autres domaines du système au-delà de la portée de la modification envisagée). Par exemple, ce qui est le plus susceptible de changer, c'est qu'une piste gagnera plus de membres de données dont addTrack () peut ou non se soucier, non cette piste sera remplacée par BetterTrack.

KeithS
la source