Die bereinigte Architektur schlägt vor, dass ein Anwendungsfall-Interakteur die tatsächliche Implementierung des Präsentators (der nach dem DIP injiziert wird) aufruft, um die Antwort / Anzeige zu handhaben. Ich sehe jedoch Leute, die diese Architektur implementieren, die Ausgabedaten vom Interaktor zurückgeben und dann den Controller (in der Adapterebene) entscheiden lassen, wie er damit umgeht. Verliert die zweite Lösung nicht nur die Verantwortung für die Anwendung, sondern definiert sie auch nicht eindeutig die Eingabe- und Ausgabeports für den Interaktor?
Eingangs- und Ausgangsanschlüsse
Angesichts der Definition der sauberen Architektur und insbesondere des kleinen Flussdiagramms, das die Beziehungen zwischen einem Controller, einem Use-Case-Interaktor und einem Presenter beschreibt, bin ich mir nicht sicher, ob ich den "Use-Case-Ausgabeport" richtig verstehe.
Eine saubere Architektur unterscheidet wie eine hexagonale Architektur zwischen primären Ports (Methoden) und sekundären Ports (Schnittstellen, die von Adaptern implementiert werden sollen). Im Anschluss an den Kommunikationsfluss erwarte ich, dass der "Use Case Input Port" ein primärer Port (also nur eine Methode) ist und der "Use Case Output Port" eine zu implementierende Schnittstelle ist, möglicherweise ein Konstruktorargument, das den eigentlichen Adapter übernimmt. damit der Interakteur es benutzen kann.
Codebeispiel
Um ein Codebeispiel zu erstellen, könnte dies der Controller-Code sein:
Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();
Die Presenter-Oberfläche:
// Use Case Output Port
interface Presenter
{
public void present(Data data);
}
Zum Schluss der Interaktor selbst:
class UseCase
{
private Repository repository;
private Presenter presenter;
public UseCase(Repository repository, Presenter presenter)
{
this.repository = repository;
this.presenter = presenter;
}
// Use Case Input Port
public void doSomething()
{
Data data = this.repository.getData();
this.presenter.present(data);
}
}
Auf dem Interakteur ruft der Moderator an
Die vorige Interpretation scheint durch das vorgenannte Diagramm selbst bestätigt zu werden, in dem die Beziehung zwischen dem Controller und dem Eingangsport durch einen durchgezogenen Pfeil mit einem "scharfen" Kopf dargestellt wird (UML für "Assoziation", was "hat ein" bedeutet, wobei das controller "hat einen" Anwendungsfall), während die Beziehung zwischen dem Presenter und dem Ausgabeport durch einen durchgezogenen Pfeil mit einem "weißen" Kopf dargestellt wird (UML für "Vererbung", was nicht diejenige für "Implementierung" ist, aber wahrscheinlich das ist sowieso die Bedeutung).
Darüber hinaus beschreibt Robert Martin in dieser Antwort auf eine andere Frage genau einen Anwendungsfall, bei dem der Interakteur den Präsentator auf eine Leseanforderung hin anruft:
Durch Klicken auf die Karte wird entweder der placePinController aufgerufen. Es sammelt die Position des Klicks und aller anderen kontextbezogenen Daten, erstellt eine placePinRequest-Datenstruktur und übergibt sie an den PlacePinInteractor, der die Position der Stecknadel überprüft, sie gegebenenfalls validiert, eine Place-Entität zum Aufzeichnen der Stecknadel erstellt und eine EditPlaceReponse erstellt Objekt und übergibt es an den EditPlacePresenter, der den Bereichseditorbildschirm aufruft.
Damit dies mit MVC gut funktioniert, könnte ich annehmen, dass die Anwendungslogik, die traditionell in den Controller eingeht, hier auf den Interaktor verschoben wird, da keine Anwendungslogik außerhalb der Anwendungsebene verloren gehen soll. Der Controller in der Adapterebene ruft einfach den Interaktor auf und konvertiert dabei möglicherweise einige kleinere Datenformate:
Die Software in dieser Schicht besteht aus einer Reihe von Adaptern, die Daten aus dem für die Anwendungsfälle und Entitäten am besten geeigneten Format in das für eine externe Agentur wie die Datenbank oder das Web am besten geeignete Format konvertieren.
aus dem Originalartikel über Schnittstellenadapter.
Auf dem Interaktor werden Daten zurückgegeben
Mein Problem bei diesem Ansatz ist jedoch, dass sich der Anwendungsfall um die Präsentation selbst kümmern muss. Nun sehe ich, dass der Zweck der Presenter
Schnittstelle darin besteht, abstrakt genug zu sein, um verschiedene Arten von Präsentatoren (GUI, Web, CLI usw.) darzustellen, und dass dies wirklich nur "Ausgabe" bedeutet, was ein Anwendungsfall sein könnte sehr gut haben, aber immer noch bin ich nicht ganz sicher damit.
Wenn ich mich im Web nach Anwendungen mit einer sauberen Architektur umsehe, sehe ich anscheinend nur Leute, die den Ausgabeport als Methode interpretieren, mit der DTO zurückgegeben wird. Das wäre so etwas wie:
Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);
// I'm omitting the changes to the classes, which are fairly obvious
Dies ist attraktiv, weil wir die Verantwortung für das "Aufrufen" der Präsentation aus dem Anwendungsfall heraus verlagern, sodass der Anwendungsfall sich nicht mehr mit dem Wissen befasst, was mit den Daten zu tun ist, sondern nur noch mit der Bereitstellung der Daten. Auch in diesem Fall wird die Abhängigkeitsregel nicht verletzt, da der Anwendungsfall immer noch nichts über die äußere Ebene weiß.
Der Anwendungsfall kontrolliert jedoch nicht mehr den Zeitpunkt, zu dem die eigentliche Präsentation durchgeführt wird (was beispielsweise nützlich sein kann, um an diesem Punkt zusätzliche Aufgaben wie die Protokollierung auszuführen oder sie bei Bedarf ganz abzubrechen). Beachten Sie außerdem, dass wir den Use-Case-Eingabeport verloren haben, da der Controller jetzt nur noch die getData()
Methode verwendet (dies ist unser neuer Ausgabeport). Darüber hinaus scheint es mir, dass wir hier das Prinzip "Tell, Don't Ask" brechen, weil wir den Interaktor auffordern, mit einigen Daten etwas zu tun, anstatt ihm zu sagen, dass es die eigentliche Sache im Internet macht erster Platz.
Auf den Punkt gebracht
Ist also eine dieser beiden Alternativen die "richtige" Interpretation des Use-Case-Ausgabeports gemäß der bereinigten Architektur? Sind sie beide lebensfähig?
Antworten:
Das ist sicherlich keine saubere , zwiebelige oder sechseckige Architektur. Das ist das :
Nicht, dass MVC so gemacht werden müsste
Sie können auf viele verschiedene Arten zwischen Modulen kommunizieren und diese als MVC bezeichnen . Mir zu sagen, dass etwas MVC verwendet, sagt mir nicht wirklich, wie die Komponenten kommunizieren. Das ist nicht standardisiert. Alles, was es mir sagt, ist, dass es mindestens drei Komponenten gibt, die sich auf ihre drei Verantwortlichkeiten konzentrieren.
Einige dieser Möglichkeiten wurden unterschiedlich benannt :
Und jeder von ihnen kann zu Recht als MVC bezeichnet werden.
Wie auch immer, keiner von denen erfasst wirklich, was die Modewort-Architekturen (Clean, Onion und Hex) von Ihnen verlangen.
Fügen Sie die Datenstrukturen hinzu, die herumgeschleudert werden (und drehen Sie sie aus irgendeinem Grund auf den Kopf), und Sie erhalten :
Eine Sache, die hier klar sein sollte, ist, dass das Antwortmodell nicht durch den Controller marschiert.
Wenn Sie Adlerauge sind, haben Sie vielleicht bemerkt, dass nur die Modewort-Architekturen kreisförmige Abhängigkeiten vollständig vermeiden . Wichtig ist, dass sich die Auswirkungen einer Codeänderung beim Durchlaufen von Komponenten nicht ausbreiten. Die Änderung wird beendet, wenn sie auf Code trifft, der sich nicht darum kümmert.
Ich frage mich, ob sie es auf den Kopf gestellt haben, damit der Kontrollfluss im Uhrzeigersinn abläuft. Mehr dazu und diese "weißen" Pfeilspitzen später.
Da die Kommunikation vom Controller zum Presenter über die Anwendungsebene erfolgen soll, ist es wahrscheinlich ein Leck, wenn der Controller einen Teil des Presenter-Auftrags ausführt. Das ist meine Hauptkritik an der VIPER-Architektur .
Warum die Trennung dieser so wichtig ist wahrscheinlich am besten durch das Studium werden könnte Segregations Command Query Verantwortung .
Dies ist die API, über die Sie die Ausgabe für diesen bestimmten Anwendungsfall senden. Es ist nicht mehr als das. Der Interaktor für diesen Anwendungsfall muss nicht wissen oder wissen wollen, ob die Ausgabe an eine GUI, eine CLI, ein Protokoll oder einen Audiolautsprecher gesendet wird. Alles, was der Interakteur wissen muss, ist die einfachste API, die es ihm ermöglicht, die Ergebnisse seiner Arbeit zu melden.
Der Grund, warum sich der Ausgabeport vom Eingabeport unterscheidet, besteht darin, dass er von der Ebene, die er abstrahiert, nicht BESITZEN darf. Das heißt, die Ebene, die sie abstrahiert, darf keine Änderungen vorgeben. Nur die Anwendungsebene und ihr Autor sollten entscheiden, dass sich der Ausgabeport ändern kann.
Dies steht im Gegensatz zu dem Eingabeport, der der Ebene gehört, die er abstrahiert. Nur der Autor der Anwendungsebene sollte entscheiden, ob sich der Eingabeport ändern soll.
Das Befolgen dieser Regeln bewahrt die Idee, dass die Anwendungsschicht oder eine innere Schicht überhaupt nichts über die äußeren Schichten weiß.
Das Wichtige an diesem "weißen" Pfeil ist, dass Sie damit Folgendes tun können:
Sie können den Kontrollfluss in die entgegengesetzte Richtung der Abhängigkeit laufen lassen! Das heißt, die innere Schicht muss nichts über die äußere Schicht wissen, und dennoch können Sie in die innere Schicht eintauchen und wieder herauskommen!
Dies hat nichts mit der Verwendung des Schlüsselworts "interface" zu tun. Sie könnten dies mit einer abstrakten Klasse tun. Mist, du könntest es mit einer (ick) konkreten Klasse machen, solange sie erweitert werden kann. Es ist einfach schön, dies mit etwas zu tun, das sich nur auf die Definition der API konzentriert, die Presenter implementieren muss. Der offene Pfeil fragt nur nach Polymorphismus. Welche Art liegt bei Ihnen?
Warum dieser Abhängigkeit der Richtungsumkehr so wichtig ist , kann durch das Studium der erlernt werden Dependency Inversion Principle . Dieses Prinzip habe ich hier auf diese Diagramme übertragen .
Nein, das ist es wirklich. Wenn Sie sicherstellen möchten, dass die inneren Schichten nichts über die äußeren Schichten wissen, können Sie die äußeren Schichten entfernen, ersetzen oder umgestalten, ohne die inneren Schichten zu beschädigen. Was sie nicht wissen, wird ihnen nicht schaden. Wenn wir das können, können wir die äußeren in das ändern, was wir wollen.
Das Problem hier ist nun, dass was auch immer weiß, wie man nach den Daten fragt, auch das sein muss, was die Daten akzeptiert. Bevor der Controller den Usecase Interactor aufrufen konnte, wusste er glücklicherweise nicht, wie das Reaktionsmodell aussehen würde, wohin es gehen sollte und wie es präsentiert werden sollte.
Studieren Sie erneut die Funktionstrennung für Befehlsabfragen , um festzustellen , warum dies wichtig ist.
Ja! Das Erzählen, nicht das Fragen, hilft dabei, dieses Objekt eher als prozedural auszurichten.
Alles, was funktioniert, ist machbar. Aber ich würde nicht sagen, dass die zweite Option, die Sie treu präsentiert haben, der sauberen Architektur folgt. Es könnte etwas sein, das funktioniert. Aber es ist nicht das, wonach Clean Architecture verlangt.
quelle
In einer Diskussion zu Ihrer Frage erklärt Onkel Bob den Zweck des Referenten in seiner Clean Architecture:
Angesichts dieses Codebeispiels:
Onkel Bob sagte dies:
(UPDATE: 31. Mai 2019)
Angesichts der Antwort von Onkel Bob denke ich, dass es nicht so wichtig ist , ob wir Option 1 ausführen (lassen Sie den Interakteur den Moderator verwenden) ...
... oder wir machen Option # 2 (lassen Sie den Interakteur eine Antwort zurücksenden, erstellen Sie einen Präsentator im Controller und übergeben Sie die Antwort dann an den Präsentator) ...
Ich persönlich ziehe Option # 1 , weil ich mag in der Lage Kontrolle sein innerhalb der ,
interactor
wenn Daten zu zeigen und Fehlermeldungen, wie in diesem Beispiel unter:... Ich möchte in der Lage sein, dies zu tun
if/else
, was mit der Präsentation innerhalbinteractor
und nicht außerhalb des Interaktors zusammenhängt.Wenn wir andererseits Option 2 ausführen, müssten wir die Fehlernachricht (en) im
response
Objekt speichern , diesesresponse
Objekt voninteractor
an an zurückgebencontroller
und dascontroller
Parsen zumresponse
Objekt machen ...Ich mag es nicht,
response
Daten nach Fehlern in der zu analysieren,controller
weil wir, wenn wir das tun, redundante Arbeit leisten - wenn wir etwas in der änderninteractor
, müssen wir auch etwas in der änderncontroller
.Auch wenn wir später entscheiden, unsere Wiederverwendung über
interactor
die Konsole, beispielsweise zur aktuellen Daten, müssen wir uns erinnern , alle diejenigen copy-pasteif/else
in diecontroller
unserer Konsolenanwendung.Wenn wir Option 1 verwenden, haben wir dies
if/else
nur an einer Stelle : derinteractor
.Wenn Sie ASP.NET MVC (oder ein ähnliches MVC-Framework) verwenden, ist Option 2 der einfachere Weg.
In einer solchen Umgebung können wir jedoch immer noch Option 1 durchführen. Hier ist ein Beispiel für die Ausführung von Option 1 in ASP.NET MVC:
(Beachten Sie, dass wir
public IActionResult Result
im Presenter unserer ASP.NET MVC App haben müssen)(Beachten Sie, dass wir
public IActionResult Result
im Presenter unserer ASP.NET MVC App haben müssen)Wenn wir uns entscheiden, eine andere App für die Konsole zu erstellen, können wir das
UseCase
oben Genannte wiederverwenden und nur dasController
undPresenter
für die Konsole erstellen :(Beachten Sie, dass wir
public IActionResult Result
im Presenter unserer Konsolen-App NICHT HABEN )quelle
Ein Anwendungsfall kann entweder den Präsentator oder zurückgegebene Daten enthalten, je nachdem, was der Anwendungsfluss erfordert.
Lassen Sie uns ein paar Begriffe verstehen, bevor wir verschiedene Anwendungsabläufe verstehen:
Ein Anwendungsfall, der die Rückgabe von Daten enthält
In einem normalen Fall gibt ein Anwendungsfall einfach ein Domänenobjekt an die Anwendungsebene zurück, das in der Anwendungsebene weiterverarbeitet werden kann, um die Anzeige in der Benutzeroberfläche zu erleichtern.
Da der Controller dafür verantwortlich ist, den Anwendungsfall aufzurufen, enthält er in diesem Fall auch eine Referenz des jeweiligen Präsentators, um die Domain zum Anzeigen der Modellzuordnung durchzuführen, bevor sie an die zu rendernde Ansicht gesendet wird.
Hier ist ein vereinfachtes Codebeispiel:
Ein Anwendungsfall mit Presenter
Es ist zwar nicht üblich, aber es ist möglich, dass der Anwendungsfall den Moderator anrufen muss. In diesem Fall ist es ratsam, anstelle der konkreten Referenz des Präsentators eine Schnittstelle (oder abstrakte Klasse) als Referenzpunkt zu betrachten (die zur Laufzeit über die Abhängigkeitsinjektion initialisiert werden sollte).
Wenn die Domäne zum Anzeigen der Modellzuordnungslogik in einer separaten Klasse (anstatt im Controller) vorhanden ist, wird auch die zirkuläre Abhängigkeit zwischen Controller und Anwendungsfall aufgehoben (wenn die Anwendungsfallklasse einen Verweis auf die Zuordnungslogik benötigt).
Nachstehend finden Sie eine vereinfachte Implementierung des Steuerungsablaufs, wie im Originalartikel veranschaulicht, die veranschaulicht, wie dies durchgeführt werden kann. Bitte beachten Sie, dass UseCaseInteractor im Gegensatz zur Darstellung der Einfachheit halber eine konkrete Klasse ist.
quelle
Obwohl ich der Antwort von @CandiedOrange im Allgemeinen zustimme, würde ich auch einen Vorteil in dem Ansatz sehen, bei dem der Interaktor nur Daten erneut ausführt, die dann vom Controller an den Präsentator übergeben werden.
Dies ist beispielsweise eine einfache Möglichkeit, die Ideen der Clean Architecture (Abhängigkeitsregel) im Kontext von Asp.Net MVC zu verwenden.
Ich habe einen Blog-Beitrag geschrieben, um näher auf diese Diskussion einzugehen : https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/
quelle
Zusamenfassend
Ja, sie sind beide realisierbar, solange beide Ansätze Inversion Of Control zwischen der Geschäftsschicht und dem Bereitstellungsmechanismus berücksichtigen . Mit dem zweiten Ansatz können wir das IOC immer noch einführen, indem wir Beobachter, Vermittler und einige andere Designmuster verwenden ...
Mit seiner Clean Architecture versucht Onkel Bob, eine Reihe bekannter Architekturen zu synthetisieren, um wichtige Konzepte und Komponenten aufzuzeigen, mit denen wir die OOP-Prinzipien weitgehend einhalten können.
Es wäre kontraproduktiv, sein UML-Klassendiagramm (das folgende Diagramm) als DAS einzigartige Clean Architecture- Design zu betrachten. Dieses Diagramm hätte zur Veranschaulichung von konkreten Beispielen gezeichnet werden können. Da es jedoch weitaus weniger abstrakt als übliche Architekturdarstellungen ist, musste er konkrete Entscheidungen treffen, unter denen das Design des Interaktor-Ausgangsports nur ein Implementierungsdetail ist .
Meine zwei Cent
Der Hauptgrund, warum ich den zurückschicke,
UseCaseResponse
ist, dass dieser Ansatz meine Anwendungsfälle flexibel hält und sowohl die Komposition zwischen ihnen als auch die Generizität ( Generalisierung und spezifische Generation ) zulässt . Ein einfaches Beispiel:Beachten Sie, dass es sich in analoger Weise um UML-Anwendungsfälle handelt, die sich gegenseitig einschließen / erweitern und für verschiedene Themen (die Entitäten) als wiederverwendbar definiert sind.
Sie sind sich nicht sicher, was Sie damit meinen. Warum müssen Sie die Darstellungsleistung "steuern"? Kontrollieren Sie es nicht, solange Sie die Use-Case-Antwort nicht zurückgeben?
Der Anwendungsfall kann in seiner Antwort einen Statuscode zurückgeben, um der Client-Schicht mitzuteilen, was genau während seines Betriebs passiert ist. HTTP-Antwortstatuscodes eignen sich besonders gut zur Beschreibung des Betriebsstatus eines Anwendungsfalls.
quelle