Disons, par exemple, que vous avez une application avec une classe largement partagée appelée User
. Cette classe expose toutes les informations sur l'utilisateur, son identifiant, son nom, les niveaux d'accès à chaque module, le 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 sorte qu'au lieu de passer cet objet utilisateur dans les classes qui en dépendent, nous ne faisons que lui transmettre des propriétés individuelles.
Une classe qui nécessite l'ID utilisateur, nécessitera simplement le GUID en userId
tant que paramètre, parfois nous pourrions également avoir besoin du nom d'utilisateur, de sorte qu'il soit transmis en tant que paramètre distinct. Dans certains cas, cela est transmis à des méthodes individuelles, donc les valeurs ne sont pas du tout 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 à la méthode ou au constructeur de 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 ouvert / fermé? Pas seulement l'acte de changer les classes existantes, mais de les mettre en place en premier lieu afin que des changements généralisés soient très probablement nécessaires à l'avenir?
Si nous venons de passer dans l' User
objet, je pourrais apporter un petit changement à la classe avec laquelle je travaille. Si je dois ajouter un paramètre, je devrai peut-être apporter des dizaines de modifications aux références à la classe.
Y a-t-il d'autres principes brisés par cette pratique? Inversion de dépendance peut-être? Bien que nous ne référençons pas une abstraction, il n'y a qu'un seul type d'utilisateur, il n'est donc pas vraiment nécessaire d'avoir une interface utilisateur.
Y a-t-il d'autres principes non SOLIDES violés, tels que les principes de programmation défensive de base?
Si mon constructeur ressemble à ceci:
MyConstructor(GUID userid, String username)
Ou ca:
MyConstructor(User theUser)
Posté:
Il a été suggéré de répondre à la question sous "ID passe ou objet?". Cela ne répond pas à la question de savoir comment la décision d'aller dans les deux sens affecte une tentative de suivre les principes SOLIDES, qui est au cœur de cette question.
I
dansSOLID
?MyConstructor
dit essentiellement "j'ai besoin d'unGuid
et d'unstring
". Alors pourquoi ne pas avoir une interface fournissant aGuid
et astring
, laisserUser
implémenter cette interface et laisserMyConstructor
dépendre d'une instance implémentant cette interface? Et si les besoinsMyConstructor
changent, changez d'interface. - Cela m'a beaucoup aidé à penser que les interfaces "appartiennent" au consommateur plutôt qu'au fournisseur . Alors pensez "en tant que consommateur, j'ai besoin de quelque chose qui fait ceci et cela" au lieu de "en tant que fournisseur, je peux faire ceci et cela".Réponses:
Il n'y a absolument rien de mal à passer un
User
objet entier en paramètre. En fait, cela pourrait aider à clarifier votre code et à rendre plus évident pour les programmeurs ce qu'une méthode prend si la signature de la méthode nécessite unUser
.Passer des types de données simples est agréable, jusqu'à ce qu'ils signifient autre chose que ce qu'ils sont. Considérez cet exemple:
Et un exemple d'utilisation:
Pouvez-vous repérer le défaut? Le compilateur ne peut pas. L '"ID utilisateur" transmis n'est qu'un entier. Nous nommons la variable
user
mais initialisons sa valeur à partir de l'blogPostRepository
objet, qui renvoie vraisemblablement desBlogPost
objets, pas desUser
objets - pourtant le code se compile et vous vous retrouvez avec une erreur d'exécution bancale.Considérons maintenant cet exemple modifié:
Peut-être que la
Bar
méthode utilise uniquement "l'ID utilisateur" mais la signature de la méthode nécessite unUser
objet. Revenons maintenant au même exemple d'utilisation qu'avant, mais modifions-le pour passer l'intégralité de l '"utilisateur" dans:Nous avons maintenant une erreur de compilation. La
blogPostRepository.Find
méthode retourne unBlogPost
objet, que nous appelons habilement "utilisateur". Ensuite, nous transmettons cet "utilisateur" à laBar
méthode et obtenons rapidement une erreur de compilation, car nous ne pouvons pas passer unBlogPost
à une méthode qui accepte unUser
.Le système de type du langage est exploité pour écrire le code correct plus rapidement et identifier les défauts au moment de la compilation plutôt qu'au moment de l'exécution.
Vraiment, devoir refactoriser beaucoup de code parce que les informations utilisateur changent n'est qu'un symptôme d'autres problèmes. En passant un
User
objet entier , 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 quelque chose au sujet de laUser
classe change.la source
Non, ce n'est pas une violation de ce principe. Ce principe vise à ne pas changer
User
de manière à affecter les autres parties du code qui l'utilisent. Vos modificationsUser
pourraient constituer une telle violation, mais cela n'est pas lié.Non. Ce que vous décrivez - en injectant uniquement les parties requises d'un objet utilisateur dans chaque méthode - est le contraire: c'est une inversion de dépendance pure.
Non. Cette approche est un moyen de codage parfaitement valable. Il 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 l'injection des valeurs utilisateur nécessaires dans les méthodes plutôt que de passer l'intégralité de l'objet utilisateur dans la méthode ou le constructeur crée des problèmes, ne le faites pas de cette façon. Il s'agit de trouver un équilibre entre les principes et le pragmatisme.
Pour répondre à votre commentaire:
Une partie du problème ici est que vous n'aimez manifestement pas cette approche, selon le commentaire "inutilement [passer] ...". Et c'est assez juste; il n'y a pas de bonne réponse ici. Si vous trouvez cela pénible, ne le faites pas de cette façon.
Cependant, en ce qui concerne le principe ouvert / fermé, si vous suivez strictement cela, alors "... changer toutes les références à ces cinq méthodes existantes ..." est une indication que ces méthodes ont été modifiées, quand elles devraient être fermé à modification. En réalité cependant, le principe ouvert / fermé est logique pour les API publiques, mais n'a pas beaucoup de sens pour les internes d'une application.
Mais alors vous vous promenez sur le territoire YAGNI et ce serait toujours orthogonal au principe. Si vous avez une méthode
Foo
qui prend un nom d'utilisateur et que vous souhaitez égalementFoo
prendre une date de naissance, en suivant le principe, vous ajoutez une nouvelle méthode;Foo
reste inchangé. Encore une fois, 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-les
User
directement. Cela vous évitera les changements à grande échelle que vous décrivez. Mais s'ils ne changent pas souvent, transmettre uniquement ce qui est nécessaire est également une bonne approche.la source
User
instance, puis interrogez cet objet pour obtenir un paramètre, vous n'inversez que partiellement les dépendances; il y a encore des demandes en cours. La véritable inversion des dépendances est 100% "dites, ne demandez pas". Mais cela a un prix complexe.Oui, la modification d'une fonction existante est une violation du principe ouvert / fermé. Vous modifiez quelque chose qui devrait être fermé à la modification en raison du changement des exigences. Une meilleure conception (pour ne pas changer lorsque les exigences changent) serait de transmettre l'utilisateur à des choses qui devraient fonctionner sur les utilisateurs.
Mais cela pourrait aller à l'encontre du principe de ségrégation d'interface, car vous pourriez transmettre beaucoup plus d'informations que la fonction n'a besoin pour faire son travail.
Donc, comme pour la plupart des choses - cela dépend .
En utilisant juste un nom d'utilisateur, la fonction sera plus flexible, en travaillant avec les noms d'utilisateur indépendamment de d'où ils viennent et sans avoir besoin de créer un objet utilisateur pleinement fonctionnel. Il offre une résilience au changement si vous pensez que la source des données va changer.
L'utilisation de l'ensemble de l'Utilisateur clarifie l'utilisation et conclut un contrat plus ferme avec ses appelants. Il offre une résilience au changement si vous pensez que davantage d'Utilisateur sera nécessaire.
la source
User.find()
. En fait , il ne devrait même pas être unUser.find
. Trouver un utilisateur ne devrait jamais être la responsabilité deUser
.User
à 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 connexion de l'utilisateur ou l'adresse est incorrecte.User
classe ne devrait pas avoir. Il peut y avoir unUserRepository
ou similaire qui traite de telles choses.Cette conception suit le modèle d'objet de paramètre . Il résout les problèmes résultant de la présence de nombreux paramètres dans la signature de la méthode.
Non. L'application de ce modèle active le principe d'ouverture / fermeture (OCP). Par exemple, des classes dérivées de
User
peuvent être fournies en tant que paramètres qui induisent un comportement différent dans la classe consommatrice.Cela peut arriver. Permettez-moi de vous expliquer sur la base des principes SOLIDES.
Le principe de responsabilité unique (SRP) peut être violé s'il a la conception comme vous l'avez expliqué:
Le problème est avec toutes les informations . Si la
User
classe 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: Dans la perspective d'une classe consommerUserAuthentication
la propriétéUser.Id
etUser.Name
sont pertinents, mais nonUser.Timezone
.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 diviséeUser.LastName
et queUser.FirstName
la classeUserAuthentication
doit également être modifiée pour cela.Heureusement, le FAI vous offre également une solution possible au problème: généralement, ces objets de paramètres ou ces objets de transport de données commencent petit et augmentent avec le temps. Si cela devient compliqué, envisagez l'approche suivante: introduisez des interfaces adaptées aux besoins des classes consommatrices. Exemple: introduisez des interfaces et laissez la
User
classe en dériver:Chaque interface doit exposer un sous-ensemble de propriétés connexes de la
User
classe nécessaire pour qu'une classe consommatrice remplisse pleinement son fonctionnement. Recherchez des groupes de propriétés. Essayez de réutiliser les interfaces. Dans le cas de la classe consommatrice,UserAuthentication
utilisezIUserAuthenticationInfo
au lieu deUser
. Ensuite, si possible, divisez laUser
classe en plusieurs classes concrètes en utilisant les interfaces comme "gabarit".la source
Face à ce problème dans mon propre code, j'ai conclu que les classes / objets de modèle de base sont la réponse.
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 prennent en grande partie les mêmes paramètres.
Mes règles générales pour les référentiels sont:
Lorsque plusieurs méthodes prennent les mêmes 2 paramètres ou plus, les paramètres doivent être regroupés en tant qu'objet modèle.
Lorsqu'une méthode prend plus de 2 paramètres, les paramètres doivent être regroupés en tant qu'objet modèle.
Les modèles peuvent hériter d'une base commune, mais seulement lorsque cela a vraiment du sens (généralement mieux refactoriser plus tard que de commencer par l'héritage à l'esprit).
Les problèmes liés à l'utilisation de modèles provenant d'autres couches / zones ne deviennent apparents que lorsque le projet commence à devenir un peu complexe. Ce n'est qu'alors que moins de code crée plus de travail ou plus de complications.
Et oui, il est tout à fait correct d'avoir 2 modèles différents avec des propriétés identiques qui servent des couches / objectifs différents (par exemple, ViewModels vs POCO).
la source
Voyons simplement les aspects individuels de SOLID:
Une chose qui tend à confondre les instincts de conception est que la classe est essentiellement destinée aux objets globaux et essentiellement en lecture seule. Dans une telle situation, la violation des abstractions ne fait pas beaucoup de mal: la simple lecture de données non modifiées crée un couplage assez faible; ce n'est que lorsqu'elle devient un énorme tas que la douleur devient perceptible.
Pour rétablir les instincts de conception, supposez simplement que l'objet n'est pas très global. De quel contexte une fonction aurait-elle besoin si l'
User
objet pouvait être muté à tout moment? Quels composants de l'objet seraient probablement mutés ensemble? Ceux-ci peuvent être séparésUser
, que ce soit en tant que sous-objet référencé ou en tant qu'interface qui expose uniquement une "tranche" de champs connexes n'est pas si important.Un autre principe: regardez les fonctions qui utilisent des parties de
User
et voyez quels champs (attributs) ont tendance à aller ensemble. C'est une bonne liste préliminaire de sous-objets - vous devez certainement vous demander s'ils vont vraiment ensemble.C'est beaucoup de travail, et c'est un peu difficile à faire, et votre code deviendra légèrement moins flexible car il deviendra un peu plus difficile d'identifier le sous-objet (sous-interface) qui doit être transmis à une fonction, en particulier si les sous-objets se chevauchent.
Le fractionnement
User
deviendra en fait laid si les sous-objets se chevauchent, alors les gens seront confus quant à celui à choisir si tous les champs requis proviennent du chevauchement. Si vous vous séparez de manière hiérarchique (par exemple, vous en avezUserMarketSegment
, entre autresUserLocation
), les gens ne seront pas sûrs du niveau auquel la fonction qu'ils écrivent est: traite-t-il des données utilisateur auLocation
niveau ou auMarketSegment
niveau? Cela n'aide pas vraiment que cela puisse changer avec le temps, c'est-à-dire que vous revenez à changer les signatures de fonction, parfois sur toute une chaîne d'appel.En d'autres termes: à moins que vous ne connaissiez vraiment votre domaine et que vous n'ayez une idée assez claire de quel module traite de quels aspects
User
, cela ne vaut pas vraiment la peine d'améliorer la structure du programme.la source
C'est une question vraiment intéressante. Cela dépend.
Si vous pensez que votre méthode peut changer à l'avenir en interne pour nécessiter différents paramètres de l'objet User, vous devez certainement passer le tout. L'avantage est que le code externe à la méthode est ensuite protégé contre les modifications au sein de la méthode en termes de paramètres qu'il utilise, ce qui, comme vous le dites, entraînerait une cascade de modifications externes. Ainsi, le passage à l'ensemble de l'utilisateur augmente l'encapsulation.
Si vous êtes sûr que vous n'aurez jamais besoin d'utiliser autre chose que de dire l'e-mail de l'utilisateur, vous devez le transmettre. L'avantage de ceci est que vous pouvez ensuite utiliser la méthode dans un plus large éventail de contextes: vous pouvez l'utiliser par exemple avec le courriel 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 la création de classes pour avoir une portée large ou étroite, y compris s'il faut injecter ou non des dépendances et si des objets sont disponibles globalement. Il y a actuellement une tendance malheureuse à penser qu'une portée plus étroite est toujours bonne. Cependant, il y a toujours un compromis entre l'encapsulation et la flexibilité comme dans ce cas.
la source
Je trouve qu'il vaut mieux passer le moins de paramètres possible et autant que nécessaire. Cela facilite les tests et ne nécessite pas de créer des objets entiers.
Dans votre exemple, si vous allez utiliser uniquement l'ID utilisateur ou le nom d'utilisateur, c'est tout ce que vous devez transmettre. Si ce modèle se répète plusieurs fois et que l'objet utilisateur réel est beaucoup plus grand, mon conseil est de créer une ou des interfaces plus petites pour cela. Il pourrait être
ou
Cela rend le test avec la simulation beaucoup plus facile et vous savez instantanément quelles valeurs sont vraiment utilisées. Sinon, vous devez souvent initialiser des objets complexes avec de nombreuses autres dépendances, bien qu'à la fin vous ayez juste besoin d'une ou deux propriétés.
la source
Voici quelque chose que j'ai rencontré de temps en temps:
User
(ouProduct
ou autre) qui a beaucoup de propriétés, même si la méthode n'en utilise que quelques-unes.User
objet entièrement rempli . Il crée une instance et initialise uniquement les propriétés dont la méthode a réellement besoin.User
argument, vous devez trouver des appels à cette méthode pour trouver d'oùUser
vient le afin de savoir quelles propriétés sont remplies. S'agit-il d'un "vrai" utilisateur avec une adresse e-mail, ou a-t-il simplement été créé pour transmettre un ID utilisateur et certaines autorisations?Si vous créez une
User
et ne remplissez que quelques propriétés car ce sont celles dont la méthode a besoin, alors l'appelant en sait vraiment plus sur le fonctionnement interne de la méthode qu'il ne devrait.Pire encore, lorsque vous avez une instance de
User
, vous devez savoir d'où elle provient pour savoir quelles propriétés sont remplies. Vous ne voulez pas avoir à le savoir.Au fil du temps, lorsque les développeurs voient
User
utilisé comme conteneur pour les arguments de méthode, ils peuvent commencer à lui ajouter des propriétés pour des scénarios à usage unique. Maintenant, ça devient laid, 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 produit encore et encore lorsque nous passons un objet juste 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 quelqu'un crée une instance de
User
et remplit simplement quelques propriétés afin de pouvoir la transmettre à une méthode. Posez votre pied dessus car c'est un chemin sombre.Dans la mesure du possible, donnez le bon exemple au prochain développeur en ne transmettant que ce dont vous avez besoin.
la source