Concevoir une classe pour prendre des classes entières en tant que paramètres plutôt que des propriétés individuelles

Supposons, par exemple, que vous ayez une application avec une classe largement partagée appelée Utilisateur . Cette classe expose toutes les informations sur l'utilisateur, son identifiant, son nom, les niveaux d'accès à chaque module, son fuseau horaire, etc.

Les données utilisateur sont évidemment largement référencées dans tout le système, mais pour une raison quelconque, le système est configuré de telle sorte qu'au lieu de transmettre cet objet utilisateur à des classes qui en dépendent, nous ne transmettons que des propriétés individuelles.

Une classe qui requiert l'identifiant de l'utilisateur, demandera simplement le GUID userId en tant que paramètre. Parfois, nous aurons peut-être également besoin du nom d'utilisateur, qui sera donc transmis en tant que paramètre séparé. Dans certains cas, cela est transmis à des méthodes individuelles, les valeurs ne sont donc pas conservées au niveau de la classe.

Chaque fois que j'ai besoin d'accéder à une information différente de la classe User, je dois apporter des modifications en ajoutant des paramètres et lorsque l'ajout d'une nouvelle surcharge n'est pas approprié, je dois également modifier chaque référence au constructeur de la méthode ou de la classe.

L'utilisateur n'est qu'un exemple. Ceci est largement pratiqué dans notre code.

Ai-je raison de penser qu'il s'agit d'une violation du principe d'ouverture/fermeture? Pas simplement le fait de changer les classes existantes, mais de les établir en premier lieu de manière à ce que des changements généralisés soient très probablement nécessaires dans le futur?

Si nous venons de passer l'objet Utilisateur , je pourrais apporter un léger changement à la classe avec laquelle je travaille. Si je dois ajouter un paramètre, il se peut que je doive apporter des dizaines de modifications aux références à la classe.

Y a-t-il d'autres principes brisés par cette pratique? L'inversion de dépendance peut-être? Bien que nous ne référencions pas une abstraction, il n'y a qu'un seul type d'utilisateur, il n'est donc pas vraiment nécessaire de disposer d'une interface utilisateur.

Existe-t-il d'autres principes non SOLID violés, tels que les principes de base de la programmation défensive?

Mon constructeur devrait-il ressembler à ceci:

MyConstructor(GUID userid, String username)

Ou ca:

MyConstructor(User theUser)

Posté:

Il a été suggéré de répondre à la question dans "ID ou objet de passe?". Cela ne répond pas à la question de savoir comment la décision d'aller dans un sens ou dans l'autre affecte la tentative de suivre les principes SOLID, qui est au cœur de cette question.

