Applicabilité du principe de responsabilité unique

40

Je suis récemment tombé sur un problème architectural apparemment trivial. J'ai eu un dépôt simple dans mon code qui s'appelait comme ceci (le code est en C #):

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges était un simple wrapper qui valide les modifications dans la base de données:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

Ensuite, après un certain temps, je devais mettre en œuvre une nouvelle logique permettant d'envoyer des notifications par courrier électronique chaque fois qu'un utilisateur était créé dans le système. Comme il y a eu beaucoup d'appels vers _userRepository.Add()et SaveChangesautour du système, j'ai décidé de mettre à jour de la manière SaveChangessuivante:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

De cette manière, du code externe pourrait s'abonner à UserCreatedEvent et gérer la logique métier nécessaire pour envoyer des notifications.

Mais on m'a fait remarquer que ma modification de SaveChangesviolait le principe de responsabilité unique et que cela SaveChangesne devrait que sauver et ne déclencher aucun événement.

Est-ce un point valable? Il me semble que la création d'un événement ici est essentiellement la même chose que la journalisation: il suffit d'ajouter une fonctionnalité latérale à la fonction. Et SRP ne vous interdit pas d'utiliser des événements de journalisation ou de déclenchement dans vos fonctions, il indique simplement qu'une telle logique doit être encapsulée dans d'autres classes et qu'il est correct qu'un référentiel appelle ces autres classes.

André Borges
la source
22
Votre réponse est: "OK, alors comment l'écririez- vous pour qu'il ne viole pas SRP mais permette tout de même un seul point de modification?"
Robert Harvey
43
Mon observation est que le fait de soulever un événement n’ajoute pas de responsabilité supplémentaire. En fait, au contraire: il délègue la responsabilité ailleurs.
Robert Harvey
Je pense que votre collègue a raison, mais votre question est valide et utile, alors votez majoritairement!
Andres F.
16
Il n’existe pas de définition définitive de la responsabilité unique. La personne qui souligne qu’elle enfreint SRP est correcte en utilisant sa définition personnelle et vous êtes correcte en utilisant votre définition. Je pense que votre conception convient parfaitement, à condition toutefois que cet événement ne soit pas unique et que d'autres fonctionnalités similaires soient réalisées de différentes manières. La cohérence est de loin, beaucoup, beaucoup plus importante à prendre en compte qu'une directive vague telle que SRP qui va à l'extrême se termine avec des tonnes de classes très faciles à comprendre que personne ne sait comment faire fonctionner dans un système.
Dunk

Réponses:

14

Oui, il peut être une exigence valable d'avoir un référentiel qui déclenche certains événements sur certaines actions comme Addou SaveChanges- et je ne vais pas remettre en question cette (comme d'autres réponses) juste parce que votre exemple spécifique de l' ajout d' utilisateurs et d' envoyer des e - mails peut sembler un peu artificiel. Dans la suite, supposons que cette exigence est parfaitement justifiée dans le contexte de votre système.

Alors oui , coder la mécanique des événements, ainsi que la journalisation et l'enregistrement dans une méthode est une violation de la SRP . Dans de nombreux cas, il s'agit probablement d'une violation acceptable, en particulier lorsque personne ne souhaite jamais répartir les responsabilités de maintenance "Enregistrer les modifications" et "Relancer l'événement" entre différentes équipes / responsables. Mais supposons un jour que quelqu'un veuille faire exactement cela, cela peut-il être résolu de manière simple, peut-être en mettant le code de ces problèmes dans différentes bibliothèques de classes?

La solution consiste à laisser votre référentiel d'origine rester responsable des modifications apportées à la base de données, rien de plus, et créer un référentiel proxy qui possède exactement la même interface publique, réutilise le référentiel d'origine et ajoute les mécanismes d'événement supplémentaires aux méthodes.

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

Vous pouvez appeler la classe de proxy a NotifyingRepositoryou ObservableRepositorysi vous le souhaitez, dans le sens de la réponse hautement votée de @ Peter (qui ne dit en fait pas comment résoudre la violation SRP, mais indique simplement que la violation est correcte).

La nouvelle et l'ancienne classe de référentiel doivent toutes deux dériver d'une interface commune, comme indiqué dans la description de modèle de proxy classique .

