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?