Anwendbarkeit des Grundsatzes der einheitlichen Verantwortung


40

Ich bin vor kurzem auf ein scheinbar triviales architektonisches Problem gestoßen. Ich hatte ein einfaches Repository in meinem Code, das wie folgt aufgerufen wurde (Code ist in C #):

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

SaveChanges war ein einfacher Wrapper, der Änderungen an der Datenbank festschreibt:

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

Nach einiger Zeit musste ich dann eine neue Logik implementieren, die jedes Mal E-Mail-Benachrichtigungen sendet, wenn ein Benutzer im System erstellt wurde. Da es viele Aufrufe zum _userRepository.Add()und SaveChangesum das System gab, habe ich beschlossen, SaveChangeswie folgt zu aktualisieren :

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

Auf diese Weise kann externer Code UserCreatedEvent abonnieren und die erforderliche Geschäftslogik verarbeiten, mit der Benachrichtigungen gesendet werden.

Es wurde mir jedoch darauf hingewiesen, dass meine Änderung SaveChangesgegen das Prinzip der Einzelverantwortung verstößt und SaveChangeslediglich Ereignisse retten und nicht auslösen sollte.

Ist das ein gültiger Punkt? Es scheint mir, dass das Auslösen eines Ereignisses hier im Wesentlichen dasselbe ist wie das Protokollieren: der Funktion wird lediglich eine Nebenfunktion hinzugefügt. Und SRP verbietet Ihnen nicht, Protokollierungs- oder Auslöseereignisse in Ihren Funktionen zu verwenden. Es heißt lediglich, dass eine solche Logik in anderen Klassen gekapselt werden sollte, und es ist in Ordnung, dass ein Repository diese anderen Klassen aufruft.


22
Ihre Antwort lautet: "OK, wie würden Sie es schreiben, damit es nicht gegen SRP verstößt, aber dennoch einen einzigen Änderungspunkt zulässt?"
Robert Harvey

43
Meiner Beobachtung nach bedeutet das Auslösen eines Ereignisses keine zusätzliche Verantwortung. Im Gegenteil: Sie delegiert die Verantwortung an einen anderen Ort.
Robert Harvey

Ich denke dein Kollege hat Recht, aber deine Frage ist richtig und nützlich, also aufgestimmt!
Andres F.

16
Es gibt keine definitive Definition einer Einzelverantwortung. Die Person, die darauf hinweist, dass sie gegen die SRP verstößt, stimmt mit ihrer persönlichen Definition überein, und Sie stimmt mit Ihrer Definition überein. Ich denke, Ihr Design ist vollkommen in Ordnung mit dem Vorbehalt, dass dieses Ereignis kein einmaliges Ereignis ist, bei dem andere ähnliche Funktionen auf unterschiedliche Weise ausgeführt werden. Konsistenz ist weitaus wichtiger, als eine vage Richtlinie wie SRP, die bis zum Äußersten getragen wurde und Tonnen von sehr einfach zu verstehenden Klassen enthält, von denen niemand weiß, wie man in einem System funktioniert.
Dunk

Antworten:


14

Ja, es kann eine gültige Anforderung sein, ein Repository zu haben, das bestimmte Ereignisse für bestimmte Aktionen wie Addoder SaveChangesauslöst. Ich werde dies nicht in Frage stellen (wie einige andere Antworten), nur weil Ihr spezielles Beispiel für das Hinzufügen von Benutzern und das Senden von E-Mails möglicherweise wie folgt aussieht ein bisschen erfunden. Lassen Sie uns im Folgenden davon ausgehen, dass diese Anforderung im Kontext Ihres Systems vollkommen gerechtfertigt ist.

Also ja , die Mechanik Ereignis codiert , sowie die Protokollierung sowie die Speicherung in einem Verfahren gegen die SRP . In vielen Fällen ist dies wahrscheinlich ein akzeptabler Verstoß, insbesondere wenn niemand die Wartungsverantwortung für "Änderungen speichern" und "Ereignis auslösen" auf verschiedene Teams / Betreuer verteilen möchte. Nehmen wir jedoch an, dass jemand eines Tages genau dies tun möchte. Kann dies auf einfache Weise gelöst werden, indem der Code dieser Bedenken in verschiedene Klassenbibliotheken gestellt wird?

Die Lösung hierfür besteht darin, Ihr ursprüngliches Repository für das Festschreiben von Änderungen an der Datenbank verantwortlich zu machen und ein Proxy- Repository mit genau derselben öffentlichen Schnittstelle zu erstellen, das ursprüngliche Repository wiederzuverwenden und den Methoden die zusätzlichen Ereignismechanismen hinzuzufügen.

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

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

Sie können die Proxy-Klasse a anrufen NotifyingRepositoryoder, ObservableRepositorywenn Sie möchten, gemäß der von @ Peter hoch bewerteten Antwort .

Die neue und die alte Repository-Klasse sollten beide von einer gemeinsamen Schnittstelle abgeleitet sein, wie in der klassischen Proxy-Musterbeschreibung gezeigt .

Initialisieren Sie dann in Ihrem ursprünglichen Code _userRepositorydurch ein Objekt der neuen EventFiringUserRepoKlasse. Auf diese Weise halten Sie das ursprüngliche Repository von der Ereignismechanik getrennt. Bei Bedarf können Sie das Ereignisauslösungs-Repository und das ursprüngliche Repository nebeneinander haben und die Anrufer entscheiden lassen, ob sie das erstere oder das letztere verwenden.

Um ein in den Kommentaren erwähntes Anliegen anzusprechen: Führt das nicht dazu, dass Proxies auf Proxies auf Proxies aufgesetzt werden, und so weiter? Tatsächlich schafft das Hinzufügen der Ereignismechanik eine Grundlage, um weitere Anforderungen des Typs "E-Mails senden" hinzuzufügen, indem Sie einfach die Ereignisse abonnieren und sich ohne zusätzliche Proxys an die SRP mit diesen Anforderungen halten. Aber das eine, was hier einmal hinzugefügt werden muss, ist die Eventmechanik selbst.

Ob sich diese Art der Trennung im Kontext Ihres Systems wirklich lohnt, müssen Sie und Ihr Prüfer selbst entscheiden. Wahrscheinlich würde ich die Protokollierung nicht vom ursprünglichen Code trennen, auch nicht durch Verwendung eines anderen Proxys, nicht durch Hinzufügen eines Protokollierers zum Listener-Ereignis, obwohl dies möglich wäre.


3
Neben dieser Antwort. Es gibt Alternativen zu Proxies wie AOP .
Laiv

1
Ich denke, Sie verpassen den Punkt, es ist nicht so, dass das Auslösen eines Ereignisses die SRP bricht, sondern dass das Auslösen eines Ereignisses nur für "neue" Benutzer voraussetzt, dass das Repo dafür verantwortlich ist, zu wissen, was einen "neuen" Benutzer ausmacht, und nicht einen "mir neu hinzugefügten" "user
Ewan

@Ewan: lies bitte die frage nochmal. Es ging um einen Ort im Code, der bestimmte Aktionen ausführt, die mit anderen Aktionen außerhalb der Verantwortlichkeit dieses Objekts gekoppelt werden müssen. Ein Peer-Reviewer stellte in Frage, dass die Aktion und die Auslösung des Ereignisses gegen die SRP verstoßen. Das Beispiel "Speichern neuer Benutzer" dient nur zu Demonstrationszwecken, nennen Sie das erfundene Beispiel, wenn Sie möchten, aber das ist IMHO nicht der Sinn der Frage.
Doc Brown

2
Dies ist die beste / richtige Antwort IMO. Es wird nicht nur SRP beibehalten, sondern auch das Open / Closed-Prinzip. Und denken Sie an all die automatisierten Tests, die Änderungen innerhalb der Klasse unterbrechen könnten. Das Ändern bestehender Tests, wenn neue Funktionen hinzugefügt werden, ist ein großer Geruch.
Keith Payne

1
Wie funktioniert diese Lösung bei einer laufenden Transaktion? Wenn dies geschieht, SaveChanges()wird der Datenbankdatensatz nicht tatsächlich erstellt, und es kann zu einem Rollback kommen. Es scheint, als müssten Sie AcceptAllChangesdas TransactionCompleted-Ereignis entweder überschreiben oder abonnieren.
John Wu

29

Das Senden einer Benachrichtigung, dass sich der persistente Datenspeicher geändert hat, erscheint beim Speichern sinnvoll.

Natürlich sollten Sie Add nicht als Sonderfall behandeln - Sie müssten auch Ereignisse für Modify und Delete auslösen. Es ist die spezielle Behandlung des "Add" -Falls, die den Leser dazu zwingt, zu erklären, warum er riecht, und letztendlich einige Leser des Codes dazu bringt, zu der Schlussfolgerung zu gelangen, dass er gegen die SRP verstoßen muss.

Ein "Benachrichtigungs" -Repository, das abgefragt, geändert und Ereignisse bei Änderungen ausgelöst werden kann, ist ein ganz normales Objekt. Sie können davon ausgehen, dass Sie in nahezu jedem Projekt mit angemessener Größe mehrere Variationen finden.


Aber ist ein "Benachrichtigungs" -Repository tatsächlich das, was Sie brauchen? Sie erwähnten C #: Viele Leute würden zustimmen, dass die Verwendung von a System.Collections.ObjectModel.ObservableCollection<>anstelle von, System.Collections.Generic.List<>wenn letzteres alles ist, was Sie benötigen, alle Arten von schlecht und falsch ist, aber nur wenige würden sofort auf SRP verweisen.

Was Sie jetzt tun, tauscht Ihr UserList _userRepositorymit einem ObservableUserCollection _userRepository. Ob dies die beste Vorgehensweise ist oder nicht, hängt von der Anwendung ab. Aber während es zweifellos das _userRepositorybeträchtlich geringere Gewicht macht, verletzt es meiner bescheidenen Meinung nach SRP nicht.


Das Problem bei der Verwendung ObservableCollectionin diesem Fall besteht darin, dass das entsprechende Ereignis nicht beim Aufruf von SaveChanges, sondern beim Aufruf von ausgelöst wird Add, was zu einem ganz anderen Verhalten als dem im Beispiel gezeigten führen würde. Sehen Sie sich meine Antwort an, wie Sie das ursprüngliche Repo leicht halten und trotzdem bei der SRP bleiben, indem Sie die Semantik intakt halten.
Doc Brown

@DocBrown Ich habe die bekannten Klassen ObservableCollection<>und List<>zum Vergleich und Kontext aufgerufen . Ich wollte nicht empfehlen, die tatsächlichen Klassen für die interne Implementierung oder die externe Schnittstelle zu verwenden.
Peter

Ok, aber selbst wenn das OP Ereignisse zu "Ändern" und "Löschen" hinzufügen würde (was ich der Einfachheit halber aus Gründen der Übersichtlichkeit aus Gründen der Übersichtlichkeit nicht in die Frage einbeziehen würde), könnte ein Prüfer leicht zu dem Schluss kommen, dass eine SRP-Verletzung. Es ist wahrscheinlich eine akzeptable, aber keine, die bei Bedarf nicht gelöst werden kann.
Doc Brown

16

Ja, es ist ein Verstoß gegen das Prinzip der einheitlichen Verantwortung und ein gültiger Punkt.

Ein besseres Design wäre, wenn ein separater Prozess "neue Benutzer" aus dem Repository abruft und die E-Mails sendet. Verfolgen Sie, welche Benutzer eine E-Mail erhalten haben, welche Fehler aufgetreten sind, welche erneut gesendet wurden usw. usw.

Auf diese Weise können Sie mit Fehlern, Abstürzen und Ähnlichem umgehen und vermeiden, dass Ihr Repository alle Anforderungen abruft, bei denen der Eindruck entsteht, dass Ereignisse eintreten, "wenn ein Commit für die Datenbank ausgeführt wird".

Das Repository weiß nicht, dass ein Benutzer, den Sie hinzufügen, ein neuer Benutzer ist. Die Verantwortung liegt einfach darin, den Benutzer zu speichern.

Es lohnt sich wahrscheinlich, die folgenden Kommentare zu erweitern.

Das Repository weiß nicht, dass ein Benutzer, den Sie hinzufügen, ein neuer Benutzer ist - ja, es hat eine Methode namens Hinzufügen. Seine Semantik impliziert, dass alle hinzugefügten Benutzer neue Benutzer sind. Kombinieren Sie alle an Add übergebenen Argumente, bevor Sie Save aufrufen - und Sie erhalten alle neuen Benutzer

Falsch. Sie haben die Konflikte "Zum Repository hinzugefügt" und "Neu".

"Zum Repository hinzugefügt" bedeutet genau das, was es sagt. Ich kann Benutzer zu verschiedenen Repositories hinzufügen, entfernen und wieder hinzufügen.

"Neu" ist ein Zustand eines Benutzers, der durch Geschäftsregeln definiert ist.

Derzeit lautet die Geschäftsregel möglicherweise "Neu == wurde gerade zum Repository hinzugefügt". Dies bedeutet jedoch nicht, dass es keine separate Verantwortung ist, diese Regel zu kennen und anzuwenden.

Sie müssen vorsichtig sein, um diese Art von datenbankorientiertem Denken zu vermeiden. Sie werden Edge-Case-Prozesse haben, die nicht neue Benutzer zum Repository hinzufügen, und wenn Sie E-Mails an sie senden, wird das gesamte Unternehmen sagen: "Natürlich sind dies keine" neuen "Benutzer! Die eigentliche Regel lautet X".

Diese Antwort ist meiner Meinung nach völlig verfehlt: Das Repo ist genau die eine zentrale Stelle im Code, die weiß, wann neue Benutzer hinzugefügt werden

Falsch. Aus den oben genannten Gründen ist es außerdem kein zentraler Ort, es sei denn, Sie fügen den E-Mail-Sendecode tatsächlich in die Klasse ein, anstatt nur ein Ereignis auszulösen.

Sie haben Anwendungen, die die Repository-Klasse verwenden, aber nicht über den Code zum Senden der E-Mail verfügen. Wenn Sie Benutzer in diesen Anwendungen hinzufügen, wird die E-Mail nicht gesendet.


11
Das Repository weiß nicht, dass ein Benutzer, den Sie hinzufügen, ein neuer Benutzer ist - ja, es hat eine Methode namens Add. Seine Semantik impliziert, dass alle hinzugefügten Benutzer neue Benutzer sind. Kombinieren Sie alle Argumente, die Addvor dem Aufruf übergeben wurden Save- und Sie erhalten alle neuen Benutzer.
Andre Borges

Ich mag diesen Vorschlag. Pragmatismus hat jedoch Vorrang vor Reinheit. Abhängig von den Umständen kann es schwierig sein, einer vorhandenen Anwendung eine völlig neue Architekturebene hinzuzufügen, wenn Sie beim Hinzufügen eines Benutzers lediglich eine einzige E-Mail senden müssen.
Alexander

Das Ereignis sagt jedoch nicht, dass ein Benutzer hinzugefügt wurde. Es heißt Benutzer erstellt. Wenn wir überlegen, die Dinge richtig zu benennen und den semantischen Unterschieden zwischen add und create zustimmen, ist das Ereignis im Snippet entweder falsch benannt oder falsch platziert. Ich glaube nicht, dass der Rezensent etwas gegen das Notyfing von Repositories hatte. Wahrscheinlich war es besorgt über die Art des Ereignisses und seine Nebenwirkungen.
27.

7
@Andre Neu im Repo, aber nicht unbedingt "neu" im geschäftlichen Sinne. Es ist die Verschmelzung dieser beiden Ideen, die die zusätzliche Verantwortung auf den ersten Blick verbirgt. Möglicherweise importiere ich eine Tonne alter Benutzer in mein neues Repository oder entferne einen Benutzer und füge ihn erneut hinzu. Es wird Geschäftsregeln darüber geben, was ein "neuer Benutzer" über "dem dB hinzugefügt wurde" hinausgeht
Ewan

1
Moderator Hinweis: Ihre Antwort ist kein journalistisches Interview. Wenn Sie Änderungen vorgenommen haben, nehmen Sie diese auf natürliche Weise in Ihre Antwort auf, ohne den gesamten Effekt "Aktuelle Nachrichten" zu erzielen. Wir sind kein Diskussionsforum.
Robert Harvey

7

Ist das ein gültiger Punkt?

Ja, obwohl es sehr von der Struktur Ihres Codes abhängt. Ich habe nicht den vollständigen Kontext, also werde ich versuchen, allgemein zu sprechen.

Es scheint mir, dass das Auslösen eines Ereignisses hier im Wesentlichen dasselbe ist wie das Protokollieren: der Funktion wird lediglich eine Nebenfunktion hinzugefügt.

Es ist absolut nicht. Die Protokollierung ist nicht Teil des Geschäftsablaufs. Sie kann deaktiviert werden. Sie sollte keine (geschäftlichen) Nebenwirkungen verursachen und den Zustand und die Gesundheit Ihrer Anwendung in keiner Weise beeinflussen, auch wenn Sie aus irgendeinem Grund keine Protokollierung durchführen konnten alles mehr. Vergleichen Sie das jetzt mit der Logik, die Sie hinzugefügt haben.

Und SRP verbietet Ihnen nicht, Protokollierungs- oder Auslöseereignisse in Ihren Funktionen zu verwenden. Es heißt lediglich, dass eine solche Logik in anderen Klassen gekapselt werden sollte, und es ist in Ordnung, dass ein Repository diese anderen Klassen aufruft.

SRP arbeitet zusammen mit ISP (S und I in SOLID). Sie haben am Ende viele Klassen und Methoden, die ganz bestimmte Dinge tun und sonst nichts. Sie sind sehr fokussiert, sehr einfach zu aktualisieren oder zu ersetzen und im Allgemeinen einfach (er) zu testen. In der Praxis gibt es natürlich auch einige größere Klassen, die sich mit der Orchestrierung befassen: Sie weisen eine Reihe von Abhängigkeiten auf und konzentrieren sich nicht auf atomisierte Aktionen, sondern auf Geschäftsaktionen, für die möglicherweise mehrere Schritte erforderlich sind. Solange der Geschäftskontext klar ist, können sie auch als Einzelverantwortung bezeichnet werden. Wie Sie jedoch richtig sagten, möchten Sie mit zunehmendem Code möglicherweise einen Teil davon in neue Klassen / Schnittstellen abstrahieren.

Nun zurück zu Ihrem speziellen Beispiel. Wenn Sie unbedingt eine Benachrichtigung senden müssen, wenn ein Benutzer erstellt wird, und möglicherweise sogar andere, speziellere Aktionen ausführen müssen, können Sie einen separaten Dienst erstellen, der diese Anforderung kapselt, z. B. UserCreationServiceeine Methode Add(user), die den Speicher (den Aufruf) verwaltet in Ihr Repository) und die Benachrichtigung als einzelne Geschäftsaktion. Oder machen Sie es in Ihrem Original-Snippet nach_userRepository.SaveChanges();


