Architecture propre: cas d'utilisation contenant le présentateur ou renvoyant des données?

42

L' architecture propre suggère de laisser un interacteur de cas d'utilisation appeler l'implémentation réelle du présentateur (qui est injectée, à la suite du DIP) pour gérer la réponse / l'affichage. Cependant, je vois des personnes implémenter cette architecture, renvoyer les données de sortie de l'interacteur, puis laisser le contrôleur (dans la couche adaptateur) décider comment le gérer. La deuxième solution laisse-t-elle échapper les responsabilités des applications hors de la couche d’application, en plus de ne pas définir clairement les ports d’entrée et de sortie vers l’interacteur?

Ports d'entrée et de sortie

Compte tenu de la définition de l' architecture propre , et en particulier du petit diagramme de flux décrivant les relations entre un contrôleur, un interacteur de cas d'utilisation et un présentateur, je ne suis pas sûr de bien comprendre ce que devrait être le "port de sortie du cas d'utilisation".

L’architecture propre, comme l’architecture hexagonale, fait la distinction entre les ports principaux (méthodes) et les ports secondaires (interfaces à implémenter par des adaptateurs). Après le flux de communication, je m'attends à ce que le "Port d'entrée du cas d'utilisation" soit un port principal (donc une méthode) et que le "Port de sortie du cas d'utilisation" soit une interface à implémenter, peut-être un argument de constructeur prenant l'adaptateur réel, afin que l'interacteur puisse l'utiliser.

Exemple de code

Pour faire un exemple de code, cela pourrait être le code du contrôleur:

Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();

L'interface du présentateur:

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

Enfin, l'interacteur lui-même:

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);
    }
}

Sur l'interacteur appelant le présentateur

