La arquitectura limpia sugiere dejar que un caso de uso interactor llamar a la aplicación real de la presentadora (que se inyecta, siguiendo el DIP) para manejar la respuesta / visualización. Sin embargo, veo personas implementando esta arquitectura, devolviendo los datos de salida del interactor y luego dejo que el controlador (en la capa del adaptador) decida cómo manejarlo. ¿La segunda solución está filtrando las responsabilidades de la aplicación fuera de la capa de aplicación, además de no definir claramente los puertos de entrada y salida para el interactor?
Puertos de entrada y salida
Teniendo en cuenta la definición de Arquitectura limpia , y especialmente el pequeño diagrama de flujo que describe las relaciones entre un controlador, un interactor de casos de uso y un presentador, no estoy seguro si entiendo correctamente cuál debería ser el "Puerto de salida de caso de uso".
La arquitectura limpia, como la arquitectura hexagonal, distingue entre puertos primarios (métodos) y puertos secundarios (interfaces que se implementarán mediante adaptadores). Siguiendo el flujo de comunicación, espero que el "Puerto de entrada de caso de uso" sea un puerto primario (por lo tanto, solo un método), y el "Puerto de salida de caso de uso" una interfaz que se implementará, tal vez un argumento del constructor que tome el adaptador real, para que el interactor pueda usarlo.
Ejemplo de código
Para hacer un ejemplo de código, este podría ser el código del controlador:
Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();
La interfaz del presentador:
// Use Case Output Port
interface Presenter
{
public void present(Data data);
}
Finalmente, el propio interactor:
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);
}
}
En el interactor llamando al presentador
La interpretación anterior parece ser confirmada por el diagrama antes mencionado, donde la relación entre el controlador y el puerto de entrada está representada por una flecha sólida con una cabeza "aguda" (UML para "asociación", que significa "tiene un", donde el el controlador "tiene un" caso de uso), mientras que la relación entre el presentador y el puerto de salida está representada por una flecha sólida con una cabeza "blanca" (UML para "herencia", que no es la de "implementación", pero probablemente ese es el significado de todos modos).
Además, en esta respuesta a otra pregunta , Robert Martin describe exactamente un caso de uso en el que el interactor llama al presentador en una solicitud de lectura:
Al hacer clic en el mapa, se invoca el placePinController. Reúne la ubicación del clic y cualquier otro dato contextual, construye una estructura de datos placePinRequest y la pasa al PlacePinInteractor que verifica la ubicación del pin, la valida si es necesario, crea una entidad Place para registrar el pin, construye una EditPlaceReponse objeto y lo pasa al EditPlacePresenter que abre la pantalla del editor de lugares.
Para que esto funcione bien con MVC, podría pensar que la lógica de la aplicación que tradicionalmente iría al controlador, aquí se mueve al interactor, porque no queremos que ninguna lógica de la aplicación se filtre fuera de la capa de la aplicación. El controlador en la capa de adaptadores simplemente llamaría al interactor, y tal vez realice una conversión de formato de datos menor en el proceso:
El software en esta capa es un conjunto de adaptadores que convierten los datos del formato más conveniente para los casos y entidades de uso, al formato más conveniente para alguna agencia externa como la Base de Datos o la Web.
del artículo original, hablando de adaptadores de interfaz.
En el interactor que devuelve datos
Sin embargo, mi problema con este enfoque es que el caso de uso debe ocuparse de la presentación en sí. Ahora, veo que el propósito de la Presenter
interfaz es ser lo suficientemente abstracto como para representar varios tipos diferentes de presentadores (GUI, Web, CLI, etc.), y que realmente solo significa "salida", que es algo que un caso de uso podría muy bien, pero aún no estoy totalmente seguro de ello.
Ahora, buscando en la Web aplicaciones de la arquitectura limpia, parece que solo encuentro personas que interpretan el puerto de salida como un método que devuelve algo de DTO. Esto sería algo como:
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
Esto es atractivo porque estamos trasladando la responsabilidad de "llamar" a la presentación fuera del caso de uso, por lo que el caso de uso ya no se preocupa por saber qué hacer con los datos, sino simplemente por proporcionar los datos. Además, en este caso todavía no estamos rompiendo la regla de dependencia, porque el caso de uso aún no sabe nada sobre la capa externa.
Sin embargo, el caso de uso no controla el momento en que la presentación real ya no se realiza (lo que puede ser útil, por ejemplo, para hacer cosas adicionales en ese punto, como iniciar sesión o abortarla por completo si es necesario). Además, observe que perdimos el Puerto de entrada de caso de uso, porque ahora el controlador solo está utilizando el getData()
método (que es nuestro nuevo puerto de salida). Además, me parece que estamos rompiendo el principio de "decir, no preguntar" aquí, porque le estamos pidiendo al interactor algunos datos para hacer algo con él, en lugar de decirle que haga lo real en el primer lugar.
Al punto
Entonces, ¿alguna de estas dos alternativas es la interpretación "correcta" del puerto de salida del caso de uso de acuerdo con la arquitectura limpia? ¿Son ambos viables?