2
Protokollierung ist nicht Teil des Geschäftsflusses - wie ist dies im Kontext von SRP relevant? Wenn der Zweck meines Ereignisses darin besteht, neue Nutzerdaten an Google Analytics zu senden, hat das Deaktivieren denselben geschäftlichen Effekt wie das Deaktivieren der Protokollierung: nicht kritisch, aber ziemlich ärgerlich. Was ist die Faustregel für das Hinzufügen / Nicht-Hinzufügen neuer Logik zu einer Funktion? "Verursacht die Deaktivierung schwerwiegende geschäftliche Nebenwirkungen?"
Andre 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 . Was ist, wenn Sie vorzeitige Ereignisse auslösen, die falsche "Nachrichten" verursachen? Was ist, wenn die Analyse "Benutzer" berücksichtigt, die aufgrund von Fehlern bei der DB-Transaktion nicht endgültig erstellt wurden? Was ist, wenn das Unternehmen Entscheidungen in falschen Räumlichkeiten trifft, die von ungenauen Daten gestützt werden? Sie konzentrieren sich zu sehr auf die technische Seite des Problems. "Manchmal kann man den Wald vor
lauter

@Laiv, du machst einen gültigen Punkt, aber das ist nicht der Punkt meiner Frage oder dieser Antwort. Die Frage ist, ob dies eine gültige Lösung im Kontext von SRP ist. Nehmen wir also an, dass keine DB-Transaktionsfehler vorliegen.
Andre Borges

