Arquitectura limpia: ¿Caso de uso que contiene el presentador o datos de retorno?

42

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 Presenterinterfaz 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?

Swawanee
fuente
3
La publicación cruzada no se recomienda. Si aquí es donde quiere que viva su pregunta, debe eliminarla de Stack Overflow.
Robert Harvey

Respuestas:

48

Clean Architecture sugiere dejar que un interactor de casos de uso llame a la implementación real del presentador (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.

Eso ciertamente no es Clean , Onion o Hexagonal Architecture. Eso es esto :

ingrese la descripción de la imagen aquí

No es que MVC tenga que hacerse de esa manera

ingrese la descripción de la imagen aquí

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 : ingrese la descripción de la imagen aquí

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.

ingrese la descripción de la imagen aquí

Agregue las estructuras de datos que se están volteando (y voltéelo boca abajo por alguna razón) y obtendrá :

ingrese la descripción de la imagen aquí

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.

¿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?

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 .

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".

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 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.

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.


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).

Lo importante de esa flecha "blanca" es que te permite hacer esto:

ingrese la descripción de la imagen aquí

¡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í .

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 interfaz del presentador 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 un caso de uso podría muy bien, pero aún no estoy totalmente seguro de ello.

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.

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.

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.

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 método getData () (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.

¡Sí! Decir, no preguntar, ayudará a mantener este objeto orientado en lugar de procedimiento.

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?

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.

naranja confitada
fuente
44
Gracias por tomarse el tiempo para escribir una explicación tan profunda.
swahnee
1
He estado tratando de entender la arquitectura limpia, y esta respuesta ha sido un recurso fantástico. ¡Muy bien hecho!
Nathan
Excelente y una respuesta detallada. Gracias por eso. ¿Puede darme algunos consejos (o señalar una explicación) sobre la actualización de la GUI durante la ejecución de UseCase, es decir, la actualización de la barra de progreso al cargar un archivo grande?
Ewoks
1
@Ewoks, como respuesta rápida a su pregunta, debe buscar en el patrón Observable. Su caso de uso podría devolver un Asunto y Notificar al Asunto de las actualizaciones de progreso. El presentador se suscribirá al asunto y responderá a las notificaciones.
Nathan
7

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:

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

El tío Bob dijo esto:

" El propósito del presentador es desacoplar los casos de uso del formato de la interfaz de usuario. En su ejemplo, la variable $ response es creada por el interactor, pero es utilizada por la vista. Esto acopla el interactor a la vista. Por ejemplo , supongamos que uno de los campos en el objeto $ response es una fecha. Ese campo sería un objeto de fecha binario que podría representarse en muchos formatos de fecha diferentes. Quiere un formato de fecha muy específico, quizás DD / MM / AAAA. ¿De quién es la responsabilidad de crear el formato? Si el interactor crea ese formato, entonces sabe demasiado acerca de la Vista. Pero si la vista toma el objeto de fecha binario, entonces sabe demasiado sobre el interactor

" . El trabajo del presentador es tomar los datos del objeto de respuesta y formatearlo para la Vista. Ni la vista ni el interactor conocen los formatos de cada uno. "

--- Tío Bob

(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) ...

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

... 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) ...

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

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:

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

... Quiero poder hacer esto if/elserelacionado con la presentación dentro interactory 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 responseobjeto, devolver ese responseobjeto del interactoral controller, y hacer que el objeto sea controller analizadoresponse ...

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

No me gusta analizar responsedatos en busca de errores en el interior controllerporque si lo hacemos estamos haciendo un trabajo redundante, si cambiamos algo en el interactor, también tenemos que cambiar algo en el controller.

Además, si luego decidimos reutilizar nuestros interactordatos actuales mediante la consola, por ejemplo, debemos recordar copiar y pegar todos los que están if/elseen la controlleraplicación de nuestra consola.

// 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 usamos la opción # 1, tendremos esto if/else solo en un lugar : el interactor.


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 Resulten el presentador de nuestra aplicación 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);
    }
}

(Tenga en cuenta que debemos tener public IActionResult Resulten el presentador de nuestra aplicación ASP.NET MVC)

Si decidimos crear otra aplicación para la consola, podemos reutilizar lo UseCaseanterior y crear solo el Controllery Presenterpara la consola:

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

(Tenga en cuenta que NO TENEMOS public IActionResult Resulten el presentador de nuestra aplicación de consola)

Jboy Flaga
fuente
Gracias por el aporte. Sin embargo, al leer la conversación, hay una cosa que no entiendo: dice que el presentador debe procesar los datos provenientes de la respuesta y, al mismo tiempo, que la respuesta no debe ser creada por el interactor. Pero entonces, ¿quién está creando la respuesta? Yo diría que el interactor debe proporcionar los datos al presentador, en el formato específico de la aplicación, que conoce el presentador, ya que la capa de adaptadores puede depender de la capa de la aplicación (pero no al revés).
swahnee
Lo siento. Tal vez se vuelve confuso porque no incluí el ejemplo de código de la discusión. Lo actualizaré para incluir el ejemplo de código.
Jboy Flaga
El tío Bob no dijo que la respuesta no debería ser creada por el interactor. La respuesta será creada por el interactor . Lo que dice el tío Bob es que la respuesta creada por el interactor será utilizada por el presentador. El presentador luego lo "formateará", colocará la respuesta formateada en un modelo de vista, luego pasará ese modelo de vista a la vista. <br/> Así lo entiendo.
Jboy Flaga
1
Eso tiene más sentido. Tenía la impresión de que "ver" era sinónimo de "presentador", ya que Clean Architecture no menciona "ver" ni "ver modelo", que creo que son únicamente conceptos MVC, que pueden o no usarse al implementar un adaptador.
swahnee
2

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:

  • Objeto de dominio : un objeto de dominio es el contenedor de datos en la capa de dominio en el que se llevan a cabo las operaciones de lógica de negocios.
  • Ver modelo : los objetos de dominio generalmente se asignan para ver modelos en la capa de aplicación para que sean compatibles y amigables con la interfaz de usuario.
  • Presentador : mientras que un controlador en la capa de aplicación generalmente invoca un caso de uso, pero es aconsejable delegar el dominio para ver la lógica de mapeo del modelo a una clase separada (siguiendo el Principio de responsabilidad única), que se llama "Presentador".

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:

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 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).

ingrese la descripción de la imagen aquí

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.

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

    }
}
Ashraf
fuente
1

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/

simplista
fuente
1

¿Caso de uso que contiene el presentador o datos de retorno?

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?


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 ...

Diagrama de clase UML del tío Bob de arquitectura limpia


Mis dos centavos

La razón principal por la que prefiero devolver el UseCaseResponsees 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:

// 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;

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).


En el interactor que devuelve datos

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).

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 ...

ClemC
fuente