L’interprétation précédente semble être confirmée par le diagramme susmentionné lui-même, où la relation entre le contrôleur et le port d’entrée est représentée par une flèche pleine dotée d’une tête "tranchante" (UML pour "association", ce qui signifie "a un", où le contrôleur "a un" cas d'utilisation), tandis que la relation entre le présentateur et le port de sortie est représentée par une flèche pleine avec une tête "blanche" (UML pour "héritage", qui n'est pas celui pour "implémentation", mais probablement c'est le sens quand même).

De plus, dans cette réponse à une autre question , Robert Martin décrit exactement un cas d'utilisation où l'interacteur appelle le présentateur à la suite d'une demande de lecture:

En cliquant sur la carte, placePinController est appelé. Il rassemble l'emplacement du clic et toutes les autres données contextuelles, construit une structure de données placePinRequest et la transmet à PlacePinInteractor qui vérifie l'emplacement de la broche, la valide si nécessaire, crée une entité Place pour enregistrer la broche, construit une EditPlaceReponse objet et le transmet à EditPlacePresenter qui ouvre l'écran d'édition de lieu.

Pour que cela fonctionne bien avec MVC, je pourrais penser que la logique applicative qui irait traditionnellement dans le contrôleur est déplacée ici vers l’interacteur, car nous ne voulons pas que la logique applicative fuit en dehors de la couche applicative. Le contrôleur de la couche adaptateurs appelle simplement l'interacteur, et effectue peut-être une conversion mineure du format de données au cours du processus:

Le logiciel de cette couche est un ensemble d’adaptateurs qui convertissent les données du format le plus pratique pour les cas d’utilisation et les entités au format le plus pratique pour certaines agences externes telles que la base de données ou le Web.

de l'article original, parle des adaptateurs d'interface.

Sur l'interacteur renvoyant des données

Cependant, mon problème avec cette approche est que le cas d'utilisation doit prendre en charge la présentation elle-même. Maintenant, je vois que le but de l' Presenterinterface est d'être assez abstrait pour représenter plusieurs types de présentateurs différents (interface graphique, Web, CLI, etc.), et que cela signifie simplement "sortie", ce qui pourrait être un cas d'utilisation. très bien avoir, mais je ne suis toujours pas totalement confiant avec cela.

Maintenant, en cherchant sur le Web des applications d’architecture propre, il semble que je ne trouve que des personnes qui interprètent le port de sortie comme une méthode permettant de renvoyer du DTO. Ce serait quelque chose comme:

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

C’est attrayant parce que nous nous éloignons de la responsabilité d’appeler la présentation hors du cas d’utilisation. Le cas d’utilisation ne se préoccupe donc plus de savoir quoi faire des données, mais simplement de fournir les données. De plus, dans ce cas, nous ne violons toujours pas la règle de dépendance, car le cas d'utilisation ne sait toujours rien sur la couche externe.

Cependant, le cas d'utilisation ne contrôle plus le moment où la présentation réelle est effectuée (ce qui peut être utile, par exemple, d'effectuer des tâches supplémentaires à ce moment-là, comme la journalisation, ou de l'annuler complètement si nécessaire). Notez également que nous avons perdu le port d'entrée de cas d'utilisation, car à présent, le contrôleur utilise uniquement la getData()méthode (notre nouveau port de sortie). De plus, il me semble que nous enfreignons ici le principe «dis, ne demande pas», parce que nous demandons à l'interacteur des données pour en faire quelque chose, plutôt que de lui demander de faire la chose réelle dans le première place.

Jusqu'au point

Ainsi, l’une de ces deux alternatives est-elle l’interprétation "correcte" du port de sortie du cas d’utilisation selon l’architecture propre? Sont-ils tous deux viables?

swahnee
la source
3
Les renvois croisés sont fortement déconseillés. Si c'est là que vous voulez que votre question évolue, supprimez-la de Stack Overflow.
Robert Harvey

Réponses:

48

L'architecture propre suggère de laisser un interacteur de cas d'utilisation appeler l'implémentation réelle du présentateur (qui est injectée, à la suite du DIP) pour gérer la réponse / l'affichage. Cependant, je vois des personnes implémenter cette architecture, renvoyer les données de sortie de l'interacteur, puis laisser le contrôleur (dans la couche adaptateur) décider comment le gérer.

Ce n'est certainement pas l' architecture propre , oignon ou hexagonal . C'est ça :

entrez la description de l'image ici

Pas que MVC doit être fait de cette façon

entrez la description de l'image ici

Vous pouvez utiliser différentes méthodes pour communiquer entre les modules et l'appeler MVC . Me dire que quelque chose utilise MVC ne me dit pas vraiment comment les composants communiquent. Ce n'est pas normalisé. Tout ce que cela me dit, c'est qu'il y a au moins trois composantes centrées sur leurs trois responsabilités.

Certains de ces moyens ont reçu des noms différents : entrez la description de l'image ici

Et chacun d'entre eux peut à juste titre être appelé MVC.

Quoi qu'il en soit, aucun de ceux-ci ne capture vraiment ce que les architectures à la mode (Clean, Onion et Hex) vous demandent tous de faire.

entrez la description de l'image ici

Ajoutez les structures de données qui sont lancées (et retournez-les pour une raison quelconque) et vous obtenez :

entrez la description de l'image ici

Une chose qui devrait être claire ici est que le modèle de réponse ne va pas à travers le contrôleur.

Si vous êtes dans le mille, vous avez peut-être remarqué que seules les architectures à mots à la mode évitent complètement les dépendances circulaires . Fait important, cela signifie que l'impact d'un changement de code ne se diffusera pas en passant au travers des composants. Le changement s'interrompt lorsqu'il détecte un code qui ne s'en soucie pas.

Je me demande s’ils l’ont retourné pour que le flux de contrôle passe dans le sens des aiguilles d’une montre. Plus sur cela, et ces flèches "blanches", plus tard.

La deuxième solution laisse-t-elle échapper les responsabilités des applications hors de la couche d’application, en plus de ne pas définir clairement les ports d’entrée et de sortie vers l’interacteur?

Étant donné que la communication entre le contrôleur et le présentateur doit passer par la "couche" de l'application, il est probable que le fait que le contrôleur fasse partie du travail des présentateurs est une fuite. C’est ma principale critique de l’ architecture de VIPER .

Pourquoi la séparation de ces éléments est-elle si importante pourrait probablement être mieux comprise en étudiant la séparation des responsabilités en matière d’interrogation des commandes .

Ports d'entrée et de sortie

Compte tenu de la définition de Clean Architecture, et en particulier du petit diagramme de flux décrivant les relations entre un contrôleur, un interacteur de cas d'utilisation et un présentateur, je ne suis pas sûr de bien comprendre ce que devrait être le "port de sortie de cas d'utilisation".

C'est l'API par laquelle vous envoyez la sortie, pour ce cas d'utilisation particulier. C'est pas plus que ça. L'interacteur de ce cas d'utilisation n'a pas besoin de savoir, ni ne veut savoir, si la sortie est dirigée vers une interface graphique, une interface de ligne de commande, un journal ou un haut-parleur audio. Tout ce que l’interacteur doit savoir, c’est l’API la plus simple possible qui lui permettra de rendre compte des résultats de son travail.

L’architecture propre, comme l’architecture hexagonale, fait la distinction entre les ports principaux (méthodes) et les ports secondaires (interfaces à implémenter par des adaptateurs). Après le flux de communication, je m'attends à ce que le "Port d'entrée du cas d'utilisation" soit un port principal (donc une méthode) et que le "Port de sortie du cas d'utilisation" soit une interface à implémenter, peut-être un argument de constructeur prenant l'adaptateur réel, afin que l'interacteur puisse l'utiliser.

La raison pour laquelle le port de sortie est différent du port d'entrée est qu'il ne doit pas être OWNED par la couche qu'il abstrait. En d’autres termes, il ne faut pas que la couche qu’elle résume lui dicte des modifications. Seule la couche d'application et son auteur doivent décider que le port de sortie peut changer.

Ceci est en contraste avec le port d'entrée qui appartient à la couche abstraite. Seul l'auteur de la couche d'application doit décider si son port d'entrée doit changer.

Le respect de ces règles préserve l’idée que la couche d’application, ou toute couche interne, ne sait rien des couches externes.


Sur l'interacteur appelant le présentateur

L’interprétation précédente semble être confirmée par le diagramme susmentionné lui-même, où la relation entre le contrôleur et le port d’entrée est représentée par une flèche pleine dotée d’une tête "tranchante" (UML pour "association", ce qui signifie "a un", où le contrôleur "a un" cas d'utilisation), tandis que la relation entre le présentateur et le port de sortie est représentée par une flèche pleine avec une tête "blanche" (UML pour "héritage", qui n'est pas celui pour "implémentation", mais probablement c'est le sens quand même).

La chose importante à propos de cette flèche "blanche" est qu'elle vous permet de faire ceci:

entrez la description de l'image ici

Vous pouvez laisser le flux de contrôle aller dans la direction opposée de la dépendance! Cela signifie que la couche interne n'a pas besoin de connaître la couche externe et pourtant vous pouvez plonger dans la couche interne et en ressortir!

Cela n'a rien à voir avec l'utilisation du mot clé "interface". Vous pouvez le faire avec une classe abstraite. Heck vous pouvez le faire avec une classe de béton (ick) tant qu'il peut être étendu. Il est simplement agréable de le faire avec quelque chose qui se concentre uniquement sur la définition de l'API que Presenter doit implémenter. La flèche ouverte demande seulement un polymorphisme. Quel genre est à vous.

On peut apprendre pourquoi il est si important d’inverser le sens de cette dépendance en étudiant le principe d’inversion de dépendance . J'ai mappé ce principe sur ces diagrammes ici .

Sur l'interacteur renvoyant des données

Cependant, mon problème avec cette approche est que le cas d'utilisation doit prendre en charge la présentation elle-même. Maintenant, je vois que le but de l’interface Presenter est d’être suffisamment abstrait pour représenter plusieurs types de présentateurs (interface graphique, Web, CLI, etc.), et que cela ne signifie en réalité que "sortie", ce qui est un cas d’utilisation. pourrait très bien avoir, mais je ne suis toujours pas totalement confiant avec cela.

Non c'est vraiment ça. Le fait de s’assurer que les couches internes ne connaissent pas les couches externes c’est que nous pouvons supprimer, remplacer ou refactoriser les couches externes avec la certitude que rien ne se cassera dans les couches internes. Ce qu'ils ne savent pas ne leur fera pas de mal. Si nous pouvons faire cela, nous pouvons changer les éléments extérieurs en ce que nous voulons.

Maintenant, en cherchant sur le Web des applications d’architecture propre, il semble que je ne trouve que des personnes qui interprètent le port de sortie comme une méthode permettant de renvoyer du DTO. Ce serait quelque chose comme:

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

C’est attrayant parce que nous nous éloignons de la responsabilité d’appeler la présentation hors du cas d’utilisation. Le cas d’utilisation ne se préoccupe donc plus de savoir quoi faire des données, mais simplement de fournir les données. De plus, dans ce cas, nous ne violons toujours pas la règle de dépendance, car le cas d'utilisation ne sait toujours rien sur la couche externe.

Le problème ici est maintenant que tout ce qui sait comment demander les données doit aussi être la chose qui accepte les données. Avant que le contrôleur ne puisse appeler Usactase Interactor, il était parfaitement inconscient de ce à quoi ressemblerait le modèle de réponse, où il devrait aller et comment il devait être présenté.

Encore une fois, étudiez s'il vous plaît la séparation des responsabilités dans les requêtes de commandes pour comprendre pourquoi c'est important.

Cependant, le cas d'utilisation ne contrôle plus le moment où la présentation réelle est effectuée (ce qui peut être utile, par exemple, d'effectuer des tâches supplémentaires à ce moment-là, comme la journalisation, ou de l'annuler complètement si nécessaire). Notez également que nous avons perdu le port d'entrée du cas d'utilisation, car à présent, le contrôleur n'utilise que la méthode getData () (notre nouveau port de sortie). De plus, il me semble que nous enfreignons ici le principe «dis, ne demande pas», parce que nous demandons à l'interacteur des données pour en faire quelque chose, plutôt que de lui demander de faire la chose réelle dans le première place.

Oui! Dire, et non pas demander, aidera à garder cet objet orienté plutôt que procédural.

Jusqu'au point

Ainsi, l’une de ces deux alternatives est-elle l’interprétation "correcte" du port de sortie du cas d’utilisation selon l’architecture propre? Sont-ils tous deux viables?

Tout ce qui fonctionne est viable. Mais je ne dirais pas que la deuxième option que vous avez présentée suit fidèlement l'architecture propre. Cela pourrait être quelque chose qui fonctionne. Mais ce n'est pas ce que l'architecture propre demande.

confits_orange
la source
4
Merci d'avoir pris le temps d'écrire une explication aussi détaillée.
Swahnee
1
J'ai essayé de comprendre l'architecture propre et cette réponse a été une ressource fantastique. Très bien fait!
Nathan
Réponse géniale et détaillée .. Merci pour cela .. Pouvez-vous me donner quelques conseils (ou une explication) sur la mise à jour de l'interface graphique lors de l'exécution de UseCase, c'est-à-dire la mise à jour de la barre de progression lors du téléchargement de gros fichiers?
Ewoks
1
@Ewoks, pour répondre rapidement à votre question, vous devriez vous pencher sur le modèle Observable. Votre cas d'utilisation peut renvoyer un sujet et informer le sujet des mises à jour. Le présentateur s'abonnerait au sujet et répondrait aux notifications.
Nathan
7

Dans une discussion liée à votre question , Oncle Bob explique le but du présentateur dans son architecture propre:

Étant donné cet exemple de code:

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()
        );
    }
}

