So vermeiden Sie gesprächige Schnittstellen

10

Hintergrund: Ich entwerfe eine Serveranwendung und erstelle separate DLLs für verschiedene Subsysteme. Nehmen wir zur Vereinfachung an, ich habe zwei Subsysteme: 1) Users2)Projects

Die öffentliche Benutzeroberfläche der Benutzer hat eine Methode wie:

IEnumerable<User> GetUser(int id);

Die öffentliche Oberfläche von Projects verfügt über eine Methode wie:

IEnumerable<User> GetProjectUsers(int projectId);

Wenn wir beispielsweise die Benutzer für ein bestimmtes Projekt anzeigen müssen, können wir aufrufen, GetProjectUsersund dies gibt Objekte mit ausreichenden Informationen zurück, um sie in einem Datagrid oder ähnlichem anzuzeigen.

Problem: Im Idealfall sollte das ProjectsSubsystem nicht auch Benutzerinformationen und nur die IDs der an einem Projekt beteiligten Benutzer speichern. Um das zu bedienen GetProjectUsers, muss es GetUserdas UsersSystem für jede Benutzer-ID aufrufen, die in seiner eigenen Datenbank gespeichert ist. Dies erfordert jedoch viele separate GetUserAufrufe, was viele separate SQL-Abfragen innerhalb des UserSubsystems verursacht. Ich habe dies nicht wirklich getestet, aber dieses gesprächige Design wirkt sich auf die Skalierbarkeit des Systems aus.

Wenn ich die Trennung der Subsysteme beiseite lege, könnte ich alle Informationen in einem einzigen Schema speichern, auf das beide Systeme zugreifen können, und Projectseinfach JOINalle Projektbenutzer in einer einzigen Abfrage abrufen. Projectsmüsste auch wissen, wie man UserObjekte aus den Abfrageergebnissen generiert . Dies bricht jedoch die Trennung, die viele Vorteile hat.

Frage: Kann jemand einen Weg vorschlagen, um die Trennung beizubehalten und gleichzeitig all diese einzelnen GetUserAnrufe zu vermeiden GetProjectUsers?


Ich hatte beispielsweise die Idee, dass Benutzer externen Systemen die Möglichkeit geben sollten, Benutzer mit einem Label-Wert-Paar zu "markieren" und Benutzer mit einem bestimmten Wert anzufordern, z.

void AddUserTag(int userId, string tag, string value);
IEnumerable<User> GetUsersByTag(string tag, string value);

Dann könnte das Projektsystem jeden Benutzer markieren, wenn er dem Projekt hinzugefügt wird:

AddUserTag(userId,"project id", myProjectId.ToString());

und während GetProjectUsers könnten alle Projektbenutzer in einem einzigen Aufruf angefordert werden:

var projectUsers = usersService.GetUsersByTag("project id", myProjectId.ToString());

Der Teil, bei dem ich mir nicht sicher bin, ist: Ja, Benutzer sind projektunabhängig, aber die Informationen zur Projektmitgliedschaft werden im Benutzersystem gespeichert, nicht in Projekten. Ich fühle mich einfach nicht natürlich, also versuche ich festzustellen, ob es hier einen großen Nachteil gibt, den ich vermisse.

Eren Ersönmez
quelle

Antworten:

10

Was in Ihrem System fehlt, ist der Cache.

Du sagst:

Dies erfordert jedoch viele separate GetUserAufrufe, was viele separate SQL-Abfragen innerhalb des UserSubsystems verursacht.

Die Anzahl der Aufrufe einer Methode muss nicht mit der Anzahl der SQL-Abfragen übereinstimmen. Sie erhalten die Informationen über den Benutzer einmal. Warum sollten Sie dieselben Informationen erneut abfragen, wenn sie sich nicht geändert haben? Sehr wahrscheinlich können Sie sogar alle Benutzer im Speicher zwischenspeichern, was zu null SQL-Abfragen führen würde (es sei denn, ein Benutzer ändert sich).

Auf der anderen Seite führen Sie ein zusätzliches Problem Projectsein , indem Sie das Subsystem dazu veranlassen, sowohl die Projekte als auch die Benutzer mit einem abzufragen INNER JOIN: Sie fragen dieselbe Information an zwei verschiedenen Stellen in Ihrem Code ab, was die Ungültigmachung des Caches äußerst schwierig macht. Als Konsequenz:

  • Entweder werden Sie später überhaupt keinen Cache mehr einführen.

  • Oder Sie verbringen Wochen oder Monate damit, zu untersuchen, was ungültig werden soll, wenn sich eine Information ändert.

  • Oder Sie fügen die Cache-Ungültigmachung an einfachen Stellen hinzu, vergessen die anderen und führen zu schwer zu findenden Fehlern.


