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 Presenter
interface é 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?
Respostas:
Certamente isso não é Arquitetura Limpa , Cebola ou Hexagonal . Isso é o seguinte :
Não que o MVC tenha que ser feito dessa maneira
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 :
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.
Adicione as estruturas de dados que estão sendo giradas (e vire-a de cabeça para baixo por algum motivo) e você obterá :
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.
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 .
É 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.
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.
O importante dessa seta "branca" é que ela permite que você faça isso:
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 .
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.
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.
Sim! Contar, não perguntar, ajudará a manter esse objeto orientado, e não processual.
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.
fonte
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:
O tio Bob disse o seguinte:
(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) ...
... 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) ...
Pessoalmente, prefiro a opção 1 porque quero poder controlar dentro de
interactor
quando mostrar dados e mensagens de erro, como neste exemplo abaixo:... Quero poder fazer
if/else
isso relacionado à apresentação dentrointeractor
e fora do interator.Se, por outro lado, fizermos a opção 2, teríamos de armazenar as mensagens de erro no
response
objeto, retornar esseresponse
objeto deinteractor
paracontroller
e fazer acontroller
análise doresponse
objeto ...Não gosto de analisar
response
dados em busca de erros dentro do,controller
porque, se fizermos isso, estamos fazendo um trabalho redundante - se mudarmos algo nointeractor
, também precisaremos mudar algo nocontroller
.Além disso, se mais tarde decidirmos reutilizar nossos
interactor
dados para apresentar usando o console, por exemplo, devemos lembrar de copiar e colar todos aquelesif/else
nocontroller
aplicativo do console.Se usarmos a opção 1, teremos isso
if/else
apenas em um lugar : ointeractor
.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 Result
no apresentador do nosso aplicativo ASP.NET MVC)(Observe que precisamos ter
public IActionResult Result
no apresentador do nosso aplicativo ASP.NET MVC)Se decidir criar outro aplicativo para o console, podemos reutilizar o
UseCase
acima e criar apenas oController
ePresenter
para o console:(Observe que NÃO TEMOS
public IActionResult Result
no apresentador do nosso aplicativo de console)fonte
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:
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:
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).
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.
fonte
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/
fonte
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 ...
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: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).
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…
fonte