Oncle Bob a dit ceci:

" L'objectif du présentateur est de découpler les cas d'utilisation du format de l'interface utilisateur. Dans votre exemple, la variable $ response est créée par l'interacteur, mais est utilisée par la vue. Cela couple l' interface à la vue. Par exemple , disons que l’un des champs de l’objet $ response est une date, un objet de date binaire pouvant être rendu dans de nombreux formats de date différents, par exemple un format de date très spécifique, peut-être JJ / MM / AAAA. Si l’interacteur crée ce format, il en sait trop sur la vue, mais si la vue utilise l’objet de date binaire, elle en sait trop sur l’interacteur.

"Le travail du présentateur est de prendre les données de l'objet de réponse et le formater pour la vue. Ni la vue ni l'interacteur ne connaissent les formats de chacun. "

--- Oncle Bob

(MISE À JOUR: 31 mai 2019)

Compte tenu de la réponse de mon oncle Bob, je pense que peu importe que nous choisissions l' option n ° 1 (laissez Interor utiliser le présentateur) ...

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);
    }
}

... ou nous faisons l' option n ° 2 (laissez l'interacteur renvoyer la réponse, créez un présentateur à l'intérieur du contrôleur, puis transmettez la réponse au présentateur) ...

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);
    }
}

Personnellement, je préfère l'option 1 parce que je veux pouvoir contrôler le interactor moment où les données et les messages d'erreur doivent être affichés, comme dans l'exemple ci-dessous:

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);
    }
}

