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' Presenter
interface 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?
Réponses:
Ce n'est certainement pas l' architecture propre , oignon ou hexagonal . C'est ça :
Pas que MVC doit être fait de cette façon
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 :
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.
Ajoutez les structures de données qui sont lancées (et retournez-les pour une raison quelconque) et vous obtenez :
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.
É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 .
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.
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.
La chose importante à propos de cette flèche "blanche" est qu'elle vous permet de faire ceci:
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 .
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.
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.
Oui! Dire, et non pas demander, aidera à garder cet objet orienté plutôt que procédural.
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.
la source
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:
Oncle Bob a dit ceci:
(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) ...
... 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) ...
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:... Je veux pouvoir faire ces choses
if/else
qui sont liées à la présentation à l'intérieurinteractor
et 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’
response
objet, renvoyer cetresponse
objet de lainteractor
à lacontroller
et effectuer l’controller
analyse de l’response
objet ...Je n'aime pas analyser les
response
données à la recherche d'erreurs à l'intérieur du,controller
car si nous le faisons, nous faisons un travail redondant - si nous changeons quelque chose dans leinteractor
, nous devons également changer quelque chose dans le fichiercontroller
.De plus, si nous décidons par la suite de réutiliser nos
interactor
données pour présenter des données à l'aide de la console, par exemple, nous devons nous rappeler de copier-coller toutes cellesif/else
de l'applicationcontroller
de notre console.Si nous utilisons l'option n ° 1, nous n'aurons cela
if/else
qu'à un seul endroit : le fichierinteractor
.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 Result
dans le présentateur de notre application ASP.NET MVC)(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 ce qui
UseCase
précède et créer uniquement leController
etPresenter
pour la console:(Notez que nous n'avons pas
public IActionResult Result
dans le présentateur de notre application de la console)la source
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:
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é:
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).
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.
la source
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/
la source
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 …
Mes deux centimes
La raison principale pour laquelle je préfère revenir
UseCaseResponse
est 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: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).
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…
la source