30
ajouté édité
Vues: 1
La seconde forme est souvent utilisée lorsque le nombre de paramètres passés est devenu trop lourd.
ajouté l'auteur sgwill, source
Duplication possible de l’ identifiant de passe ou objet?
ajouté l'auteur Will Dean, source
@gnat: Ce n'est certainement pas un doublon. Le doublon possible concerne le chaînage de méthodes pour atteindre une hiérarchie d'objets profonde. Cette question ne semble pas poser de question à ce sujet.
ajouté l'auteur DenNukem, source
Le mot «analyser» n’a aucun sens dans le contexte dans lequel vous l’utilisez. Voulez-vous dire «passer» à la place?
ajouté l'auteur Mark Ransom, source
Les deux sont valables. Le coût de la création d'une instance valide d'utilisateur peut ne pas en valoir la peine dans certains cas où vous avez simplement besoin de l'id et du nom d'utilisateur. Le coût de la création d'une instance valide d'utilisateur et de sa transmission peut en valoir la peine. Ces coûts peuvent être la maintenance, les performances, la lisibilité, etc.
ajouté l'auteur chack, source
ajouté l'auteur gnat, source
Qu'en est-il du I dans SOLID ? MyConstructor dit maintenant "J'ai besoin d'un Guid et d'une chaîne ". Alors pourquoi ne pas avoir une interface fournissant un Guid et une chaîne , laissez utilisateur implémenter cette interface et laissez MyConstructor sur une instance implémentant cette interface? Et si les besoins de MyConstructor changent, changez l'interface. - Cela m'a beaucoup aidé de penser que les interfaces doivent "appartenir" au consommateur plutôt qu'au fournisseur . Pensez donc "en tant que consommateur , il me faut quelque chose qui fait ceci et cela" au lieu de "en tant que fournisseur , je peux faire ceci ou cela".
ajouté l'auteur stoeff, source
La première chose que je n'aime pas dans la première signature est qu'il n'y a aucune garantie que l'ID utilisateur et le nom d'utilisateur proviennent du même utilisateur. C'est un bug potentiel qui est évité en passant autour de l'utilisateur partout. Mais la décision dépend vraiment de ce que les méthodes appelées font avec les arguments.
ajouté l'auteur Minihawk, source
Il est difficile de dire avec certitude sans un exemple concret concret d’une classe avec MyConstructor , mais pour moi, cela semble être un problème de plus haut niveau - c’est-à-dire que vous pouvez éviter ce problème avec une meilleure encapsulation. Avec une bonne encapsulation et le respect du principe "Dis, ne demande pas", vous ne devriez pas avoir besoin de transmettre les valeurs de propriété d'un Utilisateur à un constructeur. En pratique, si cela n’est pas faisable, tout le reste sera un compromis.
ajouté l'auteur Ant P, source
ID de passe ou objet? En aucun cas, il n’explique comment on peut juger que la décision d’agir dans un sens ou dans l’autre repose sur les principes de conception de logiciels, ce qui est au cœur de cette question.
ajouté l'auteur Jimbo, source

9 Réponses

Il n'y a absolument rien de mal à passer un objet Utilisateur entier en tant que paramètre. En fait, cela peut aider à clarifier votre code et à rendre plus évident pour les programmeurs ce que prend une méthode si la signature de la méthode nécessite un Utilisateur .

Passer des types de données simples est bien, jusqu'à ce qu'ils signifient autre chose que ce qu'ils sont. Considérons cet exemple:

public class Foo
{
    public void Bar(int userId)
    {
       //...
    }
}

Et un exemple d'utilisation:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

Pouvez-vous repérer le défaut? Le compilateur ne peut pas. "L'identifiant de l'utilisateur" transmis est juste un entier. Nous nommons la variable utilisateur mais initialisons sa valeur à partir de l'objet blogPostRepository , qui renvoie vraisemblablement des objets BlogPost , et non Utilisateur . objets - mais le code est compilé et vous vous retrouvez avec une erreur d’exécution.

Considérons maintenant cet exemple modifié:

public class Foo
{
    public void Bar(User user)
    {
       //...
    }
}

Peut-être que la méthode Bar utilise uniquement "l'ID utilisateur" mais que la signature de la méthode nécessite un objet Utilisateur . Revenons maintenant au même exemple d’utilisation, mais modifions-le de manière à transmettre l’utilisateur entier dans:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

Nous avons maintenant une erreur de compilation. La méthode blogPostRepository.Find renvoie un objet BlogPost , que nous appelons intelligemment "utilisateur". Ensuite, nous passons cet "utilisateur" à la méthode Bar et obtenons rapidement une erreur de compilation, car nous ne pouvons pas transmettre un BlogPost à une méthode qui accepte un utilisateur . .

Le système de types du langage est utilisé pour écrire plus rapidement le code correct et identifier les défauts au moment de la compilation plutôt que de l'exécution.

Vraiment, avoir à refactoriser beaucoup de code parce que les informations de l'utilisateur change n'est que le symptôme d'autres problèmes. En passant un objet User complet, vous bénéficiez des avantages ci-dessus, en plus de ne pas avoir à refactoriser toutes les signatures de méthode qui acceptent les informations utilisateur lorsque la classe User est modifiée. .

