it-swarm.com.de

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

Das Clean Architecture schlägt vor, dass ein Anwendungsfall-Interaktor die tatsächliche Implementierung des Präsentators (der nach dem DIP injiziert wird) aufruft, um die Antwort/Anzeige zu verarbeiten. Ich sehe jedoch Leute, die diese Architektur implementieren, die Ausgabedaten vom Interaktor zurückgeben und dann den Controller (in der Adapterschicht) entscheiden lassen, wie er damit umgehen soll. Verliert die zweite Lösung die Anwendungsverantwortung aus der Anwendungsschicht und definiert die Eingabe- und Ausgabeports für den Interaktor nicht klar?

Eingangs- und Ausgangsanschlüsse

In Anbetracht der Definition von der sauberen Architektur und insbesondere des kleinen Flussdiagramms, das die Beziehungen zwischen einem Controller, einem Anwendungsfall-Interaktor und einem Präsentator beschreibt, bin ich mir nicht sicher, ob ich den "Anwendungsfall-Ausgabeport" richtig verstehe " sollte sein.

Eine saubere Architektur unterscheidet wie eine hexagonale Architektur zwischen primären Ports (Methoden) und sekundären Ports (Schnittstellen, die von Adaptern implementiert werden sollen). Nach dem Kommunikationsfluss erwarte ich, dass der "Use Case Input Port" ein primärer Port (also nur eine Methode) und der "Use Case Output Port" eine zu implementierende Schnittstelle ist, möglicherweise ein Konstruktorargument, das den tatsächlichen Adapter verwendet. damit der Interaktor es verwenden 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 Präsentationsoberfläche:

// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}

Schließlich 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 Interaktor, der den Moderator anruft

Die vorherige Interpretation scheint durch das oben erwähnte Diagramm selbst bestätigt zu werden, in dem die Beziehung zwischen der Steuerung und dem Eingangsport durch einen durchgezogenen Pfeil mit einem "scharfen" Kopf dargestellt wird (UML für "Assoziation", was "hat ein" bedeutet, wobei die Controller "hat einen" Anwendungsfall), während die Beziehung zwischen dem Präsentator und dem Ausgabeport durch einen durchgezogenen Pfeil mit einem "weißen" Kopf dargestellt wird (UML für "Vererbung", was nicht die für "Implementierung" ist, aber wahrscheinlich das ist sowieso die Bedeutung).

Darüber hinaus beschreibt Robert Martin in diese Antwort auf eine andere Frage genau einen Anwendungsfall, in dem der Interaktor den Präsentator auf eine Leseanforderung hin anruft:

Durch Klicken auf die Karte wird entweder der placePinController aufgerufen. Es erfasst die Position des Klicks und alle anderen Kontextdaten, erstellt eine placePinRequest-Datenstruktur und übergibt sie an den PlacePinInteractor, der die Position des Pins überprüft, sie bei Bedarf überprüft, eine Place-Entität zum Aufzeichnen des Pins erstellt und eine EditPlaceReponse erstellt Objekt und übergibt es an den EditPlacePresenter, der den Ortseditor-Bildschirm aufruft.

Damit dies mit MVC gut funktioniert, könnte ich denken, dass die Anwendungslogik, die traditionell in den Controller eingeht, hier in den Interaktor verschoben wird, da keine Anwendungslogik außerhalb der Anwendungsschicht auslaufen soll. Der Controller in der Adapterschicht ruft einfach den Interaktor auf und führt dabei möglicherweise eine geringfügige Konvertierung des Datenformats durch:

Die Software in dieser Schicht besteht aus einer Reihe von Adaptern, die Daten aus dem für 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. Jetzt sehe ich, dass der Zweck der Presenter -Schnittstelle abstrakt genug ist, um verschiedene Arten von Präsentatoren (GUI, Web, CLI usw.) darzustellen, und dass es wirklich nur "Ausgabe" bedeutet, was ist etwas, das ein Anwendungsfall sehr gut haben könnte, aber ich bin immer noch nicht ganz sicher.

