Arquitetura

MVVM

Arquitetura onde a View se liga ao ViewModel por data binding declarativo — o ViewModel expõe estado observável e comandos, a View os observa automaticamente, e o ViewModel nunca referencia a View.

Intenção

Eliminar o acoplamento imperativo entre lógica de apresentação e View por meio de data binding declarativo: a View declara "meu texto exibe viewModel.nomeUsuario" e o runtime de binding sincroniza automaticamente quando o ViewModel muda de estado. O ViewModel nunca chama a View; ele não a conhece.

MVVM foi descrito por John Gossman em 2005, no contexto do WPF (Windows Presentation Foundation) e depois do Silverlight — ambos da Microsoft. O objetivo original era explorar ao máximo o sistema de binding do XAML: designers poderiam trabalhar na View em XAML enquanto desenvolvedores trabalhavam no ViewModel em C#, sem dependência direta entre os dois.

O padrão migrou para o mundo web com Knockout.js (2010), Angular.js (2010), Vue.js (2014) e Angular (2016). Em todas essas plataformas o mecanismo subjacente é o mesmo: o ViewModel expõe propriedades reativas (observáveis), a View as consome por binding declarativo, e qualquer mudança no ViewModel propaga automaticamente para a View. O padrão Observer é o fundamento de todo sistema de binding.

Problema

O MVP (com Presenter) resolve a testabilidade da lógica de UI, mas mantém um problema de manutenção: para cada dado que precisa aparecer na tela, o Presenter precisa chamar explicitamente um método da IView (view.exibirNome(nome)). Conforme a tela cresce, esse código de "empurrar dados" cresce proporcionalmente, e qualquer nova propriedade visual exige atualizar a interface, a View e o Presenter.

  • Sincronização imperativa e frágil: o Presenter precisa lembrar de chamar todos os métodos de atualização na ordem correta. Se um estado do Presenter muda e o método de atualização correspondente não é chamado, a View fica desatualizada silenciosamente.
  • Two-way binding custoso no MVP: quando o usuário edita um campo e o dado deve voltar ao Presenter, é necessário um listener explícito para cada campo. O MVVM resolve isso com two-way binding: a View atualiza o ViewModel automaticamente quando o usuário digita, e o ViewModel atualiza a View quando o estado muda.
  • Acoplamento da View à interface IView: no MVP, adicionar uma nova propriedade visual requer atualizar a IView, a View e o Presenter. No MVVM, basta adicionar a propriedade ao ViewModel e fazer o binding na View — sem alterar nenhum contrato de interface.

O MVVM resolve esses problemas ao substituir o protocolo imperativo (Presenter chama métodos) por um protocolo declarativo (View observa o ViewModel). A sincronização deixa de ser responsabilidade de código manual e passa a ser responsabilidade do runtime de binding.

Estrutura

  ┌────────────────────────────────────────────────────────────────┐
  │                          Usuário                               │
  │                    (interage com a View)                       │
  └──────────────────────────┬─────────────────────────────────────┘
                             │ eventos (clique, input)
                             ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                          VIEW                                   │
  │  - declarativa: define o binding com o ViewModel                │
  │  - NÃO contém lógica de apresentação                           │
  │  - NÃO conhece o Model                                         │
  │  - observa o ViewModel via data binding (reativo)              │
  │  - delega ações ao ViewModel por meio de comandos/eventos       │
  └──────────────────┬──────────────────────────────────────────────┘
                     │ binding bidirecional (two-way)
                     │ View observa ViewModel (Observer por baixo)
                     ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                       VIEWMODEL                                 │
  │  - expõe estado observável (propriedades reativas)             │
  │  - expõe comandos (ações que a View pode invocar)              │
  │  - NÃO conhece a View (sem referência, sem interface IView)    │
  │  - consulta e atualiza o Model                                  │
  │  - transformações de dados para exibição ficam aqui            │
  └──────────────────┬──────────────────────────────────────────────┘
                     │ usa
                     ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                        MODEL                                    │
  │  - dados + regras de negócio                                    │
  │  - ignorante da View e do ViewModel                            │
  └─────────────────────────────────────────────────────────────────┘

  Dependências:
    View       → ViewModel      (via binding declarativo — observa estado)
    ViewModel  → Model          (lê e atualiza dados)
    ViewModel  → (ninguém)      (não conhece a View concreta)
    Model      → (ninguém)      (não conhece ViewModel nem View)

  Diferença crítica em relação ao MVP:
    MVP:  Presenter chama view.exibirNome(nome) — imperativo
    MVVM: View observa viewModel.nome — declarativo e automático

Two-way binding

No two-way binding, a sincronização é bidirecional: quando o ViewModel muda uma propriedade, a View atualiza; quando o usuário edita um campo na View, o ViewModel recebe o novo valor automaticamente. Em Vue.js isso é a diretiva v-model; em Angular é [(ngModel)]; em WPF é o Mode=TwoWay no binding XAML. O two-way binding é especialmente útil em formulários.