31
ajouté
@ NickHartley: Dans le contexte de Java ou C#, ces objets seraient alloués sur le tas et les déchets collectés. Certainement quelque chose à considérer avec les goûts de C ++, cependant.
ajouté l'auteur DenNukem, source
@Ian: Bien que pour être honnête, même en travaillant en C#, j'ai été très tenté d'envelopper les identifiants et le genre dans un fichier Struct juste pour donner un peu plus de clarté.
ajouté l'auteur DenNukem, source
@Ian: Je pense que l'encapsulation d'un identifiant dans son propre type entoure le problème d'origine soulevé par l'OP, ce qui correspond aux modifications structurelles apportées à la classe User, obligeant à refactoriser de nombreuses signatures de méthodes. Passer tout l'objet utilisateur résout ce problème.
ajouté l'auteur DenNukem, source
Point secondaire mineur - si Utilisateur est stocké sur la pile et doit donc être copié dans tous ces emplacements, il n'y a rien de mal à faire passer un pointeur à la place. Vous bénéficiez des mêmes avantages. Si votre classe User est volumineuse, vous ne finissez pas non plus par copier en permanence des données inutiles. C’est quelque chose que vous souhaiteriez profiler pour voir si cela fait une différence, sauf si votre classe Utilisateur est trop grosse ridiculement .
ajouté l'auteur Lispnik, source
"il n'y a rien de mal à faire passer un pointeur à la place." Ou une référence, pour éviter tous les problèmes avec les pointeurs que vous pourriez rencontrer.
ajouté l'auteur Yay295, source
Bien sûr, ce genre de style de programmation est assez fastidieux, en particulier dans un langage qui n’a pas de support syntaxique intéressant (Haskell est bien pour ce style par exemple, puisque vous pouvez simplement faire correspondre l’identifiant "UserID id") .
ajouté l'auteur omega1, source
Je dirais que votre raisonnement en lui-même indique en fait que les champs sont des champs de passage, mais que les champs sont des enveloppes triviales de la valeur réelle. Dans cet exemple, un utilisateur a un champ de type UserID et un utilisateur a un seul champ de valeur entière. Maintenant, la déclaration de Bar vous indique immédiatement que Bar n'utilise pas toutes les informations relatives à l'utilisateur, mais uniquement son identifiant, mais vous ne pouvez toujours pas commettre d'erreurs stupides telles que transmettre un entier qui ne provient pas d'un identifiant utilisateur à Bar.
ajouté l'auteur omega1, source
Et si je devais ajouter que le cours avec lequel je travaille est sérialisé et envoyé régulièrement au client. Donc, il y a une incitation à garder la lumière.
ajouté l'auteur Jimbo, source

Ai-je raison de penser qu'il s'agit d'une violation du principe d'ouverture/fermeture?

Non, ce n'est pas une violation de ce principe. Ce principe concerne le fait de ne pas modifier Utilisateur d'une manière qui affecte les autres parties du code qui l'utilise. Vos modifications sur Utilisateur peuvent constituer une telle violation, mais elles ne sont pas liées.

D'autres principes sont-ils enfreints par cette pratique? Inversion de dépendance peut-être?

Non. Ce que vous décrivez (en n'injectant que les parties requises d'un objet utilisateur dans chaque méthode), c'est l'inverse: il s'agit d'une inversion de dépendance pure.

Existe-t-il d'autres principes non SOLID non respectés, tels que les principes de base de la programmation défensive?

Non, cette approche est un moyen de codage parfaitement valide. Cela ne viole pas de tels principes.