Wenn ich mich jetzt im Web nach Anwendungen der sauberen Architektur umsehe, finde ich anscheinend nur Leute, die den Ausgabeport als eine Methode interpretieren, die DTO zurückgibt. 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, da wir die Verantwortung für das "Aufrufen" der Präsentation aus dem Anwendungsfall heraus verlagern, sodass es im Anwendungsfall nicht mehr darum geht, zu wissen, was mit den Daten zu tun ist, sondern lediglich darum, die Daten bereitzustellen. Auch in diesem Fall brechen wir die Abhängigkeitsregel immer noch nicht, da der Anwendungsfall immer noch nichts über die äußere Schicht weiß.

Der Anwendungsfall steuert jedoch nicht mehr den Moment, in dem die eigentliche Präsentation ausgeführt wird (was nützlich sein kann, um beispielsweise an diesem Punkt zusätzliche Aufgaben wie die Protokollierung auszuführen oder sie bei Bedarf ganz abzubrechen). Beachten Sie auch, dass wir den Use Case-Eingabeport verloren haben, da der Controller jetzt nur die Methode getData() verwendet (die unser neuer Ausgabeport ist). Außerdem scheint es mir, dass wir hier gegen das "Tell, Don't Ask" -Prinzip verstoßen, weil wir den Interaktor bitten, dass einige Daten etwas damit anfangen, anstatt ihm zu sagen, dass er das eigentliche Ding im tun soll erster Platz.

Auf den Punkt

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

53
swahnee

Die Clean Architecture schlägt vor, dass ein Anwendungsfall-Interaktor die tatsächliche Implementierung des Präsentators (der nach dem DIP injiziert wird) aufruft, um die Antwort/Anzeige zu verarbeiten. Ich sehe jedoch Leute, die diese Architektur implementieren, die Ausgabedaten vom Interaktor zurückgeben und dann den Controller (in der Adapterschicht) entscheiden lassen, wie damit umgegangen werden soll es.

Das ist sicherlich nicht sauber , Zwiebel oder sechseckig Architektur. Das ist this :