Beim erneuten Lesen Ihrer Frage stelle ich ein Schlüsselwort fest, das ich beim ersten Mal übersehen habe: Skalierbarkeit . Als Faustregel können Sie dem nächsten Muster folgen:

  1. Fragen Sie sich, ob das System langsam ist (dh eine nicht funktionierende Leistungsanforderung verletzt oder einfach ein Albtraum ist).

    Wenn das System nicht langsam ist, kümmern Sie sich nicht um die Leistung. Sorgen Sie sich um sauberen Code, Lesbarkeit, Wartbarkeit, Tests, Zweigstellenabdeckung, sauberes Design, detaillierte und leicht verständliche Dokumentation und gute Codekommentare.

  2. Wenn ja, suchen Sie nach dem Engpass. Sie tun dies nicht durch Raten, sondern durch Profilieren . Durch die Profilerstellung bestimmen Sie die genaue Position des Engpasses ( vorausgesetzt , Sie können fast jedes Mal etwas falsch machen , wenn Sie raten ) und können sich nun auf diesen Teil des Codes konzentrieren.

  3. Suchen Sie nach Lösungen, sobald der Engpass gefunden wurde. Sie tun dies, indem Sie raten, Benchmarking durchführen, Profile erstellen, Alternativen schreiben, Compiler-Optimierungen verstehen, Optimierungen verstehen, die Ihnen überlassen sind, Fragen zum Stapelüberlauf stellen und bei Bedarf auf einfache Sprachen (einschließlich Assembler) umsteigen.

Was ist das eigentliche Problem mit dem ProjectsSubsystem, das Informationen zum UsersSubsystem anfordert?

Das mögliche zukünftige Problem der Skalierbarkeit? Dies ist kein Problem. Die Skalierbarkeit kann zu einem Albtraum werden, wenn Sie anfangen, alles in einer monolithischen Lösung zusammenzuführen oder dieselben Daten von mehreren Standorten aus abzufragen (wie unten erläutert, da es schwierig ist, den Cache einzuführen).

Wenn bereits ein erkennbares Leistungsproblem vorliegt, suchen Sie in Schritt 2 nach dem Engpass.

Wenn es den Anschein hat, dass der Engpass tatsächlich besteht und auf die Tatsache zurückzuführen ist, dass ProjectsAnforderungen für Benutzer über das UsersSubsystem gestellt werden (und sich auf der Ebene der Datenbankabfrage befinden), sollten Sie nur dann nach einer Alternative suchen.

Die häufigste Alternative wäre die Implementierung von Caching, wodurch die Anzahl der Abfragen drastisch reduziert wird. Wenn Sie sich in einer Situation befinden, in der das Caching nicht hilft, zeigt Ihnen eine weitere Profilerstellung möglicherweise, dass Sie die Anzahl der Abfragen reduzieren, Datenbankindizes hinzufügen (oder entfernen), mehr Hardware werfen oder das gesamte System komplett neu gestalten müssen .

Arseni Mourzenko
quelle
Sofern ich Sie nicht missverstehe, sagen Sie "Behalten Sie die einzelnen GetUser-Aufrufe bei, verwenden Sie jedoch Caching, um DB-Roundtrips zu vermeiden".
Eren Ersönmez
@ ErenErsönmez: GetUserAnstatt die Datenbank abzufragen , wird im Cache gesucht . Dies bedeutet, dass es eigentlich egal ist, wie oft Sie aufrufen GetUser, da Daten aus dem Speicher anstelle der Datenbank geladen werden (es sei denn, der Cache wurde ungültig gemacht).
Arseni Mourzenko
Dies ist ein guter Vorschlag, da ich das Hauptproblem, das darin besteht, "das Geschwätz loszuwerden, ohne Systeme zu einem einzigen System zusammenzuführen", nicht gut hervorgehoben habe. Mein Beispiel für Benutzer und Projekte würde Sie natürlich zu der Annahme führen, dass es eine relativ kleine Anzahl von und sich selten ändernden Benutzern gibt. Vielleicht wäre ein besseres Beispiel Dokumente und Projekte gewesen. Stellen Sie sich vor, Sie haben ein paar Millionen Dokumente, täglich werden Tausende hinzugefügt, und das Projektsystem verwendet das Dokumentensystem zum Speichern seiner Dokumente. Würden Sie dann immer noch Caching empfehlen? Wahrscheinlich nein, richtig?
Eren Ersönmez
@ ErenErsönmez: Je mehr Daten Sie haben, desto kritischer wird das Caching. Vergleichen Sie als Faustregel die Anzahl der Lesevorgänge mit der Anzahl der Schreibvorgänge. Wenn "Tausende" Dokumente pro Tag hinzugefügt werden und es Millionen von selectAbfragen pro Tag gibt, sollten Sie das Caching verwenden. Wenn Sie jedoch Milliarden von Entitäten zu einer Datenbank hinzufügen, aber nur einige Tausend von selects mit sehr selektiven wheres erhalten, ist das Caching möglicherweise nicht so nützlich.
Arseni Mourzenko
Sie haben wahrscheinlich Recht - ich versuche wahrscheinlich, ein Problem zu beheben, das ich noch nicht habe. Ich werde wahrscheinlich so implementieren, wie es ist, und versuchen, es später bei Bedarf zu verbessern. Wenn das Caching nicht angemessen ist, weil beispielsweise Entitäten nach dem Hinzufügen wahrscheinlich nur 1-2 Mal gelesen werden, könnte die mögliche Lösung, die ich an die Frage angehängt habe, funktionieren? Sehen Sie ein großes Problem damit?
Eren Ersönmez