Sie bitten mich im Grunde, Ihnen zu sagen, was Sie hören möchten. Ich gebe dir nur Spielraum. Ein größerer Entscheidungsspielraum, ob SRP wichtig ist oder nicht, da SRP ohne den richtigen Kontext nutzlos ist. IMO ist Ihre Herangehensweise an das Unternehmen falsch, da Sie sich nur auf die technische Lösung konzentrieren. Sie sollten dem gesamten Kontext genügend Relevanz geben. Und ja, die DB könnte ausfallen. Es besteht die Möglichkeit, dass dies passiert, und Sie sollten dies nicht auslassen, da bekanntlich Dinge passieren und diese Dinge Ihre Meinung in Bezug auf Zweifel an SRP oder anderen bewährten Praktiken ändern können.
Laiv

1
Denken Sie jedoch daran, dass Prinzipien keine in Stein gemeißelten Regeln sind. Sie sind durchlässig (adaptiv). Wie Sie sehen, sind sie offen für Interpretationen. Ihr Rezensent hat eine Interpretation und Sie haben eine andere. Versuchen Sie zu sehen, was Sie sehen, lösen Sie seine Zweifel und Bedenken oder lassen Sie ihn Ihre lösen. Die "richtige" Antwort finden Sie hier nicht. Die richtige Antwort liegt bei Ihnen und Ihrem Prüfer. Er fragt zuerst nach den Anforderungen (funktional und nicht funktional) des Projekts.
Laiv

