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.
// ── VIEWMODEL genérico (sem framework) ───────────────────────
// Mostra o mecanismo de Observable puro — o que frameworks fazem internamente.
type Listener<T> = (valor: T) => void;
class Observable<T> {
private valor: T;
private listeners: Listener<T>[] = [];
constructor(inicial: T) {
this.valor = inicial;
}
get(): T { return this.valor; }
set(novoValor: T): void {
this.valor = novoValor;
this.listeners.forEach(fn => fn(novoValor));
}
subscribe(fn: Listener<T>): void {
this.listeners.push(fn);
}
}
// ── MODEL ─────────────────────────────────────────────────────
class UsuarioService {
buscarPorId(id: string): { nome: string; email: string } {
return { nome: 'Ana', email: 'ana@exemplo.com' };
}
}
// ── VIEWMODEL ─────────────────────────────────────────────────
// Expõe Observables. A View se registra como observadora.
// O ViewModel nunca sabe quantas Views estão observando.
class PerfilViewModel {
readonly nome = new Observable('');
readonly erro = new Observable('');
readonly ocupado = new Observable(false);
constructor(private readonly service: UsuarioService) {}
// Comando: a View chama; o ViewModel atualiza o estado observável
carregarPerfil(id: string): void {
this.ocupado.set(true);
this.erro.set('');
try {
const usuario = this.service.buscarPorId(id);
this.nome.set(usuario.nome);
} catch {
this.erro.set('Erro ao carregar perfil.');
} finally {
this.ocupado.set(false);
}
}
}
// ── BINDING manual (o que o framework faria automaticamente) ──
const vm = new PerfilViewModel(new UsuarioService());
const elNome = document.getElementById('nome')!;
const elErro = document.getElementById('erro')!;
// View "observa" o ViewModel — substitui o view.exibirNome() do MVP
vm.nome.subscribe(v => { elNome.textContent = v; });
vm.erro.subscribe(v => { elErro.textContent = v; });
// Comando disparado pela View
document.getElementById('btn-carregar')!
.addEventListener('click', () => vm.carregarPerfil('1'));
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.