web-dev-qa-db-fra.com

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

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

Ports d'entrée et de sortie

Compte tenu de la définition de Clean Architecture , et en particulier du petit organigramme 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 le "Port de sortie de cas d'utilisation" " devrait être.

L'architecture propre, comme l'architecture hexagonale, fait la distinction entre les ports primaires (méthodes) et les ports secondaires (interfaces à implémenter par les adaptateurs). Après le flux de communication, je m'attends à ce que le "port d'entrée de cas d'utilisation" soit un port principal (donc, juste une méthode), et le "port de sortie de cas d'utilisation" une interface à implémenter, peut-être un argument 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 avec une tête "pointue" (UML pour "association", signifiant "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 celle pour "implémentation", mais probablement c'est le sens de toute façon).

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 sur une demande de lecture:

En cliquant sur la carte, le lieuPinController est appelé. Il rassemble l'emplacement du clic et toute autre donnée contextuelle, construit une structure de données placePinRequest et la transmet à PlacePinInteractor qui vérifie l'emplacement de la broche, le valide si nécessaire, crée une entité Place pour enregistrer la broche, construit une EditPlaceReponse et le transmet à EditPlacePresenter qui affiche l'écran de l'éditeur de lieu.

Pour que cela fonctionne bien avec MVC, je pourrais penser que la logique d'application qui, traditionnellement, irait dans le contrôleur, est déplacée ici vers l'interacteur, car nous ne voulons pas qu'une logique d'application fuit en dehors de la couche d'application. Le contrôleur de la couche adaptateurs appelle simplement l'interacteur et peut-être effectuer une conversion de format de données mineure dans le 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 une agence externe telle que la base de données ou le Web.

de l'article d'origine, en parlant d'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'interface Presenter est d'être suffisamment abstrait pour représenter plusieurs types différents de présentateurs (GUI, Web, CLI, etc.), et qu'il signifie vraiment juste "sortie", ce qui est quelque chose qu'un cas d'utilisation pourrait très bien avoir, mais je ne suis pas totalement sûr de lui.

Maintenant, en parcourant le Web pour trouver des applications de l'architecture propre, il semble que je ne trouve que des gens qui interprètent le port de sortie comme une méthode renvoyant 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

Ceci est intéressant car nous déplaçons la responsabilité d '"appeler" la présentation hors du cas d'utilisation, de sorte que le cas d'utilisation ne se préoccupe plus de savoir quoi faire des données, plutôt simplement de fournir les données. De plus, dans ce cas, nous n'enfreignons toujours pas la règle de dépendance, car le cas d'utilisation ne sait toujours rien de 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 pour faire des choses supplémentaires à ce stade, comme la journalisation, ou pour l'interrompre complètement si nécessaire). Notez également que nous avons perdu le port d'entrée du cas d'utilisation, car maintenant le contrôleur utilise uniquement la méthode getData() (qui est notre nouveau port de sortie). De plus, il me semble que nous ne respectons pas le principe "dites, ne demandez pas" ici, parce que nous demandons à l'interacteur des données pour en faire quelque chose, plutôt que de lui dire 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 les deux viables?

53
swahnee

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é, après le DIP) pour gérer la réponse/l'affichage. Cependant, je vois des gens implémenter cette architecture, renvoyant les données de sortie de l'interacteur, puis laisser le contrôleur (dans la couche adaptateur) décider comment gérer il.

Ce n'est certainement pas Clean , Onion , ou Hexagonal Architecture. C'est ceci :

enter image description here

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

enter image description here

Vous pouvez utiliser de nombreuses façons différentes 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 standardisé. Tout ce que cela me dit, c'est qu'il y a au moins trois composantes axées sur leurs trois responsabilités.

Certaines de ces façons ont été données noms différents : enter image description here

Et chacun de ceux-ci peut à juste titre être appelé MVC.

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

enter image description here

