Arquitetura limpa: caso de uso que contém o apresentador ou retorna dados?

42

A Arquitetura Limpa sugere permitir que um interator do caso de uso chame a implementação real do apresentador (que é injetado, seguindo o DIP) para lidar com a resposta / exibição. No entanto, vejo pessoas implementando essa arquitetura, retornando os dados de saída do interator e, em seguida, deixando o controlador (na camada do adaptador) decidir como lidar com isso. A segunda solução está vazando responsabilidades da aplicação para fora da camada de aplicação, além de não definir claramente as portas de entrada e saída para o interator?

Portas de entrada e saída

Considerando a definição de Arquitetura Limpa e, especialmente, o pequeno diagrama de fluxo que descreve os relacionamentos entre um controlador, um interator de caso de uso e um apresentador, não tenho certeza se entendi corretamente qual deve ser a "Porta de Saída de Caso de Uso".

Arquitetura limpa, como arquitetura hexagonal, distingue entre portas primárias (métodos) e portas secundárias (interfaces a serem implementadas pelos adaptadores). Após o fluxo de comunicação, espero que a "Porta de entrada de casos de uso" seja uma porta primária (portanto, apenas um método) e a "Porta de saída de casos de uso" uma interface a ser implementada, talvez um argumento construtor que aceite o adaptador real, para que o interator possa usá-lo.

Exemplo de código

Para criar um exemplo de código, este pode ser o código do controlador:

Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();

A interface do apresentador:

// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}

Finalmente, o próprio interator:

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

No interator chamando o apresentador

A interpretação anterior parece ser confirmada pelo próprio diagrama acima mencionado, em que a relação entre o controlador e a porta de entrada é representada por uma seta sólida com uma cabeça "afiada" (UML para "associação", significando "tem um", onde o O controlador "possui um" caso de uso), enquanto a relação entre o apresentador e a porta de saída é representada por uma seta sólida com uma cabeça "branca" (UML para "herança", que não é a "implementação", mas provavelmente esse é o significado de qualquer maneira).

Além disso, nesta resposta a outra pergunta , Robert Martin descreve exatamente um caso de uso em que o interator chama o apresentador mediante uma solicitação de leitura:

Clicar no mapa faz com que o placePinController seja chamado. Ele reúne a localização do clique e quaisquer outros dados contextuais, constrói uma estrutura de dados placePinRequest e a transmite ao PlacePinInteractor que verifica a localização do pino, valida-o, se necessário, cria uma entidade Place para registrar o pino, constrói um EditPlaceReponse objeto e o passa para o EditPlacePresenter, que exibe a tela do editor de local.

Para fazer isso funcionar bem com o MVC, eu poderia pensar que a lógica do aplicativo que tradicionalmente entra no controlador, é movida para o interator, porque não queremos que nenhuma lógica do aplicativo vaze para fora da camada de aplicativo. O controlador na camada de adaptadores chamaria o interator e talvez fizesse alguma conversão menor no formato de dados no processo:

O software nesta camada é um conjunto de adaptadores que convertem dados do formato mais conveniente para os casos de uso e entidades, para o formato mais conveniente para alguma agência externa, como o Banco de Dados ou a Web.

do artigo original, falando sobre adaptadores de interface.

No interator retornando dados

No entanto, meu problema com essa abordagem é que o caso de uso deve cuidar da própria apresentação. Agora, vejo que o objetivo da Presenterinterface é ser abstrato o suficiente para representar vários tipos diferentes de apresentadores (GUI, Web, CLI etc.), e que realmente significa apenas "saída", que é algo que um caso de uso pode muito bem, mas ainda não estou totalmente confiante com isso.

Agora, olhando pela Web aplicativos da arquitetura limpa, pareço encontrar apenas pessoas interpretando a porta de saída como um método retornando algum DTO. Isso seria 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

