MVVM e padrão de serviço

13

Estou criando um aplicativo WPF usando o padrão MVVM. No momento, meus viewmodels chamam a camada de serviço para recuperar modelos (como não é relevante para o viewmodel) e convertê-los em viewmodels. Estou usando a injeção de construtor para passar o serviço necessário para o viewmodel.

É facilmente testável e funciona bem para viewmodels com poucas dependências, mas assim que tento criar viewModels para modelos complexos, tenho um construtor com MUITOS serviços injetados (um para recuperar cada dependência e uma lista de todos os valores disponíveis para vincular a um itemsSource, por exemplo). Eu estou querendo saber como lidar com vários serviços como esse e ainda tenho um modelo de exibição que eu possa testar facilmente.

Estou pensando em algumas soluções:

  1. Criando um singleton de serviços (IServices) contendo todos os serviços disponíveis como interfaces. Exemplo: Services.Current.XXXService.Retrieve (), Services.Current.YYYService.Retrieve (). Dessa forma, não tenho um construtor enorme com muitos parâmetros de serviços.

  2. Criando uma fachada para os serviços usados ​​pelo viewModel e passando esse objeto no ctor do meu viewmodel. Mas então, terei que criar uma fachada para cada um dos meus modelos de visualização complexos, e pode ser um pouco demais ...

O que você acha que é a maneira "certa" de implementar esse tipo de arquitetura?

alfa-alfa
fonte
Eu acho que a maneira "certa" de fazer isso é criar uma camada separada que chame os serviços e faça o casting necessário para criar o ViewModel. Seu ViewModels não deve ser responsável por se criar.
Amy Blankenship
@AmyBlankenship: os modelos de exibição não devem ter que (ou necessariamente ser capazes de) criar a si mesmos, mas inevitavelmente às vezes serão responsáveis ​​por criar outros modelos de exibição . Um contêiner de IoC com suporte automático de fábrica é uma grande ajuda aqui.
Aaronaught
"Será que às vezes" e "deve" são dois animais diferentes;)
Amy Blankenship
@AmyBlankenship: Você está sugerindo que os modelos de vista não devem criar outros modelos de vista? Essa é uma pílula difícil de engolir. Posso entender dizendo que os modelos de vista não devem ser usados newpara criar outros modelos de vista, mas pense em algo tão simples quanto um aplicativo MDI em que clicar no botão ou menu "novo documento" adicionará uma nova guia ou abrirá uma nova janela. O shell / condutor deve ser capaz de criar novas instâncias de algo , mesmo se estiver oculto atrás de uma ou algumas camadas de indireção.
Aaronaught
Bem, certamente deve ter a capacidade de solicitar que uma Visualização seja feita em algum lugar. Mas para fazer isso sozinho? Não no meu mundo :). Mas, novamente, no mundo em que vivo, chamamos a VM de "Modelo de Apresentação".
Amy Blankenship

Respostas:

22

De fato, essas duas soluções são ruins.

Criando um singleton de serviços (IServices) contendo todos os serviços disponíveis como interfaces. Exemplo: Services.Current.XXXService.Retrieve (), Services.Current.YYYService.Retrieve (). Dessa forma, não tenho um construtor enorme com muitos parâmetros de serviços.

Este é essencialmente o Service Locator Pattern , que é um antipadrão. Se você fizer isso, não poderá mais entender do que o modelo de visualização realmente depende sem examinar sua implementação privada, o que tornará muito difícil testar ou refatorar.

Criando uma fachada para os serviços usados ​​pelo viewModel e passando esse objeto no ctor do meu viewmodel. Mas então, terei que criar uma fachada para cada um dos meus modelos de visualização complexos, e pode ser um pouco demais ...

Isso não é tanto um anti-padrão, mas é um cheiro de código. Essencialmente, você está criando um objeto de parâmetro , mas o objetivo do padrão de refatoração do pedido é lidar com conjuntos de parâmetros que são usados ​​com frequência e em muitos lugares diferentes , enquanto esse parâmetro só seria usado uma vez. Como você mencionou, isso criaria um grande número de códigos sem nenhum benefício real e não funcionaria bem com muitos contêineres de IoC.

De fato, ambas as estratégias acima estão negligenciando a questão geral, que é que o acoplamento é muito alto entre os modelos e serviços de exibição . Simplesmente ocultar essas dependências em um localizador de serviço ou objeto de parâmetro na verdade não altera de quantos outros objetos o modelo de exibição depende.

Pense em como você testaria em unidade um desses modelos de exibição. Qual será o tamanho do seu código de configuração? Quantas coisas precisam ser inicializadas para que funcione?

Muitas pessoas que começam com o MVVM tentam criar modelos de exibição para uma tela inteira , o que é fundamentalmente a abordagem errada. O MVVM tem tudo a ver com composição , e uma tela com muitas funções deve ser composta por vários modelos de vista diferentes, cada um dos quais depende de apenas um ou alguns modelos / serviços internos. Se eles precisam se comunicar, você o faz através de pub / sub (intermediário de mensagens, barramento de eventos, etc.)

O que você realmente precisa fazer é refatorar seus modelos de exibição para que eles tenham menos dependências . Então, se você precisar ter uma "tela" agregada, crie outro modelo de vista para agregar os modelos de vista menores. Esse modelo de visualização agregado não precisa fazer muito por si só, portanto, por sua vez, também é bastante fácil de entender e testar.