Mais l'inversion de dépendance n'est qu'un principe; ce n'est pas une loi incassable. Et la DI pure peut ajouter de la complexité au système. Si vous constatez que le simple fait d'injecter les valeurs utilisateur nécessaires dans les méthodes, plutôt que de transmettre l'objet utilisateur complet à la méthode ou au constructeur, crée des problèmes, ne procédez pas ainsi. Il s’agit d’obtenir un équilibre entre principes et pragmatisme.

Pour répondre à votre commentaire:

Il est difficile d'analyser inutilement une nouvelle valeur sur cinq niveaux de la chaîne, puis de modifier toutes les références aux cinq méthodes existantes ...

Une partie du problème ici est que vous n'aimez clairement pas cette approche, comme le dit le commentaire "inutilement [pass] ...". Et c'est assez juste. il n'y a pas de bonne réponse ici. Si vous trouvez cela fastidieux, ne le faites pas de cette façon.

Cependant, en ce qui concerne le principe d'ouverture/fermeture, si vous suivez strictement ce qui suit, "... modifiez toutes les références aux cinq méthodes existantes ..." indique que ces méthodes ont été modifiées, alors qu'elles devraient être modifiées. fermé à la modification. En réalité, le principe ouvert/fermé est logique pour les API publiques, mais pas pour les composants internes d'une application.

... mais un plan visant à respecter ce principe dans la mesure du possible inclurait des stratégies visant à réduire le besoin de changement futur?

Mais ensuite, vous vous aventurez dans le territoire YAGNI et il serait toujours orthogonal au principe. Si vous avez la méthode Foo qui prend un nom d'utilisateur et que vous souhaitez ensuite que Foo prenne également une date de naissance, en suivant le principe, vous ajoutez une nouvelle méthode; Foo reste inchangé. Là encore, c'est une bonne pratique pour les API publiques, mais c'est un non-sens pour le code interne.

Comme mentionné précédemment, il s'agit d'équilibre et de bon sens pour une situation donnée. Si ces paramètres changent souvent, alors oui, utilisez directement Utilisateur . Cela vous évitera les changements à grande échelle que vous décrivez. Mais s'ils ne changent pas souvent, alors ne faire que transmettre ce qui est nécessaire est également une bonne approche.

17
ajouté
@ Jimbo, j'ai mis à jour ma réponse pour essayer de répondre à votre commentaire.
ajouté l'auteur David Arno, source
@ JamesEllis-Jones, inversion de dépendance bascule les dépendances de "demander", à "dire". Si vous transmettez une instance User , puis interrogez cet objet pour obtenir un paramètre, vous n'inversez que partiellement les dépendances. il y a encore des demandes qui se passent. La véritable inversion de dépendance est de 100% "dis, ne demande pas". Mais cela a un prix complexe.
ajouté l'auteur David Arno, source
Ce n'est pas l'inversion de dépendance de transmettre les paramètres d'utilisateur plutôt que d'utilisateur lui-même.
ajouté l'auteur Aaron, source
@DavidAmo Je suis certainement d'accord sur le fait que ceci est similaire à l'inversion de dépendance en ce sens que les deux choses sont des techniques pour limiter la portée de la classe, c'est-à-dire restreindre ce qu'elle sait au minimum, et que cela présente un coût complexe. Juste en ce qui concerne la définition habituelle de l'inversion de dépendance, je ne serais toujours pas d'accord, car cela impliquerait le remplacement d'une dépendance concrète par une dépendance abstraite.
ajouté l'auteur Aaron, source
Il est difficile d'analyser inutilement une nouvelle valeur sur cinq niveaux de la chaîne, puis de modifier toutes les références à ces cinq méthodes existantes. Pourquoi le principe Open/Closed s’appliquerait-il uniquement à la classe User et non également à la classe que je suis en train de modifier, qui est également utilisée par d’autres classes? Je suis conscient que le principe vise spécifiquement à éviter le changement, mais un plan visant à respecter ce principe dans la mesure du possible inclurait des stratégies visant à réduire le besoin de changement pour l'avenir.
ajouté l'auteur Jimbo, source
J'apprécie votre contribution. BTW. Même Robert C Martin n'accepte pas le principe Open/Closed. C'est une règle de base qui sera inévitablement enfreinte. L’application du principe est un exercice consistant à tenter de le respecter autant que possible. C'est pourquoi j'ai utilisé le mot "praticable" précédemment.
ajouté l'auteur Jimbo, source