4

In der SRP geht es theoretisch um Menschen , wie Onkel Bob in seinem Artikel Das Prinzip der Einzelverantwortung erklärt . Vielen Dank, dass Sie Robert Harvey in Ihrem Kommentar zur Verfügung gestellt haben.

Die richtige Frage lautet:

Welcher "Stakeholder" hat die Anforderung "E-Mails senden" hinzugefügt?

Wenn dieser Stakeholder auch für die Datenpersistenz verantwortlich ist (unwahrscheinlich, aber möglich), verstößt dies nicht gegen die SRP. Ansonsten ist es so.


2
Interessant - ich habe noch nie von dieser Interpretation des SRP gehört. Haben Sie Hinweise auf weitere Informationen / Literatur zu dieser Interpretation?
Sleske

2
@sleske: Von Onkel Bob selbst : "Und hier geht es um den Kern des Grundsatzes der Einzelverantwortung. Bei diesem Grundsatz geht es um Menschen. Wenn Sie ein Softwaremodul schreiben, möchten Sie sicherstellen, dass diese Änderungen nur entstehen können, wenn Änderungen angefordert werden von einer einzelnen Person oder vielmehr einer einzelnen eng gekoppelten Gruppe von Personen, die eine einzelne eng definierte Geschäftsfunktion repräsentieren. "
Robert Harvey

