Dans le passé, j'ai généralement effectué la majeure partie de ma manipulation d'un objet dans la méthode principale en cours de création / mise à jour, mais je me suis retrouvé à adopter une approche différente récemment, et je suis curieux de savoir si c'est une mauvaise pratique.
Voici un exemple. Disons que j'ai un référentiel qui accepte une User
entité, mais avant d'insérer l'entité, nous appelons certaines méthodes pour nous assurer que tous ses champs sont définis sur ce que nous voulons. Maintenant, plutôt que d'appeler des méthodes et de définir les valeurs de champ à partir de la méthode Insert, j'appelle une série de méthodes de préparation qui façonnent l'objet avant son insertion.
Ancienne méthode:
public void InsertUser(User user) {
user.Username = GenerateUsername(user);
user.Password = GeneratePassword(user);
context.Users.Add(user);
}
Nouvelles méthodes:
public void InsertUser(User user) {
SetUsername(user);
SetPassword(user);
context.Users.Add(user);
}
private void SetUsername(User user) {
var username = "random business logic";
user.Username = username;
}
private void SetPassword(User user) {
var password = "more business logic";
user.Password = password;
}
Fondamentalement, la pratique de définir la valeur d'une propriété à partir d'une autre méthode est-elle une mauvaise pratique?
la source
user
on passé par référence, le code pourrait tirer sur ce dernier des mains et de le remplacer par de l'appelant simplement en disant, par exemple,user = null;
.Réponses:
Le problème ici est qu'un
User
peut contenir en fait deux choses différentes:Une entité utilisateur complète, qui peut être transmise à votre magasin de données.
L'ensemble des éléments de données requis de l'appelant pour commencer le processus de création d'une entité utilisateur. Le système doit ajouter un nom d'utilisateur et un mot de passe avant qu'il ne soit vraiment un utilisateur valide comme au n ° 1 ci-dessus.
Cela comprend une nuance non documentée de votre modèle d'objet qui n'est pas du tout exprimée dans votre système de type. Il vous suffit de le "connaître" en tant que développeur. Ce n'est pas génial, et cela conduit à des modèles de code étranges comme celui que vous rencontrez.
Je suggère que vous ayez besoin de deux entités, par exemple une
User
classe et uneEnrollRequest
classe. Ce dernier peut contenir tout ce que vous devez savoir pour créer un utilisateur. Il ressemblerait à votre classe d'utilisateurs, mais sans le nom d'utilisateur et le mot de passe. Ensuite, vous pouvez faire ceci:L'appelant démarre avec uniquement les informations d'inscription et récupère l'utilisateur terminé après son insertion. De cette façon, vous évitez de muter des classes et vous avez également une distinction sécurisée entre un utilisateur qui est inséré et un autre qui ne l'est pas.
la source
InsertUser
est susceptible d'avoir l'effet secondaire de l'insertion. Je ne sais pas ce que "icky" signifie dans ce contexte. Quant à l'utilisation d'un "utilisateur" qui n'a pas été ajouté, vous semblez avoir manqué le point. S'il n'a pas été ajouté, ce n'est pas un utilisateur, juste une demande de création d'un utilisateur. Vous êtes bien sûr libre de travailler avec la demande.Les effets secondaires sont corrects tant qu'ils ne surviennent pas de façon inattendue. Il n'y a donc rien de mal en général lorsqu'un référentiel a une méthode acceptant un utilisateur et change son état interne. Mais à mon humble avis, un nom de méthode comme
InsertUser
ne le communique pas clairement , et c'est ce qui le rend sujet aux erreurs. Lors de l'utilisation de votre repo, je m'attendrais à un appel commepour modifier l'état interne des référentiels, pas l'état de l'objet utilisateur. Ce problème existe dans les deux de vos mises en œuvre, comment le
InsertUser
fait est interne complètement hors de propos.Pour résoudre ce problème, vous pouvez soit
initialisation distincte de l'insertion (donc l'appelant
InsertUser
doit fournir unUser
objet entièrement initialisé , ou,intégrer l'initialisation dans le processus de construction de l'objet utilisateur (comme suggéré par certaines des autres réponses), ou
essayez simplement de trouver un meilleur nom pour la méthode qui exprime plus clairement ce qu'elle fait.
Alors , choisissez un nom de méthode comme
PrepareAndInsertUser
,OrchestrateUserInsertion
,InsertUserWithNewNameAndPassword
ou tout ce que vous préférez faire l'effet secondaire plus clair.Bien sûr, un nom de méthode aussi long indique que la méthode fait peut-être «trop» (violation SRP), mais parfois vous ne voulez pas ou ne pouvez pas facilement résoudre ce problème, et c'est la solution la moins intrusive et pragmatique.
la source
J'examine les deux options que vous avez choisies et je dois dire que je préfère de loin l'ancienne méthode plutôt que la nouvelle méthode proposée. Il y a plusieurs raisons à cela, même si elles font fondamentalement la même chose.
Dans les deux cas, vous définissez le
user.UserName
etuser.Password
. J'ai des réserves sur l'élément de mot de passe, mais ces réserves ne sont pas pertinentes pour le sujet traité.Implications de la modification des objets de référence
Ancienne méthode vs nouvelle méthode
L'ancienne méthode rendait les choses plus faciles à tester:
GenerateUserName()
est testable indépendamment. Vous pouvez écrire des tests sur cette méthode et vous assurer que les noms sont générés correctementGenerateUserName(User user)
et conserver cette testabilitéLa nouvelle méthode masque les mutations:
User
objet change jusqu'à 2 couches de profondeurUser
objet sont plus surprenantes dans ce casSetUserName()
fait plus que définir un nom d'utilisateur. Ce n'est pas vrai dans la publicité, ce qui rend plus difficile pour les nouveaux développeurs de découvrir comment les choses fonctionnent dans votre applicationla source
Je suis entièrement d'accord avec la réponse de John Wu. Sa suggestion est bonne. Mais il manque légèrement votre question directe.
Pas intrinsèquement.
Vous ne pouvez pas aller trop loin, car vous rencontrerez un comportement inattendu. Par exemple
PrintName(myPerson)
, ne devrait pas changer l'objet personne, car la méthode implique qu'elle ne s'intéresse qu'à la lecture des valeurs existantes. Mais c'est un argument différent de celui de votre cas, carSetUsername(user)
cela implique fortement qu'il va définir des valeurs.C'est en fait une approche que j'utilise souvent pour les tests unitaires / d'intégration, où je crée une méthode spécifiquement pour modifier un objet afin de définir ses valeurs à une situation particulière que je veux tester.
Par exemple:
Je m'attends explicitement à ce que la
ArrrangeContractDeletedStatus
méthode change l'état de l'myContract
objet.Le principal avantage est que la méthode me permet de tester la suppression de contrat avec différents contrats initiaux; Par exemple, un contrat qui a un long historique de statut ou qui n'a pas d'historique de statut précédent, un contrat qui a un historique de statut intentionnellement erroné, un contrat que mon utilisateur de test n'est pas autorisé à supprimer.
Si j'avais fusionné
CreateEmptyContract
etArrrangeContractDeletedStatus
en une seule méthode; Je devrais créer plusieurs variantes de cette méthode pour chaque contrat différent que je voudrais tester dans un état supprimé.Et bien que je puisse faire quelque chose comme:
C'est soit redondant (puisque je change de toute façon l'objet), soit je me force maintenant à faire un clone profond de l'
myContract
objet; ce qui est excessivement difficile si vous voulez couvrir tous les cas (imaginez si je veux plusieurs propriétés de navigation sur plusieurs niveaux. Dois-je les cloner toutes? Seule l'entité de niveau supérieur? ... Tant de questions, tant d'attentes implicites )Changer l'objet est le moyen le plus simple d'obtenir ce que je veux sans avoir à faire plus de travail juste pour éviter de ne pas changer l'objet par principe.
Donc, la réponse directe à votre question est que ce n'est pas une mauvaise pratique en soi, tant que vous ne masquez pas que la méthode est susceptible de changer l'objet transmis. Pour votre situation actuelle, cela est clairement expliqué par le nom de la méthode.
la source
Je trouve l'ancienne comme la nouvelle problématique.
À l'ancienne:
1)
InsertUser(User user)
En lisant ce nom de méthode qui apparaît dans mon IDE, la première chose qui me vient à l'esprit est
Le nom de la méthode doit être lu
AddUserToContext
. Au moins, c'est ce que la méthode fait à la fin.2)
InsertUser(User user)
Cela viole clairement le principe de la moindre surprise. Disons, j'ai fait mon travail jusqu'à présent et créé une nouvelle instance de a
User
et lui ai donné unname
et mis unpassword
, je serais surpris par:a) cela n'insère pas seulement un utilisateur dans quelque chose
b) cela sape également mon intention d'insérer l'utilisateur tel quel; le nom et le mot de passe ont été remplacés.
c) cela indique-t-il qu'il s'agit également d'une violation du principe de responsabilité unique.
Nouvelle façon:
1)
InsertUser(User user)
Violation toujours du SRP et principe de la moindre surprise
2)
SetUsername(User user)
Question
Mieux:
SetRandomName
ou quelque chose qui reflète l'intention.3)
SetPassword(User user)
Question
Mieux:
SetRandomPassword
ou quelque chose qui reflète l'intention.Suggestion:
Ce que je préférerais lire serait quelque chose comme:
Concernant votre question initiale:
Non, c'est plus une question de goût.
la source