Saubere Architektur: Use Case mit Presenter oder Rückgabe von Daten?

42

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 PresenterSchnittstelle 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?

swahnee
quelle
3
Von Crossposting wird dringend abgeraten. Wenn Sie möchten, dass Ihre Frage hier lebendig wird, sollten Sie sie aus dem Stapelüberlauf löschen.
Robert Harvey

Antworten:

48

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.

Das ist sicherlich keine saubere , zwiebelige oder sechseckige Architektur. Das ist das :

Bildbeschreibung hier eingeben

Nicht, dass MVC so gemacht werden müsste

Bildbeschreibung hier eingeben

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 : Bildbeschreibung hier eingeben

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.

Bildbeschreibung hier eingeben

Fügen Sie die Datenstrukturen hinzu, die herumgeschleudert werden (und drehen Sie sie aus irgendeinem Grund auf den Kopf), und Sie erhalten :

Bildbeschreibung hier eingeben

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.

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?

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 .

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.

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.

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.

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


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

Das Wichtige an diesem "weißen" Pfeil ist, dass Sie damit Folgendes tun können:

Bildbeschreibung hier eingeben

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 .

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-Oberfläche darin besteht, abstrakt genug zu sein, um verschiedene Arten von Presentern (GUI, Web, CLI usw.) darzustellen, und dass dies wirklich nur "Ausgabe" bedeutet, was ein Anwendungsfall ist könnte sehr gut haben, aber ich bin immer noch nicht ganz sicher damit.

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.

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

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.

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

Ja! Das Erzählen, nicht das Fragen, hilft dabei, dieses Objekt eher als prozedural auszurichten.

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?

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.

kandierte_orange
quelle
4
Vielen Dank, dass Sie sich die Zeit genommen haben, eine so ausführliche Erklärung zu verfassen.
Swahnee
1
Ich habe versucht, mich mit der sauberen Architektur zu beschäftigen, und diese Antwort war eine fantastische Ressource. Sehr gut gemacht!
Nathan
Tolle und ausführliche Antwort. Danke dafür. Können Sie mir einige Tipps (oder Erklärungen) zur Aktualisierung der GUI während des UseCase-Laufs geben, dh Fortschrittsbalkenaktualisierung beim Hochladen einer großen Datei?
Ewoks
1
@Ewoks, als schnelle Antwort auf Ihre Frage sollten Sie sich das Observable-Muster ansehen. Ihr Anwendungsfall könnte einen Betreff zurückgeben und den Betreff über Fortschrittsaktualisierungen informieren. Der Moderator würde den Betreff abonnieren und auf die Benachrichtigungen antworten.
Nathan
7

In einer Diskussion zu Ihrer Frage erklärt Onkel Bob den Zweck des Referenten in seiner Clean Architecture:

Angesichts dieses Codebeispiels:

namespace Some\Controller;

class UserController extends Controller {
    public function registerAction() {
        // Build the Request object
        $request = new RegisterRequest();
        $request->name = $this->getRequest()->get('username');
        $request->pass = $this->getRequest()->get('password');

        // Build the Interactor
        $usecase = new RegisterUser();

        // Execute the Interactors method and retrieve the response
        $response = $usecase->register($request);

        // Pass the result to the view
        $this->render(
            '/user/registration/template.html.twig', 
            array('id' =>  $response->getId()
        );
    }
}

Onkel Bob sagte dies:

" Der Zweck des Präsentators besteht darin, die Anwendungsfälle vom Format der Benutzeroberfläche zu entkoppeln. In Ihrem Beispiel wird die Antwortvariable" $ "vom Interakteur erstellt, aber von der Ansicht verwendet. Dadurch wird der Interakteur mit der Ansicht gekoppelt. Zum Beispiel Nehmen wir an, eines der Felder im $ response-Objekt ist ein Datum.Dieses Feld wäre ein binäres Datumsobjekt, das in vielen verschiedenen Datumsformaten gerendert werden könnte.Das Datumsformat ist sehr spezifisch, vielleicht TT / MM / JJJJ. Wessen Verantwortung ist es, das Format zu erstellen? Wenn der Interakteur dieses Format erstellt, weiß er zu viel über die Ansicht. Wenn die Ansicht das binäre Datumsobjekt verwendet, weiß er zu viel über den Interakteur.

" die Daten aus dem Antwortobjekt und formatieren sie für die Ansicht. Weder die Ansicht noch der Interakteur kennen die Formate des jeweils anderen. "

--- Onkel Bob

(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) ...

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

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

class Controller
{
    public void ExecuteUseCase(Data data)
    {
        Request request = ...
        UseCase useCase = new UseCase(repository);
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        presenter.Show(response);
    }
}

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:

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... Ich möchte in der Lage sein, dies zu tun if/else, was mit der Präsentation innerhalb interactorund nicht außerhalb des Interaktors zusammenhängt.

Wenn wir andererseits Option 2 ausführen, müssten wir die Fehlernachricht (en) im responseObjekt speichern , dieses responseObjekt von interactoran an zurückgeben controllerund das controller Parsen zum responseObjekt machen ...

class UseCase
{
    public Response Execute(Request request)
    {
        Response response = new Response();
        if (<invalid request>) 
        {
            response.AddError("...");
        }

        if (<there is another error>) 
        {
            response.AddError("another error...");
        }

        if (response.HasNoErrors)
        {
            response.Whatever = ...
        }

        ...
        return response;
    }
}
class Controller
{
    private UseCase useCase;

