Arquitetura

MVP

Evolução do MVC onde a View é completamente passiva — ela delega toda lógica de apresentação ao Presenter, que a medeia por meio de uma interface, tornando a lógica de UI totalmente testável sem instanciar a tela.

Intenção

Remover toda lógica de apresentação da View e transferi-la ao Presenter, de modo que a View fique tão "burra" que possa ser substituída por um mock nos testes. O Presenter conhece a View apenas por uma interface estreita — e nunca por referências a widgets ou componentes concretos de UI.

MVP surgiu no início dos anos 1990 em trabalhos de Mike Potel (Taligent) e foi popularizado no mundo Android e Windows Forms como solução para o problema que o MVC clássico deixava em aberto: no MVC web, o Controller era fino e delegava ao Model, mas no MVC de aplicações desktop a View tendia a acumular lógica de apresentação — escolha de cor, formatação, habilitação de botões — misturada com eventos de UI.

O MVP resolve isso de forma direta: a View implementa uma interface (por exemplo IUserView) que expõe apenas operações de exibição e notificação de eventos. O Presenter recebe essa interface por injeção e a usa para ler dados do Model e empurrar resultados de volta à View. A View nunca fala com o Model; o Presenter é o único mediador.

Problema

Em interfaces gráficas (desktop, mobile, formulários web complexos), o padrão MVC clássico não define claramente quem é responsável pela lógica de apresentação — aquela camada entre "dado bruto" e "pixel na tela": formatação de valores, habilitação condicional de controles, validação de formulário, escolha de mensagem de erro. As consequências práticas são:

  • View com código de negócio de apresentação: a View começa a conter condicionais como "se o saldo for negativo, exibir em vermelho" — lógica que pertence a quem conhece o estado, não a quem desenha pixels.
  • Testabilidade nula da lógica de UI: para testar se a mensagem de erro aparece corretamente é preciso instanciar a tela inteira — com framework gráfico, thread de UI, evento de clique. O ciclo de feedback é lento e frágil.
  • Acoplamento direto View-Model: se a View lê o Model diretamente (como no MVC clássico com Observer), qualquer mudança no modelo pode obrigar mudança na View — os dois evoluem acoplados.

MVP resolve esses três problemas: o Presenter é testável com um mock da interface IView; a View não conhece o Model; e a lógica de apresentação tem um lugar definido — no Presenter.

Estrutura

  ┌────────────────────────────────────────────────────────────────┐
  │                          Usuário                               │
  │                    (interage com a View)                       │
  └───────────────────────────┬────────────────────────────────────┘
                              │ evento (clique, input)
                              ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                          VIEW                               │
  │  - implementa IView (interface estreita)                    │
  │  - exibe dados que o Presenter empurra                      │
  │  - delega TODOS os eventos ao Presenter                     │
  │  - NÃO conhece o Model                                      │
  └───────────────────────────┬─────────────────────────────────┘
                              │ notifica eventos via IView
                              ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                       PRESENTER                             │
  │  - recebe IView por injeção (nunca a classe concreta)       │
  │  - contém toda a lógica de apresentação                     │
  │  - consulta/atualiza o Model                                │
  │  - empurra resultados chamando métodos da IView             │
  └───────────────────────────┬─────────────────────────────────┘
                              │ usa
                              ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                        MODEL                                │
  │  - dados + regras de negócio                                │
  │  - completamente ignorante da View e do Presenter           │
  └─────────────────────────────────────────────────────────────┘

  Dependências:
    View        → IView          (implementa a interface)
    Presenter   → IView          (depende da abstração, não da View concreta)
    Presenter   → Model          (lê e atualiza dados)
    Model       → (ninguém)      (não conhece View nem Presenter)

Variante: Passive View

Na variante Passive View (a mais comum e estrita), a View não tem absolutamente nenhuma lógica: ela apenas expõe propriedades (campos de texto, labels, estado de botões) que o Presenter lê e escreve. A View não formata dados, não valida entradas, não toma decisões visuais — tudo isso é responsabilidade do Presenter. O ganho é a testabilidade máxima: o Presenter pode ser testado com um mock trivial de IView.

Variante: Supervising Controller

Na variante Supervising Controller (ou Supervising Presenter), a View é um pouco menos passiva: ela executa binding simples e direto com o Model para casos triviais (como exibir um nome de campo sem transformação), enquanto o Presenter intervém apenas para lógica de apresentação mais complexa. Essa variante é um meio-termo entre MVP e MVVM — ganha simplicidade no Presenter ao custo de um pequeno acoplamento residual da View ao Model.

Como funciona

A interface IView

A peça central do MVP é a interface que a View implementa. Ela deve ser estreita — apenas os métodos que o Presenter realmente precisa chamar. Não deve expor widgets, elementos de DOM, componentes de framework ou qualquer tipo concreto de UI. Deve falar em termos de dados e intenções: exibirNomeUsuario(nome: string), mostrarErroEmail(mensagem: string), habilitarBotaoSalvar(habilitado: boolean).