Puis, dans votre code d'origine, initialisez _userRepositorypar un objet de la nouvelle EventFiringUserRepoclasse. De cette façon, vous gardez le référentiel d'origine séparé de la mécanique des événements. Si nécessaire, vous pouvez placer côte à côte le référentiel de déclenchement d'événements et le référentiel d'origine et laisser les appelants décider s'ils utilisent l'un ou l'autre.

Pour répondre à une préoccupation mentionnée dans les commentaires: cela ne mène-t-il pas à des procurations en plus de procurations à des procurations, et ainsi de suite? En fait, l'ajout de mécanismes d'événements crée une base permettant d'ajouter d'autres exigences du type "envoyer des courriels" en souscrivant simplement aux événements. Vous devez donc vous en tenir au SRP avec ces exigences, sans autre mandataire. Mais la seule chose à ajouter une fois est la mécanique d’événement elle-même.

Si ce type de séparation en vaut vraiment la peine dans le contexte de votre système, vous et votre relecteur devez en décider vous-même. Je ne voudrais probablement pas séparer la journalisation du code d'origine, ni en utilisant un autre proxy, ni en ajoutant un consignateur à l'événement d'écoute, même si cela serait possible.

Doc Brown
la source
3
En plus de cette réponse. Il existe des alternatives aux mandataires, comme AOP .
Laiv
1
Je pense que vous ratez le problème, ce n'est pas que soulever un événement casse le PÉR, c'est que soulever un événement uniquement pour les "nouveaux" utilisateurs nécessite que le référant soit responsable de savoir ce qui constitue un "nouvel" utilisateur plutôt qu'un "nouvellement ajouté à moi" "utilisateur
Ewan
@Ewan: veuillez lire la question à nouveau. Il s'agissait d'un endroit dans le code qui effectue certaines actions qui doivent être associées à d'autres actions en dehors de la réactivité de cet objet. Et mettre l’action et l’événement en un seul endroit a été mis en doute par un examinateur comme une violation du PÉR. L’exemple de «sauvegarde de nouveaux utilisateurs» sert uniquement à des fins de démonstration, appelez l’exemple conçu si vous le souhaitez, mais c’est à mon humble avis pas le point de la question.
Doc Brown
2
C’est la meilleure réponse / réponse correcte OMI. Non seulement il maintient SRP, mais il maintient également le principe Open / Closed. Et pensez à tous les tests automatisés que des changements au sein de la classe pourraient casser. La modification de tests existants lorsque de nouvelles fonctionnalités sont ajoutées est une odeur nauséabonde.
Keith Payne
1
Comment cette solution fonctionne-t-elle si une transaction est en cours? Lorsque cela se produit, SaveChanges()l’enregistrement de base de données n’est pas créé et il pourrait être annulé. On dirait que vous devez remplacer AcceptAllChangesou souscrire à l'événement TransactionCompleted.
John Wu
29

L'envoi d'une notification indiquant que le magasin de données persistantes a été modifié semble être une bonne chose à faire lors de la sauvegarde.

Bien entendu, vous ne devez pas considérer Add comme un cas spécial - vous devez également déclencher des événements pour Modify et Delete. C'est le traitement spécial du cas "Ajouter" qui sent, oblige le lecteur à expliquer pourquoi il sent, et amène finalement certains lecteurs du code à conclure qu'il doit violer le PÉR.

Un référentiel "notifiant" qui peut être interrogé, modifié et déclenche des événements lors de modifications est un objet parfaitement normal. Vous pouvez vous attendre à en trouver plusieurs variantes dans presque tous les projets de taille décente.


Mais un référentiel "notifiant" est-il réellement ce dont vous avez besoin? Vous avez mentionné C #: de nombreuses personnes s'accorderaient pour dire que l'utilisation de a System.Collections.ObjectModel.ObservableCollection<>au lieu de System.Collections.Generic.List<>lorsque ce dernier est tout ce dont vous avez besoin est une panoplie de mauvaises et de mauvaises erreurs, mais peu de gens se dirigent immédiatement vers SRP.

Ce que vous faites maintenant, c'est échanger votre UserList _userRepositoryavec un ObservableUserCollection _userRepository. Si tel est le meilleur plan d'action ou non dépend de l'application. Mais bien que cela rend incontestablement le _userRepositorypoids considérablement moins léger, à mon humble avis, il ne viole pas le PÉR.