Ajoutez les structures de données projetées (et retournez-les à l'envers pour une raison quelconque) et vous obtenez :

enter image description here

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

Si vous êtes aigle, vous avez peut-être remarqué que seules les architectures de mots à la mode évitent complètement dépendances circulaires . Plus important encore, cela signifie que l'impact d'un changement de code ne se propagera pas en parcourant les composants. Le changement s'arrêtera lorsqu'il frappera du code qui ne s'en souciera pas.

Je me demande s'ils l'ont renversé pour que le flux de contrôle passe dans le sens horaire. Plus sur cela, et ces pointes de flèches "blanches", plus tard.

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

Étant donné que la communication du contrôleur au présentateur est censée passer par la "couche" de l'application, alors oui, faire en sorte que le contrôleur fasse partie du travail du présentateur est probablement une fuite. Ceci est ma principale critique de architecture VIPER .

Pourquoi séparer ceux-ci est si important pourrait probablement être mieux compris en étudiant Command Query Responsibility Segregation .

Ports d'entrée et de sortie

Compte tenu de la définition de l'architecture propre, et en particulier du petit organigramme 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. Ce n'est pas plus que ça. L'interaction de ce cas d'utilisation n'a pas besoin de savoir, ni de vouloir savoir, si la sortie va vers une interface graphique, une CLI, 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 rapporter les résultats de son travail.

L'architecture propre, comme l'architecture hexagonale, fait la distinction entre les ports primaires (méthodes) et les ports secondaires (interfaces à implémenter par les adaptateurs). Après le flux de communication, je m'attends à ce que le "port d'entrée de cas d'utilisation" soit un port principal (donc, juste une méthode), et le "port de sortie de cas d'utilisation" une interface à implémenter, peut-être un argument 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 appartenir à la couche qu'il résume. Autrement dit, la couche qu'il résume ne doit pas être autorisée à lui dicter des modifications. Seule la couche application et son auteur doivent décider que le port de sortie peut changer.

Cela contraste avec le port d'entrée qui appartient à la couche qu'il résume. 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 du tout sur les 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 avec une tête "pointue" (UML pour "association", signifiant "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 celle pour "implémentation", mais probablement c'est le sens de toute façon).

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

enter image description here

Vous pouvez laisser le flux de contrôle aller dans le sens inverse de la dépendance! Cela signifie que la couche intérieure n'a pas à connaître la couche extérieure et pourtant vous pouvez plonger dans la couche intérieure et en ressortir!

Faire 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 concrète (ick) tant qu'elle peut être étendue. C'est tout 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 ne demande que du polymorphisme. Quel genre vous appartient.

Pourquoi inverser le sens de cette dépendance est si important peut être appris en étudiant le Dependency Inversion Principle . 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 différents de présentateurs (GUI, Web, CLI, etc.), et qu'il signifie vraiment juste "sortie", ce qui est quelque chose d'un cas d'utilisation pourrait très bien avoir, mais je ne suis toujours pas totalement confiant avec elle.

Non, c'est vraiment ça. Le fait de s'assurer que les couches internes ne connaissent pas les couches externes est que nous pouvons supprimer, remplacer ou remanier les couches externes en étant sûr que cela ne cassera rien dans les couches internes. Ce qu'ils ignorent ne leur fera pas de mal. Si nous pouvons le faire, nous pouvons changer les extérieurs en ce que nous voulons.

Maintenant, en parcourant le Web pour trouver des applications de l'architecture propre, il semble que je ne trouve que des gens qui interprètent le port de sortie comme une méthode renvoyant 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

Ceci est intéressant car nous déplaçons la responsabilité d '"appeler" la présentation hors du cas d'utilisation, de sorte que le cas d'utilisation ne se préoccupe plus de savoir quoi faire des données, plutôt simplement de fournir les données. De plus, dans ce cas, nous n'enfreignons toujours pas la règle de dépendance, car le cas d'utilisation ne sait toujours rien de la couche externe.

Le problème ici est maintenant ce qui sait comment demander les données doit également être la chose qui accepte les données. Avant que le contrôleur puisse appeler l'interprète Usecase, ignorant à quoi ressemblerait le modèle de réponse, où il devrait aller et, hé, comment le présenter.

Encore une fois, veuillez étudier Ségrégation de responsabilité des requêtes de commande pour voir 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 pour faire des choses supplémentaires à ce stade, comme la journalisation, ou pour l'interrompre complètement si nécessaire). Notez également que nous avons perdu le port d'entrée du cas d'utilisation, car maintenant le contrôleur utilise uniquement la méthode getData () (qui est notre nouveau port de sortie). De plus, il me semble que nous ne respectons pas le principe "dites, ne demandez pas" ici, parce que nous demandons à l'interacteur des données pour en faire quelque chose, plutôt que de lui dire de faire la chose réelle dans le première place.

Oui! Dire, 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 les 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.

58
candied_orange

Dans une discussion liée à votre question , l'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()
        );
    }
}