O Presenter

O Presenter é injetado com a IView (e com o Model ou repositório que precisar). Ele registra-se como listener dos eventos da View — ou a View chama diretamente métodos do Presenter quando o usuário age. Quando um evento ocorre, o Presenter executa a lógica, consulta o Model se necessário e chama os métodos da IView para atualizar a tela. O Presenter nunca instancia a View; ele a recebe pronta.

Exemplo de código

// ── MODEL — dados e regras de negócio ────────────────────────
class UsuarioModel {
  buscarPorId(id: string): { nome: string; email: string } | null {
    // acessa repositório; a View nunca chama isto diretamente
    return { nome: 'Ana', email: 'ana@exemplo.com' };
  }
}

// ── IVIEW — interface estreita que a View implementa ─────────
// Só operações de exibição e notificação — nenhum widget concreto.
interface IPerfilView {
  exibirNome(nome: string): void;
  exibirEmail(email: string): void;
  mostrarErro(mensagem: string): void;
  habilitarEdicao(habilitado: boolean): void;
}

// ── PRESENTER — toda a lógica de apresentação ─────────────────
class PerfilPresenter {
  constructor(
    private readonly view: IPerfilView,
    private readonly model: UsuarioModel,
  ) {}

  carregarPerfil(id: string): void {
    const usuario = this.model.buscarPorId(id);
    if (!usuario) {
      this.view.mostrarErro('Usuário não encontrado.');
      this.view.habilitarEdicao(false);
      return;
    }
    this.view.exibirNome(usuario.nome);
    this.view.exibirEmail(usuario.email);
    this.view.habilitarEdicao(true);
  }
}

// ── VIEW CONCRETA — implementa IPerfilView ────────────────────
// Pode ser DOM, React, Android Activity, Windows Form etc.
class PerfilViewDOM implements IPerfilView {
  exibirNome(nome: string): void {
    document.getElementById('nome')!.textContent = nome;
  }
  exibirEmail(email: string): void {
    document.getElementById('email')!.textContent = email;
  }
  mostrarErro(mensagem: string): void {
    document.getElementById('erro')!.textContent = mensagem;
  }
  habilitarEdicao(habilitado: boolean): void {
    (document.getElementById('btn-editar') as HTMLButtonElement)
      .disabled = !habilitado;
  }
}

// ── MOCK DE VIEW — usado nos testes unitários do Presenter ────
class MockPerfilView implements IPerfilView {
  nomExibido = '';
  erroExibido = '';
  edicaoHabilitada = false;
  exibirNome(nome: string): void  { this.nomExibido = nome; }
  exibirEmail(_email: string): void { /* ignorado no teste */ }
  mostrarErro(msg: string): void  { this.erroExibido = msg; }
  habilitarEdicao(h: boolean): void { this.edicaoHabilitada = h; }
}

Note que o PerfilPresenter pode ser testado unitariamente instanciando um MockPerfilView e um UsuarioModel com dados fictícios — sem framework gráfico, sem DOM, sem HTTP. Esse é o ganho central do MVP.

Quando usar

  • Aplicações desktop ou mobile com lógica de apresentação complexa: formulários com validação condicional, habilitação dinâmica de controles, formatação dependente de regras — são cenários onde o Presenter tem trabalho real a fazer e a testabilidade sem UI física é um ganho imediato.
  • Quando a testabilidade da lógica de UI é prioritária: MVP permite testar todo o comportamento visual (o que aparece, quando aparece, em que estado) sem instanciar um único widget ou abrir um browser. Para aplicações Android legadas ou Windows Forms, era a principal estratégia de teste.
  • Quando a View pode ser trocada sem alterar a lógica: ao definir IView como contrato, é possível ter uma View gráfica para produção e uma View de console para testes ou scripts — o Presenter não muda.

Quando evitar

  • Frameworks modernos com data binding nativo (Vue, Angular, React + state management): nesses ambientes o binding declarativo elimina a necessidade de o Presenter "empurrar" dados explicitamente para a View. O padrão adequado passa a ser MVVM ou arquiteturas baseadas em estado reativo (Flux, Redux, Signals).
  • Views com lógica mínima ou nenhuma: se a View é apenas um template estático que renderiza dados sem lógica condicional relevante, a camada Presenter é desnecessária — um MVC simples é mais produtivo.
  • Equipes sem familiaridade com inversão de dependência: a IView é um contrato que precisa ser mantido consistente entre View e Presenter. Se a equipe não estiver disciplinada, a interface vaza detalhes concretos de UI (parâmetros do tipo HTMLElement ou Button), destruindo a testabilidade que o MVP promete.

Prós e contras

