intermediate Por Mathias Paulenko

Patrón Model-View-Presenter (MVP)

Separa la lógica de presentación de la view introduciendo un presenter que media entre el model y una view pasiva, habilitando código UI testeable.

Temas: design

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Descripción General

El Patrón Model-View-Presenter (MVP) separa una aplicación en tres componentes: el Model (lógica de negocio y datos), la View (display de UI) y el Presenter (mediador que maneja input de usuario y actualiza tanto Model como View). La View es pasiva — delega todas las acciones de usuario al Presenter y es actualizada por el Presenter en respuesta.

MVP es especialmente valioso para crear código UI testeable. Dado que el Presenter contiene toda la lógica de presentación y no tiene dependencia de frameworks de UI, puede ser unit tested en aislamiento con Views mockeadas.

Cuándo Usar

Usa el Patrón MVP cuando:

  • Necesitas lógica UI altamente testeable sin dependencias de browser o GUI
  • La tecnología de view puede cambiar (web, desktop, mobile) pero la lógica de negocio permanece
  • Quieres mantener la view lo más simple y pasiva posible
  • Múltiples views necesitan compartir la misma lógica de presentación

Cuándo Evitar

  • UIs simples donde el overhead de tres componentes separados no está justificado
  • Aplicaciones donde la view necesita ser altamente reactiva (MVVM es mejor)
  • El presenter se convierte en una God class que sabe demasiado de model y view
  • Frameworks que naturalmente favorecen otros patrones (React favorece component-based sobre MVP)

Solución

Python

from typing import Protocol

# Model
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

class UserRepository:
    def __init__(self):
        self._users = {"1": User("Alice", "alice@example.com")}

    def find_by_id(self, user_id: str) -> User:
        return self._users.get(user_id)


# View Interface
class UserView(Protocol):
    def set_name(self, name: str): ...
    def set_email(self, email: str): ...
    def show_error(self, message: str): ...


# Presenter
class UserPresenter:
    def __init__(self, view: UserView, repository: UserRepository):
        self.view = view
        self.repository = repository

    def load_user(self, user_id: str):
        user = self.repository.find_by_id(user_id)
        if user:
            self.view.set_name(user.name)
            self.view.set_email(user.email)
        else:
            self.view.show_error("User not found")

    def save_user(self, user_id: str, name: str, email: str):
        if not name or not email:
            self.view.show_error("Name and email are required")
            return
        # Save logic...
        self.view.set_name(name)
        self.view.set_email(email)


# Concrete View (Console)
class ConsoleUserView:
    def __init__(self):
        self.name = ""
        self.email = ""
        self.error = ""

    def set_name(self, name: str):
        self.name = name
        print(f"Name: {name}")

    def set_email(self, email: str):
        self.email = email
        print(f"Email: {email}")

    def show_error(self, message: str):
        self.error = message
        print(f"Error: {message}")


# Uso
view = ConsoleUserView()
presenter = UserPresenter(view, UserRepository())
presenter.load_user("1")

Java

public class User {
    private final String name;
    private final String email;
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

class UserRepository {
    private final java.util.Map<String, User> users = new java.util.HashMap<>();
    public UserRepository() {
        users.put("1", new User("Alice", "alice@example.com"));
    }
    public User findById(String id) { return users.get(id); }
}

interface UserView {
    void setName(String name);
    void setEmail(String email);
    void showError(String message);
}

class UserPresenter {
    private final UserView view;
    private final UserRepository repository;

    public UserPresenter(UserView view, UserRepository repository) {
        this.view = view;
        this.repository = repository;
    }

    public void loadUser(String userId) {
        User user = repository.findById(userId);
        if (user != null) {
            view.setName(user.getName());
            view.setEmail(user.getEmail());
        } else {
            view.showError("User not found");
        }
    }

    public void saveUser(String userId, String name, String email) {
        if (name == null || email == null || name.isEmpty() || email.isEmpty()) {
            view.showError("Name and email are required");
            return;
        }
        view.setName(name);
        view.setEmail(email);
    }
}

class ConsoleUserView implements UserView {
    public void setName(String name) { System.out.println("Name: " + name); }
    public void setEmail(String email) { System.out.println("Email: " + email); }
    public void showError(String message) { System.out.println("Error: " + message); }
}

// Uso
UserView view = new ConsoleUserView();
UserPresenter presenter = new UserPresenter(view, new UserRepository());
presenter.loadUser("1");

JavaScript

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  constructor() {
    this.users = new Map([['1', new User('Alice', 'alice@example.com')]]);
  }
  findById(id) {
    return this.users.get(id);
  }
}

