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 SaveChanges
n'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 SaveChanges
mé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 SaveChanges
est 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 using
bloc 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.