Como funciona

Estado observável

O coração do MVVM é a propriedade observável: uma propriedade do ViewModel que, quando muda de valor, notifica todos os observadores registrados. O mecanismo subjacente é o padrão Observer: o ViewModel é o sujeito (Subject) e as Views são os observadores. Em frameworks modernos esse mecanismo é chamado de reatividade, signals, refs ou observables — mas o princípio é o mesmo.

Comandos

Além de estado, o ViewModel expõe comandos: ações que a View pode invocar quando o usuário age (clica em um botão, submete um formulário). O comando encapsula a lógica de resposta ao evento — sem que a View saiba o que o comando faz internamente. O ViewModel também pode expor propriedades que indicam se um comando está habilitado, eliminando a necessidade de a View decidir isso por conta própria.

Exemplo de código

// ── MODEL — dados e regras de negócio ────────────────────────
// Completamente ignorante de Vue, do ViewModel e da View.
class UsuarioService {
  async buscarPorId(id: string): Promise<{ nome: string; email: string }> {
    // acessa API ou repositório
    return { nome: 'Ana', email: 'ana@exemplo.com' };
  }
}

// ── VIEWMODEL — Vue 3 Composition API ────────────────────────
// Expõe estado reativo e comandos. Nunca referencia a View.
// Em Vue 3, este bloco vai dentro de <script setup> ou em usePerfilViewModel().
import { ref, computed } from 'vue';

function usePerfilViewModel(service: UsuarioService) {
  // Estado observável — quando muda, a View atualiza automaticamente
  const nome      = ref('');
  const email     = ref('');
  const carregando = ref(false);
  const erro      = ref('');

  // Computed — derivado do estado; recalcula automaticamente
  const resumo = computed(() => nome.value ? `Olá, ${nome.value}` : '');

  // Comando — ação que a View invoca; não retorna nada para a View
  async function carregarPerfil(id: string): Promise<void> {
    carregando.value = true;
    erro.value = '';
    try {
      const usuario = await service.buscarPorId(id);
      nome.value  = usuario.nome;
      email.value = usuario.email;
    } catch {
      erro.value = 'Não foi possível carregar o perfil.';
    } finally {
      carregando.value = false;
    }
  }

  return { nome, email, carregando, erro, resumo, carregarPerfil };
}

// ── VIEW — template declarativo (Vue SFC) ─────────────────────
// <template> abaixo (não é código executável — apenas ilustrativo)
//
// <p v-if="carregando">Carregando...</p>
// <p v-if="erro">{{ erro }}</p>
// <p>{{ resumo }}</p>
// <input v-model="email" />          <!-- two-way binding -->
// <button @click="carregarPerfil('1')">Carregar</button>
//
// A View não contém nenhuma lógica — apenas binding e eventos.

O ponto central: o ViewModel nunca chama métodos na View. Ele apenas muda suas propriedades observáveis e qualquer View registrada como observadora sincroniza automaticamente. Isso é o inverso do MVP, onde o Presenter chama view.exibirNome(nome) explicitamente.

Quando usar

  • Frameworks com suporte nativo a data binding: Vue, Angular, SwiftUI, Jetpack Compose, WPF, Knockout. Nesses ambientes o MVVM é o padrão natural — o framework já fornece o mecanismo de binding e o ViewModel se encaixa sem atrito.
  • UIs com estado complexo e múltiplas atualizações simultâneas: quando muitas propriedades visuais derivam do mesmo estado, o binding declarativo é mais seguro do que chamadas imperativas: você não precisa lembrar de chamar view.atualizarX() para cada peça — o binding cuida disso.
  • Formulários com two-way binding: aplicações de entrada de dados onde o usuário edita campos e o ViewModel precisa refletir essas edições imediatamente se beneficiam muito do two-way binding.
  • Quando múltiplas Views precisam observar o mesmo ViewModel: por exemplo, um painel de resumo e um formulário de edição observando o mesmo ViewModel de produto. O ViewModel não muda; as Views se registram de forma independente.

Quando evitar

  • Sem suporte a binding no ambiente: implementar um sistema de binding manualmente (Observables, listeners em cada propriedade) para adotar MVVM em código vanilla sem framework é raramente justificável. O resultado costuma ser o padrão Observer manual com sobrecarga de infraestrutura.
  • Views simples e estáticas: se a tela exibe dados sem interatividade complexa, um MVC simples é mais direto. Criar ViewModel com propriedades reativas para uma página de apenas leitura é overengineering.
  • Quando o binding mágico dificulta a depuração: em interfaces com binding bidireccional extenso e ViewModels com muitas propriedades derivadas (computed), rastrear a origem de uma mudança inesperada na UI pode ser trabalhoso. Se a equipe não tem experiência com o modelo reativo, considere começar com MVP ou um MVC simples.