L'oncle Bob a dit ceci:

" L'objectif du présentateur est de dissocier les cas d'utilisation du format de l'interface utilisateur. Dans votre exemple, la variable $ response est créée par l'interaction , mais est utilisé par la vue. Ceci associe l'interacteur à la vue. Par exemple, disons que l'un des champs de l'objet $ response est une date. Ce champ serait un objet de date binaire qui pourrait être rendu dans de nombreux formats de date. Le veut un format de date très spécifique, peut-être JJ/MM/AAAA. A qui revient la responsabilité de créer le format? Si l'interacteur crée ce format, alors il en sait trop sur la Vue. Mais si la vue prend le binaire objet date alors il en sait trop sur l'interacteur.

"Le travail du présentateur consiste à prendre les données de l'objet de réponse et à les formater pour la vue. Ni la vue ni l'interaction ne connaissent les formats des autres. "

--- Oncle Bob

(MISE À JOUR: 31 mai 2019)

Compte tenu de la réponse de l'oncle Bob, je pense peu importe que nous le fassions option # 1 (laissez l'interacteur 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 option # 2 (laissez l'interacteur retourner la réponse, créer un présentateur à l'intérieur du contrôleur, puis passer 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 à l'intérieur du interactor quand pour afficher les données et les messages d'erreur, comme cet 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 if/else qui sont liés à la présentation à l'intérieur de interactor et non à l'extérieur de l'interacteur.

Si d'autre part nous faisons l'option # 2, nous devons stocker le (s) message (s) d'erreur dans l'objet response, renvoyer cet objet response de interactor à controller, et faites analyser controller l'objet response ...

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 données response pour les erreurs à l'intérieur du controller parce que si nous faisons cela, nous faisons un travail redondant --- si nous changeons quelque chose dans le interactor, nous doivent également changer quelque chose dans le controller.

De plus, si nous décidons plus tard de réutiliser notre interactor pour présenter des données à l'aide de la console, par exemple, nous devons nous rappeler de copier-coller tous ces if/else dans le controller de notre application 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 # 1, nous aurons cette if/else en un seul endroit : le interactor.


Si vous utilisez ASP.NET MVC (ou d'autres frameworks MVC similaires), l'option # 2 est la solution plus facile.

Mais nous pouvons toujours faire l'option n ° 1 dans ce type d'environnement. Voici un exemple de réalisation de l'option # 1 dans ASP.NET MVC:

(Notez que nous devons avoir public IActionResult Result dans 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 Result dans 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 le UseCase ci-dessus et créer uniquement le Controller et Presenter pour 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 Result dans le présentateur de notre application console)

11
Jboy Flaga

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

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

  • Objet de domaine : un objet de domaine est le conteneur de données dans la couche de domaine sur lequel les opérations de logique métier sont effectuées.
  • Afficher 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.
  • Presenter : Bien qu'un contrôleur dans la couche application invoque généralement un cas d'utilisation, mais il est conseillé de déléguer le domaine pour afficher la logique de mappage de modèle dans une classe distincte (suivant Principe de responsabilité unique), qui est appelé "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 pour le rendre convivial à afficher dans l'interface utilisateur.

Comme le contrôleur est responsable d'appeler le cas d'utilisation, dans ce cas, il contient également une référence du présentateur respectif pour conduire le domaine afin de visualiser le mappage de modèle avant de l'envoyer à la vue à rendre.

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 un présentateur

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é en temps d'exécution via l'injection de dépendances).