Se você fez isso corretamente, deve ser óbvio apenas olhando o código, porque você terá modelos de visualização curtos, sucintos, específicos e testáveis.

Aaronaught
fonte
Sim, é isso que provavelmente vou acabar fazendo! Muito obrigado senhor.
alfa-alfa
Bem, eu já presumi que ele já havia tentado isso, mas não teve sucesso. @ alfa-alfa
Euphoric
@Euphoric: Como você "não consegue" isso? Como Yoda diria: faça ou não, não há tentativa.
precisa saber é o seguinte
@Aaronaught Por exemplo, ele realmente precisa de todos os dados em um único modelo de exibição. Talvez ele tenha grade e colunas diferentes venham de serviços diferentes. Você não pode fazer isso com a composição.
Euphoric
@Euphoric: Na verdade, você pode resolver isso com composição, mas isso pode ser feito abaixo do nível do modelo de exibição. É simplesmente uma questão de criar as abstrações certas. Nesse caso, você precisa apenas de um serviço para manipular a consulta inicial para obter uma lista de IDs e uma sequência / lista / matriz de "enriquecedores" que anotam com suas próprias informações. Faça da própria grade seu próprio modelo de visualização, e você resolveu o problema com efetivamente duas dependências, e é extremamente fácil de testar.
Aaronaught 4/11
1

Eu poderia escrever um livro sobre isso ... na verdade eu sou;)

Primeiro, não existe uma maneira universalmente "correta" de fazer as coisas. Você precisa levar outros fatores em consideração.

É possível que seus serviços sejam granulados demais. Agrupar os serviços com fachadas que fornecem a interface que um Viewmodel específico ou mesmo um cluster de ViewModels relacionados usaria pode ser uma solução melhor.

Mais simples ainda seria agrupar os serviços em uma única fachada que todos os modelos de exibição usam. É claro que isso pode ser uma interface muito grande com muitas funções desnecessárias para o cenário médio. Mas eu diria que não é diferente de um roteador de mensagens que lida com todas as mensagens no seu sistema.

De fato, o que vi muitas arquiteturas eventualmente evoluirem é um barramento de mensagens construído em torno de algo como o padrão Agregador de Eventos. O teste é fácil porque a classe de teste apenas registra um ouvinte no EA e dispara o evento apropriado em resposta. Mas esse é um cenário avançado que leva tempo para crescer. Eu digo começar com a fachada unificadora e partir daí.

Michael Brown
fonte
Uma enorme fachada de serviço é muito diferente de um intermediário de mensagens. É quase no extremo oposto do espectro de dependência. Uma característica marcante dessa arquitetura é que o remetente não sabe nada sobre o receptor e pode haver muitos receptores (pub / sub ou multicast). Talvez você esteja confundindo isso com o "remoting" no estilo RPC, que está apenas expondo um serviço tradicional por meio de um protocolo remoto, e o remetente ainda está acoplado ao recebimento, tanto fisicamente (endereço do terminal) quanto logicamente (valor de retorno).
precisa saber é o seguinte
A semelhança é que a fachada age como um roteador, o chamador não sabe qual serviço / serviço lida com a chamada, assim como um cliente que envia uma mensagem não sabe quem lida com a mensagem.
Michael Brown
Sim, mas a fachada é então um objeto de Deus . Ele tem todas as dependências que os modelos de exibição têm, provavelmente mais porque será compartilhado por vários. Na verdade, você eliminou os benefícios do acoplamento flexível pelo qual trabalhou tão duro, porque agora, sempre que algo toca a mega-fachada, você não tem idéia de que funcionalidade realmente depende. Imagem escrevendo um teste de unidade para uma classe que usa a fachada. Você cria uma simulação para a fachada. Agora, quais métodos você zomba? Como é o seu código de configuração?
Aaronaught
Isso é muito diferente de um intermediário de mensagens, porque o intermediário também não sabe nada sobre a implementação dos manipuladores de mensagens . Ele usa a IoC sob o capô. A fachada sabe tudo sobre os destinatários porque precisa encaminhar as chamadas para eles. O barramento tem acoplamento zero; a fachada tem um acoplamento eferente obscenamente alto. Quase tudo o que você muda, em qualquer lugar, também afeta a fachada.
Aaronaught
Acho que parte da confusão aqui - e vejo bastante isso - é exatamente o que significa dependência . Se você tem uma classe que depende de outra classe, mas chama 4 métodos dessa classe, ela tem 4 dependências, não 1. Colocar tudo atrás de uma fachada não altera o número de dependências, apenas as torna mais difíceis de entender .
Aaronaught
0

Por que não combinar os dois?

Crie uma fachada e coloque todos os serviços que seus modelos de exibição usam. Então você pode ter uma fachada única para todos os seus modelos de exibição sem a palavra S. ruim.

Ou você pode usar injeção de propriedade em vez de injeção de construtor. Mas, então, você precisa garantir que eles estejam sendo injetados corretamente.

Eufórico
fonte
Essa seria uma resposta melhor se você fornecesse um exemplo em pseudo-C #.
Robert Harvey