Vielen Dank, Robert. IMO, der Name "Prinzip der Einzelverantwortung" ist schrecklich, da er sich einfach anhört, aber zu wenige Leute setzen sich mit der beabsichtigten Bedeutung von "Verantwortung" auseinander. Ein bisschen so, wie sich OOP von vielen seiner ursprünglichen Konzepte abgewandelt hat und jetzt ein ziemlich bedeutungsloser Begriff ist.
user949300

1
Ja. Genau das ist mit dem Begriff REST passiert. Sogar Roy Fielding sagt, dass die Leute es falsch benutzen.
Robert Harvey

Obwohl das Zitat verwandt ist, denke ich, dass bei dieser Antwort nicht berücksichtigt wird, dass die Anforderung "E-Mails senden" keine der direkten Anforderungen ist, um die es in der SRP-Verstoßfrage geht. Wenn Sie jedoch "Welcher" Stakeholder "die Anforderung" Ereigniserhöhung "hinzufügt" , wird diese Antwort stärker auf die eigentliche Frage bezogen. Ich habe meine eigene Antwort ein wenig geändert, um dies klarer zu machen.
Doc Brown

2

Während es technisch gesehen nichts auszusetzen gibt, wenn Repositorys Ereignisse melden, würde ich vorschlagen, es unter funktionalen Gesichtspunkten zu betrachten, bei denen die Bequemlichkeit einige Bedenken aufwirft.

