Ich schreibe viel Code, der drei grundlegende Schritte umfasst.
- Holen Sie sich Daten von irgendwoher.
- Transformiere diese Daten.
- Legen Sie diese Daten irgendwo ab.
Normalerweise verwende ich drei Arten von Klassen - inspiriert von ihren jeweiligen Designmustern.
- Fabriken - um ein Objekt aus einer Ressource zu erstellen.
- Mediatoren - Um die Fabrik zu nutzen, führen Sie die Transformation durch und verwenden Sie dann den Kommandanten.
- Kommandanten - um diese Daten woanders abzulegen.
Meine Klassen sind in der Regel recht klein, oft eine einzelne (öffentliche) Methode, z. B. Daten abrufen, Daten transformieren, arbeiten, Daten speichern. Dies führt zu einer Zunahme der Klassen, funktioniert aber im Allgemeinen gut.
Wenn ich zum Testen komme, habe ich Probleme damit, eng gekoppelte Tests durchzuführen. Beispielsweise;
- Factory - Liest Dateien von der Festplatte.
- Commander - schreibt Dateien auf die Festplatte.
Ich kann nicht eins ohne das andere testen. Ich könnte zusätzlichen 'Test'-Code schreiben, um auch das Lesen / Schreiben der Festplatte durchzuführen, aber dann wiederhole ich mich.
Mit Blick auf .Net verfolgt die File- Klasse einen anderen Ansatz. Sie kombiniert die Verantwortlichkeiten (meiner) Fabrik und des Kommandanten miteinander. Es verfügt über Funktionen zum Erstellen, Löschen, Vorhandensein und Lesen an einem Ort.
Sollte ich versuchen, dem Beispiel von .Net zu folgen und - insbesondere im Umgang mit externen Ressourcen - meine Klassen zusammen zu kombinieren? Der Code ist immer noch gekoppelt, aber eher beabsichtigt - er geschieht eher bei der ursprünglichen Implementierung als bei den Tests.
Ist mein Problem hier, dass ich das Prinzip der Einzelverantwortung etwas übereifrig angewendet habe? Ich habe separate Klassen, die für Lesen und Schreiben verantwortlich sind. Wenn ich eine kombinierte Klasse haben könnte, die für den Umgang mit einer bestimmten Ressource verantwortlich ist, z. B. einer Systemfestplatte.
quelle
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.
- Beachten Sie, dass Sie "Verantwortung" mit "zu tun" verbinden. Eine Verantwortung ist eher ein "Problembereich". Die Dateiklasse ist dafür verantwortlich , Dateivorgänge auszuführen.File
Bibliothek von C # ist,File
soweit wir wissen, dass die Klasse nur eine Fassade sein kann, die alle Dateioperationen an einem einzigen Ort - in der Klasse - platziert, aber intern ähnliche Lese- / Schreibklassen wie Ihre verwendet enthalten tatsächlich die kompliziertere Logik für die Dateiverwaltung. Eine solche Klasse (theFile
) würde immer noch die SRP einhalten, da der Prozess der tatsächlichen Arbeit mit dem Dateisystem hinter einer anderen Ebene abstrahiert würde - höchstwahrscheinlich mit einer einheitlichen Schnittstelle. Nicht zu sagen, dass es der Fall ist, aber es könnte sein. :)Antworten:
Das Prinzip der Einzelverantwortung zu befolgen mag Sie hierher geführt haben, aber wo Sie sich befinden, hat einen anderen Namen.
Verantwortlichkeitstrennung für Befehlsabfragen
Studieren Sie das und ich denke, Sie werden es nach einem vertrauten Muster finden und Sie sind nicht allein, wenn Sie sich fragen, wie weit Sie gehen sollen. Der Härtetest ist, wenn das Befolgen dieses Tests Ihnen echte Vorteile bringt oder wenn es nur ein blindes Mantra ist, dem Sie folgen, damit Sie nicht nachdenken müssen.
Sie haben Bedenken hinsichtlich des Testens geäußert. Ich denke nicht, dass das Befolgen von CQRS das Schreiben von testbarem Code ausschließt. Möglicherweise folgen Sie CQRS einfach so, dass Ihr Code nicht testbar ist.
Es ist hilfreich zu wissen, wie man Polymorphismus verwendet, um Quellcode-Abhängigkeiten zu invertieren, ohne den Kontrollfluss ändern zu müssen. Ich bin mir nicht sicher, wo Ihre Fähigkeiten beim Schreiben von Tests liegen.
Ein Wort der Vorsicht, den Gewohnheiten zu folgen, die Sie in Bibliotheken finden, ist nicht optimal. Bibliotheken haben ihre eigenen Bedürfnisse und sind ehrlich gesagt alt. Selbst das beste Beispiel ist also nur das beste Beispiel von damals.
Dies bedeutet nicht, dass es keine perfekt gültigen Beispiele gibt, die CQRS nicht folgen. Es wird immer ein bisschen schmerzhaft sein, ihm zu folgen. Es ist nicht immer eine, die es wert ist, bezahlt zu werden. Aber wenn Sie es brauchen, werden Sie froh sein, dass Sie es benutzt haben.
Wenn Sie es verwenden, beachten Sie dieses warnende Wort:
quelle
Sie benötigen eine breitere Perspektive, um festzustellen, ob der Code dem Prinzip der Einzelverantwortung entspricht. Es kann nicht nur durch die Analyse des Codes selbst beantwortet werden. Sie müssen überlegen, welche Kräfte oder Akteure dazu führen könnten, dass sich die Anforderungen in Zukunft ändern.
Nehmen wir an, Sie speichern Anwendungsdaten in einer XML-Datei. Welche Faktoren können dazu führen, dass Sie den Code zum Lesen oder Schreiben ändern? Einige Möglichkeiten:
In all diesen Fällen müssen Sie sowohl die Lese- als auch die Schreiblogik ändern . Mit anderen Worten, sie sind keine getrennten Verantwortlichkeiten.
Stellen wir uns jedoch ein anderes Szenario vor: Ihre Anwendung ist Teil einer Datenverarbeitungspipeline. Es liest einige CSV-Dateien, die von einem separaten System generiert wurden, führt einige Analysen und Verarbeitungen durch und gibt dann eine andere Datei aus, die von einem dritten System verarbeitet werden soll. In diesem Fall sind Lesen und Schreiben unabhängige Aufgaben und sollten entkoppelt werden.
Fazit: Sie können im Allgemeinen nicht sagen, ob das Lesen und Schreiben von Dateien separate Verantwortlichkeiten sind. Dies hängt von den Rollen in der Anwendung ab. Aber basierend auf Ihrem Hinweis zum Testen würde ich vermuten, dass es in Ihrem Fall eine einzige Verantwortung ist.
quelle
Im Allgemeinen haben Sie die richtige Idee.
Klingt so, als hätten Sie drei Verantwortlichkeiten. IMO der "Mediator" kann zu viel tun. Ich denke, Sie sollten zunächst Ihre drei Verantwortlichkeiten modellieren:
Dann kann ein Programm ausgedrückt werden als:
Ich denke nicht, dass dies ein Problem ist. IMO viele kleine zusammenhängende, testbare Klassen sind besser als große, weniger zusammenhängende Klassen.
Jedes Stück sollte unabhängig testbar sein. Wie oben modelliert, können Sie das Lesen / Schreiben in eine Datei wie folgt darstellen:
Sie können Integrationstests schreiben, um diese Klassen zu testen und zu überprüfen, ob sie in das Dateisystem lesen und in dieses schreiben. Der Rest der Logik kann als Transformationen geschrieben werden. Wenn die Dateien beispielsweise im JSON-Format vorliegen, können Sie die
String
s transformieren .Dann können Sie in richtige Objekte verwandeln:
Jedes davon ist unabhängig testbar. Sie können auch Unit - Test
program
oben durch spöttischreader
,transformer
undwriter
.quelle
FileWriter
indem Sie direkt aus dem Dateisystem lesen, anstatt zu verwendenFileReader
. Es liegt wirklich an Ihnen, was Ihre Ziele im Test sind. Wenn Sie verwendenFileReader
, wird der Test abgebrochen , wenn einerFileReader
oderFileWriter
mehrere Fehler auftreten. Das Debuggen kann länger dauern.Der Fokus liegt hier also darauf, was sie miteinander verbindet . Übergeben Sie ein Objekt zwischen den beiden (z. B. a
File
?). Dann ist es die Datei, mit der sie gekoppelt sind, nicht miteinander.Von dem, was Sie gesagt haben, haben Sie Ihre Klassen getrennt. Die Falle ist, dass Sie sie zusammen testen, weil es einfacher oder "sinnvoll" ist .
Warum muss die Eingabe
Commander
von einer Festplatte stammen? Alles, was es interessiert, ist das Schreiben mit einer bestimmten Eingabe. Dann können Sie überprüfen, ob die Datei korrekt geschrieben wurde, indem Sie die im Test enthaltenen Informationen verwenden .Der eigentliche Teil, auf den Sie testen,
Factory
lautet: "Wird diese Datei korrekt gelesen und das Richtige ausgegeben?" Verspotten Sie die Datei, bevor Sie sie im Test lesen .Alternativ ist es in Ordnung zu testen, ob Factory und Commander miteinander verbunden sind - dies entspricht recht gut dem Integrationstest. Die Frage hier ist eher eine Frage, ob Sie sie separat testen können oder nicht.
quelle
Es ist ein typischer prozeduraler Ansatz, über den David Parnas 1972 schrieb. Sie konzentrieren sich darauf, wie die Dinge laufen. Sie nehmen die konkrete Lösung Ihres Problems als ein übergeordnetes Muster, das immer falsch ist.
Wenn Sie einen objektorientierten Ansatz verfolgen, konzentriere ich mich lieber auf Ihre Domain . Worum geht es? Was sind die Hauptaufgaben Ihres Systems? Was sind die Hauptkonzepte in der Sprache Ihrer Domain-Experten? Verstehen Sie also Ihre Domäne, zerlegen Sie sie, behandeln Sie übergeordnete Verantwortungsbereiche als Ihre Module , behandeln Sie Konzepte auf niedrigerer Ebene, die als Substantive dargestellt werden, als Ihre Objekte. Hier ist ein Beispiel, das ich zu einer kürzlich gestellten Frage gegeben habe. Es ist sehr relevant.
Und es gibt ein offensichtliches Problem mit dem Zusammenhalt, Sie haben es selbst erwähnt. Wenn Sie eine Eingabelogik ändern und Tests darauf schreiben, beweist dies in keiner Weise, dass Ihre Funktionalität funktioniert, da Sie möglicherweise vergessen, diese Daten an die nächste Ebene weiterzugeben. Sehen Sie, diese Schichten sind intrinsisch gekoppelt. Und eine künstliche Entkopplung macht die Sache noch schlimmer. Ich weiß das selbst: 7 Jahre Projekt mit 100 Mannjahren hinter meinen Schultern, komplett in diesem Stil geschrieben. Lauf davon, wenn du kannst.
Und im Großen und Ganzen SRP-Sache. Es geht um Zusammenhalt , der auf Ihren Problembereich angewendet wird, dh auf Ihre Domäne. Das ist das Grundprinzip von SRP. Dies führt dazu, dass Objekte intelligent sind und ihre Verantwortung für sich selbst übernehmen. Niemand kontrolliert sie, niemand liefert ihnen Daten. Sie kombinieren Daten und Verhalten und legen nur letztere offen. Ihre Objekte kombinieren also sowohl Rohdatenvalidierung, Datentransformation (dh Verhalten) als auch Persistenz. Es könnte wie folgt aussehen:
Infolgedessen gibt es einige zusammenhängende Klassen, die einige Funktionen darstellen. Beachten Sie, dass die Validierung normalerweise für Wertobjekte gilt - zumindest beim DDD- Ansatz.
quelle
Achten Sie bei der Arbeit mit dem Dateisystem auf undichte Abstraktionen. Ich habe gesehen, dass diese viel zu oft vernachlässigt wurden und die von Ihnen beschriebenen Symptome aufweisen.
Wenn die Klasse Daten verarbeitet, die aus diesen Dateien stammen / in diese Dateien gelangen, wird das Dateisystem zum Implementierungsdetail (I / O) und sollte von diesen getrennt werden. Diese Klassen (Factory / Commander / Mediator) sollten das Dateisystem nicht kennen, es sei denn, ihre einzige Aufgabe besteht darin, die bereitgestellten Daten zu speichern / lesen. Klassen, die sich mit Dateisystemen befassen, sollten kontextspezifische Parameter wie Pfade (möglicherweise über den Konstruktor übergeben) enthalten, damit die Schnittstelle ihre Natur nicht preisgibt (das Wort "Datei" im Schnittstellennamen ist meistens ein Geruch).
quelle
Meiner Meinung nach klingt es so, als ob Sie den richtigen Weg eingeschlagen haben, aber nicht weit genug gegangen sind. Ich denke, es ist richtig, die Funktionalität in verschiedene Klassen aufzuteilen, die eines tun und es gut machen.
Um noch einen Schritt weiter zu gehen, sollten Sie Schnittstellen für Ihre Factory-, Mediator- und Commander-Klassen erstellen. Dann können Sie verspottete Versionen dieser Klassen verwenden, wenn Sie Ihre Komponententests für die konkreten Implementierungen der anderen schreiben. Mit den Mocks können Sie überprüfen, ob Methoden in der richtigen Reihenfolge und mit den richtigen Parametern aufgerufen werden und ob sich der zu testende Code mit unterschiedlichen Rückgabewerten ordnungsgemäß verhält.
Sie können auch das Lesen / Schreiben der Daten abstrahieren. Sie gehen jetzt zu einem Dateisystem, möchten aber möglicherweise irgendwann in der Zukunft zu einer Datenbank oder sogar zu einem Socket gehen. Ihre Mediatorklasse sollte sich nicht ändern müssen, wenn sich die Quelle / das Ziel der Daten ändert.
quelle