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 entité User
, mais avant d'insérer l'entité, nous appelons quelques 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?
Le problème ici est qu'un User
peut en fait contenir 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 afin de 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 classe User
et une classe EnrollRequest
. 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:
public User InsertUser(EnrollRequest request) {
var userName = GenerateUserName();
var password = GeneratePassword();
//You might want to replace this with a factory call, but "new" works here as an example
var newUser = new User
(
request.Name,
request.Email,
userName,
password
);
context.Users.Add(user);
return newUser;
}
L'appelant ne démarre qu'avec 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 de type sécurisé entre un utilisateur qui est inséré et un qui ne l'est pas.
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 comme
repo.InsertUser(user);
pour modifier l'état interne des référentiels, pas l'état de l'objet utilisateur. Ce problème existe dans les deux de vos implémentations, comment InsertUser
fait cela en interne est complètement hors de propos.
Pour résoudre ce problème, vous pouvez soit
initialisation distincte de l'insertion (donc l'appelant de InsertUser
doit fournir un objet User
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.
Choisissez donc un nom de méthode comme PrepareAndInsertUser
, OrchestrateUserInsertion
, InsertUserWithNewNameAndPassword
ou tout ce que vous préférez pour rendre 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.
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 user.UserName
Et user.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 en question.
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
est en train de changer jusqu'à ce que vous alliez 2 couches en profondeurUser
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 applicationJe 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.
Fondamentalement, la pratique de définir la valeur d'une propriété à partir d'une autre méthode est-elle une mauvaise pratique?
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'à lire les valeurs existantes. Mais c'est un argument différent de celui de votre cas, puisque SetUsername(user)
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:
var myContract = CreateEmptyContract();
ArrrangeContractDeletedStatus(myContract);
Je m'attends explicitement à ce que la méthode ArrrangeContractDeletedStatus
change l'état de l'objet myContract
.
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
et ArrrangeContractDeletedStatus
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:
myContract = ArrrangeContractDeletedStatus(myContract);
C'est soit redondant (puisque je change de toute façon l'objet), soit je me force maintenant à faire un clone profond de l'objet myContract
; 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 brouillez pas que la méthode est susceptible de changer l'objet passé. Pour votre situation actuelle, cela est clairement expliqué par le nom de la méthode.
Je trouve l'ancienne aussi bien que la nouvelle problématique.
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
Où l'utilisateur est-il inséré?
Le nom de la méthode doit lire 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 d'un User
et lui ai donné un name
et défini un password
, je serais surpris:
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.
1) InsertUser(User user)
Violation toujours du SRP et principe de la moindre surprise
2) SetUsername(User user)
Question
Définir le nom d'utilisateur? À quoi?
Mieux: SetRandomName
ou quelque chose qui reflète l'intention.
3) SetPassword(User user)
Question
Définir le mot de passe? À quoi?
Mieux: SetRandomPassword
ou quelque chose qui reflète l'intention.
Ce que je préférerais lire serait quelque chose comme:
public User GenerateRandomUser(UserFactory Uf){
User u = Uf.GetUser();
u.Name = GenerateRandomUserName();
u.Password = GenerateRandomUserPassword();
return u;
}
...
public void AddUserToContext(User u){
this.context.Users.Add(u);
}
Concernant votre question initiale:
La modification d'un objet transmis par référence est-elle une mauvaise pratique?
Non, c'est plus une question de goût.