Oui, changer une fonction existante est une violation du principe Ouvert/Fermé. Vous modifiez quelque chose qui devrait être fermé à modification en raison de modifications des exigences. Une meilleure conception (pour ne pas changer lorsque les exigences changent) serait de transmettre à l'utilisateur les éléments que devrait travailler sur les utilisateurs.

Mais cela pourrait aller à l’encontre du principe de ségrégation d’interface, car vous pourriez transmettre moyen plus d’informations que la fonction n’a besoin de faire son travail.

Ainsi, comme dans la plupart des cas, cela dépend .

En utilisant simplement un nom d’utilisateur, la fonction est plus flexible, vous pouvez utiliser des noms d’utilisateur indépendamment de leur origine et sans avoir à créer un objet User pleinement fonctionnel. Cela permet de faire preuve de résilience si vous pensez que la source de données va changer.

L'utilisation de l'ensemble de l'utilisateur permet de clarifier l'utilisation et de renforcer le contrat avec les appelants. Cela permet de faire preuve de résilience face au changement si vous pensez que davantage d'utilisateurs seront nécessaires.

10
ajouté
@dcorking Je dirais que c'est l'une des implications du principe de responsabilité unique. Savoir où les utilisateurs sont stockés et comment les récupérer par ID est une responsabilité distincte d'une classe User . Il peut y avoir un UserRepository ou similaire qui traite de telles choses.
ajouté l'auteur Dan, source
@dcorking - enh. Le fait que vous transmettez une référence qui se trouve être petite est une coïncidence technique. Vous couplez l'intégralité de utilisateur à la fonction. Peut-être que cela a du sens. Mais peut-être que la fonction ne devrait se soucier que du nom de l'utilisateur - et transmettre des informations telles que la date de participation de l'utilisateur, ou l'adresse est incorrecte.
ajouté l'auteur Telastyn, source
@dcorking, " Lorsque vous transmettez (utilisateur), vous transmettez le minimum d'informations, une référence à un objet ". Vous transmettez le maximum d'informations relatives à cet objet: l'objet entier. " la méthode appelée peut toujours appeler User.find (userid) ...". Dans un système bien conçu, cela ne serait pas possible car la méthode en question n'aurait pas accès à User.find() . En fait, il ne devrait même pas exister un User.find . La recherche d’un utilisateur ne devrait jamais incomber à Utilisateur .
ajouté l'auteur David Arno, source
+1 mais je ne suis pas sûr de votre phrase "vous pourriez transmettre beaucoup plus d'informations". Lorsque vous passez (utilisateur), vous transmettez le minimum d’informations, une référence à un objet. Certes, la référence peut être utilisée pour obtenir plus d'informations, mais cela signifie que le code d'appel n'a pas à l'obtenir. Lorsque vous transmettez (ID utilisateur GUID, nom d'utilisateur chaîne), la méthode appelée peut toujours appeler User.find (ID utilisateur) pour rechercher l'interface publique de l'objet. Vous ne cachez donc rien.
ajouté l'auteur jroith, source
@DavidArno c'est peut-être la clé d'une réponse claire à OP. À qui incombe la responsabilité de trouver un utilisateur? Existe-t-il un nom pour le principe de conception consistant à séparer le chercheur/l’usine de la classe?
ajouté l'auteur jroith, source

Cette conception suit le Modèle d'objet de paramètre . Il résout les problèmes liés à la présence de nombreux paramètres dans la signature de la méthode.

Ai-je raison de penser qu'il s'agit d'une violation du principe d'ouverture/fermeture?

Non. L'application de ce modèle active le principe d'ouverture/fermeture (OCP). Par exemple, les classes dérivées de Utilisateur peuvent être fournies en tant que paramètres, ce qui induit un comportement différent dans la classe consommatrice.

D'autres principes sont-ils enfreints par cette pratique?

Cela peut arriver. Permettez-moi de vous expliquer sur la base des principes SOLID.

Le principe de responsabilité unique (SRP) peut être enfreint s'il présente le design que vous avez expliqué:

Cette classe expose toutes les informations sur l'utilisateur, son identifiant, son nom, les niveaux d'accès à chaque module, son fuseau horaire, etc.

Le problème concerne toutes les informations . Si la classe User a de nombreuses propriétés, elle devient un énorme objet de transfert de données qui transporte des informations non liées du point de vue des classes consommatrices. Exemple: Du point de vue d'une classe consommatrice UserAuthentication , la propriété User.Id et User.Name sont pertinentes, mais pas Utilisateur. Fuseau horaire .

Le principe de ségrégation d'interface (ISP) est également violé avec un raisonnement similaire mais ajoute une autre perspective. Exemple: supposons qu'une classe consommatrice UserManagement nécessite que la propriété User.Name soit scindée en User.LastName et User.FirstName la classe UserAuthentication doit également être modifiée pour cela.

Heureusement, le fournisseur de services Internet vous offre également un moyen de résoudre le problème: généralement, ces objets de paramètre ou ces objets de transport de données démarrent lentement et augmentent avec le temps. Si cela devient trop compliqué, envisagez l’approche suivante: présentez des interfaces adaptées aux besoins des classes consommatrices. Exemple: présentez les interfaces et laissez la classe User en dériver:

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

Chaque interface doit exposer un sous-ensemble de propriétés connexes de la classe User nécessaires à une classe consommatrice pour remplir son opération. Rechercher des grappes de propriétés. Essayez de réutiliser les interfaces. Dans le cas de la classe consommatrice UserAuthentication , utilisez IUserAuthenticationInfo au lieu de Utilisateur . Ensuite, si possible, divisez la classe Utilisateur en plusieurs classes concrètes à l'aide des interfaces en tant que "pochoir".

3
ajouté
Une fois que l'utilisateur est compliqué, il y a une explosion combinatoire de sous-interfaces possibles, par exemple, si l'utilisateur n'a que 3 propriétés, il y a 7 combinaisons possibles. Votre proposition semble bien mais est impraticable.
ajouté l'auteur MrG, source
1. Analytiquement, vous avez raison. Toutefois, selon la manière dont le domaine est modélisé, les informations connexes ont tendance à se regrouper. En pratique, il n’est donc pas nécessaire de traiter toutes les combinaisons possibles d’interfaces et de propriétés. 2. L’approche exposée n’était pas censée constituer une solution universelle, mais j’aimerais peut-être ajouter un peu plus de possibilités et de possibilités dans la réponse.
ajouté l'auteur Theo Lenndorff, source

Lorsque confronté à ce problème dans mon propre code, j'ai conclu que les classes/objets de modèles de base sont la solution.

Un exemple courant serait le modèle de référentiel. Souvent, lors de l'interrogation d'une base de données via des référentiels, de nombreuses méthodes du référentiel utilisent un grand nombre de paramètres identiques.

Mes règles de base pour les référentiels sont les suivantes:

  • Lorsque plusieurs méthodes utilisent les mêmes 2 paramètres ou plus, ils doivent être regroupés dans un objet de modèle.

  • Lorsqu'une méthode prend plus de 2 paramètres, ceux-ci doivent être regroupés en tant qu'objet de modèle.

  • Les modèles peuvent hériter d'une base commune, mais uniquement lorsque cela a du sens (en règle générale, il est préférable de refactoriser plus tard que de commencer avec l'héritage en tête).


