Est-il correct pour une fonction de modifier un paramètre

17

Nous avons une couche de données qui enveloppe Linq To SQL. Dans cette couche de données, nous avons cette méthode (simplifiée)

int InsertReport(Report report)
{
    db.Reports.InsertOnSubmit(report);
    db.SubmitChanges();
    return report.ID; 
}

Lors de la soumission des modifications, l'ID du rapport est mis à jour avec la valeur dans la base de données que nous renvoyons ensuite.

Du côté appelant, cela ressemble à ceci (simplifié)

var report = new Report();
DataLayer.InsertReport(report);
// Do something with report.ID

En regardant le code, l'ID a été défini dans la fonction InsertReport comme une sorte d'effet secondaire, puis nous ignorons la valeur de retour.

Ma question est, devrais-je compter sur l'effet secondaire et faire quelque chose comme ça à la place.

void InsertReport(Report report)
{
    db.Reports.InsertOnSubmit(report);
    db.SubmitChanges();
}

ou devons-nous l'empêcher

int InsertReport(Report report)
{
    var newReport = report.Clone();
    db.Reports.InsertOnSubmit(newReport);
    db.SubmitChanges();
    return newReport.ID; 
}

peut-être même

Report InsertReport(Report report)
{
    var newReport = report.Clone();
    db.Reports.InsertOnSubmit(newReport);
    db.SubmitChanges();
    return newReport; 
}

Cette question a été soulevée lorsque nous avons créé un test unitaire et constaté qu'il n'était pas vraiment clair que la propriété ID des paramètres du rapport était mise à jour et que pour se moquer du comportement des effets secondaires, une odeur de code si vous voulez.

John Petrak
la source
2
C'est à cela que sert la documentation de l'API.

Réponses:

16

Oui, c'est OK et assez courant. Cependant, cela peut ne pas être évident, comme vous l'avez découvert.

En général, les méthodes de type persistance ont tendance à renvoyer l'instance mise à jour de l'objet. C'est:

Report InsertReport(Report report)
{        
    db.Reports.InsertOnSubmit(report);
    db.SubmitChanges();
    return report; 
}

Oui, vous retournez le même objet que celui que vous avez transmis, mais cela rend l'API plus claire. Il n'y a pas besoin du clone - s'il y a quelque chose qui causera de la confusion si, comme dans votre code d'origine, l'appelant continue à utiliser l'objet qu'il a transmis.

Une autre option consiste à utiliser un DTO

Report InsertReport(ReportDTO dto)
{
    var newReport = Report.Create(dto);
    db.Reports.InsertOnSubmit(newReport);
    db.SubmitChanges();
    return newReport; 
}

De cette façon, l'API est très évidente, et l'appelant ne peut pas essayer accidentellement d'utiliser l'objet passé / modifié. Selon ce que fait votre code, cela peut cependant être un peu pénible.

Ben Hughes
la source
Je ne retournerais pas l'objet si Ira n'était pas nécessaire. Cela ressemble à un traitement supplémentaire et à une utilisation (excessive) de la mémoire. Je vais pour un vide ou une exception, sinon par besoin. Sinon, je vote pour votre échantillon DTO.
Indépendant
Toutes ces réponses résument à peu près nos propres discussions. Cela semble être une question sans véritable réponse. Votre déclaration "Oui, vous retournez le même objet que celui que vous avez transmis, mais cela rend l'API plus claire." à peu près répondu à notre question.
John Petrak
4

OMI, c'est un cas rare où l'effet secondaire du changement est souhaitable - parce que votre entité de rapport a un ID, il pourrait déjà être supposé avoir les préoccupations d'un DTO, et il y a sans doute l' obligation ORM de s'assurer que le rapport en mémoire l'entité est synchronisée avec l'objet de représentation de la base de données.

StuartLC
la source
6
+1 - après avoir inséré une entité dans la base de données, vous vous attendez à ce qu'elle ait un ID. Il serait plus surprenant que l'entité revienne sans elle.
MattDavey
2

Le problème est que sans documentation, ce n'est pas clair du tout ce que fait la méthode et surtout pourquoi retourne-t-elle un entier.

La solution la plus simple serait d'utiliser un nom différent pour votre méthode. Quelque chose comme:

int GenerateIdAndInsert(Report report)

Pourtant, cela n'est pas assez ambigu: si, comme en C #, l'instance de l' reportobjet est passée par référence, il serait difficile de savoir si l' reportobjet d' origine a été modifié, ou si la méthode l'a cloné et modifié uniquement le clone. Si vous choisissez de modifier l'objet d'origine, il serait préférable de nommer la méthode:

void ChangeIdAndInsert(Report report)

Une solution plus compliquée (et peut-être moins optimale) consiste à refactoriser fortement le code. Qu'en est-il de:

using (var transaction = new TransactionScope())
{
    var id = this.Data.GenerateReportId(); // We need to find an available ID...
    this.Data.AddReportWithId(id, report); // ... and use this ID to insert a report.
    transaction.Complete();
}
Arseni Mourzenko
la source
2

Les programmeurs s'attendent généralement à ce que seules les méthodes d'instance d'un objet puissent changer son état. En d'autres termes, je ne suis pas surpris de report.insert()modifier l'ID du rapport et c'est facile à vérifier. Il n'est pas facile de se demander pour chaque méthode de l'application entière si elle change l'identifiant du rapport ou non.

Je dirais également que peut-être IDne devrait même pas appartenir du Reporttout. Comme il ne contient pas un identifiant valide depuis si longtemps, vous avez vraiment deux objets différents avant et après l'insertion, avec un comportement différent. L'objet "avant" peut être inséré, mais il ne peut pas être récupéré, mis à jour ou supprimé. L'objet "après" est exactement le contraire. L'un a un identifiant et l'autre pas. La façon dont ils sont affichés peut être différente. Les listes dans lesquelles ils apparaissent peuvent être différentes. Les autorisations utilisateur associées peuvent être différentes. Ce sont tous les deux des «rapports» au sens anglais du terme, mais ils sont très différents.

D'un autre côté, votre code peut être assez simple pour qu'un seul objet suffise, mais c'est quelque chose à considérer si votre code est parsemé de if (validId) {...} else {...}.

Karl Bielefeldt
la source
0

Non ce n'est pas OK! Il convient pour une procédure de modifier un paramètre uniquement dans les langages procéduraux, où il n'y a pas d'autre moyen; dans les langages POO, appelez la méthode de changement sur l'objet, dans ce cas sur le rapport (quelque chose comme report.generateNewId ()).

Dans ce cas, votre méthode fait 2 choses, donc casse SRP: elle insère un enregistrement dans la base de données et elle génère un nouvel identifiant. L'appelant n'est pas en mesure de savoir que votre méthode génère également un nouvel identifiant car il est simplement appelé insertRecord ().

m3th0dman
la source
3
Euh ... et si on db.Reports.InsertOnSubmit(report)appelle la méthode de changement sur l'objet?
Stephen C
Ce n'est pas grave ... cela devrait être évité, mais dans ce cas, LINQ to SQL effectue la modification des paramètres, ce n'est donc pas comme si l'OP pouvait éviter cela sans l'effet de clone de saut de cercle (qui est sa propre violation de SRP).
Telastyn
@StephenC Je disais que vous devriez appeler cette méthode sur l'objet de rapport; dans ce cas, il est inutile de passer le même objet comme paramètre.
m3th0dman
@Telastyn Je parlais dans le cas général; les bonnes pratiques ne peuvent être respectées à 100%. Dans son cas particulier, personne ne peut déduire quel est le meilleur moyen à partir de 5 lignes de code ...
m3th0dman