Prós e contras

Prós

  • ViewModel testável sem instanciar a View — apenas verifique o estado das propriedades observáveis após invocar os comandos.
  • Separação limpa: ViewModel não conhece a View; a View não conhece o Model.
  • Two-way binding simplifica formulários e sincronização bidirecional.
  • Múltiplas Views podem observar o mesmo ViewModel sem modificação no ViewModel.
  • Integração natural com frameworks reativos modernos (Vue, Angular, SwiftUI, Compose).

Contras

  • Requer suporte do ambiente: sem framework com binding nativo, a implementação manual de Observables é pesada.
  • Risco de ViewModel gordo: sem disciplina, o ViewModel acumula lógica de negócio, orquestração de casos de uso e transformações que pertencem ao Model — os mesmos problemas do Presenter gordo no MVP.
  • Binding mágico dificulta debugging: rastrear por que um dado na View mudou exige entender o grafo de dependências reativas, que pode ser não óbvio em aplicações grandes.
  • Curva de aprendizado do modelo reativo: desenvolvedores acostumados com programação imperativa levam tempo para pensar em termos de estado observável e derivações automáticas.

Armadilhas comuns

1. ViewModel gordo

A mesma armadilha do Presenter gordo no MVP, agora no ViewModel: regras de validação de negócio, chamadas diretas a APIs externas, orquestração de múltiplos casos de uso e cálculos de domínio acabam no ViewModel porque é "o lugar mais conveniente". O ViewModel deve conter apenas lógica de apresentação — transformar dados do Model para exibição, agregar estado para a View, expor comandos que delegam ao Model. Lógica de negócio pertence ao Model (ou a serviços de aplicação).

Regra prática: o ViewModel deve ser testável com um mock simples do Model. Se os testes do ViewModel precisam de mocks de cinco serviços diferentes, o ViewModel está fazendo coisas demais.

2. Lógica de domínio no ViewModel

Validações de negócio ("o desconto máximo é 30%"), cálculos de preço, regras de elegibilidade — nada disso pertence ao ViewModel. O ViewModel pode expor se um campo é válido para fins de exibição (por exemplo, emailValido: boolean), mas a regra que define o que é válido deve residir no Model. Se você deletar o ViewModel, as regras de negócio não devem desaparecer com ele.

3. Two-way binding gerando ciclos

Com two-way binding, é possível criar ciclos acidentais: a View atualiza o ViewModel, o ViewModel deriva uma propriedade, que atualiza a View, que atualiza o ViewModel novamente. Frameworks modernos têm mecanismos para quebrar esses ciclos (detecção de mudança batch, glitch-free propagation), mas o desenvolvedor precisa conhecer esses mecanismos para evitar comportamento inesperado em formulários complexos.

4. Confundir MVVM com MVP

A distinção é precisa e importa: no MVP, o Presenter chama métodos na View (view.exibirNome(nome)) — o fluxo é imperativo e o Presenter conhece a View via interface. No MVVM, o ViewModel expõe propriedades observáveis e não sabe que a View existe — a View se registra como observadora e o binding cuida da sincronização. Se o seu "ViewModel" tem um método atualizarView() ou guarda uma referência à View, você tem MVP com o nome trocado.

Arquiteturas e padrões relacionados

O MVC é o ancestral do MVVM. No MVC clássico, a View pode observar o Model diretamente via Observer; no MVVM a View não conhece o Model — ela observa exclusivamente o ViewModel. O ViewModel assume o papel de mediador e tradutor de dados, um papel que no MVC clássico era exercido parcialmente pelo Controller e parcialmente pelo Observer direto View-Model.

O MVP é o irmão mais próximo do MVVM — ambos separam a lógica de apresentação em um componente dedicado (Presenter ou ViewModel) e isolam o Model da View. A diferença fundamental: no MVP o Presenter empurra dados para a View de forma imperativa, chamando métodos de uma interface IView; no MVVM o ViewModel expõe estado observável e a View se sincroniza automaticamente via binding. Em ambientes sem suporte a binding, MVP é mais simples de implementar manualmente; em ambientes com binding nativo, MVVM é mais natural.

O Observer é o mecanismo que torna o MVVM possível. Toda propriedade observável do ViewModel é um Subject Observer: quando seu valor muda, todos os bindings registrados como observadores são notificados e atualizam os elementos correspondentes na View. Frameworks como Vue implementam isso com proxies reativos; Angular com Signals ou Zone.js; Knockout com ko.observable(). A abstração muda, mas o padrão Observer está sempre por baixo.

O padrão Command é frequentemente usado para modelar as ações expostas pelo ViewModel: cada botão na View é vinculado a um Command no ViewModel que encapsula a ação e opcionalmente uma condição de habilitação (podeExecutar(): boolean). Isso mantém a View completamente declarativa — ela não sabe o que o comando faz, apenas se pode executá-lo.