Les problèmes d'utilisation de modèles d'autres couches/zones ne deviennent apparents que lorsque le projet commence à devenir un peu complexe. C'est seulement à ce moment-là que vous trouvez que moins de code crée plus de travail ou plus de complications.

Et oui, c'est tout à fait bien d'avoir 2 modèles différents avec des propriétés identiques qui servent différentes couches/différents objectifs (ie. ViewModels vs POCOs).

2
ajouté

Vérifions simplement les aspects individuels de SOLID:

  • Une seule responsabilité: éventuellement si les gens ont tendance à ne faire circuler que des sections de la classe.
  • Open/closed: Peu importe où les sections de la classe sont transmises, uniquement lorsque l'objet complet est transmis. (Je pense que c'est là que la dissonance cognitive entre en jeu: vous devez changer de code distant, mais la classe elle-même semble bien fonctionner.)
  • Substitution de Liskov: aucun problème, nous ne faisons pas de sous-classes.
  • Inversion de dépendance (dépend d'abstractions et non de données concrètes). Ouais, c'est violé: les gens n'ont pas d'abstractions, ils extraient des éléments concrets de la classe et les transmettent. Je pense que c'est le principal problème ici.

Une chose qui tend à confondre les instincts de conception est que la classe est essentiellement pour des objets globaux, et essentiellement en lecture seule. Dans une telle situation, violer des abstractions ne fait pas très mal: la simple lecture de données non modifiées crée un couplage assez faible; lorsque la douleur devient énorme, la douleur devient perceptible.
Pour rétablir les instincts de conception, supposons simplement que l'objet n'est pas très global. De quel contexte une fonction aurait-elle besoin si l'objet Utilisateur pouvait être muté à tout moment? Quels composants de l'objet seraient probablement mutés ensemble? Ceux-ci peuvent être séparés de Utilisateur , que ce soit en tant que sous-objet référencé ou en tant qu'interface ne présentant qu'une "tranche" de champs liés, ce n'est pas si important.

Autre principe: examinez les fonctions qui utilisent des parties de Utilisateur et voyez quels champs (attributs) ont tendance à aller ensemble. C'est une bonne liste préliminaire de sous-objets - vous devez absolument déterminer s'ils vont ou non de pair.

Cela demande beaucoup de travail et c'est un peu difficile à faire, et votre code deviendra un peu moins flexible, car il deviendra un peu plus difficile d'identifier le sous-objet (sous-interface) devant être transmis à une fonction, en particulier si les sous-objets se chevauchent.

Fractionner Utilisateur deviendra vraiment moche si les sous-objets se chevauchent, les gens ne comprendront pas lequel choisir si tous les champs obligatoires sont issus du chevauchement. Si vous vous séparez hiérarchiquement (par exemple, vous avez UserMarketSegment qui contient, entre autres, UserLocation ), les utilisateurs ne seront pas certains du niveau de la fonction à laquelle ils écrivent: traite-t-il des données utilisateur au niveau Emplacement ou au niveau MarketSegment ? Cela n’aide pas vraiment que cela puisse changer avec le temps, c’est-à-dire que vous devez revenir à la modification des signatures de fonction, parfois sur toute la chaîne d’appel.

En d'autres termes: à moins de connaître parfaitement votre domaine et d'avoir une idée assez précise du module traitant des aspects de Utilisateur , l'amélioration de la structure du programme ne vaut vraiment pas la peine.

2
ajouté

Je trouve qu'il est préférable de transmettre le moins de paramètres possible et autant que nécessaire. Cela facilite les tests et ne nécessite pas la création d'objets entiers.

Dans votre exemple, si vous n'utilisez que l'ID utilisateur ou le nom d'utilisateur, c'est tout ce que vous devriez passer. Si ce motif se répète plusieurs fois et que l'objet utilisateur réel est beaucoup plus grand, je vous conseille de créer une ou plusieurs interfaces plus petites pour cela. Il pourrait être

interface IIdentifieable
{
    Guid ID { get; }
}

ou

interface INameable
{
    string Name { get; }
}

This makes testing with mocking a lot easier and you instantly know which values are really used. Otherwise you often need to initizialize complex objects with many other dependencies although at the end you just need one outwo properties.

1
ajouté

C'est une question vraiment intéressante. Cela dépend.

Si vous pensez que votre méthode peut changer à l’avenir en interne pour exiger des paramètres différents de l’objet User, vous devez certainement passer l’ensemble du processus. L'avantage est que le code externe à la méthode est alors protégé contre les modifications au sein de la méthode en termes de paramètres utilisés, ce qui, comme vous le dites, provoquerait une cascade de modifications en externe. Donc, passer dans tout l'utilisateur augmente l'encapsulation.

Si vous êtes certain de ne pas avoir besoin d'utiliser autre chose que de dire le courrier électronique de l'utilisateur, vous devez l'indiquer. L'avantage est que vous pouvez ensuite utiliser la méthode dans un plus grand nombre de contextes: vous pouvez par exemple l'utiliser avec le courrier électronique d'une entreprise ou avec un courriel que quelqu'un vient de taper. Cela augmente la flexibilité.

Cela fait partie d'un éventail plus large de questions sur les classes de construction devant avoir une portée large ou étroite, y compris s'il faut ou non injecter des dépendances et s'il faut avoir des objets disponibles globalement. Il existe actuellement une tendance malheureuse à penser qu'une portée plus étroite est toujours une bonne chose. Cependant, il y a toujours un compromis entre encapsulation et flexibilité comme dans ce cas.

1
ajouté

Voici quelque chose que j'ai rencontré de temps en temps:

  • Une méthode prend un argument de type Utilisateur (ou Produit ou autre) comportant de nombreuses propriétés, même si la méthode n'en utilise que quelques-unes.
  • Pour une raison quelconque, une partie du code doit appeler cette méthode même si elle n'a pas d'objet User entièrement rempli. Il crée une instance et initialise uniquement les propriétés dont la méthode a réellement besoin.
  • Cela se produit plusieurs fois.
  • Après un certain temps, lorsque vous rencontrez une méthode avec un argument User , vous devez trouver des appels à cette méthode pour rechercher l'origine de User . que vous sachiez quelles propriétés sont remplies. S'agit-il d'un "vrai" utilisateur avec une adresse e-mail ou a-t-il été créé pour transmettre un ID utilisateur et des autorisations?

Si vous créez un utilisateur et ne remplissez que quelques propriétés, car ce sont celles dont la méthode a besoin, l'appelant en sait vraiment plus sur le fonctionnement interne de la méthode qu'il ne le devrait.

Pire encore, lorsque vous disposez d'une instance de Utilisateur , vous devez savoir d'où elle provient afin de savoir quelles propriétés sont renseignées. Vous ne voulez pas avoir à le savoir.

Au fil du temps, lorsque les développeurs voient que Utilisateur est utilisé comme conteneur pour les arguments de méthode, ils peuvent commencer à lui ajouter des propriétés pour les scénarios à usage unique. Maintenant, ça devient moche, parce que la classe est encombrée de propriétés qui seront presque toujours nulles ou par défaut.

Une telle corruption n'est pas inévitable, mais elle se répète lorsque nous transmettons un objet simplement parce que nous avons besoin d'accéder à quelques-unes de ses propriétés. La zone de danger est la première fois que vous voyez quelqu'un créer une instance de Utilisateur et ne remplir que quelques propriétés pour pouvoir le transmettre à une méthode. Mettez votre pied dessus parce que c'est un chemin sombre.

Si possible, donnez le bon exemple au prochain développeur en ne transmettant que ce que vous devez transmettre.

1
ajouté