Isso é atraente porque estamos movendo a responsabilidade de "chamar" a apresentação para fora do caso de uso, para que o caso de uso não se preocupe mais em saber o que fazer com os dados, mas apenas em fornecê-los. Além disso, neste caso, ainda não estamos violando a regra de dependência, porque o caso de uso ainda não sabe nada sobre a camada externa.

No entanto, o caso de uso não controla o momento em que a apresentação real é mais executada (o que pode ser útil, por exemplo, para fazer coisas adicionais nesse ponto, como registrar ou anulá-la completamente, se necessário). Além disso, observe que perdemos a porta de entrada de casos de uso, porque agora o controlador está usando apenas o getData()método (que é a nossa nova porta de saída). Além disso, parece-me que estamos quebrando o princípio "diga, não pergunte" aqui, porque estamos solicitando ao interator alguns dados para fazer algo com ele, em vez de dizer para ele fazer a coisa real no primeiro lugar.

Ao ponto

Então, alguma dessas duas alternativas é a interpretação "correta" da porta de saída de casos de uso de acordo com a arquitetura limpa? Ambos são viáveis?

swahnee
fonte
3
A postagem cruzada é fortemente desencorajada. Se é aqui que você deseja que sua pergunta seja exibida, exclua-a do Stack Overflow.
Robert Harvey

Respostas:

48

A Arquitetura Limpa sugere permitir que um interator do caso de uso chame a implementação real do apresentador (que é injetado, seguindo o DIP) para lidar com a resposta / exibição. No entanto, vejo pessoas implementando essa arquitetura, retornando os dados de saída do interator e, em seguida, deixando o controlador (na camada do adaptador) decidir como lidar com isso.

Certamente isso não é Arquitetura Limpa , Cebola ou Hexagonal . Isso é o seguinte :

insira a descrição da imagem aqui

Não que o MVC tenha que ser feito dessa maneira

insira a descrição da imagem aqui

Você pode usar muitas maneiras diferentes de se comunicar entre os módulos e chamá-lo de MVC . Dizer-me que algo usa o MVC não me diz realmente como os componentes se comunicam. Isso não é padronizado. Tudo o que me diz é que existem pelo menos três componentes focados em suas três responsabilidades.

Algumas dessas maneiras receberam nomes diferentes : insira a descrição da imagem aqui

E cada uma delas pode ser justificadamente chamada de MVC.

De qualquer forma, nenhum deles realmente captura o que as arquiteturas de chavão (Clean, Onion e Hex) estão pedindo para você fazer.

insira a descrição da imagem aqui

Adicione as estruturas de dados que estão sendo giradas (e vire-a de cabeça para baixo por algum motivo) e você obterá :

insira a descrição da imagem aqui

Uma coisa que deve ficar clara aqui é que o modelo de resposta não vai marchar pelo controlador.

Se você estiver interessado, deve ter notado que apenas as arquiteturas de chavão evitam completamente dependências circulares . É importante ressaltar que isso significa que o impacto de uma alteração de código não se espalhará pelo ciclo de componentes. A alteração será interrompida quando atingir o código que não se importa.

Gostaria de saber se eles viraram de cabeça para baixo para que o fluxo de controle passasse no sentido horário. Mais sobre isso, e essas setas "brancas", mais tarde.

A segunda solução está vazando responsabilidades da aplicação para fora da camada de aplicação, além de não definir claramente as portas de entrada e saída para o interator?

Como a comunicação do Controller com o Presenter deve passar pela "camada" do aplicativo, sim, fazer o Controller fazer parte do trabalho do Presenters provavelmente é um vazamento. Esta é minha principal crítica à arquitetura VIPER .

Por que separar isso é tão importante provavelmente poderia ser melhor entendido estudando a segregação de responsabilidade de consulta de comando .

Portas de entrada e saída

Considerando a definição de Arquitetura Limpa, e especialmente o pequeno diagrama de fluxo que descreve os relacionamentos entre um controlador, um interator de casos de uso e um apresentador, não tenho certeza se entendi corretamente qual deve ser a "Porta de Saída de Caso de Uso".