... Je veux pouvoir faire ces choses if/elsequi sont liées à la présentation à l'intérieur interactoret non à l'extérieur de l'interacteur.

Si par contre nous faisons l’option n ° 2, nous devrions stocker le (s) message (s) d’erreur dans l’ responseobjet, renvoyer cet responseobjet de la interactorà la controlleret effectuer l’ controller analyse de l’ responseobjet ...

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);
        }
    }
}

Je n'aime pas analyser les responsedonnées à la recherche d'erreurs à l'intérieur du, controllercar si nous le faisons, nous faisons un travail redondant - si nous changeons quelque chose dans le interactor, nous devons également changer quelque chose dans le fichier controller.

De plus, si nous décidons par la suite de réutiliser nos interactordonnées pour présenter des données à l'aide de la console, par exemple, nous devons nous rappeler de copier-coller toutes celles if/elsede l'application controllerde notre console.

// 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);
}

Si nous utilisons l'option n ° 1, nous n'aurons cela if/else qu'à un seul endroit : le fichier interactor.


Si vous utilisez ASP.NET MVC (ou d’autres frameworks similaires de MVC), l’option n ° 2 est la solution la plus simple.

Mais nous pouvons toujours utiliser l'option n ° 1 dans ce type d'environnement. Voici un exemple d'utilisation de l'option n ° 1 dans ASP.NET MVC:

(Notez que nous devons avoir public IActionResult Resultdans le présentateur de notre application ASP.NET MVC)

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);
    }
}

(Notez que nous devons avoir public IActionResult Resultdans le présentateur de notre application ASP.NET MVC)

Si nous décidons de créer une autre application pour la console, nous pouvons réutiliser ce qui UseCaseprécède et créer uniquement le Controlleret Presenterpour la console:

// 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);
    }
}

(Notez que nous n'avons pas public IActionResult Resultdans le présentateur de notre application de la console)

Jboy Flaga
la source
Merci pour la contribution. En lisant la conversation, cependant, il y a une chose que je ne comprends pas: il dit que le présentateur devrait restituer les données provenant de la réponse et que, parallèlement, la réponse ne devrait pas être créée par l'interacteur. Mais alors qui crée la réponse? Je dirais que l'interacteur devrait fournir les données au présentateur, dans le format spécifique à l'application, que le présentateur connaît, car la couche adaptateurs peut dépendre de la couche application (mais pas l'inverse).
swahnee
Je suis désolé. Cela devient peut-être déroutant parce que je n'ai pas inclus l'exemple de code tiré de la discussion. Je vais le mettre à jour pour inclure l'exemple de code.
Jboy Flaga
Oncle Bob n'a pas dit que la réponse ne devrait pas être créée par l'interacteur. La réponse sera créée par l'interacteur . Ce que dit Oncle Bob, c'est que la réponse créée par l'interacteur sera utilisée par le présentateur. Le présentateur va ensuite "le formater", mettre la réponse formatée dans un modèle de vue, puis transmettre ce modèle à la vue. <br/> C'est ce que je comprends.
Jboy Flaga
1
Cela fait plus de sens. J'avais l'impression que "view" était synonyme de "présentateur", car Clean Architecture ne mentionne ni "view" ni "viewmodel", qui, selon moi, ne sont que des concepts MVC, qui peuvent ou non être utilisés lors de la mise en œuvre d'une application. adaptateur.
swahnee
2

Un cas d'utilisation peut contenir le présentateur ou renvoyer des données, en fonction de ce qui est requis par le flux d'application.

Comprenons quelques termes avant de comprendre différents flux d’application:

  • Objet de domaine : un objet de domaine est le conteneur de données de la couche de domaine sur lequel les opérations de la logique métier sont effectuées.
  • Voir le modèle : les objets de domaine sont généralement mappés pour afficher les modèles dans la couche d'application afin de les rendre compatibles et conviviaux pour l'interface utilisateur.
  • Présentateur : Tandis qu'un contrôleur de la couche application appelle généralement un cas d'utilisation, il est conseillé de déléguer le domaine pour afficher la logique de mappage de modèle dans une classe distincte (selon le principe de responsabilité unique), appelée "Présentateur".

Un cas d'utilisation contenant des données renvoyées