Das Erstellen eines Benutzers, das Entscheiden, was ein neuer Benutzer ist, und seine Beständigkeit sind drei verschiedene Dinge .

Voraussetzung von mir

Berücksichtigen Sie die vorherige Voraussetzung, bevor Sie entscheiden, ob das Repository der richtige Ort für die Benachrichtigung von Geschäftsereignissen ist (unabhängig von der SRP). Beachten Sie, dass ich das Geschäftsereignis sagte , weil es für mich UserCreatedeine andere Konnotation hat als UserStoredoder UserAdded 1 . Ich würde auch davon ausgehen, dass sich jedes dieser Ereignisse an ein anderes Publikum richtet.

Auf der einen Seite ist das Erstellen von Benutzern eine geschäftsspezifische Regel, die möglicherweise keine Persistenz beinhaltet. Möglicherweise sind mehr Geschäftsvorgänge und mehr Datenbank- / Netzwerkvorgänge erforderlich. Vorgänge, die der Persistenzschicht nicht bekannt sind. Die Persistenzschicht verfügt nicht über genügend Kontext, um zu entscheiden, ob der Anwendungsfall erfolgreich beendet wurde oder nicht.

Auf der anderen Seite ist es nicht unbedingt wahr, dass _dataContext.SaveChanges();der Benutzer erfolgreich bestanden hat. Dies hängt von der Transaktionsspanne der Datenbank ab. Zum Beispiel könnte es für Datenbanken wie MongoDB zutreffen, welche Transaktionen atomar sind, aber nicht für herkömmliche RDBMS, die ACID-Transaktionen implementieren, bei denen möglicherweise mehr Transaktionen beteiligt sind und noch festgeschrieben werden müssen.

Ist das ein gültiger Punkt?

Es könnte sein. Ich kann jedoch sagen, dass es nicht nur um SRP (technisch gesehen), sondern auch um Komfort (funktional gesehen) geht.

  • Ist es praktisch, Geschäftsereignisse von Komponenten aus auszulösen, die nicht über die laufenden Geschäftsvorgänge informiert sind?
  • Stellen sie den richtigen Ort genauso dar wie den richtigen Moment, um dies zu tun?
  • Soll ich zulassen, dass diese Komponenten meine Geschäftslogik durch Benachrichtigungen wie diese orchestrieren?
  • Kann ich die durch vorzeitige Ereignisse verursachten Nebenwirkungen aufheben? 2

