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?