web-dev-qa-db-fra.com

La modification d'un objet transmis par référence est-elle une mauvaise pratique?

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?

12
JD Davis

Le problème ici est qu'un User peut en fait contenir deux choses différentes:

  1. Une entité utilisateur complète, qui peut être transmise à votre magasin de données.

  2. 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.

10
John Wu

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 InsertUserne 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.

5
Doc Brown

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

  • Cela rend la programmation simultanée plus difficile - mais toutes les applications ne sont pas multithread
  • Ces modifications peuvent être surprenantes, surtout si rien dans la méthode ne suggère que cela se produirait
  • Ces surprises peuvent rendre la maintenance plus difficile

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 correctement
  • Si le nom requiert des informations de l'objet utilisateur, vous pouvez remplacer la signature par GenerateUserName(User user) et conserver cette testabilité

La nouvelle méthode masque les mutations:

  • Vous ne savez pas que l'objet User est en train de changer jusqu'à ce que vous alliez 2 couches en profondeur
  • Les modifications apportées à l'objet User sont plus surprenantes dans ce cas
  • SetUserName() 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 application
3
Berin Loritsch

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.

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.

1
Flater

Je trouve l'ancienne aussi bien que 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

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.

Nouvelle façon:

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.


Suggestion:

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.

0
Thomas Junk