class UserPresenter {
  constructor(view, repository) {
    this.view = view;
    this.repository = repository;
  }

  loadUser(userId) {
    const user = this.repository.findById(userId);
    if (user) {
      this.view.setName(user.name);
      this.view.setEmail(user.email);
    } else {
      this.view.showError('User not found');
    }
  }

  saveUser(userId, name, email) {
    if (!name || !email) {
      this.view.showError('Name and email are required');
      return;
    }
    this.view.setName(name);
    this.view.setEmail(email);
  }
}

class ConsoleUserView {
  setName(name) { console.log('Name:', name); }
  setEmail(email) { console.log('Email:', email); }
  showError(message) { console.log('Error:', message); }
}

// Uso
const view = new ConsoleUserView();
const presenter = new UserPresenter(view, new UserRepository());
presenter.loadUser('1');

Explicación

MVP divide la responsabilidad de la siguiente manera:

  • Model: Contiene estructuras de datos y reglas de negocio. No tiene conocimiento de la UI.
  • View: Muestra datos y reenvía acciones de usuario al Presenter. Contiene cero lógica.
  • Presenter: Actúa como intermediario.

La característica clave es que la View es pasiva — no extrae datos del Model. Todos los datos fluyen a través del Presenter.

Variantes

VarianteRol de la ViewCaso de Uso
Passive ViewLa view no tiene lógica en absolutoMáxima testeabilidad
Supervising ControllerLa view puede bind propiedades simples directamente al ModelReduce boilerplate del presenter
MVCEl Controller maneja input, la View observa el ModelFrameworks como Rails, Django
MVVMLa View se bind a propiedades del ViewModelUIs reactivas (WPF, Vue, Angular)

Lo que funciona

  • Haz la View una interfaz. Esto habilita unit testing del Presenter con una View mockeada.
  • Mantén la View tonta. Sin lógica de negocio, sin transformación de datos, sin toma de decisiones.
  • El Presenter no debería conocer el framework de UI. Habla a una interfaz abstracta de View.
  • Un Presenter por pantalla. No reutilices un Presenter a través de views no relacionadas.
  • El Model debería ser framework-agnostic. Funciona con o sin UI.

Errores Comunes

  • La View conoce el Model. La View solo debería conocer al Presenter.
  • Presenter filtra código de framework. Importar clases de UI toolkit dificulta el testing.
  • La View no es pasiva. Si la View llama métodos del Model directamente, es MVC, no MVP.
  • Presenter como God class. Si crece demasiado, dividir en presenters específicos por caso de uso.
  • Acoplamiento fuerte entre View y Presenter. Usa interfaces para que cualquiera pueda ser swapeado.

Ejemplos del Mundo Real

Android (temprano)

Antes de Jetpack Compose, los desarrolladores de Android usaban MVP para separar Activities (Views) de Presenters, haciendo la lógica de negocio testeable sin el emulador de Android.

GWT (Google Web Toolkit)

Las aplicaciones GWT comúnmente usaban MVP para separar código de widgets (View) de lógica de aplicación (Presenter) para testeabilidad.

WinForms / WebForms

Los frameworks UI tempranos de Microsoft usaban una variación de MVP donde los archivos code-behind actuaban como Presenters entre la view declarativa y el data model.

Puntos Clave

  • Aplica patrón model-view-presenter (mvp) cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
  • Tests break when the design changes: favor stable contracts over internal structure.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Errores Comunes en Producción

  • Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
  • Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
  • Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
  • Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
  • Ignorar modos de fallo que el patrón no cubre.
  • Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
  • Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
  • Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.

Preguntas frecuentes

Cuál es la diferencia entre MVP y MVC?
En MVC, la View observa el Model directamente. En MVP, toda la comunicación pasa a través del Presenter y la View es pasiva.
Cuál es la diferencia entre MVP y MVVM?
[MVVM](/patterns/model-view-viewmodel-pattern/) usa data binding bidireccional entre View y ViewModel. MVP usa llamadas a métodos explícitas a través de una interfaz.
Puedo usar MVP con React?
Puedes, pero el modelo de componentes de React naturalmente favorece componentes container/presentational o hooks en lugar de MVP clásico.