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?
Respuestas:
Eso ciertamente no es Clean , Onion o Hexagonal Architecture. Eso es esto :
No es que MVC tenga que hacerse de esa manera
Puede usar muchas formas diferentes de comunicarse entre módulos y llamarlo MVC . Decirme que algo usa MVC realmente no me dice cómo se comunican los componentes. Eso no está estandarizado. Todo lo que me dice es que hay al menos tres componentes centrados en sus tres responsabilidades.
Algunas de esas formas han recibido diferentes nombres :
Y cada uno de ellos puede llamarse justificadamente MVC.
De todos modos, ninguno de ellos realmente capta lo que las arquitecturas de palabras de moda (Clean, Onion y Hex) te están pidiendo que hagas.
Agregue las estructuras de datos que se están volteando (y voltéelo boca abajo por alguna razón) y obtendrá :
Una cosa que debería quedar clara aquí es que el modelo de respuesta no marcha a través del controlador.
Si tienes ojo de águila, es posible que hayas notado que solo las arquitecturas de palabras de moda evitan completamente las dependencias circulares . Es importante destacar que significa que el impacto de un cambio de código no se propagará al recorrer los componentes. El cambio se detendrá cuando llegue a un código que no le importa.
Me pregunto si lo voltearon para que el flujo de control pasara en sentido horario. Más sobre eso, y estas puntas de flecha "blancas", más adelante.
Dado que la comunicación del Controlador al Presentador está destinada a pasar por la "capa" de la aplicación, entonces sí, hacer que el Controlador haga parte del trabajo de Presentadores probablemente sea una fuga. Esta es mi principal crítica de la arquitectura VIPER .
Por qué separarlos es tan importante probablemente podría entenderse mejor estudiando la segregación de responsabilidad de consulta de comando .
Es la API a través de la cual envía la salida, para este caso de uso particular. No es más que eso. El interactor para este caso de uso no necesita saber, ni querer saber, si la salida va a una GUI, una CLI, un registro o un altavoz de audio. Todo lo que el interactor necesita saber es la API más simple posible que le permitirá informar los resultados de su trabajo.
La razón por la que el puerto de salida es diferente del puerto de entrada es que no debe ser PROPIETARIO de la capa que abstrae. Es decir, no se debe permitir que la capa que abstrae le dicte cambios. Solo la capa de aplicación y su autor deben decidir que el puerto de salida puede cambiar.
Esto está en contraste con el puerto de entrada que es propiedad de la capa que abstrae. Solo el autor de la capa de aplicación debe decidir si su puerto de entrada debe cambiar.
Seguir estas reglas conserva la idea de que la capa de aplicación, o cualquier capa interna, no sabe nada sobre las capas externas.
Lo importante de esa flecha "blanca" es que te permite hacer esto:
¡Puede dejar que el flujo de control vaya en la dirección opuesta de dependencia! ¡Eso significa que la capa interna no tiene que saber sobre la capa externa y, sin embargo, puede sumergirse en la capa interna y volver a salir!
Hacer eso no tiene nada que ver con usar la palabra clave "interfaz". Podrías hacer esto con una clase abstracta. Diablos, podrías hacerlo con una clase concreta (ick) siempre que pueda extenderse. Simplemente es bueno hacerlo con algo que se centre solo en definir la API que Presenter debe implementar. La flecha abierta solo pide polimorfismo. De qué tipo depende de usted.
Por qué invertir la dirección de esa dependencia es tan importante se puede aprender al estudiar el Principio de Inversión de la Dependencia . Mapeé ese principio en estos diagramas aquí .
No, eso es todo. El punto de asegurarnos de que las capas internas no conozcan las capas externas es que podemos eliminar, reemplazar o refactorizar las capas externas confiando en que al hacerlo no se romperá nada en las capas internas. Lo que no saben no les hará daño. Si podemos hacer eso, podemos cambiar los externos a lo que queramos.
El problema aquí ahora es que lo que sepa cómo pedir los datos también debe ser lo que los acepte. Antes de que el Controlador pudiera llamar al Usecase Interactor felizmente inconsciente de cómo se vería el Modelo de Respuesta, a dónde debería ir y, heh, cómo presentarlo.
Nuevamente, estudie la segregación de responsabilidad de consulta de comandos para ver por qué eso es importante.
¡Sí! Decir, no preguntar, ayudará a mantener este objeto orientado en lugar de procedimiento.
Cualquier cosa que funcione es viable. Pero no diría que la segunda opción que presentó fielmente sigue a Clean Architecture. Puede ser algo que funcione. Pero no es lo que pide Clean Architecture.
fuente
En una discusión relacionada con su pregunta , el tío Bob explica el propósito del presentador en su Clean Architecture:
Dado este ejemplo de código:
El tío Bob dijo esto:
(ACTUALIZACIÓN: 31 de mayo de 2019)
Dada la respuesta del tío Bob, creo que no importa mucho si hacemos la opción # 1 (dejemos que interactor use el presentador) ...
... o hacemos la opción n. ° 2 (deje que el interactor devuelva la respuesta, cree un presentador dentro del controlador y luego pase la respuesta al presentador) ...
Personalmente, prefiero la opción # 1 porque yo quiero ser capaz de control dentro de la
interactor
que para mostrar los datos y mensajes de error, como en este ejemplo a continuación:... Quiero poder hacer esto
if/else
relacionado con la presentación dentrointeractor
y no fuera del interactor.Si, por otro lado, hacemos la opción n. ° 2, tendríamos que almacenar el (los) mensaje (s) de error en el
response
objeto, devolver eseresponse
objeto delinteractor
alcontroller
, y hacer que el objeto seacontroller
analizadoresponse
...No me gusta analizar
response
datos en busca de errores en el interiorcontroller
porque si lo hacemos estamos haciendo un trabajo redundante, si cambiamos algo en elinteractor
, también tenemos que cambiar algo en elcontroller
.Además, si luego decidimos reutilizar nuestros
interactor
datos actuales mediante la consola, por ejemplo, debemos recordar copiar y pegar todos los que estánif/else
en lacontroller
aplicación de nuestra consola.Si usamos la opción # 1, tendremos esto
if/else
solo en un lugar : elinteractor
.Si está utilizando ASP.NET MVC (u otros marcos MVC similares), la opción # 2 es la forma más fácil de hacerlo.
Pero aún podemos hacer la opción # 1 en ese tipo de entorno. Aquí hay un ejemplo de cómo hacer la opción # 1 en ASP.NET MVC:
(Tenga en cuenta que debemos tener
public IActionResult Result
en el presentador de nuestra aplicación ASP.NET MVC)(Tenga en cuenta que debemos tener
public IActionResult Result
en el presentador de nuestra aplicación ASP.NET MVC)Si decidimos crear otra aplicación para la consola, podemos reutilizar lo
UseCase
anterior y crear solo elController
yPresenter
para la consola:(Tenga en cuenta que NO TENEMOS
public IActionResult Result
en el presentador de nuestra aplicación de consola)fuente
Un caso de uso puede contener el presentador o los datos de retorno, depende de lo que requiera el flujo de la aplicación.
Comprendamos algunos términos antes de comprender los diferentes flujos de aplicación:
Un caso de uso que contiene datos devueltos
En un caso habitual, un caso de uso simplemente devuelve un objeto de dominio a la capa de aplicación que puede procesarse aún más en la capa de aplicación para que sea fácil de mostrar en la interfaz de usuario.
Como el controlador es responsable de invocar el caso de uso, en este caso también contiene una referencia del presentador respectivo para llevar a cabo el dominio para ver el mapeo del modelo antes de enviarlo para que se visualice.
Aquí hay un ejemplo de código simplificado:
Un presentador de caso de uso
Si bien no es común, es posible que el caso de uso deba llamar al presentador. En ese caso, en lugar de mantener la referencia concreta del presentador, es aconsejable considerar una interfaz (o clase abstracta) como punto de referencia (que debe inicializarse en tiempo de ejecución mediante inyección de dependencia).
Tener el dominio para ver la lógica de mapeo del modelo en una clase separada (en lugar de dentro del controlador) también rompe la dependencia circular entre el controlador y el caso de uso (cuando la clase de caso de uso requiere referencia a la lógica de mapeo).
A continuación se muestra una implementación simplificada del flujo de control como se ilustra en el artículo original, que demuestra cómo se puede hacer. Tenga en cuenta que, a diferencia de lo que se muestra en el diagrama, en aras de la simplicidad UseCaseInteractor es una clase concreta.
fuente
Aunque generalmente estoy de acuerdo con la respuesta de @CandiedOrange, también vería beneficios en el enfoque en el que el interactor simplemente vuelve a ejecutar los datos que luego pasa el controlador al presentador.
Esta, por ejemplo, es una manera simple de utilizar las ideas de la Arquitectura limpia (Regla de dependencia) en el contexto de Asp.Net MVC.
He escrito una publicación de blog para profundizar en esta discusión: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/
fuente
En breve
Sí, ambos son viables siempre que ambos enfoques tengan en cuenta la Inversión de control entre la capa empresarial y el mecanismo de entrega. Con el segundo enfoque, aún podemos introducir el COI utilizando patrones de diseño de observadores, mediadores y otros pocos ...
Con su Arquitectura limpia , el intento del tío Bob es sintetizar un conjunto de arquitecturas conocidas para revelar conceptos y componentes importantes para que podamos cumplir ampliamente con los principios de la OOP.
Sería contraproducente considerar su diagrama de clase UML (el diagrama a continuación) como EL diseño único de Arquitectura Limpia . Este diagrama podría haberse dibujado en aras de ejemplos concretos ... Sin embargo, dado que es mucho menos abstracto que las representaciones de arquitectura habituales, tuvo que tomar decisiones concretas entre las cuales el diseño del puerto de salida del interactor, que es solo un detalle de implementación ...
Mis dos centavos
La razón principal por la que prefiero devolver el
UseCaseResponse
es que este enfoque mantiene mis casos de uso flexibles , permitiendo tanto la composición entre ellos como la genérica ( generalización y generación específica ). Un ejemplo básico:Tenga en cuenta que está análogamente más cerca de los casos de uso de UML que incluyen / se extienden entre sí y se define como reutilizable en diferentes temas (las entidades).
No estoy seguro de entender lo que quieres decir con esto, ¿por qué necesitarías "controlar" la presentación de la presentación? ¿No lo controla mientras no devuelva la respuesta del caso de uso?
El caso de uso puede devolver en su respuesta un código de estado para informar a la capa del cliente qué sucedió exactamente durante su operación. Los códigos de estado de respuesta HTTP son particularmente adecuados para describir el estado operativo de un caso de uso ...
fuente