    public Controller(UseCase useCase)
    {
        this.useCase = useCase;
    }

    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        if (response.ErrorMessages.Count > 0)
        {
            if (response.ErrorMessages.Contains(<invalid request>))
            {
                presenter.ShowError("...");
            }
            else if (response.ErrorMessages.Contains("another error")
            {
                presenter.ShowError("another error...");
            }
        }
        else
        {
            presenter.Show(response);
        }
    }
}

Ich mag es nicht, responseDaten nach Fehlern in der zu analysieren, controllerweil wir, wenn wir das tun, redundante Arbeit leisten - wenn wir etwas in der ändern interactor, müssen wir auch etwas in der ändern controller.

Auch wenn wir später entscheiden, unsere Wiederverwendung über interactordie Konsole, beispielsweise zur aktuellen Daten, müssen wir uns erinnern , alle diejenigen copy-paste if/elsein die controllerunserer Konsolenanwendung.

// in the controller for our console app
if (response.ErrorMessages.Count > 0)
{
    if (response.ErrorMessages.Contains(<invalid request>))
    {
        presenterForConsole.ShowError("...");
    }
    else if (response.ErrorMessages.Contains("another error")
    {
        presenterForConsole.ShowError("another error...");
    }
}
else
{
    presenterForConsole.Present(response);
}

Wenn wir Option 1 verwenden, haben wir dies if/else nur an einer Stelle : der interactor.


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 Resultim Presenter unserer ASP.NET MVC App haben müssen)

class UseCase
{
    private Repository repository;

    public UseCase(Repository repository)
    {
        this.repository = repository;
    }

    public void Execute(Request request, Presenter presenter)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {
            ...
        }
        this.presenter.Show(response);
    }
}
// controller for ASP.NET app

class AspNetController
{
    private UseCase useCase;

    public AspNetController(UseCase useCase)
    {
        this.useCase = useCase;
    }

    [HttpPost("dosomething")]
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new AspNetPresenter();
        useCase.Execute(request, presenter);
        return presenter.Result;
    }
}
// presenter for ASP.NET app

public class AspNetPresenter
{
    public IActionResult Result { get; private set; }

    public AspNetPresenter(...)
    {
    }

    public async void Show(Response response)
    {
        Result = new OkObjectResult(new { });
    }

    public void ShowError(string errorMessage)
    {
        Result = new BadRequestObjectResult(errorMessage);
    }
}

(Beachten Sie, dass wir public IActionResult Resultim 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 UseCaseoben Genannte wiederverwenden und nur das Controllerund Presenterfür die Konsole erstellen :

// controller for console app

class ConsoleController
{    
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new ConsolePresenter();
        useCase.Execute(request, presenter);
    }
}
// presenter for console app

public class ConsolePresenter
{
    public ConsolePresenter(...)
    {
    }

    public async void Show(Response response)
    {
        // write response to console
    }

    public void ShowError(string errorMessage)
    {
        Console.WriteLine("Error: " + errorMessage);
    }
}

(Beachten Sie, dass wir public IActionResult Resultim Presenter unserer Konsolen-App NICHT HABEN )

