Onion Architecture PopUpService
Assumindo esse tipo de arquitetura, quer você a chame de limpa, ddd, cebola ou hexagonal, onde as dependências apontam para dentro e a lógica do aplicativo deve ser fortemente desacoplada da interface do usuário/apresentação.
Como faço para lidar com algo como mostrar um pop-up? Um pop-up teria que ser realmente implementado na camada de apresentação, obviamente. Mas as classes/casos de uso/serviços/a lógica real que precisaria exibir um pop-up e agir de acordo com a entrada do usuário reside na camada do aplicativo.
Então eu crio uma interface IPopUpService na camada de aplicativo. De repente, tenho acesso a ele na camada de persistência. Posso literalmente colocar pop-ups em minhas consultas de banco de dados. Isso parece uma grande falha.
Como uma observação lateral, por que os exemplos dessa arquitetura são fornecidos apenas como APIs da Web? Em palestras ou soluções de exemplo no github. É suposto ser frontend agnóstico, mas as pessoas sempre acabam de alguma forma com o mais fácil.
Meu aplicativo específico é um wpf mvvm. Mvvm é outro estilo de arquitetura que deve desacoplar a lógica da interface do usuário. No entanto, eu me deparo com os mesmos problemas. Meus viewmodels estão em um projeto não wpf, usando um IPopUpService. No entanto, isso permite que meus repositórios o usem também. Eu não faço isso, mas tenho a opção, o que me parece errado.
A coisa toda de "colocar tudo na camada de aplicativo por meio de interfaces" parece semelhante a ter apenas uma camada, um grande projeto. Eu entendo que uma dependência de um IPopUpService não é o mesmo que uma dependência do PopUpBox do wpf, mas parece próximo o suficiente. Estou pensando errado em algum lugar? Limitar o acesso não é apenas uma consideração?
Respostas
Normalmente, você não coloca um serviço "pop-up" geral em nenhum lugar, pois um pop-up é um conceito puramente relacionado a uma determinada implementação da interface do usuário.
Para encontrar a alternativa certa, é preciso analisar o caso concreto.
- Se você precisar de informações adicionais para fazer uma consulta ao banco de dados em sua camada de persistência, o caso de uso já deve entregar essas informações para a chamada de persistência. Assim, o caso de uso - que normalmente inclui algum tipo de entrada e saída de interface do usuário - já deve "saber" quais informações são necessárias e passar essas informações para a chamada inicial.
- Se, de alguma forma, seu domínio principal quiser transmitir informações a uma parte interessada, use algum tipo de serviço de mensagens. Esta mensagem pode ser captada pela interface do usuário para mostrar um pop-up, esta mensagem também pode ser captada por um logger ou um serviço de e-mail, ou qualquer número de implementações alternativas.
Estes são apenas dois exemplos, mas espero que ilustrem como você deve mudar sua maneira de planejar o aplicativo da pergunta "o que acontece na interface do usuário" para "o que acontece no aplicativo".
A ideia é que você possa usar o domínio principal e, em menor grau, os serviços em mais de um único contexto (UI). Por exemplo, o componente de persistência deve ser projetado para que também possa ser usado em um lote ou em uma chamada de descanso. Isso exclui absolutamente a capacidade de solicitar informações adicionais no meio de uma operação.
Você se deparou com algo que é tão óbvio que a maioria das pessoas simplesmente não vê: essas arquiteturas realmente não dissociam nada .
Para fazer o que você deseja nesses tipos de arquiteturas, você terá que exportar/publicar todos os dados relacionados a esse pop-up e fazer com que a interface do usuário use esses dados para exibir o pop-up. Isso é obviamente o oposto do desacoplamento. Você precisa de algo novo de um lado e quase sempre tem que modificar o outro lado também.
A maneira que usei para lidar com a dissonância cognitiva disso é que racionalizei que estava apenas publicando alguns dados no núcleo. Não era sobre o pop-up, poderia ser usado para outra coisa também. Claro que isso raramente ou nunca foi o caso.
É claro que esta é a razão pela qual todos os exemplos disso são baseados na web e geralmente muito triviais.
Então, você descobriu algo real. Continue cavando, não confie em ninguém (há muito conteúdo ruim por aí, alguns de autores famosos) :). Sempre tente as coisas por si mesmo.
Algo assim funcionaria?
Something.Ui.WpfApplication
+ Services
- PopupService
Something.Ui
+ Contracts
- IPopupService
+ ViewModels
Something.Data
+ Repositories
Something.Core
+ Contracts
+ Domain
Isso isola seus objetos de visualização em sua camada de visualização (apresentação).
Os contratos de núcleo/domínio são todas as operações de negócios importantes, mas, de fato, cada camada também pode ter seus próprios contratos.
As camadas superiores implementam as camadas abaixo.
Os contratos são normalmente usados para intercambialidade, então talvez você possa acabar com algo assim
Something.Ui.WpfApplication
+ Services
- RedPopUpService
Something.Ui.WinFormsApp
+ Services
- BluePopUpService
Something.Ui
+ Contracts
- IPopUpService
+ Services
- PopUpSharedLogic
+ ViewModels
"Então eu crio uma interface IPopUpService na camada de aplicação. De repente eu tenho acesso a ela na camada de persistência."
O fato de você poder fazer referência a algo não significa que deva ; você deseja controlar cuidadosamente o número de interdependências (para minimizar a complexidade; você não deseja uma teia emaranhada).
Além disso, a própria camada de persistência essencialmente implementa várias abstrações relacionadas à persistência definidas por (e dentro) da camada de aplicação (por exemplo, uma ou várias interfaces). Portanto, você poderia dizer praticamente a mesma coisa para eles (você pode referenciá-los a partir de partes não persistentes da camada externa - não significa que você deva). Portanto, há uma espécie de decomposição, mesmo que apenas implícita: as próprias camadas não são monolíticas. As dependências que atravessam as camadas são, de um modo geral, limitadas a partes específicas de uma camada interna, elas não abrangem tudo.
A própria camada de aplicação será decomposta de alguma forma (por funcionalidades, casos de uso, subsistemas, o que for...); novamente, o objetivo é controlar a complexidade minimizando as dependências. Essa decomposição pode ser principalmente lógica, mas você também pode, em algum momento, decidir dividir a camada em várias DLLs, digamos. Ainda é uma camada, mas os componentes de uma camada externa farão referência apenas às DLLs de que precisam, não a todas. Você não deveria ter que fazer grandes alterações de design em seu aplicativo para poder dividi-lo em DLLs como essa.
Agora, à medida que você desenvolve seu sistema e obtém uma melhor compreensão do domínio do problema, certos conceitos mudarão, outros surgirão e outros serão descartados. Sua IPopUpServiceé uma interface estreitamente especializada, conceitualmente. É o que Robert Martin chamaria de porta de saída; uma abstração que permite invocar um serviço de nível inferior sem depender dele. Essa abstração particular, porém, não é tão abstrata assim; isso não é, em si, uma coisa ruim, especialmente nesta fase. Mas isso significa que a suposição padrão é que haverá algum tipo de GUI envolvida.
Agora, se uma classe na camada de persistência implementasse IPopUpService, isso seria bastante estranho, concordo. Só para esclarecer uma coisa antes de continuar. Você disse:
"De repente, tenho acesso a [IPopUpService] na camada de persistência."
Não é que você tenha acesso a ele, ao contrário, você está tendo uma dependência dele e está fornecendo uma implementação para a camada de aplicativo usar. O problema é que você pegou uma interface estritamente especializada e deu a ela um propósito mais amplo sem renomeá-la e reconceitualizá-la.
Se você esquecer o aspecto GUI por enquanto, o que IPopUpServicerealmente faz é algo nesse sentido: ele envia um sinal para uma camada externa para indicar algum resultado relacionado à lógica de negócios (sucesso, falha, etc.). E eu quero dizer "envia um sinal", porque a camada de aplicativo está apenas chamando o método em uma variável de tipo abstrato; o efeito colateral (o pop-up sendo mostrado) acontece na camada de apresentação.
Mas esses sinais podem ter relevância comercial própria, então você pode querer que eles resultem em mais do que apenas um pop-up sendo exibido. Por exemplo, você pode querer fazer um log do evento e armazená-lo em um arquivo e/ou em uma tabela de banco de dados. Então você pode acabar generalizando essa interface (para algo como IOperationResultConsumer1 ) para que ela possa ser usada em todos esses cenários, com as camadas de apresentação, banco de dados e infraestrutura implementando suas próprias versões cada uma (uma para mostrar um pop-up, a outra dois para armazenar um evento ou uma entrada de log).
Ou você pode decidir, com base em outros fatores e restrições, que faz mais sentido ter interfaces separadas para essas coisas. Por exemplo, pode haver diferenças sutis na lógica que não podem ser facilmente generalizadas, tornando a abordagem de interface unificada mais complicada.
Para resumir, meus dois pontos principais são que (1) a visão em camadas do sistema é uma visão de alto nível e, indo além disso, você deve estar atento a como organiza o código dentro das camadas e ser deliberado sobre sua dependências e (2) as abstrações que você criou inicialmente não são necessariamente aquelas com as quais você terminará - haverá (ou deveria haver) uma certa quantidade de refinamento ao longo do tempo.
1 Não consegui pensar em um nome melhor para ATM.