Es scheint mir, dass das Auslösen eines Ereignisses hier im Wesentlichen dasselbe ist wie das Aufzeichnen

Absolut nicht. Die Protokollierung sollte jedoch keine Nebenwirkungen haben, da Sie vermuteten, dass das Ereignis UserCreatedwahrscheinlich andere Geschäftsvorgänge auslöst. Wie Benachrichtigungen. 3

es heißt nur, dass eine solche Logik in anderen Klassen gekapselt werden sollte, und es ist in Ordnung, dass ein Repository diese anderen Klassen aufruft

Nicht unbedingt wahr. SRP ist nicht nur ein klassenspezifisches Anliegen. Es funktioniert auf verschiedenen Abstraktionsebenen, wie Ebenen, Bibliotheken und Systemen! Es geht um Zusammenhalt, darum, zusammenzuhalten, was sich aus den gleichen Gründen durch die Hand der gleichen Stakeholder ändert . Wenn sich die Benutzererstellung ( Anwendungsfall ) ändert, ändert sich wahrscheinlich auch der Zeitpunkt und die Gründe für das Eintreten des Ereignisses.


1: Dinge angemessen zu benennen ist auch wichtig.

2: Angenommen, wir haben UserCreatednach gesendet _dataContext.SaveChanges();, aber die gesamte Datenbanktransaktion ist später aufgrund von Verbindungsproblemen oder Verstößen gegen Einschränkungen fehlgeschlagen. Seien Sie vorsichtig bei der vorzeitigen Übertragung von Ereignissen, da die Nebenwirkungen nur schwer rückgängig gemacht werden können (sofern dies überhaupt möglich ist).

3: Nicht angemessen behandelte Benachrichtigungsprozesse können dazu führen, dass Sie Benachrichtigungen auslösen, die nicht rückgängig gemacht werden können


1
+1 Sehr guter Punkt über die Transaktionsspanne. Es kann verfrüht sein, zu behaupten, dass der Benutzer erstellt wurde, da Rollbacks auftreten können. und anders als bei einem Protokoll hat wahrscheinlich ein anderer Teil der App etwas mit dem Ereignis zu tun.
Andres F.

2
Genau. Ereignisse bezeichnen Gewissheit. Es ist etwas passiert, aber es ist vorbei.
Laiv

1
@Laiv: Außer wenn sie es nicht tun. Microsoft hat alle Arten von Veranstaltungen mit dem Präfix Beforeoder Previewdas macht keine Garantien überhaupt Gewißheit.
Robert Harvey

1
@ jpmc26: Ohne eine Alternative ist dein Vorschlag nicht hilfreich.
Robert Harvey

1
@ jpmc26: Ihre Antwort lautet also "Wechsel zu einem völlig anderen Entwicklungs-Ökosystem mit völlig anderen Werkzeugen und Leistungsmerkmalen". Nennen Sie mich anders, aber ich würde mir vorstellen, dass dies für die überwiegende Mehrheit der Entwicklungsbemühungen nicht machbar ist.
Robert Harvey

1

Nein, dies verstößt nicht gegen die SRP.

Viele scheinen der Meinung zu sein, dass das Prinzip der Einzelverantwortung bedeutet, dass eine Funktion nur "eine Sache" tun sollte und dann in die Diskussion darüber verwickelt wird, was "eine Sache" ausmacht.

Aber das ist nicht das, was das Prinzip bedeutet. Es geht um Bedenken auf Unternehmensebene. Eine Klasse sollte nicht mehrere Anliegen oder Anforderungen implementieren, die sich auf Unternehmensebene unabhängig voneinander ändern können. Nehmen wir an, eine Klasse speichert den Benutzer und sendet eine fest codierte Begrüßungsnachricht per E-Mail. Mehrere unabhängige Bedenken können dazu führen, dass sich die Anforderungen einer solchen Klasse ändern. Der Designer könnte verlangen, dass sich das HTML / Stylesheet der E-Mail ändert. Der Kommunikationsexperte kann eine Änderung des Mailtextes verlangen. Und der UX-Experte könnte entscheiden, dass die E-Mail tatsächlich an einer anderen Stelle im Onboarding-Ablauf gesendet werden soll. Die Klasse unterliegt also mehreren Anforderungsänderungen aus unabhängigen Quellen. Dies verstößt gegen das SRP.

Das Auslösen eines Ereignisses verstößt dann jedoch nicht gegen die SRP, da das Ereignis nur vom Speichern des Benutzers und nicht von anderen Belangen abhängt. Ereignisse sind eine wirklich gute Möglichkeit, die SRP aufrechtzuerhalten, da Sie durch das Speichern eine E-Mail auslösen können, ohne dass das Repository von der E-Mail betroffen ist oder auch nur davon Kenntnis hat.


