Sorgen Sie sich nicht um das Prinzip der Einzelverantwortung. Hier hilft es Ihnen nicht, eine gute Entscheidung zu treffen, da Sie ein bestimmtes Konzept subjektiv als "Verantwortung" auswählen können. Sie können sagen, dass die Verantwortung der Klasse darin besteht, die Datenpersistenz in der Datenbank zu verwalten, oder dass die Verantwortung darin besteht, die gesamte Arbeit im Zusammenhang mit der Erstellung eines Benutzers auszuführen. Dies sind nur verschiedene Verhaltensebenen der Anwendung, und beide sind gültige konzeptionelle Ausdrücke einer "einzelnen Verantwortung". Daher ist dieses Prinzip für die Lösung Ihres Problems nicht hilfreich.
Das nützlichste Prinzip in diesem Fall ist das Prinzip der geringsten Überraschung . Stellen wir also die Frage: Ist es überraschend, dass ein Repository mit der primären Rolle, Daten in einer Datenbank zu speichern, auch E-Mails sendet?
Ja, es ist sehr überraschend. Dies sind zwei völlig getrennte externe Systeme, und der Name SaveChanges
bedeutet nicht, dass auch Benachrichtigungen gesendet werden. Die Tatsache, dass Sie dies an ein Ereignis delegieren, macht das Verhalten noch überraschender, da jemand, der den Code liest, nicht mehr leicht erkennen kann, welche zusätzlichen Verhaltensweisen aufgerufen werden. Indirektion beeinträchtigt die Lesbarkeit. Manchmal sind die Vorteile die Kosten für die Lesbarkeit wert, aber nicht, wenn Sie automatisch ein zusätzliches externes System aufrufen, dessen Auswirkungen für Endbenutzer erkennbar sind. (Protokollierung kann hier ausgeschlossen werden, da es sich im Wesentlichen um eine Aufzeichnung zum Debuggen handelt. Endbenutzer verbrauchen das Protokoll nicht, sodass es nicht schadet, immer zu protokollieren.) Schlimmer noch, dies verringert die Flexibilität beim Timing Dies macht es unmöglich, andere Vorgänge zwischen dem Speichern und der Benachrichtigung zu verschachteln.
Wenn Ihr Code normalerweise eine Benachrichtigung senden muss, wenn ein Benutzer erfolgreich erstellt wurde, können Sie eine Methode erstellen, die Folgendes ausführt:
public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Ob dies einen Mehrwert bringt, hängt jedoch von den Besonderheiten Ihrer Anwendung ab.
Ich würde eigentlich die Existenz der SaveChanges
Methode überhaupt entmutigen . Diese Methode schreibt vermutlich eine Datenbanktransaktion fest, aber andere Repositorys haben möglicherweise die Datenbank in derselben Transaktion geändert . Die Tatsache, dass sie alle festschreiben, ist erneut überraschend, da sie SaveChanges
speziell an diese Instanz des Benutzer-Repositorys gebunden ist.
Das einfachste Muster für die Verwaltung einer Datenbanktransaktion ist ein äußerer using
Block:
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Dies gibt dem Programmierer eine explizite Kontrolle darüber, wann Änderungen für alle Repositorys gespeichert werden, erzwingt, dass der Code die Abfolge der Ereignisse, die vor einem Commit auftreten müssen, explizit dokumentiert, stellt sicher, dass ein Rollback bei einem Fehler ausgegeben wird (vorausgesetzt, dass DataContext.Dispose
ein Rollback ausgegeben wird ), und vermeidet, dass sie ausgeblendet werden Verbindungen zwischen stateful Klassen.
Ich würde es auch vorziehen, die E-Mail nicht direkt in der Anfrage zu senden. Es wäre robuster, die Notwendigkeit einer Benachrichtigung in einer Warteschlange aufzuzeichnen . Dies würde eine bessere Fehlerbehandlung ermöglichen. Insbesondere wenn beim Versenden der E-Mail ein Fehler auftritt, kann dieser später erneut versucht werden, ohne dass das Speichern des Benutzers unterbrochen wird, und es wird vermieden, dass der Benutzer erstellt wird, die Site jedoch einen Fehler zurückgibt.
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
Es ist besser, zuerst die Benachrichtigungswarteschlange festzuschreiben, da der Benutzer der Warteschlange überprüfen kann, ob der Benutzer vorhanden ist, bevor er die E-Mail sendet, falls der context.SaveChanges()
Anruf fehlschlägt. (Andernfalls benötigen Sie eine vollständige Zwei-Phasen-Commit-Strategie, um Heisenbugs zu vermeiden.)
Das Endergebnis soll praktisch sein. Denken Sie tatsächlich über die Konsequenzen (sowohl in Bezug auf das Risiko als auch den Nutzen) nach, die das Schreiben von Code auf eine bestimmte Art und Weise hat. Ich finde, dass das Prinzip der "Einzelverantwortung" mir dabei nicht sehr oft hilft, während das Prinzip der geringsten Überraschung mir oft hilft, in den Kopf eines anderen Entwicklers zu geraten (sozusagen) und darüber nachzudenken, was passieren könnte.