Peter
la source
Le problème de l'utilisation ObservableCollectiondans ce cas est qu'il déclenche l'événement équivalent non pas lors de l'appel à SaveChanges, mais lors de l'appel à Addce qui entraînerait un comportement très différent de celui présenté dans l'exemple. Voir ma réponse pour savoir comment garder le référentiel original léger tout en respectant le PRS en conservant la sémantique intacte.
Doc Brown
@DocBrown J'ai invoqué les classes connues ObservableCollection<>et List<>pour la comparaison et le contexte. Je ne voulais pas recommander l'utilisation des classes réelles pour l'implémentation interne ou l'interface externe.
Peter
Ok, mais même si le PO ajoutait des événements à "Modifier" et à "Supprimer" (je pense que le PO laissait de côté pour garder la question concise, par souci de simplicité), je pense qu'un relecteur pourrait facilement arriver à la conclusion de une violation du PÉR. C'est probablement une solution acceptable, mais aucune qui ne puisse être résolue si nécessaire.
Doc Brown
16

Oui, c'est une violation du principe de responsabilité unique et un argument valable.

Une meilleure conception consisterait à avoir un processus séparé pour extraire les «nouveaux utilisateurs» du référentiel et envoyer les courriels. Garder une trace des utilisateurs qui ont reçu un email, des échecs, des renvois, etc., etc.

De cette façon, vous pouvez gérer les erreurs, les plantages, etc., tout en évitant que votre référentiel ne saisisse toutes les exigences, ce qui laisse penser que des événements se produisent "lorsqu'un élément est validé dans la base de données".

Le référentiel ne sait pas qu'un utilisateur que vous ajoutez est un nouvel utilisateur. Sa responsabilité consiste simplement à stocker l'utilisateur.

Cela vaut probablement la peine de développer les commentaires ci-dessous.

Le référentiel ne sait pas qu'un utilisateur que vous ajoutez est un nouvel utilisateur. Oui, il dispose d'une méthode appelée Ajouter. Sa sémantique implique que tous les utilisateurs ajoutés sont de nouveaux utilisateurs. Combinez tous les arguments passés à Ajouter avant d'appeler Enregistrer - et vous obtenez tous les nouveaux utilisateurs

Incorrect. Vous associez "Ajouté au référentiel" et "Nouveau".

"Ajouté au référentiel" signifie exactement ce qu'il dit. Je peux ajouter et supprimer et ré-ajouter des utilisateurs à différents référentiels.

"Nouveau" est un état d'utilisateur défini par des règles commerciales.

Actuellement, la règle de gestion peut être "Le nouveau = = vient d'être ajouté au référentiel", mais cela ne signifie pas que ce n'est pas une responsabilité distincte de connaître et d'appliquer cette règle.

Vous devez faire attention à éviter ce genre de pensée centrée sur la base de données. Vous aurez des processus de cas marginaux qui ajoutent des utilisateurs non-nouveaux au référentiel et lorsque vous leur envoyez des courriels, toute l'entreprise dira "Bien sûr, ce ne sont pas de" nouveaux "utilisateurs! La règle réelle est X".

Cette réponse est IMHO manque vraiment le point: le repo est exactement la place centrale dans le code qui sait quand de nouveaux utilisateurs sont ajoutés

Incorrect. Pour les raisons ci-dessus, en outre, il ne s'agit pas d'un emplacement central, sauf si vous incluez réellement le code d'envoi du courrier électronique dans la classe plutôt que de simplement déclencher un événement.

Vous aurez des applications qui utilisent la classe de référentiel, mais vous n'avez pas le code pour envoyer l'e-mail. Lorsque vous ajoutez des utilisateurs à ces applications, le courrier électronique ne sera pas envoyé.

