MVP: Model View Presenter
MVP (Model-View-Presenter) — архитектурный паттерн, в котором Presenter выступает посредником между Model и View: View пассивна и делегирует всю логику Presenter-у.
Зачем нужно
В MVC View часто содержит логику (обработчики событий, форматирование). MVP идёт дальше: View становится максимально «тупой» — только отображает данные и делегирует события Presenter-у. Presenter не знает о конкретном View (только об интерфейсе), что делает его независимо тестируемым. MVP популярен в Android-разработке и был широко используем на фронтенде до появления компонентных фреймворков.
Где используется
- Android разработка (исторически основной паттерн до ViewModel)
- Backbone.js приложения
- Тестирование UI-логики в изоляции от DOM
- Проекты, где важна полная testability презентационного слоя
MVP vs MVC
MVC:
View ←──── Controller ────► Model
View может напрямую читать Model
MVP:
View ←────► Presenter ────► Model
View НЕ знает о Model, только о Presenter
Presenter НЕ знает о конкретном View (только IView)
Реализация MVP
// Model — данные и бизнес-логика
class UserModel {
private users: User = ;
async fetchUsers: Promise<User> {
const response = await fetch('/api/users');
this.users = await response.json();
return this.users;
}
filterByRole(role: string): User {
return this.users.filter(u => u.role === role);
}
}
// View Interface — что Presenter ожидает от View
interface IUserView {
showUsers(users: User): void;
showLoading(isLoading: boolean): void;
showError(message: string): void;
onRoleFilter: (role: string) => void; // колбэк для Presenter
}
// Presenter — вся логика, не зависит от DOM
class UserPresenter {
constructor(
private view: IUserView, // зависимость через интерфейс
private model: UserModel,
) {
// Подписываемся на события View
this.view.onRoleFilter = (role) => this.handleRoleFilter(role);
}
async loadUsers: Promise<void> {
this.view.showLoading(true);
try {
const users = await this.model.fetchUsers;
this.view.showUsers(users);
} catch (error) {
this.view.showError('Не удалось загрузить пользователей');
} finally {
this.view.showLoading(false);
}
}
handleRoleFilter(role: string): void {
const filtered = this.model.filterByRole(role);
this.view.showUsers(filtered);
}
}
// Concrete View — реализует интерфейс
class UserListView implements IUserView {
private container: HTMLElement;
onRoleFilter: (role: string) => void = () => {};
constructor(containerId: string) {
this.container = document.getElementById(containerId)!;
document.getElementById('role-filter')!.addEventListener('change', (e) => {
this.onRoleFilter((e.target as HTMLSelectElement).value);
});
}
showUsers(users: User): void {
this.container.innerHTML = users
.map(u => `<li>${u.name} (${u.role})</li>`)
.join('');
}
showLoading(isLoading: boolean): void {
document.getElementById('loader')!.hidden = !isLoading;
}
showError(message: string): void {
document.getElementById('error')!.textContent = message;
}
}
// Сборка
const view = new UserListView('user-list');
const model = new UserModel();
const presenter = new UserPresenter(view, model);
presenter.loadUsers;
// В тесте: mock View
const mockView: IUserView = {
showUsers: jest.fn,
showLoading: jest.fn,
showError: jest.fn,
onRoleFilter: => {},
};
const presenter = new UserPresenter(mockView, model);
Частые ошибки
- Fat View — логика «просачивается» в View; View должна быть «пассивной», только рендерить и делегировать.
- Presenter с DOM-зависимостями — если Presenter обращается к
document, он перестаёт быть тестируемым без браузера. - MVP вместо MVVM для React — MVVM с его data binding лучше подходит для компонентных фреймворков; MVP — для vanilla JS или легаси.