(enter image description here

Nicht dass MVC so gemacht werden muss

(enter image description here

Sie können auf viele verschiedene Arten zwischen Modulen kommunizieren und es MVC nennen . Wenn Sie mir sagen, dass MVC verwendet wird, kann ich nicht wirklich sagen, wie die Komponenten kommunizieren. Das ist nicht standardisiert. Es sagt mir nur, dass es mindestens drei Komponenten gibt, die sich auf ihre drei Verantwortlichkeiten konzentrieren.

Einige dieser Möglichkeiten wurden angegeben nterschiedliche Namen : enter image description here

Und jeder von diesen kann zu Recht als MVC bezeichnet werden.

Wie auch immer, keiner von ihnen erfasst wirklich, was die Schlagwortarchitekturen (Clean, Onion und Hex) von Ihnen verlangen.

(enter image description here

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

(enter image description here

Eine Sache, die hier klar sein sollte, ist, dass das Antwortmodell nicht durch den Controller marschiert.

Wenn Sie Adleraugen haben, haben Sie vielleicht bemerkt, dass nur die Schlagwortarchitekturen zirkuläre Abhängigkeiten vollständig vermeiden. Wichtig ist, dass sich die Auswirkungen einer Codeänderung nicht durch das Durchlaufen von Komponenten 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 verläuft. Mehr dazu und diese "weißen" Pfeilspitzen später.

Verliert die zweite Lösung die Anwendungsverantwortung aus der Anwendungsschicht und definiert die Eingabe- und Ausgabeports für den Interaktor nicht klar?

Da die Kommunikation vom Controller zum Presenter über die "Anwendungsschicht" der Anwendung erfolgen soll, ist es wahrscheinlich ein Leck, den Controller dazu zu bringen, einen Teil des Presenters-Jobs auszuführen. Dies ist meine Hauptkritik an VIPER-Architektur .

Warum das Trennen dieser so wichtig ist, lässt sich wahrscheinlich am besten anhand von Command Query Responsibility Segregation verstehen.

Eingangs- und Ausgangsanschlüsse

Angesichts der Definition der sauberen Architektur und insbesondere des kleinen Flussdiagramms, das die Beziehungen zwischen einem Controller, einem Anwendungsfall-Interaktor und einem Präsentator beschreibt, bin ich mir nicht sicher, ob ich den "Anwendungsfall-Ausgabeport" richtig verstehe.

Es ist die API, über die Sie die Ausgabe für diesen speziellen Anwendungsfall senden. Es ist nicht mehr als das. Der Interaktor für diesen Anwendungsfall muss nicht wissen oder möchte wissen, ob die Ausgabe an eine GUI, eine CLI, ein Protokoll oder einen Audio-Lautsprecher erfolgt. Alles, was der Interaktor wissen muss, ist die einfachste API, mit der er die Ergebnisse seiner Arbeit melden kann.

Eine saubere Architektur unterscheidet wie eine hexagonale Architektur zwischen primären Ports (Methoden) und sekundären Ports (Schnittstellen, die von Adaptern implementiert werden sollen). Nach dem Kommunikationsfluss erwarte ich, dass der "Use Case Input Port" ein primärer Port (also nur eine Methode) und der "Use Case Output Port" eine zu implementierende Schnittstelle ist, möglicherweise ein Konstruktorargument, das den tatsächlichen Adapter verwendet. damit der Interaktor es verwenden kann.

Der Grund, warum sich der Ausgabeport vom Eingabeport unterscheidet, ist, dass er nicht von der Ebene besessen werden darf, die er abstrahiert. Das heißt, die Ebene, die abstrahiert wird, darf keine Änderungen diktieren. Nur die Anwendungsschicht und ihr Autor sollten entscheiden, dass sich der Ausgabeport ändern kann.

Dies steht im Gegensatz zu dem Eingabeport, der der zu abstrahierenden Ebene gehört. 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 beliebige innere Schicht überhaupt nichts über die äußeren Schichten weiß.


Auf dem Interaktor, der den Moderator anruft

Die vorherige Interpretation scheint durch das oben erwähnte Diagramm selbst bestätigt zu werden, in dem die Beziehung zwischen der Steuerung und dem Eingangsport durch einen durchgezogenen Pfeil mit einem "scharfen" Kopf dargestellt wird (UML für "Assoziation", was "hat ein" bedeutet, wobei die Controller "hat einen" Anwendungsfall), während die Beziehung zwischen dem Präsentator und dem Ausgabeport durch einen durchgezogenen Pfeil mit einem "weißen" Kopf dargestellt wird (UML für "Vererbung", was nicht die 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:

(enter image description here

Sie können den Kontrollfluss in die entgegengesetzte Richtung der Abhängigkeit gehen lassen! Das bedeutet, dass die innere Schicht nichts über die äußere Schicht wissen muss und Sie dennoch in die innere Schicht eintauchen und wieder herauskommen können!

Dies hat nichts mit der Verwendung des Schlüsselworts "interface" zu tun. Sie können dies mit einer abstrakten Klasse tun. Du könntest es mit einer (ick) konkreten Klasse machen, solange es 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 an dir?.

Warum es so wichtig ist, die Richtung dieser Abhängigkeit umzukehren, lässt sich anhand des Dependency Inversion Principle lernen. Ich habe dieses Prinzip auf diese Diagramme abgebildet hier .

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. Jetzt sehe ich, dass der Zweck der Presenter-Oberfläche abstrakt genug ist, um verschiedene Arten von Präsentatoren (GUI, Web, CLI usw.) darzustellen, und dass dies wirklich nur "Ausgabe" bedeutet, was ein Anwendungsfall ist Vielleicht sehr gut, aber ich bin immer noch nicht ganz sicher.

Nein, das ist es wirklich. Um sicherzustellen, dass die inneren Schichten nichts über die äußeren Schichten wissen, können wir die äußeren Schichten entfernen, ersetzen oder umgestalten, um sicherzugehen, dass dadurch nichts in den inneren Schichten beschädigt wird. Was sie nicht wissen, wird ihnen nicht schaden. Wenn wir das können, können wir die äußeren nach Belieben ändern.

Wenn ich mich jetzt im Web nach Anwendungen der sauberen Architektur umsehe, finde ich anscheinend nur Leute, die den Ausgabeport als eine Methode interpretieren, die DTO zurückgibt. 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, da wir die Verantwortung für das "Aufrufen" der Präsentation aus dem Anwendungsfall heraus verlagern, sodass es im Anwendungsfall nicht mehr darum geht, zu wissen, was mit den Daten zu tun ist, sondern lediglich darum, die Daten bereitzustellen. Auch in diesem Fall brechen wir die Abhängigkeitsregel immer noch nicht, da der Anwendungsfall immer noch nichts über die äußere Schicht weiß.

Das Problem hier ist nun, dass alles, was weiß, wie man nach den Daten fragt, auch das sein muss, was die Daten akzeptiert. Bevor der Controller den Usecase Interactor anrufen konnte, wusste er glücklicherweise nicht, wie das Antwortmodell aussehen würde, wohin es gehen sollte und wie es präsentiert werden sollte.

Bitte studieren Sie erneut Command Query Responsibility Segregation , um zu sehen, warum dies wichtig ist.

Der Anwendungsfall steuert jedoch nicht mehr den Moment, in dem die eigentliche Präsentation ausgeführt wird (was nützlich sein kann, um beispielsweise an diesem Punkt zusätzliche Aufgaben wie die Protokollierung auszuführen oder sie bei Bedarf ganz abzubrechen). Beachten Sie auch, dass wir den Use Case-Eingabeport verloren haben, da der Controller jetzt nur die Methode getData () verwendet (die unser neuer Ausgabeport ist). Außerdem scheint es mir, dass wir hier gegen das "Tell, Don't Ask" -Prinzip verstoßen, weil wir den Interaktor bitten, einige Daten zu verwenden, um etwas damit zu tun, anstatt ihm zu sagen, dass er das eigentliche in der erster Platz.

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

Auf den Punkt

Ist eine dieser beiden Alternativen die "richtige" Interpretation des Use-Case-Ausgangsports gemäß der Clean Architecture? Sind sie beide lebensfähig?

Alles was funktioniert ist lebensfähig. Aber ich würde nicht sagen, dass die zweite Option, die Sie vorgestellt haben, Clean Architecture folgt. Es könnte etwas sein, das funktioniert. Aber es ist nicht das, was Clean Architecture verlangt.

58
candied_orange

In einer Diskussion zu Ihrer Frage erklärt Onkel Bob den Zweck des Präsentators 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 Variable $ response vom Interaktor erstellt Dies koppelt den Interaktor mit der Ansicht. Angenommen, eines der Felder im $ response-Objekt ist ein Datum. Dieses Feld wäre ein binäres Datumsobjekt, das auf viele verschiedene Arten gerendert werden könnte Datumsformate. Sie möchten ein ganz bestimmtes Datumsformat, z. B. TT/MM/JJJJ. Wessen Verantwortung liegt es, das Format zu erstellen? Wenn der Interaktor dieses Format erstellt, weiß er zu viel über die Ansicht. Wenn die Ansicht jedoch die Binärdatei verwendet Datumsobjekt dann weiß es zu viel über den Interaktor.

"Die Aufgabe des Präsentators besteht darin, die Daten aus dem Antwortobjekt zu entnehmen und für die Ansicht zu formatieren. Weder die Ansicht noch der Interaktor kennen die Formate des anderen. "

--- Onkel Bob

(UPDATE: 31. Mai 2019)

Angesichts dieser Antwort von Onkel Bob denke ich , dass es nicht wichtig ist , ob wir Option # 1 (Interaktor Presenter verwenden lassen) ...

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 tun Option # 2 (lassen Sie den Interaktor die Antwort zurückgeben, erstellen Sie einen Präsentator innerhalb des Controllers und übergeben Sie die Antwort 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 bevorzuge Option 1 , weil ich in der Lage sein möchte, innerhalb von interactor when um Daten und Fehlermeldungen anzuzeigen, wie in diesem Beispiel unten:

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, diese if/else Zu tun, die sich auf die Präsentation innerhalb des interactor und nicht außerhalb des Interaktors beziehen.

Wenn wir andererseits Option 2 ausführen, müssten wir die Fehlermeldung (en) im Objekt response speichern und das Objekt response vom Objekt interactor an zurückgeben das controller und machen das controller parse das response Objekt ...

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, response -Daten auf Fehler innerhalb von controller zu analysieren, denn wenn wir das tun, machen wir redundante Arbeit --- wenn wir etwas in interactor ändern, wir müssen auch etwas im controller ändern.

Wenn wir uns später dazu entschließen, unser interactor wiederzuverwenden, um beispielsweise Daten über die Konsole zu präsentieren, müssen wir daran denken, alle if/else In controller von zu kopieren und einzufügen unsere Konsolen-App.

// 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 dieses if/else nur an einer Stelle : dem interactor.


Wenn Sie ASP.NET MVC (oder andere ähnliche MVC-Frameworks) verwenden, ist Option 2 der Weg einfacher.

Aber wir können in dieser Umgebung immer noch Option 1 ausfü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 Präsentator 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 Result Im Präsentator 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 oben genannte UseCase wiederverwenden und nur das Controller und Presenter fü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 im Präsentator unserer Konsolen-App NICHT public IActionResult Result HABEN)

11
Jboy Flaga

Ein Anwendungsfall kann entweder den Präsentator enthalten oder Daten zurückgeben, je nachdem, was vom Anwendungsablauf verlangt wird.

Lassen Sie uns einige 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 zum Anzeigen von Modellen in der Anwendungsschicht zugeordnet, um sie kompatibel und benutzerfreundlich zu gestalten.
  • Presenter : Während ein Controller in der Anwendungsschicht normalerweise einen Anwendungsfall aufruft, ist es ratsam, die Domäne zu delegieren, um die Modellzuordnungslogik an eine separate Klasse anzuzeigen (siehe unten) Prinzip der Einzelverantwortung), das als „Präsentator“ bezeichnet wird.

Ein Anwendungsfall mit zurückgegebenen Daten

In einem normalen Fall gibt ein Anwendungsfall einfach ein Domänenobjekt an die Anwendungsschicht zurück, das in der Anwendungsschicht weiterverarbeitet werden kann, um die Anzeige in der Benutzeroberfläche zu vereinfachen.

Da der Controller dafür verantwortlich ist, den Anwendungsfall aufzurufen, enthält er in diesem Fall auch eine Referenz des jeweiligen Präsentators, um eine Domäne zum Anzeigen der Modellzuordnung durchzuführen, bevor diese zur Ansicht zum Rendern 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

Dies ist zwar nicht üblich, aber es ist möglich, dass der Anwendungsfall den Präsentator 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 (anstelle des Controllers) vorhanden ist, wird auch die zirkuläre Abhängigkeit zwischen Controller und Anwendungsfall aufgehoben (wenn die Anwendungsfallklasse einen Verweis auf die Zuordnungslogik benötigt).

(enter image description here

Im Folgenden finden Sie eine vereinfachte Implementierung des Kontrollflusses, wie im Originalartikel dargestellt, die zeigt, wie dies durchgeführt werden kann. Bitte beachten Sie, dass UseCaseInteractor im Gegensatz zur Abbildung aus Gründen der Einfachheit 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;
        }

    }
}
2
Ashraf

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

Ist eine dieser beiden Alternativen die "richtige" Interpretation des Use-Case-Ausgangsports gemäß der Clean Architecture? Sind sie beide lebensfähig?


Zusamenfassend

Ja, beide sind realisierbar, solange beide Ansätze Inversion Of Control zwischen der Geschäftsschicht und dem Bereitstellungsmechanismus berücksichtigen. Mit dem zweiten Ansatz sind wir immer noch in der Lage, IOC) einzuführen, indem wir Beobachter, Vermittler und einige andere Entwurfsmuster verwenden ...

Onkel Bob versucht mit seiner Clean Architecture Eine Reihe bekannter Architekturen zu synthetisieren, um wichtige Konzepte und Komponenten aufzudecken, die wir weitgehend einhalten müssen, um die Prinzipien von OOP) einzuhalten.