Ewan
la source
11
Le référentiel ne sait pas qu'un utilisateur que vous ajoutez est un nouvel utilisateur . Oui, il possède une méthode appelée Add. Sa sémantique implique que tous les utilisateurs ajoutés sont de nouveaux utilisateurs. Combinez tous les arguments passés Addavant d'appeler Save- et vous obtenez tous les nouveaux utilisateurs.
André Borges
J'aime cette suggestion. Cependant, le pragmatisme prévaut sur la pureté. Selon les circonstances, il peut être difficile de justifier l'ajout d'une couche architecturale entièrement nouvelle à une application existante si tout ce que vous avez à faire est d'envoyer littéralement un seul courrier électronique lorsqu'un utilisateur est ajouté.
Alexander
Mais l'événement ne dit pas que l'utilisateur a été ajouté. Il dit utilisateur créé. Si nous considérons nommer les choses correctement et si nous sommes d’accord avec les différences sémantiques entre ajouter et créer, l’événement dans l’extrait est soit mal nommé, soit mal placé. Je ne pense pas que le critique ait eu quelque chose contre les référentiels. Il était probablement préoccupé par le type d’événement et ses effets secondaires.
Laiv
7
@Andre Nouveau sur le repo, mais pas nécessairement "nouveau" au sens des affaires. C'est la fusion de ces deux idées qui cache la responsabilité supplémentaire à première vue. Je pourrais importer une tonne d'anciens utilisateurs dans mon nouveau référentiel, ou supprimer et rajouter un utilisateur, etc. Il y aura des règles commerciales concernant ce qu'un "nouvel utilisateur" est au-delà "a été ajouté au dB"
Ewan
1
Note du modérateur: Votre réponse n'est pas une interview journalistique. Si vous avez des modifications, incorporez-les naturellement dans votre réponse sans créer l’effet «d’actualité». Nous ne sommes pas un forum de discussion.
Robert Harvey
7

Est-ce un point valable?

Oui, bien que cela dépende beaucoup de la structure de votre code. Je n'ai pas le contexte complet alors je vais essayer de parler en général.

Il me semble que la création d'un événement ici est essentiellement la même chose que la journalisation: il suffit d'ajouter une fonctionnalité latérale à la fonction.

Ce n'est absolument pas. La journalisation ne fait pas partie du flux commercial, elle peut être désactivée, ne doit pas causer d’effets secondaires (commerciaux) et ne doit en aucune manière influer sur l’état et la santé de votre application, même si, pour une raison quelconque, vous ne pouviez pas vous connecter. plus rien. Maintenant, comparez cela avec la logique que vous avez ajoutée.

Et SRP ne vous interdit pas d'utiliser des événements de journalisation ou de déclenchement dans vos fonctions, il indique simplement qu'une telle logique doit être encapsulée dans d'autres classes et qu'il est correct qu'un référentiel appelle ces autres classes.

SRP fonctionne en tandem avec ISP (S et I dans SOLID). Vous vous retrouvez avec de nombreuses classes et méthodes qui font des choses très spécifiques et rien d’autre. Ils sont très ciblés, très faciles à mettre à jour ou à remplacer et, en général, plus faciles à tester. Bien entendu, dans la pratique, vous aurez également quelques classes plus importantes qui traitent de l’orchestration: elles auront un certain nombre de dépendances et ne se concentreront pas sur des actions atomisées, mais sur des actions commerciales, qui peuvent nécessiter plusieurs étapes. Tant que le contexte commercial est clair, ils peuvent également être qualifiés de responsabilité unique, mais comme vous l'avez dit à juste titre, à mesure que le code évolue, vous souhaiterez peut-être en extraire une partie dans de nouvelles classes / interfaces.