Dans un cas habituel, un cas d'utilisation renvoie simplement un objet de domaine à la couche d'application, qui peut ensuite être traité dans la couche d'application afin de faciliter son affichage dans l'interface utilisateur.

Étant donné que le contrôleur est chargé d'invoquer le cas d'utilisation, il contient également une référence du présentateur respectif pour que le domaine puisse visualiser le mappage de modèle avant de l'envoyer à afficher.

Voici un exemple de code simplifié:

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;
        }

    }
}

Un cas d'utilisation contenant Presenter

Bien que cela ne soit pas courant, il est possible que le cas d'utilisation doive appeler le présentateur. Dans ce cas, au lieu de conserver la référence concrète du présentateur, il est conseillé de considérer une interface (ou classe abstraite) comme point de référence (qui devrait être initialisé au moment de l'exécution via l'injection de dépendance).

Avoir le domaine pour afficher la logique de mappage de modèle dans une classe séparée (au lieu de l'intérieur de l'automate) rompt également la dépendance circulaire entre l'automate et le cas d'utilisation (lorsque la classe de cas d'utilisation requiert une référence à la logique de mappage).

entrez la description de l'image ici

Vous trouverez ci-dessous une implémentation simplifiée du flux de contrôle, illustrée dans l'article d'origine, qui montre comment procéder. Veuillez noter que, contrairement au schéma, par souci de simplicité, UseCaseInteractor est une classe concrète.

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
la source
1

Bien que je sois généralement d’accord avec la réponse de @CandiedOrange, je verrais également un avantage dans l’approche selon laquelle l’interacteur retourne juste les données qui sont ensuite transmises au contrôleur par le contrôleur.

C'est par exemple un moyen simple d'utiliser les idées de la Clean Architecture (Dependency Rule) dans le contexte de Asp.Net MVC.

J'ai écrit un article de blog pour approfondir cette discussion: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

plaintif
la source
1

Cas d'utilisation contenant le présentateur ou renvoyant des données?

Ainsi, l’une de ces deux alternatives est-elle l’interprétation "correcte" du port de sortie du cas d’utilisation selon l’architecture propre? Sont-ils tous deux viables?


En bref

Oui, ils sont tous deux viables tant que les deux approches prennent en compte l’ inversion du contrôle entre la couche de gestion et le mécanisme de diffusion. Avec la seconde approche, nous sommes toujours en mesure de présenter la COI en faisant appel à des modèles de conception d'observateur, médiateur ...

Avec son architecture propre , Oncle Bob tente de synthétiser un ensemble d'architectures connues afin de révéler des concepts et des composants importants pour nous permettre de nous conformer largement aux principes de la programmation orientée objet.

Il serait contre-productif de considérer son diagramme de classes UML (le diagramme ci-dessous) comme LA conception unique de l' architecture propre . Ce diagramme aurait pu être dessiné à des fins d’ exemples concrets … Cependant, comme il est beaucoup moins abstrait que les représentations d’architecture habituelles, il a du faire des choix concrets parmi lesquels la conception du port de sortie de l’interacteur qui n’est qu’un détail d’implémentation

Diagramme de classes UML d'Oncle Bob sur l'architecture propre


Mes deux centimes

La raison principale pour laquelle je préfère revenir UseCaseResponseest que cette approche garde la souplesse dans mes cas d'utilisation , permettant à la fois la composition entre eux et la généricité ( généralisation et génération spécifique ). Un exemple de base:

// 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;

Notez qu’il est analogue aux cas d’utilisation UML s’incluant / se prolongeant et défini comme réutilisable sur différents sujets (les entités).


Sur l'interacteur renvoyant des données

Cependant, le cas d'utilisation ne contrôle plus le moment où la présentation réelle est effectuée (ce qui peut être utile, par exemple, d'effectuer des tâches supplémentaires à ce moment-là, comme la journalisation, ou de l'annuler complètement si nécessaire).

Pas sûr de comprendre ce que vous entendez par là, pourquoi devriez-vous "contrôler" la présentation? Ne le contrôlez-vous pas tant que vous ne renvoyez pas la réponse à un cas d'utilisation?

Le cas d'utilisation peut renvoyer dans sa réponse un code d'état pour informer la couche cliente du déroulement exact de son opération. Les codes d'état de réponse HTTP sont particulièrement bien adaptés pour décrire l'état de fonctionnement d'un cas d'utilisation…

ClemC
la source