É a API pela qual você envia a saída, para este caso de uso específico. Não é mais do que isso. O interator para este caso de uso não precisa saber, nem quer saber, se a saída está indo para uma GUI, uma CLI, um log ou um alto-falante de áudio. Tudo o que o interator precisa saber é a API mais simples possível que permitirá que ele relate os resultados de seu trabalho.

Arquitetura limpa, como arquitetura hexagonal, distingue entre portas primárias (métodos) e portas secundárias (interfaces a serem implementadas pelos adaptadores). Após o fluxo de comunicação, espero que a "Porta de entrada de casos de uso" seja uma porta primária (portanto, apenas um método) e a "Porta de saída de casos de uso" uma interface a ser implementada, talvez um argumento construtor que aceite o adaptador real, para que o interator possa usá-lo.

A razão pela qual a porta de saída é diferente da porta de entrada é que ela não deve ser PROPRIETÁRIA pela camada que abstrai. Ou seja, a camada que abstrai não deve permitir que as alterações sejam ditadas. Somente a camada de aplicação e seu autor devem decidir que a porta de saída pode mudar.

Isso contrasta com a porta de entrada que pertence à camada que abstrai. Somente o autor da camada de aplicativo deve decidir se sua porta de entrada deve mudar.

Seguir essas regras preserva a idéia de que a camada de aplicativo, ou qualquer camada interna, não sabe nada sobre as camadas externas.


No interator chamando o apresentador

A interpretação anterior parece ser confirmada pelo próprio diagrama acima mencionado, em que a relação entre o controlador e a porta de entrada é representada por uma seta sólida com uma cabeça "afiada" (UML para "associação", significando "tem um", onde o O controlador "possui um" caso de uso), enquanto a relação entre o apresentador e a porta de saída é representada por uma seta sólida com uma cabeça "branca" (UML para "herança", que não é a "implementação", mas provavelmente esse é o significado de qualquer maneira).

O importante dessa seta "branca" é que ela permite que você faça isso:

insira a descrição da imagem aqui

Você pode deixar o fluxo de controle ir na direção oposta da dependência! Isso significa que a camada interna não precisa saber sobre a camada externa e, no entanto, você pode mergulhar na camada interna e voltar!

Isso não tem nada a ver com o uso da palavra-chave "interface". Você poderia fazer isso com uma classe abstrata. Heck, você poderia fazê-lo com uma classe de concreto (ick), desde que possa ser estendida. É simplesmente bom fazer isso com algo que se concentre apenas na definição da API que o Presenter deve implementar. A seta aberta está apenas pedindo polimorfismo. Que tipo é com você.

Por que reverter a direção dessa dependência é tão importante pode ser aprendido estudando o Princípio de Inversão da Dependência . Mapeei esse princípio para esses diagramas aqui .

No interator retornando dados

No entanto, meu problema com essa abordagem é que o caso de uso deve cuidar da própria apresentação. Agora, vejo que o objetivo da interface do Presenter é ser abstrato o suficiente para representar vários tipos diferentes de apresentadores (GUI, Web, CLI etc.), e que realmente significa apenas "saída", que é um caso de uso pode muito bem ter, mas ainda não estou totalmente confiante com isso.

Não, é isso mesmo. O objetivo de garantir que as camadas internas não saibam sobre as camadas externas é que podemos remover, substituir ou refatorar as camadas externas, confiantes de que isso não quebrará nada nas camadas internas. O que eles não sabem, não os machuca. Se pudermos fazer isso, podemos mudar os externos para o que quisermos.

Agora, olhando pela Web aplicativos da arquitetura limpa, pareço encontrar apenas pessoas interpretando a porta de saída como um método retornando algum DTO. Isso seria 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