Es wäre kontraproduktiv, sein UML-Klassendiagramm (das folgende Diagramm) als DAS einzigartige Saubere Architektur Design zu betrachten. Dieses Diagramm hätte aus Gründen von konkreten Beispielen… gezeichnet werden können. Da es jedoch weit weniger abstrakt als übliche Architekturdarstellungen ist, musste er konkrete Entscheidungen treffen, unter denen das Design des Interaktor-Ausgangsports nur ein - ist. Implementierungsdetail

(Uncle Bob's UML class diagram of Clean Architecture


Meine zwei Cent

Der Hauptgrund, warum ich es vorziehe, UseCaseResponse zurückzugeben, ist, dass dieser Ansatz meine Anwendungsfälle flexibel hält und beide Komposition zwischen ihnen und zulässt. Generizität ​​( Verallgemeinerung und spezifische Generation). 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 analog zu UML-Anwendungsfällen einschließlich/Erweiterung einander und definiert als wiederverwendbar zu verschiedenen Themen (den Entitäten) ist.


Auf dem Interaktor werden Daten zurückgegeben

Der Anwendungsfall steuert jedoch nicht mehr den Moment, in dem die eigentliche Präsentation ausgeführt wird (was nützlich sein kann, um beispielsweise 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 sollten Sie die Präsentationsleistung "kontrollieren" müssen? 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…

1
ClemC

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 tiefer in diese Diskussion einzutauchen: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

1
plainionist

Der Hauptgrund für die Verwendung eines Präsentators ist die Einzelverantwortung/Trennung von Bedenken. Die Situation mit Web-APIs ist etwas trübe, da moderne Frameworks Inhaltsverhandlungen durchführen und das Wire-Format (z. B. JSON vs XML) für Sie übernehmen.

Ich bin stark dafür, dass der Anwendungsfall den Präsentator direkt anruft, da dies bedeutet, dass das Anwendungsfall-Antwortmodell "nicht durch den Controller läuft", wie oben angegeben. Der Ansatz, bei dem der Controller Verkehrspolizist mit dem Präsentator spielt und den Status aus dem Präsentator usw. herauszieht, ist jedoch umständlich.

Wenn Sie den Präsentator zum Delegierten machen, können Sie etwas tun, das ein wenig sauberer aussieht, aber jetzt haben Sie die vom Framework bereitgestellte Inhaltsverhandlung verloren, zumindest mit diesem Demo-Code. Ich vermute, Sie können OkResult vielleicht noch mehr direkt erweitern und wieder auf die Inhaltsverhandlung zurückgreifen.

[HttpGet]
public IActionResult List()
{
    return new GridEntriesPresenter(presenter =>
       _listGridEntriesUseCase.ListAsync(presenter));
}

Dann ist GridEntriesPresenter etwas, das sich erweitert

public class ActionResultPresenter<T> : IActionResult
{
    private readonly Func<Func<T, Task>, Task> _handler;

    public ActionResultPresenter(Func<Func<T, Task>, Task> handler)
    {
        _handler = handler;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        await _handler(async responseModel =>
        {
            context.HttpContext.Response.ContentType = "application/json";
            context.HttpContext.Response.StatusCode = 200;
            await context.HttpContext.Response.StartAsync();

            await SerializeAsync(context.HttpContext.Response.Body, responseModel);

            await context.HttpContext.Response.CompleteAsync();
        });
    }

    ... 
}
public class GridEntriesPresenter : ActionResultPresenter<IEnumerable<GridEntryResponseModel>>
{
    public GridEntriesPresenter(Func<Func<IEnumerable<GridEntryResponseModel>, Task>, Task> handler) : base(handler)
    {
    }

    protected override Task SerializeAsync(Stream stream, IEnumerable<GridEntryResponseModel> responseModel)
    {
        ...
        return SerializeJsonAsync(stream, new {items, allItems, totalCount, pageCount, page, pageSize});
    }
}

Und Ihr Anwendungsfall sieht folgendermaßen aus:

public class ListGridEntriesUseCase : IListGridEntriesUseCase
{
    private readonly IActivityRollups _activityRollups;

    public ListGridEntriesUseCase(IActivityRollups activityRollups)
    {
        _activityRollups = activityRollups;
    }

    public async Task ListAsync(int skip, int take, Func<IEnumerable<GridEntryResponseModel>, Task> presentAsync)
    {
        var activityRollups = await _activityRollups.ListAsync(skip, take);
        var gridEntries = activityRollups.Select(x => new GridEntryResponseModel
        {
            ...
        });
        await presentAsync(gridEntries);
    }
}

Aber jetzt benutzt du das umständliche Func<T> Syntax (aber IDE Unterstützung hilft hier, zumindest wenn Sie Rider verwenden), und Ihr Controller sieht nur sauber aus, weil er nicht explizit Typen deklariert, was möglicherweise Betrug ist.

0
kayjtea