Le fait d'avoir le domaine pour afficher la logique de mappage de modèle dans une classe distincte (au lieu de l'intérieur du contrôleur) rompt également la dépendance circulaire entre le contrôleur et le cas d'utilisation (lorsqu'une référence à la logique de mappage est requise par la classe de cas d'utilisation).

enter image description here

Vous trouverez ci-dessous une implémentation simplifiée du flux de contrôle, comme illustré dans l'article d'origine, qui montre comment cela peut être fait. Veuillez noter que contrairement à ce qui est montré dans le diagramme, pour des raisons 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;
        }

    }
}
2
Ashraf

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 les deux viables?


En bref

Oui, ils sont tous deux viables tant que les deux approches prennent en compte l'inversion de contrôle entre la couche métier et le mécanisme de livraison. Avec la deuxième approche, nous sommes toujours en mesure d'introduire IOC en utilisant l'observateur, le médiateur, quelques autres modèles de conception ...

Avec son Clean Architecture, la tentative d'oncle Bob est de synthétiser un tas d'architectures connues pour révéler des concepts et des composants importants pour que nous puissions nous conformer largement aux principes de OOP.

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

Uncle Bob's UML class diagram of Clean Architecture


Mes deux centimes

La principale raison pour laquelle je préfère retourner le UseCaseResponse est que cette approche conserve mes cas d'utilisation flexible, permettant à la fois la composition entre eux et 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 de façon analogue plus proche des cas d'utilisation UML y compris/étendant les uns des autres 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 pour faire des choses supplémentaires à ce stade, comme la journalisation, ou pour l'interrompre complètement si nécessaire).

Vous n'êtes pas sûr de comprendre ce que vous voulez dire par là, pourquoi auriez-vous besoin de "contrôler" la présentation des informations? Ne le contrôlez-vous pas tant que vous ne renvoyez pas la réponse du cas d'utilisation?

Le cas d'utilisation peut renvoyer dans sa réponse un code d'état pour faire savoir à la couche client ce qui s'est passé exactement pendant son fonctionnement. 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…

1
ClemC

Même si je suis généralement d'accord avec la réponse de @CandiedOrange, je verrais également un avantage dans l'approche selon laquelle l'interacteur retombe simplement les données qui sont ensuite transmises par le contrôleur au présentateur.

C'est par exemple un moyen simple d'utiliser les idées de l'architecture propre (règle de dépendance) dans le contexte d'Asp.Net MVC.

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

1
plainionist

La principale raison d'utiliser un présentateur est la responsabilité unique/la séparation des préoccupations. La situation avec les API Web est un peu trouble car les cadres modernes feront la négociation de contenu et prendront soin du format de fil (disons JSON vs XML) pour vous.

Je suis fortement en faveur du cas d'utilisation appelant directement le présentateur, car cela signifie que le modèle de réponse du cas d'utilisation "ne passe pas par le contrôleur" comme indiqué ci-dessus. Cependant, l'approche où le contrôleur joue le flic du trafic avec le présentateur et tire l'état du présentateur, etc., est maladroite.

Si vous faites du présentateur un délégué, vous pouvez faire quelque chose qui semble un peu plus propre, mais maintenant vous avez perdu la négociation de contenu fournie par le framework, au moins avec ce code de démonstration. Je soupçonne que vous pouvez faire plus en étendant OkResult directement et en recourant à la négociation de contenu.

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

Ensuite, GridEntriesPresenter est alors quelque chose qui étend

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

Et votre cas d'utilisation ressemble à ceci:

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

Mais maintenant, vous utilisez le maladroit Func<T> syntaxe (mais IDE vous aidera ici, au moins si vous utilisez Rider), et votre contrôleur ne semble propre que parce qu'il ne déclare pas explicitement les types, ce qui est peut-être de la triche.

0
kayjtea