Isso é atraente porque estamos movendo a responsabilidade de "chamar" a apresentação para fora do caso de uso, para que o caso de uso não se preocupe mais em saber o que fazer com os dados, mas apenas em fornecê-los. Além disso, neste caso, ainda não estamos violando a regra de dependência, porque o caso de uso ainda não sabe nada sobre a camada externa.

O problema aqui é agora tudo o que sabe como solicitar os dados também deve ser o que aceita os dados. Antes que o Controlador pudesse ligar para o Usecase Interactor, sem saber como seria o Modelo de Resposta, para onde deveria ir e, eh, como apresentá-lo.

Novamente, estude a segregação de responsabilidade de consulta de comando para ver por que isso é importante.

No entanto, o caso de uso não controla o momento em que a apresentação real é mais executada (o que pode ser útil, por exemplo, para fazer coisas adicionais nesse ponto, como registrar ou anulá-la completamente, se necessário). Além disso, observe que perdemos a porta de entrada de casos de uso, porque agora o controlador está usando apenas o método getData () (que é a nossa nova porta de saída). Além disso, parece-me que estamos quebrando o princípio "diga, não pergunte" aqui, porque estamos solicitando ao interator alguns dados para fazer algo com ele, em vez de dizer para ele fazer a coisa real no primeiro lugar.

Sim! Contar, não perguntar, ajudará a manter esse objeto orientado, e não processual.

Ao ponto

Então, alguma dessas duas alternativas é a interpretação "correta" da porta de saída de casos de uso de acordo com a arquitetura limpa? Ambos são viáveis?

Tudo o que funciona é viável. Mas eu não diria que a segunda opção que você apresentou fielmente segue a Arquitetura Limpa. Pode ser algo que funciona. Mas não é o que a Arquitetura Limpa pede.

candied_orange
fonte
4
Obrigado por dedicar um tempo para escrever uma explicação tão aprofundada.
swahnee
1
Estou tentando entender a arquitetura limpa, e essa resposta tem sido um recurso fantástico. Muito bem feito!
Nathan
Ótima e detalhada resposta .. Obrigado por isso. Você pode me dar algumas dicas (ou apontar para explicações) sobre a atualização da GUI durante a execução do UseCase, ou seja, atualização da barra de progresso durante o upload de um arquivo grande?
Ewoks
1
@ Ewoks, como resposta rápida à sua pergunta, você deve examinar o padrão Observável. Seu caso de uso pode retornar um Assunto e notificar o Assunto sobre atualizações de progresso. O apresentador se inscreve no assunto e responde às notificações.
21419 Nathan
7

Em uma discussão relacionada à sua pergunta , o tio Bob explica o propósito do apresentador em sua arquitetura limpa:

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

O tio Bob disse o seguinte:

" O objetivo do apresentador é dissociar os casos de uso do formato da interface do usuário. No seu exemplo, a variável $ response é criada pelo interator, mas é usada pela visualização. Isso acopla o interator à visualização. Por exemplo , digamos que um dos campos no objeto $ response seja uma data. Esse campo seria um objeto de data binário que pode ser renderizado em muitos formatos de data diferentes. O deseja um formato de data muito específico, talvez DD / MM / AAAA. De quem é a responsabilidade de criar o formato? Se o interator criar esse formato, ele saberá muito sobre a Visualização. Mas se a visualização usar o objeto de data binária, saberá muito sobre o interator.

"O trabalho do apresentador é aceitar os dados do objeto de resposta e formate-os para a Visualização. Nem a visão nem o interator sabem sobre os formatos um do outro. "

--- Tio Bob

(ATUALIZAÇÃO: 31 de maio de 2019)

Dada a resposta do tio Bob, acho que não importa muito se fazemos a opção 1 (deixe o interator usar o apresentador) ...

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

... ou escolhemos a opção 2 (deixe o interator retornar a resposta, criar um apresentador dentro do controlador e depois passar a resposta ao apresentador) ...

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

Pessoalmente, prefiro a opção 1 porque quero poder controlar dentro de interactor quando mostrar dados e mensagens de erro, como neste exemplo abaixo:

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