Jboy Flaga
quelle
Danke für den Beitrag. Beim Lesen der Konversation verstehe ich jedoch eines nicht: Er sagt, dass der Präsentator die aus der Antwort stammenden Daten rendern und gleichzeitig die Antwort nicht vom Interakteur erstellt werden sollte. Aber wer schafft dann die Antwort? Ich würde sagen, der Interakteur sollte dem Präsentator die Daten in dem anwendungsspezifischen Format bereitstellen, das dem Präsentator bekannt ist, da die Adapterschicht von der Anwendungsschicht abhängen kann (aber nicht umgekehrt).
Swahnee
Es tut mir Leid. Vielleicht wird es verwirrend, weil ich das Codebeispiel aus der Diskussion nicht aufgenommen habe. Ich werde es aktualisieren, um das Codebeispiel aufzunehmen.
Jboy Flaga
Onkel Bob sagte nicht, dass die Antwort nicht vom Interakteur erstellt werden sollte. Die Antwort wird vom Interakteur erstellt . Was Onkel Bob sagt, ist, dass die vom Interakteur erstellte Antwort vom Präsentator verwendet wird. Der Präsentator wird es dann "formatieren", die formatierte Antwort einem Ansichtsmodell zuweisen und dieses Ansichtsmodell dann an die Ansicht übergeben. <br/> So verstehe ich es.
Jboy Flaga
1
Das macht mehr Sinn. Ich hatte den Eindruck, dass "view" ein Synonym für "presenter" ist, da Clean Architecture weder "view" noch "viewmodel" erwähnt, da es sich meiner Meinung nach nur um MVC - Konzepte handelt, die möglicherweise bei der Implementierung eines verwendet werden Adapter.
Swahnee
2

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:

  • Domänenobjekt : Ein Domänenobjekt ist der Datencontainer in der Domänenschicht, auf dem Geschäftslogikoperationen ausgeführt werden.
  • Modell anzeigen: Domänenobjekte werden normalerweise so zugeordnet, dass sie Modelle in der Anwendungsebene anzeigen, damit sie kompatibel und benutzerfreundlich sind.
  • Presenter : Während ein Controller in der Anwendungsschicht normalerweise einen Anwendungsfall aufruft, ist es ratsam, die Domäne so zu delegieren, dass die Modellzuordnungslogik in einer separaten Klasse (nach dem Prinzip der Einzelverantwortung) angezeigt wird, die als "Presenter" bezeichnet wird.

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:

namespace SimpleCleanArchitecture
{
    public class OutputDTO
    {
        //fields
    }

    public class Presenter 
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    public class Domain
    {
        //fields
    }

    public class UseCaseInteractor
    {
        public Domain Process(Domain domain)
        {
            // additional processing takes place here
            return domain;
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            UseCaseInteractor userCase = new UseCaseInteractor();
            var domain = userCase.Process(new Domain());//passing dummy domain(for demonstration purpose) to process
            var presenter = new Presenter();//presenter might be initiated via dependency injection.

            return new View(presenter.Present(domain));
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

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

Bildbeschreibung hier eingeben

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.

namespace CleanArchitectureWithPresenterInUseCase
{
    public class Domain
    {
        //fields
    }

    public class OutputDTO
    {
        //fields
    }

    // Use Case Output Port
    public interface IPresenter
    {
        OutputDTO Present(Domain domain);
    }

    public class Presenter: IPresenter
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    // Use Case Input Port / Interactor   
    public class UseCaseInteractor
    {
        IPresenter _presenter;
        public UseCaseInteractor (IPresenter presenter)
        {
            _presenter = presenter;
        }

        public OutputDTO Process(Domain domain)
        {
            return _presenter.Present(domain);
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            IPresenter presenter = new Presenter();//presenter might be initiated via dependency injection.
            UseCaseInteractor userCase = new UseCaseInteractor(presenter);
            var outputDTO = userCase.Process(new Domain());//passing dummy domain (for demonstration purpose) to process
            return new View(outputDTO);
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}
Ashraf
quelle
1

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/

Plainionist
quelle
1

Anwendungsfall, der den Präsentator enthält oder Daten zurückgibt?

Ist also eine dieser beiden Alternativen die "richtige" Interpretation des Use-Case-Ausgabeports gemäß der bereinigten Architektur? Sind sie beide lebensfähig?


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 .

Onkel Bobs UML-Klassendiagramm für saubere Architektur


Meine zwei Cent

Der Hauptgrund, warum ich den zurückschicke, UseCaseResponseist, 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:

// A generic "entity type agnostic" use case encapsulating the interaction logic itself.
class UpdateUseCase implements UpdateUseCaseInterface
{
    function __construct(EntityGatewayInterface $entityGateway, GetUseCaseInterface $getUseCase)
    {
        $this->entityGateway = $entityGateway;
        $this->getUseCase = $getUseCase;
    }

    public function execute(UpdateUseCaseRequestInterface $request) : UpdateUseCaseResponseInterface
    {
        $getUseCaseResponse = $this->getUseCase->execute($request);

        // Update the entity and build the response...

        return $response;
    }
}

// "entity type aware" use cases encapsulating the interaction logic WITH the specific entity type.
final class UpdatePostUseCase extends UpdateUseCase;
final class UpdateProductUseCase extends UpdateUseCase;

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.


Auf dem Interaktor werden Daten zurückgegeben

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

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.

ClemC
quelle