Revenons maintenant à votre exemple particulier. Si vous devez absolument envoyer une notification à chaque fois qu'un utilisateur est créé et peut-être même effectuer d'autres actions plus spécialisées, vous pouvez créer un service séparé qui encapsule cette exigence, comme par exemple UserCreationService, qui expose une méthode Add(user), qui gère à la fois le stockage (l'appel dans votre référentiel) et la notification en une seule action commerciale. Ou faites-le dans votre extrait original, après_userRepository.SaveChanges();

async
la source
2
La journalisation ne fait pas partie du flux d’activités - en quoi cela est-il pertinent dans le contexte de SRP? Si le but de mon événement était d'envoyer de nouvelles données utilisateur à Google Analytics, sa désactivation aurait le même effet commercial que la désactivation de la journalisation: pas critique, mais plutôt bouleversant. Quelle est la règle empirique pour ajouter / ne pas ajouter de nouvelle logique à une fonction? "Sa désactivation causera-t-elle des effets secondaires majeurs sur l'entreprise?"
André Borges
2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . Que se passe-t-il si vous déclenchez des événements prématurés provoquant de fausses "nouvelles"? Que se passe-t-il si les analyses prennent en compte des "utilisateurs" qui n'ont finalement pas été créés en raison d'erreurs dans la transaction de base de données? Que se passe-t-il si l'entreprise prend des décisions sur de fausses prémisses, étayées par des données imprécises? Vous êtes trop concentré sur le côté technique de la question. "Parfois, on ne voit pas le bois pour les arbres" "
Laiv
@ Laiv, vous faites valoir un argument valable, mais ce n'est pas le but de ma question, ni de cette réponse. La question est de savoir s'il s'agit d'une solution valide dans le contexte de SRP. Supposons donc qu'il n'y a pas d'erreur de transaction dans la base de données.
André Borges
Vous me demandez essentiellement de vous dire ce que vous voulez entendre. Je vous donne juste la portée. Une plus grande marge de manœuvre pour que vous puissiez décider si un PRS est important ou non, car ce dernier est inutile sans le contexte approprié. OMI, la façon dont vous abordez le problème est incorrecte car vous vous concentrez uniquement sur la solution technique. Vous devriez donner assez de pertinence à l'ensemble du contexte. Et oui, DB pourrait échouer. Il y a une chance pour que cela se produise et vous ne devriez pas l'omettre, car, comme vous le savez, certaines choses se produisent et pourraient vous faire changer d'avis sur les doutes concernant les PÉR ou d'autres bonnes pratiques.
Laiv
1
Cela dit, rappelez-vous que les principes ne sont pas des règles écrites dans la pierre. Ils sont perméables (adaptatifs). Comme vous pouvez le constater, ils sont ouverts à interprétation. Votre critique a une interprétation et vous en avez une autre. Essayez de voir ce que vous voyez, de résoudre ses doutes et préoccupations, ou laissez-le résoudre les vôtres. Vous ne trouverez pas la "bonne" réponse ici. La réponse correcte est à vous et à votre critique de trouver, en demandant d’abord les exigences (fonctionnelles et non fonctionnelles) du projet.
Laiv
4

En théorie, SRP concerne les gens , comme Oncle Bob l'explique dans son article Le principe de la responsabilité unique . Merci Robert Harvey pour le fournir dans votre commentaire.

La bonne question est:

Quelle "partie prenante" a ajouté l'exigence "envoyer des courriels"?

Si cette partie prenante est également responsable de la persistance des données (improbable mais possible), cela ne constitue pas une violation du PRS. Sinon, c'est le cas.

utilisateur949300
la source
2
Intéressant - je n'ai jamais entendu parler de cette interprétation du PÉR. Avez-vous des indications pour plus d'informations / de littérature sur cette interprétation?
Sleske
2
@sleske: de l' oncle Bob lui - même : "Et cela touche au cœur du principe de responsabilité unique. Ce principe concerne les personnes. Lorsque vous écrivez un module logiciel, vous voulez vous assurer que lorsque des modifications sont demandées, ces modifications ne peuvent provenir que provenant d'une seule personne, ou plutôt d'un seul groupe de personnes étroitement liées représentant une seule fonction commerciale étroitement définie. "
Robert Harvey
Merci Robert. OMI, le nom "Principe de responsabilité unique" est terrible, car cela semble simple, mais trop peu de personnes suivent la signification de "responsabilité". Un peu comme la façon dont la programmation orientée objet a été mutée à partir de plusieurs de ses concepts originaux, et est maintenant un terme relativement dépourvu de sens.
user949300
1
Oui. C'est ce qui est arrivé au terme REST. Même Roy Fielding dit que les gens l'utilisent mal.
Robert Harvey
Bien que la cite soit liée, je pense que cette réponse manque que l'exigence "envoyer des courriels" ne fait pas partie des exigences directes sur lesquelles porte la question de la violation du PRS. Cependant, en disant "Quelle" partie prenante "a ajouté l'exigence" d'événement de relance "" , cette réponse deviendrait plus liée à la question réelle. J'ai modifié un peu ma propre réponse pour que cela soit plus clair.
Doc Brown
2

Bien que, techniquement, les référentiels notifiant des événements soient parfaits, je suggérerais de le regarder d'un point de vue fonctionnel lorsque sa commodité soulève certaines préoccupations.

Créer un utilisateur, décider de ce qu’est un nouvel utilisateur et de sa persistance sont 3 choses différentes .

Mon local

Prenez en compte le principe précédent avant de décider si le référentiel est le lieu approprié pour notifier les événements métier (quel que soit le PRS). Notez que j'ai dit événement commercial parce que pour moi UserCreateda une connotation différente de UserStoredou UserAdded 1 . Je considérerais également que chacun de ces événements s'adresse à des publics différents.

D'un côté, la création d'utilisateurs est une règle spécifique à l'entreprise qui peut impliquer ou non la persistance. Cela pourrait impliquer davantage d'opérations commerciales, impliquant davantage d'opérations de base de données / réseau. Opérations ignorées par la couche de persistance. La couche de persistance n'a pas assez de contexte pour décider si le cas d'utilisation s'est terminé avec succès ou non.

D'un autre côté, ce n'est pas nécessairement vrai que _dataContext.SaveChanges();l'utilisateur a persisté. Cela dépendra de la durée de transaction de la base de données. Par exemple, cela pourrait être vrai pour des bases de données telles que MongoDB, dont les transactions sont atomiques, mais pas pour les SGBDR traditionnels mettant en œuvre des transactions ACID où davantage de transactions pourraient être impliquées et non encore validées.

Est-ce un point valable?

Il pourrait être. Cependant, j'oserais dire que ce n'est pas seulement une question de PRS (techniquement parlant), mais aussi de commodité (fonctionnellement parlant).

  • Est-il commode de déclencher des événements commerciaux à partir de composants ignorant les opérations commerciales en cours?
  • Représentent-ils le bon endroit autant que le bon moment pour le faire?
  • Devrais-je autoriser ces composants à orchestrer ma logique métier au moyen de notifications comme celle-ci?
  • Pourrais-je invalider les effets secondaires causés par des événements prématurés? 2

Il me semble que la relance d’un événement ici est essentiellement la même chose que l’exploitation forestière.

Absolument pas. La journalisation est censée n'avoir aucun effet secondaire, car vous avez suggéré que l'événement UserCreatedprovoquerait probablement d'autres opérations commerciales. Comme les notifications. 3

il dit simplement qu'une telle logique doit être encapsulée dans d'autres classes et qu'il est correct qu'un référentiel appelle ces autres classes

Pas nécessairement vrai. SRP n'est pas une préoccupation spécifique à la classe. Il fonctionne à différents niveaux d'abstractions, comme les couches, les bibliothèques et les systèmes! Il s'agit de cohésion, de garder ensemble ce qui change pour les mêmes raisons de la part des mêmes parties prenantes . Si la création de l'utilisateur ( cas d'utilisation ) change, c'est probablement le moment et les raisons pour lesquelles l'événement se produit changent également.


1: Nommer les choses de manière adéquate est également important.

2: Disons que nous avons envoyé UserCreatedaprès _dataContext.SaveChanges();, mais que la transaction de base de données entière a échoué ultérieurement en raison de problèmes de connexion ou de violations de contraintes. Soyez prudent avec la diffusion prématurée des événements, car ses effets secondaires peuvent être difficiles à annuler (si cela est même possible).

3: Les processus de notification non traités correctement peuvent vous amener à envoyer des notifications impossibles à annuler / supprimer>

Laiv
la source
1
+1 Très bon point sur la durée de la transaction. Il peut être prématuré d'affirmer que l'utilisateur a été créé, car des retours en arrière peuvent avoir lieu; et contrairement à un journal, il est probable qu'une autre partie de l'application fasse quelque chose avec l'événement.
Andres F.
2
Exactement. Les événements dénotent la certitude. Quelque chose est arrivé mais c'est fini.
Laiv
1
@ Laiv: Sauf quand ils ne le font pas. Microsoft a toutes sortes d’événements préfixés Beforeou Previewqui ne donnent aucune garantie de certitude.
Robert Harvey
1
@ jpmc26: Sans alternative, votre suggestion n'est pas utile.
Robert Harvey
1
@ jpmc26: Votre réponse est donc "passez à un écosystème de développement complètement différent avec un ensemble d'outils et de performances totalement différent." Appelez-moi le contraire, mais j'imagine que c'est impossible pour la grande majorité des efforts de développement.
Robert Harvey
1

Non, cela ne viole pas le PÉR.

Beaucoup semblent penser que le principe de responsabilité unique signifie qu'une fonction ne doit faire «qu'une chose», puis se laisser prendre dans la discussion sur ce qui constitue «une chose».

Mais ce n’est pas ce que signifie le principe. Il s'agit de préoccupations au niveau des entreprises. Une classe ne doit pas implémenter plusieurs préoccupations ou exigences pouvant changer indépendamment au niveau de l'entreprise. Disons qu'une classe à la fois stocke l'utilisateur et envoie un message de bienvenue codé en dur par courrier électronique. Plusieurs préoccupations indépendantes pourraient entraîner le changement des exigences d’une telle classe. Le concepteur peut exiger que le code HTML / feuille de style du courrier soit modifié. L’expert en communication pourrait exiger que le libellé du courrier soit modifié. Et l'expert UX pourrait décider que le courrier devrait être envoyé à un point différent du flux d'intégration. La classe est donc sujette à de nombreuses modifications d'exigences provenant de sources indépendantes. Cela viole le SRP.

Mais déclencher un événement ne constitue pas une violation de la SRP, car il ne dépend que de la sauvegarde de l'utilisateur et non d'une autre préoccupation. Les événements sont en fait un très bon moyen de respecter le SRP, car vous pouvez avoir un courrier électronique déclenché par la sauvegarde sans que le référentiel soit affecté par le courrier, ou même qu'il en soit informé.

JacquesB
la source
1

Ne vous inquiétez pas du principe de responsabilité unique. Cela ne vous aidera pas à prendre une bonne décision ici, car vous pouvez choisir subjectivement un concept particulier en tant que "responsabilité". Vous pouvez dire que la classe est responsable de la gestion de la persistance des données dans la base de données ou que sa responsabilité consiste à effectuer tout le travail lié à la création d'un utilisateur. Ce ne sont que différents niveaux de comportement de l'application, et ce sont deux expressions conceptuelles valables d'une "responsabilité unique". Donc, ce principe n'est pas utile pour résoudre votre problème.

Le principe le plus utile à appliquer dans ce cas est le principe de la moindre surprise . Alors posons la question suivante: est-il étonnant qu’un référentiel ayant pour rôle principal de conserver des données dans une base de données envoie également des courriers électroniques?

Oui, c'est très surprenant. Ce sont deux systèmes externes complètement séparés, et le nom SaveChangesn'implique pas l'envoi de notifications. Le fait que vous déléguiez ceci à un événement rend le comportement encore plus surprenant, car une personne qui lit le code ne peut plus facilement voir quels comportements supplémentaires sont invoqués. L'indirection nuit à la lisibilité. Parfois, les avantages sont dignes des coûts de lisibilité, mais pas lorsque vous appelez automatiquement un système externe supplémentaire ayant des effets observables pour les utilisateurs finaux. (La journalisation peut être exclue ici car son effet consiste essentiellement à conserver des enregistrements à des fins de débogage. Les utilisateurs finaux ne consomment pas le journal, il n'y a donc aucun inconvénient à ce que la journalisation soit toujours effectuée.) Pire encore, cela réduit la flexibilité du minutage. d’envoyer le courrier électronique, rendant impossible l’entrelacement d’autres opérations entre la sauvegarde et la notification.

Si votre code doit généralement envoyer une notification lorsqu'un utilisateur est créé avec succès, vous pouvez créer une méthode pour le faire:

public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
    repo.Add(user);
    repo.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Mais si cela ajoute de la valeur dépend des spécificités de votre application.


En fait, je découragerais l'existence de la SaveChangesméthode. Cette méthode va probablement valider une transaction de base de données, mais d' autres référentiels peuvent avoir modifié la base de données dans la même transaction . Le fait qu'il les engage toutes est à nouveau surprenant, car il SaveChangesest spécifiquement lié à cette instance du référentiel d'utilisateurs.

Le modèle le plus simple pour gérer une transaction de base de données est un usingbloc externe :

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    context.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Cela donne au programmeur un contrôle explicite sur le moment où les modifications de tous les référentiels sont enregistrées, oblige le code à documenter explicitement la séquence d'événements devant se produire avant une validation, garantit qu'une annulation est émise en cas d'erreur (en supposant qu'une annulation est émise DataContext.Dispose) et évite les occultations. les connexions entre les classes avec état.

Je préférerais également ne pas envoyer l'e-mail directement dans la demande. Il serait plus robuste d’enregistrer le besoin d’une notification dans une file d’attente. Cela permettrait une meilleure gestion des échecs. En particulier, si une erreur survient lors de l'envoi du courrier électronique, il peut être réessayé ultérieurement sans interrompre la sauvegarde de l'utilisateur. Cela évite le cas où l'utilisateur est créé mais une erreur est renvoyée par le site.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

Il est préférable de valider d'abord la file d'attente de notification, car le consommateur de la file d'attente peut vérifier que l'utilisateur existe avant d'envoyer le courrier électronique, en cas context.SaveChanges()d'échec de l' appel. (Sinon, vous aurez besoin d'une stratégie de validation complète en deux phases pour éviter les heisenbugs.)


L'essentiel est d'être pratique. Réfléchissez réellement aux conséquences (à la fois en termes de risque et d’avantages) de l’écriture de code d’une manière particulière. Je trouve que le "principe de responsabilité unique" ne m'aide pas très souvent à faire cela, alors que le "principe de moindre surprise" m'aide souvent à entrer dans la tête d'un autre développeur (pour ainsi dire) et à réfléchir à ce qui pourrait arriver.

jpmc26
la source
4
est-il surprenant qu’un référentiel dont le rôle principal est de conserver des données dans une base de données envoie également des courriers électroniques - je pense que vous avez mal compris le sens de ma question. Mon référentiel n'envoie pas de courrier électronique. Cela déclenche simplement un événement, et la façon dont cet événement est géré est la responsabilité du code externe.
André Borges
4
Vous faites essentiellement l'argument "n'utilisez pas les événements."
Robert Harvey
3
[haussement d'épaules] Les événements sont au cœur de la plupart des frameworks d'interface utilisateur. Éliminez les événements et ces cadres ne fonctionnent pas du tout.
Robert Harvey
2
@ jpmc26: Cela s'appelle ASP.NET Webforms. Ça craint.
Robert Harvey
2
My repository is not sending emails. It just raises an eventde cause à effet. Le référentiel déclenche le processus de notification.
Laiv
0

Actuellement, il SaveChangesfait deux choses: il enregistre les modifications et enregistre les résultats. Maintenant, vous voulez ajouter quelque chose: envoyer des notifications par courrier électronique.

Vous avez eu l’idée intelligente d’y ajouter un événement, mais celui-ci a été critiqué pour avoir enfreint le principe de responsabilité unique (SRP), sans se rendre compte qu’il avait déjà été violé.

Pour obtenir une solution SRP pure, commencez par déclencher l'événement, puis appelez tous les points d'ancrage pour cet événement, qui sont désormais au nombre de trois: enregistrer, consigner et enfin envoyer des courriels.

Soit vous déclenchez l’événement d’abord, soit vous devez ajouter quelque chose SaveChanges. Votre solution est un hybride entre les deux. Il ne traite pas de la violation existante, mais encourage à l'empêcher de se multiplier au-delà de trois choses. Refactoriser le code existant pour le rendre conforme à SRP pourrait nécessiter plus de travail que ce qui est strictement nécessaire. C'est à vous de décider jusqu'où ils veulent aller.

CJ Dennis
la source
-1

Le code violait déjà le SRP - la même classe était responsable de la communication avec le contexte de données et de la journalisation.

Vous venez de l'améliorer pour avoir 3 responsabilités.

Une façon de réduire les responsabilités à une responsabilité serait d’abstraire le _userRepository; faites-en un diffuseur de commande.

Il a un ensemble de commandes, plus un ensemble d'auditeurs. Il reçoit des commandes et les diffuse à ses auditeurs. Peut-être ces auditeurs sont-ils commandés et peuvent-ils même dire que la commande a échoué (à son tour, elle est diffusée aux auditeurs déjà avertis).

Maintenant, la plupart des commandes ne peuvent avoir qu'un seul auditeur (le contexte de données). SaveChanges, avant vos modifications, a 2 - le contexte de données, puis l'enregistreur.

Votre modification ajoute ensuite un autre écouteur pour enregistrer les modifications, qui consiste à générer de nouveaux événements créés par l'utilisateur dans le service d'événements.

Il y a quelques avantages à cela. Vous pouvez maintenant supprimer, mettre à niveau ou répliquer le code de journalisation sans vous soucier du reste de votre code. Vous pouvez ajouter plus de déclencheurs lors des modifications de sauvegarde pour plus de choses qui en ont besoin.

Tout cela est décidé lors de la _userRepositorycréation et de la connexion de l'outil (ou peut-être que ces fonctionnalités supplémentaires sont ajoutées / supprimées à la volée; la possibilité d'ajouter / d'améliorer la journalisation pendant l'exécution de l'application pourrait être utile).

Yakk
la source