... Quero poder fazer if/elseisso relacionado à apresentação dentro interactore fora do interator.

Se, por outro lado, fizermos a opção 2, teríamos de armazenar as mensagens de erro no responseobjeto, retornar esse responseobjeto de interactorpara controllere fazer a controller análise do responseobjeto ...

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

Não gosto de analisar responsedados em busca de erros dentro do, controllerporque, se fizermos isso, estamos fazendo um trabalho redundante - se mudarmos algo no interactor, também precisaremos mudar algo no controller.

Além disso, se mais tarde decidirmos reutilizar nossos interactordados para apresentar usando o console, por exemplo, devemos lembrar de copiar e colar todos aqueles if/elseno controlleraplicativo do console.

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

Se usarmos a opção 1, teremos isso if/else apenas em um lugar : o interactor.


Se você estiver usando o ASP.NET MVC (ou outras estruturas similares do MVC), a opção 2 é o caminho mais fácil .

Mas ainda podemos fazer a opção 1 nesse tipo de ambiente. Aqui está um exemplo de como fazer a opção 1 no ASP.NET MVC:

(Observe que precisamos ter public IActionResult Resultno apresentador do nosso aplicativo 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);
    }
}

(Observe que precisamos ter public IActionResult Resultno apresentador do nosso aplicativo ASP.NET MVC)

Se decidir criar outro aplicativo para o console, podemos reutilizar o UseCaseacima e criar apenas o Controllere Presenterpara o console:

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

(Observe que NÃO TEMOS public IActionResult Resultno apresentador do nosso aplicativo de console)

Jboy Flaga
fonte
Obrigado pela contribuição. Ao ler a conversa, no entanto, há uma coisa que eu não entendo: ele diz que o apresentador deve renderizar os dados provenientes da resposta e ao mesmo tempo que a resposta não deve ser criada pelo interator. Mas então quem está criando a resposta? Eu diria que o interator deve fornecer os dados ao apresentador, no formato específico do aplicativo, que é conhecido pelo apresentador, já que a camada de adaptadores pode depender da camada de aplicativo (mas não o contrário).
swahnee
Eu sinto Muito. Talvez fique confuso porque não incluí o exemplo de código da discussão. Vou atualizá-lo para incluir o exemplo de código.
Jboy Flaga
Tio Bob não disse que a resposta não deveria ser criada pelo interator. A resposta será criada pelo interator . O que o tio Bob está dizendo é que a resposta criada pelo interator será usada pelo apresentador. O apresentador irá "formatá-lo", colocar a resposta formatada em um viewmodel e depois passar esse viewmodel para a view. <br/> É assim que eu entendo.
Jboy Flaga
1
Isso faz mais sentido. Fiquei com a impressão de que "view" era sinônimo de "apresentador", pois a Arquitetura Limpa não menciona "view" nem "viewmodel", que acredito serem apenas conceitos MVC, que podem ou não ser usados ​​na implementação de um adaptador.
swahnee
2

Um caso de uso pode conter o apresentador ou os dados retornados, depende do que é requerido pelo fluxo do aplicativo.

Vamos entender alguns termos antes de entender os diferentes fluxos de aplicativos:

  • Objeto de domínio : um objeto de domínio é o contêiner de dados na camada de domínio em que as operações da lógica de negócios são realizadas.
  • Visualizar modelo : os objetos de domínio geralmente são mapeados para visualizar modelos na camada de aplicativos para torná-los compatíveis e amigáveis ​​à interface do usuário.
  • Apresentador : Embora um controlador na camada de aplicativo normalmente invoque um caso de uso, é aconselhável delegar o domínio para exibir a lógica de mapeamento do modelo para separar a classe (seguindo o Princípio de responsabilidade única), que é chamado de "Apresentador".

Um caso de uso que contém dados retornados

Em um caso usual, um caso de uso simplesmente retorna um objeto de domínio para a camada de aplicativo que pode ser processado ainda mais na camada de aplicativo para facilitar a exibição na interface do usuário.