1

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 SaveChangesbedeutet 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 SaveChangesMethode ü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 SaveChangesspeziell an diese Instanz des Benutzer-Repositorys gebunden ist.

Das einfachste Muster für die Verwaltung einer Datenbanktransaktion ist ein äußerer usingBlock:

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


4
ist es überraschend, dass ein Repository mit der primären Rolle, Daten in einer Datenbank zu speichern, auch E-Mails sendet - ich denke, Sie haben den Punkt meiner Frage verpasst. Mein Repository sendet keine E-Mails. Es wird nur ein Ereignis ausgelöst und wie dieses Ereignis behandelt wird - liegt in der Verantwortung von externem Code.
Andre Borges

4
Sie machen im Grunde das Argument "keine Ereignisse verwenden."
Robert Harvey

3
[Achselzucken] Ereignisse spielen in den meisten UI-Frameworks eine zentrale Rolle. Beseitigen Sie Ereignisse, und diese Frameworks funktionieren überhaupt nicht.
Robert Harvey

2
@ jpmc26: Es heißt ASP.NET Webforms. Es nervt.
Robert Harvey

2
My repository is not sending emails. It just raises an eventUrsache Wirkung. Das Repository löst den Benachrichtigungsprozess aus.
Laiv

0

Derzeit SaveChangestut zwei Dinge: Es speichert die Änderungen und meldet , dass es so tut. Jetzt möchten Sie noch etwas hinzufügen: E-Mail-Benachrichtigungen senden.

Sie hatten die clevere Idee, ein Ereignis hinzuzufügen, das jedoch wegen Verstoßes gegen das Einzelverantwortungsprinzip (Single Responsibility Principle, SRP) kritisiert wurde, ohne zu bemerken, dass es bereits verletzt wurde.

Um eine reine SRP-Lösung zu erhalten, lösen Sie zuerst das Ereignis aus und rufen Sie dann alle Hooks für dieses Ereignis auf, von denen es jetzt drei gibt: Speichern, Protokollieren und endgültiges Senden von E-Mails.

Entweder triggern Sie das Ereignis zuerst oder Sie müssen es ergänzen SaveChanges. Ihre Lösung ist eine Mischung aus beiden. Es geht nicht auf die bestehende Verletzung ein, sondern regt an, zu verhindern, dass sie über drei Dinge hinausgeht. Das Umgestalten des vorhandenen Codes zur Einhaltung von SRP erfordert möglicherweise mehr Arbeit als unbedingt erforderlich. Es liegt an Ihrem Projekt, wie weit es mit SRP gehen soll.


-1

Der Code hat bereits die SRP verletzt - dieselbe Klasse war für die Kommunikation mit dem Datenkontext und die Protokollierung verantwortlich.

Sie aktualisieren es einfach, um 3 Verantwortlichkeiten zu haben.

Eine Möglichkeit, die Verantwortung auf 1 zu reduzieren, besteht darin, das zu abstrahieren _userRepository. mache es zu einem Befehlssender.

Es verfügt über eine Reihe von Befehlen sowie eine Reihe von Listenern. Es bekommt Befehle und sendet sie an seine Zuhörer. Möglicherweise werden diese Listener angewiesen, und vielleicht können sie sogar sagen, dass der Befehl fehlgeschlagen ist (was wiederum an Listener gesendet wird, die bereits benachrichtigt wurden).

Jetzt haben die meisten Befehle möglicherweise nur einen Listener (den Datenkontext). SaveChanges hat vor Ihren Änderungen 2 - den Datenkontext und dann den Logger.

Ihre Änderung fügt dann einen weiteren Listener hinzu, um Änderungen zu speichern, dh neue vom Benutzer erstellte Ereignisse im Ereignisdienst auszulösen.

Dies hat einige Vorteile. Sie können den Protokollierungscode jetzt entfernen, aktualisieren oder replizieren, ohne sich um den Rest Ihres Codes zu kümmern. Sie können beim Speichern von Änderungen weitere Trigger für weitere Dinge hinzufügen, die dies benötigen.

All dies wird entschieden, wenn das _userRepositoryerstellt und verkabelt wird (oder wenn diese zusätzlichen Funktionen im laufenden Betrieb hinzugefügt / entfernt werden; die Möglichkeit, die Protokollierung hinzuzufügen / zu verbessern, während die Anwendung ausgeführt wird, könnte von Nutzen sein).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.