Prós

  • Presenter completamente testável sem instanciar a View real — basta um mock da IView.
  • View passiva: pode ser trocada (DOM, Android, console) sem alterar o Presenter.
  • Separação clara entre lógica de apresentação (Presenter) e renderização (View).
  • Model totalmente isolado da camada de apresentação — a View nunca o toca diretamente.
  • Contratos explícitos via IView facilitam o trabalho paralelo entre quem desenvolve a UI e quem implementa a lógica.

Contras

  • Mais código de infraestrutura: para cada View é necessária uma interface IView, um Presenter e testes do Presenter — o MVP multiplica artefatos mesmo para telas simples.
  • Risco de Presenter gordo: sem disciplina, toda a lógica de aplicação migra para o Presenter, que passa a orquestrar casos de uso, validar regras de negócio e formatar dados — misturando responsabilidades que pertencem ao Model.
  • Manutenção da IView: cada mudança na tela que exige um novo dado empurrado pelo Presenter requer atualização da interface, da View concreta e do mock de testes.
  • Menos adequado para UIs reativas modernas: o fluxo "Presenter chama view.exibir()" é imperativo e não se encaixa naturalmente em arquiteturas baseadas em estado observável.

Armadilhas comuns

1. Presenter gordo

A armadilha mais frequente no MVP. O Presenter começa focado em apresentação, mas vai acumulando chamadas a serviços externos, regras de validação de negócio, orquestração de múltiplos casos de uso e até transformações de dados que pertencem ao Model. O resultado é um Presenter com centenas de linhas que mistura apresentação, aplicação e domínio. A regra prática é clara: o Presenter deve delegar ao Model (ou a serviços de aplicação) tudo que não é decisão de como exibir um dado na tela.

Regra prática: se o Presenter contém cálculos de negócio, consultas a banco ou chamadas a APIs externas diretamente (sem passar por serviços ou repositórios), ele está gordo. Extraia essas responsabilidades para o Model ou para uma camada de aplicação.

2. IView que vaza widgets concretos

Se a interface IView define métodos como getEmailTextField(): TextField ou recebe parâmetros do tipo Button ou HTMLInputElement, o Presenter passa a depender do framework gráfico — e a testabilidade colapsa. A IView deve falar apenas em termos de dados e intenções: obterEmailDigitado(): string, habilitarBotaoConfirmar(habilitado: boolean): void. Nenhum tipo de UI concreta deve aparecer nos parâmetros ou retornos da interface.

3. View que bypassa o Presenter

Em aplicações com acesso fácil ao repositório ou ao Model (por injeção de dependência global ou singleton), é tentador a View chamar o Model diretamente "só para ler um dado rápido". Esse bypass destrói a invariante central do MVP: a View não conhece o Model. Uma vez introduzida, essa exceção tende a se multiplicar, degradando o padrão para um MVC mal estruturado.

4. Confundir MVP com MVC renomeado

No MVC web clássico (Laravel, Rails, Django), o Controller recebe a requisição, chama o Model e passa dados para a View renderizar. A View pode até ler dados do Model diretamente em alguns frameworks. No MVP, a View nunca conhece o Model — essa é a distinção definidora. Se o seu "Presenter" ainda passa o objeto Model inteiro para a View renderizar, você tem MVC com o Controller renomeado, não MVP.

Arquiteturas e padrões relacionados

O MVC é o precursor direto do MVP. A diferença central é o papel da View: no MVC clássico, a View pode observar o Model diretamente (via padrão Observer) e se atualizar sem passar pelo Controller; no MVP, a View nunca toca o Model — o Presenter é o único elo entre os dois. MVP pode ser visto como um MVC onde o Controller foi renomeado para Presenter e a View foi tornada completamente passiva.

O Observer é o mecanismo que o MVP usa para que a View notifique o Presenter de eventos de usuário: a View dispara eventos (clique, submit, digitação) e o Presenter é o observador registrado. Alternativamente, a View chama diretamente métodos do Presenter — mas mesmo nesse caso o padrão Observer está implícito no modelo de eventos da plataforma (DOM events, Android listeners).

O MVVM pode ser entendido como uma evolução do MVP para ambientes com data binding nativo. A diferença-chave: no MVP o Presenter empurra dados para a View chamando métodos da IView explicitamente; no MVVM o ViewModel expõe estado observável e o binding sincroniza a View automaticamente — o ViewModel não chama a View, ele não a conhece. Em UIs reativas modernas (Vue, Angular, SwiftUI, Compose), MVVM é mais natural que MVP.

O Humble Object é um padrão de testabilidade descrito por Gerard Meszaros que formaliza exatamente o que o MVP pratica: separar o código difícil de testar (a View com framework gráfico) do código fácil de testar (o Presenter com lógica pura). A View no MVP é literalmente um Humble Object — ela não tem lógica própria para testar.