Como o controlador é responsável por chamar o caso de uso, nesse caso, ele também contém uma referência do respectivo apresentador para conduzir o domínio para visualizar o mapeamento do modelo antes de enviá-lo para a exibição a ser renderizada.

Aqui está um exemplo 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;
        }

    }
}

Um caso de uso que contém o apresentador

Embora não seja comum, mas é possível que o caso de uso precise chamar o apresentador. Nesse caso, em vez de manter a referência concreta do apresentador, é recomendável considerar uma interface (ou classe abstrata) como o ponto de referência (que deve ser inicializado em tempo de execução por injeção de dependência).

Ter o domínio para visualizar a lógica de mapeamento do modelo em uma classe separada (em vez de dentro do controlador) também quebra a dependência circular entre o controlador e o caso de uso (quando a classe de casos de uso requer referência à lógica de mapeamento).

insira a descrição da imagem aqui

Abaixo está a implementação simplificada do fluxo de controle, conforme ilustrado no artigo original, que demonstra como isso pode ser feito. Observe que, diferentemente do mostrado no diagrama, por uma questão de simplicidade, UseCaseInteractor é uma classe 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
fonte
1

Mesmo que eu concorde com a resposta de @CandiedOrange, também veria benefícios na abordagem em que o interator apenas recupera os dados que são passados ​​pelo controlador ao apresentador.

Esta, por exemplo, é uma maneira simples de usar as idéias da Arquitetura Limpa (Regra de Dependência) no contexto do Asp.Net MVC.

Eu escrevi uma postagem no blog para aprofundar essa discussão: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

clarionista
fonte
1

Caso de uso que contém o apresentador ou retorna dados?

Então, alguma dessas duas alternativas é a interpretação "correta" da porta de saída de casos de uso de acordo com a arquitetura limpa? Ambos são viáveis?


Em resumo

Sim, ambas são viáveis, desde que as duas abordagens levem em consideração Inversão de controle entre a camada de negócios e o mecanismo de entrega. Com a segunda abordagem, ainda somos capazes de introduzir o COI usando observadores, mediadores e outros poucos padrões de design ...

Com sua arquitetura limpa , a tentativa do tio Bob é sintetizar várias arquiteturas conhecidas para revelar conceitos e componentes importantes para que cumpramos amplamente os princípios de POO.

Seria contraproducente considerar o diagrama de classes UML (o diagrama abaixo) como o design exclusivo da arquitetura limpa . Este diagrama poderia ter sido tirada por uma questão de exemplos concretos ... No entanto, uma vez que é muito menos abstrato do que representações arquitetura habituais que ele teve de fazer escolhas concretas entre os quais o design porta de saída interator que é apenas um detalhe de implementação ...

Diagrama de classe UML do tio Bob de arquitetura limpa


Meus dois centavos

A principal razão pela qual prefiro retornar o UseCaseResponseé que essa abordagem mantém meus casos de uso flexíveis , permitindo composição entre eles e genéricos ( generalização e geração específica ). Um exemplo 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;

Observe que é analogamente mais próximo dos casos de uso da UML, incluindo / estendendo -se e definidos como reutilizáveis em diferentes assuntos (as entidades).


No interator retornando dados

No entanto, o caso de uso não controla o momento em que a apresentação real é mais executada (o que pode ser útil, por exemplo, para fazer coisas adicionais nesse ponto, como registrar ou anulá-la completamente, se necessário).

Não tendo certeza de entender o que você quer dizer com isso, por que você precisaria "controlar" a performance da apresentação? Você não controla enquanto não retorna a resposta do caso de uso?

O caso de uso pode retornar em sua resposta um código de status para informar à camada do cliente o que aconteceu exatamente durante sua operação. Os códigos de status de resposta HTTP são particularmente adequados para descrever o status da operação de um caso de uso…

ClemC
fonte