Lapa Knowledge Base
Веб-архитектуры

MVP

Паттерн архитектуры Model-View-Presenter

MVP — это архитектурный паттерн, который является эволюцией MVC и обеспечивает еще более четкое разделение ответственности между компонентами. В MVP View становится пассивным компонентом, а вся логика представления переносится в Presenter.

Что такое MVP?

MVP — это архитектурный паттерн, который:

  • Максимально разделяет ответственность — каждый компонент имеет четкую роль
  • Делает View пассивным — представление только отображает данные
  • Централизует логику в Presenter — вся логика представления в одном месте
  • Повышает тестируемость — Presenter можно тестировать без UI

Компоненты MVP

Model (Модель)

Модель представляет данные и бизнес-логику приложения.

Ответственности:

  • Структура данных
  • Бизнес-правила
  • Валидация
  • Взаимодействие с внешними источниками данных

View (Представление)

Представление — это пассивный пользовательский интерфейс.

Ответственности:

  • Отображение данных (полученных от Presenter)
  • Передача событий пользователя в Presenter
  • НЕ содержит логики представления
  • НЕ обрабатывает данные

Presenter (Презентер)

Презентер содержит всю логику представления и координирует взаимодействие между Model и View.

Ответственности:

  • Обработка событий от View
  • Вызов методов Model
  • Преобразование данных для отображения
  • Управление состоянием View
  • Валидация пользовательского ввода

Схема взаимодействия

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    View     │    │ Presenter   │    │    Model    │
│             │    │             │    │             │
│ • Display   │◄───┤ • Logic     ├───►│ • Data      │
│ • Events    ├───►│ • Commands  │    │ • Business  │
│ • Passive   │    │ • State     │    │ • API       │
└─────────────┘    └─────────────┘    └─────────────┘
       ▲                   │                   │
       │                   │                   │
       └───────────────────┼───────────────────┘
                    User Interaction

Примеры реализации

MVP в Vanilla JavaScript

Model

// models/UserModel.js
class UserModel {
    constructor() {
        this.users = [];
        this.currentUser = null;
    }
    
    // Получение всех пользователей
    async getAllUsers() {
        try {
            const response = await fetch('/api/users');
            if (!response.ok) {
                throw new Error('Ошибка при загрузке пользователей');
            }
            this.users = await response.json();
            return this.users;
        } catch (error) {
            throw new Error(`Ошибка при загрузке пользователей: ${error.message}`);
        }
    }
    
    // Получение пользователя по ID
    async getUserById(id) {
        try {
            const response = await fetch(`/api/users/${id}`);
            if (!response.ok) {
                throw new Error('Пользователь не найден');
            }
            this.currentUser = await response.json();
            return this.currentUser;
        } catch (error) {
            throw new Error(`Ошибка при загрузке пользователя: ${error.message}`);
        }
    }
    
    // Создание нового пользователя
    async createUser(userData) {
        try {
            const response = await fetch('/api/users', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(userData)
            });
            
            if (!response.ok) {
                throw new Error('Ошибка при создании пользователя');
            }
            
            const newUser = await response.json();
            this.users.push(newUser);
            return newUser;
        } catch (error) {
            throw new Error(`Ошибка при создании пользователя: ${error.message}`);
        }
    }
    
    // Обновление пользователя
    async updateUser(id, userData) {
        try {
            const response = await fetch(`/api/users/${id}`, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(userData)
            });
            
            if (!response.ok) {
                throw new Error('Ошибка при обновлении пользователя');
            }
            
            const updatedUser = await response.json();
            const index = this.users.findIndex(user => user.id === id);
            if (index !== -1) {
                this.users[index] = updatedUser;
            }
            return updatedUser;
        } catch (error) {
            throw new Error(`Ошибка при обновлении пользователя: ${error.message}`);
        }
    }
    
    // Удаление пользователя
    async deleteUser(id) {
        try {
            const response = await fetch(`/api/users/${id}`, {
                method: 'DELETE'
            });
            
            if (!response.ok) {
                throw new Error('Ошибка при удалении пользователя');
            }
            
            this.users = this.users.filter(user => user.id !== id);
            return true;
        } catch (error) {
            throw new Error(`Ошибка при удалении пользователя: ${error.message}`);
        }
    }
    
    // Валидация данных пользователя
    validateUser(userData) {
        const errors = {};
        
        if (!userData.name || userData.name.trim().length < 2) {
            errors.name = 'Имя должно содержать минимум 2 символа';
        }
        
        if (!userData.email || !this.isValidEmail(userData.email)) {
            errors.email = 'Некорректный email адрес';
        }
        
        if (!userData.password || userData.password.length < 6) {
            errors.password = 'Пароль должен содержать минимум 6 символов';
        }
        
        return errors;
    }
    
    isValidEmail(email) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(email);
    }
}

export default UserModel;

View

// views/UserView.js
class UserView {
    constructor() {
        this.container = document.getElementById('user-container');
        this.form = document.getElementById('user-form');
        this.userList = document.getElementById('user-list');
        this.errorContainer = document.getElementById('error-container');
        this.loadingIndicator = document.getElementById('loading-indicator');
        
        this.bindEvents();
    }
    
    bindEvents() {
        // События формы
        if (this.form) {
            this.form.addEventListener('submit', (e) => {
                e.preventDefault();
                const formData = this.getFormData();
                this.onFormSubmit(formData);
            });
        }
        
        // События кнопок
        const addButton = document.getElementById('add-user-btn');
        if (addButton) {
            addButton.addEventListener('click', () => {
                this.onAddUserClick();
            });
        }
        
        const cancelButton = document.getElementById('cancel-btn');
        if (cancelButton) {
            cancelButton.addEventListener('click', () => {
                this.onCancelClick();
            });
        }
    }
    
    // Отображение списка пользователей
    displayUsers(users) {
        if (!this.userList) return;
        
        this.userList.innerHTML = users.map(user => `
            <div class="user-card" data-user-id="1">
                <h3>Имя пользователя</h3>
                <p>email@example.com</p>
                <span class="role-badge user">user</span>
                <div class="user-actions">
                    <button class="edit-btn" data-user-id="1">Редактировать</button>
                    <button class="delete-btn" data-user-id="1">Удалить</button>
                </div>
            </div>
        `).join('');
        
        // Привязка событий к кнопкам
        this.bindUserActions();
    }
    
    bindUserActions() {
        // Кнопки редактирования
        const editButtons = this.userList.querySelectorAll('.edit-btn');
        editButtons.forEach(button => {
            button.addEventListener('click', (e) => {
                const userId = e.target.getAttribute('data-user-id');
                this.onEditUserClick(userId);
            });
        });
        
        // Кнопки удаления
        const deleteButtons = this.userList.querySelectorAll('.delete-btn');
        deleteButtons.forEach(button => {
            button.addEventListener('click', (e) => {
                const userId = e.target.getAttribute('data-user-id');
                this.onDeleteUserClick(userId);
            });
        });
    }
    
    // Отображение формы
    showForm(user = null) {
        if (!this.form) return;
        
        this.form.style.display = 'block';
        this.userList.style.display = 'none';
        
        if (user) {
            // Режим редактирования
            document.getElementById('user-id').value = user.id;
            document.getElementById('user-name').value = user.name;
            document.getElementById('user-email').value = user.email;
            document.getElementById('user-role').value = user.role;
            document.getElementById('form-title').textContent = 'Редактирование пользователя';
            document.getElementById('submit-btn').textContent = 'Обновить';
        } else {
            // Режим создания
            this.clearForm();
            document.getElementById('form-title').textContent = 'Создание пользователя';
            document.getElementById('submit-btn').textContent = 'Создать';
        }
    }
    
    // Скрытие формы
    hideForm() {
        if (!this.form) return;
        
        this.form.style.display = 'none';
        this.userList.style.display = 'block';
        this.clearForm();
    }
    
    // Очистка формы
    clearForm() {
        if (!this.form) return;
        
        document.getElementById('user-id').value = '';
        document.getElementById('user-name').value = '';
        document.getElementById('user-email').value = '';
        document.getElementById('user-role').value = 'user';
        document.getElementById('user-password').value = '';
    }
    
    // Получение данных формы
    getFormData() {
        return {
            id: document.getElementById('user-id').value,
            name: document.getElementById('user-name').value,
            email: document.getElementById('user-email').value,
            role: document.getElementById('user-role').value,
            password: document.getElementById('user-password').value
        };
    }
    
    // Отображение ошибок валидации
    displayValidationErrors(errors) {
        this.clearErrors();
        
        Object.keys(errors).forEach(field => {
            const errorElement = document.createElement('div');
            errorElement.className = 'error-message';
            errorElement.textContent = errors[field];
            
            const fieldElement = document.getElementById('user-' + field);
            if (fieldElement) {
                fieldElement.parentNode.appendChild(errorElement);
                fieldElement.classList.add('error');
            }
        });
    }
    
    // Очистка ошибок
    clearErrors() {
        const errorMessages = this.form.querySelectorAll('.error-message');
        errorMessages.forEach(error => error.remove());
        
        const errorFields = this.form.querySelectorAll('.error');
        errorFields.forEach(field => field.classList.remove('error'));
    }
    
    // Отображение общей ошибки
    displayError(message) {
        if (!this.errorContainer) return;
        
        this.errorContainer.innerHTML = `
            <div class="alert alert-error">
                Сообщение об ошибке
            </div>
        `;
    }
    
    // Очистка ошибок
    clearError() {
        if (!this.errorContainer) return;
        
        this.errorContainer.innerHTML = '';
    }
    
    // Отображение состояния загрузки
    showLoading() {
        if (this.loadingIndicator) {
            this.loadingIndicator.style.display = 'block';
        }
    }
    
    // Скрытие состояния загрузки
    hideLoading() {
        if (this.loadingIndicator) {
            this.loadingIndicator.style.display = 'none';
        }
    }
    
    // Отображение сообщения об успехе
    showSuccess(message) {
        if (!this.errorContainer) return;
        
        this.errorContainer.innerHTML = `
            <div class="alert alert-success">
                Сообщение об ошибке
            </div>
        `;
        
        setTimeout(() => {
            this.clearError();
        }, 3000);
    }
    
    // Callback методы (вызываются Presenter'ом)
    onFormSubmit(formData) {
        // Переопределяется в Presenter
    }
    
    onAddUserClick() {
        // Переопределяется в Presenter
    }
    
    onEditUserClick(userId) {
        // Переопределяется в Presenter
    }
    
    onDeleteUserClick(userId) {
        // Переопределяется в Presenter
    }
    
    onCancelClick() {
        // Переопределяется в Presenter
    }
}

export default UserView;

Presenter

// presenters/UserPresenter.js
import UserModel from '../models/UserModel.js';
import UserView from '../views/UserView.js';

class UserPresenter {
    constructor(model, view) {
        this.model = model;
        this.view = view;
        
        this.bindViewEvents();
    }
    
    bindViewEvents() {
        // Привязка callback'ов к View
        this.view.onFormSubmit = (formData) => this.handleFormSubmit(formData);
        this.view.onAddUserClick = () => this.handleAddUserClick();
        this.view.onEditUserClick = (userId) => this.handleEditUserClick(userId);
        this.view.onDeleteUserClick = (userId) => this.handleDeleteUserClick(userId);
        this.view.onCancelClick = () => this.handleCancelClick();
    }
    
    // Инициализация презентера
    async init() {
        try {
            this.view.showLoading();
            await this.loadUsers();
        } catch (error) {
            this.view.displayError('Ошибка при загрузке пользователей');
        } finally {
            this.view.hideLoading();
        }
    }
    
    // Загрузка пользователей
    async loadUsers() {
        try {
            const users = await this.model.getAllUsers();
            this.view.displayUsers(users);
            this.view.clearError();
        } catch (error) {
            this.view.displayError(error.message);
        }
    }
    
    // Обработка отправки формы
    async handleFormSubmit(formData) {
        try {
            this.view.showLoading();
            this.view.clearErrors();
            
            // Валидация данных
            const errors = this.model.validateUser(formData);
            if (Object.keys(errors).length > 0) {
                this.view.displayValidationErrors(errors);
                return;
            }
            
            // Сохранение пользователя
            if (formData.id) {
                // Обновление существующего пользователя
                await this.model.updateUser(formData.id, formData);
                this.view.showSuccess('Пользователь успешно обновлен');
            } else {
                // Создание нового пользователя
                await this.model.createUser(formData);
                this.view.showSuccess('Пользователь успешно создан');
            }
            
            // Обновление списка и скрытие формы
            await this.loadUsers();
            this.view.hideForm();
            
        } catch (error) {
            this.view.displayError(error.message);
        } finally {
            this.view.hideLoading();
        }
    }
    
    // Обработка клика "Добавить пользователя"
    handleAddUserClick() {
        this.view.showForm();
        this.view.clearError();
    }
    
    // Обработка клика "Редактировать пользователя"
    async handleEditUserClick(userId) {
        try {
            this.view.showLoading();
            const user = await this.model.getUserById(userId);
            this.view.showForm(user);
            this.view.clearError();
        } catch (error) {
            this.view.displayError(error.message);
        } finally {
            this.view.hideLoading();
        }
    }
    
    // Обработка клика "Удалить пользователя"
    async handleDeleteUserClick(userId) {
        try {
            // Подтверждение удаления
            const user = this.model.users.find(u => u.id == userId);
            if (!user) {
                this.view.displayError('Пользователь не найден');
                return;
            }
            
            if (!confirm('Вы уверены, что хотите удалить пользователя?')) {
                return;
            }
            
            this.view.showLoading();
            await this.model.deleteUser(userId);
            this.view.showSuccess('Пользователь успешно удален');
            await this.loadUsers();
            
        } catch (error) {
            this.view.displayError(error.message);
        } finally {
            this.view.hideLoading();
        }
    }
    
    // Обработка клика "Отмена"
    handleCancelClick() {
        this.view.hideForm();
        this.view.clearError();
    }
}

export default UserPresenter;

Инициализация приложения

// app.js
import UserModel from './models/UserModel.js';
import UserView from './views/UserView.js';
import UserPresenter from './presenters/UserPresenter.js';

// Инициализация приложения
document.addEventListener('DOMContentLoaded', async () => {
    const model = new UserModel();
    const view = new UserView();
    const presenter = new UserPresenter(model, view);
    
    await presenter.init();
});

MVP в React (с хуками)

Model (Custom Hook)

// hooks/useUserModel.js
import { useState, useCallback } from 'react';

export const useUserModel = () => {
    const [users, setUsers] = useState([]);
    const [currentUser, setCurrentUser] = useState(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    
    const validateUser = useCallback((userData) => {
        const errors = {};
        
        if (!userData.name || userData.name.trim().length < 2) {
            errors.name = 'Имя должно содержать минимум 2 символа';
        }
        
        if (!userData.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) {
            errors.email = 'Некорректный email адрес';
        }
        
        if (!userData.password || userData.password.length < 6) {
            errors.password = 'Пароль должен содержать минимум 6 символов';
        }
        
        return errors;
    }, []);
    
    const getAllUsers = useCallback(async () => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch('/api/users');
            if (!response.ok) {
                throw new Error('Ошибка при загрузке пользователей');
            }
            const usersData = await response.json();
            setUsers(usersData);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    }, []);
    
    const getUserById = useCallback(async (id) => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch(`/api/users/${id}`);
            if (!response.ok) {
                throw new Error('Пользователь не найден');
            }
            const userData = await response.json();
            setCurrentUser(userData);
            return userData;
        } catch (err) {
            setError(err.message);
            throw err;
        } finally {
            setLoading(false);
        }
    }, []);
    
    const createUser = useCallback(async (userData) => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch('/api/users', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(userData)
            });
            
            if (!response.ok) {
                throw new Error('Ошибка при создании пользователя');
            }
            
            const newUser = await response.json();
            setUsers(prev => [...prev, newUser]);
            return newUser;
        } catch (err) {
            setError(err.message);
            throw err;
        } finally {
            setLoading(false);
        }
    }, []);
    
    const updateUser = useCallback(async (id, userData) => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch(`/api/users/${id}`, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(userData)
            });
            
            if (!response.ok) {
                throw new Error('Ошибка при обновлении пользователя');
            }
            
            const updatedUser = await response.json();
            setUsers(prev => prev.map(user => 
                user.id === id ? updatedUser : user
            ));
            return updatedUser;
        } catch (err) {
            setError(err.message);
            throw err;
        } finally {
            setLoading(false);
        }
    }, []);
    
    const deleteUser = useCallback(async (id) => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch(`/api/users/${id}`, {
                method: 'DELETE'
            });
            
            if (!response.ok) {
                throw new Error('Ошибка при удалении пользователя');
            }
            
            setUsers(prev => prev.filter(user => user.id !== id));
        } catch (err) {
            setError(err.message);
            throw err;
        } finally {
            setLoading(false);
        }
    }, []);
    
    return {
        users,
        currentUser,
        loading,
        error,
        validateUser,
        getAllUsers,
        getUserById,
        createUser,
        updateUser,
        deleteUser
    };
};

Presenter (Custom Hook)

// hooks/useUserPresenter.js
import { useState, useCallback } from 'react';
import { useUserModel } from './useUserModel';

export const useUserPresenter = () => {
    const model = useUserModel();
    const [showForm, setShowForm] = useState(false);
    const [editingUser, setEditingUser] = useState(null);
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        role: 'user',
        password: ''
    });
    const [formErrors, setFormErrors] = useState({});
    
    const handleLoadUsers = useCallback(async () => {
        await model.getAllUsers();
    }, [model]);
    
    const handleShowCreateForm = useCallback(() => {
        setEditingUser(null);
        setFormData({
            name: '',
            email: '',
            role: 'user',
            password: ''
        });
        setFormErrors({});
        setShowForm(true);
    }, []);
    
    const handleShowEditForm = useCallback(async (userId) => {
        try {
            const user = await model.getUserById(userId);
            setEditingUser(user);
            setFormData({
                name: user.name,
                email: user.email,
                role: user.role,
                password: ''
            });
            setFormErrors({});
            setShowForm(true);
        } catch (error) {
            // Ошибка уже обработана в модели
        }
    }, [model]);
    
    const handleHideForm = useCallback(() => {
        setShowForm(false);
        setEditingUser(null);
        setFormData({
            name: '',
            email: '',
            role: 'user',
            password: ''
        });
        setFormErrors({});
    }, []);
    
    const handleFormSubmit = useCallback(async () => {
        setFormErrors({});
        
        // Валидация
        const errors = model.validateUser(formData);
        if (Object.keys(errors).length > 0) {
            setFormErrors(errors);
            return;
        }
        
        try {
            if (editingUser) {
                await model.updateUser(editingUser.id, formData);
            } else {
                await model.createUser(formData);
            }
            
            setShowForm(false);
            setEditingUser(null);
            setFormData({
                name: '',
                email: '',
                role: 'user',
                password: ''
            });
        } catch (error) {
            // Ошибка уже обработана в модели
        }
    }, [model, formData, editingUser]);
    
    const handleDeleteUser = useCallback(async (user) => {
        if (window.confirm(`Вы уверены, что хотите удалить пользователя "${user.name}"?`)) {
            await model.deleteUser(user.id);
        }
    }, [model]);
    
    const handleFormDataChange = useCallback((field, value) => {
        setFormData(prev => ({
            ...prev,
            [field]: value
        }));
        
        // Очистка ошибки для поля при изменении
        if (formErrors[field]) {
            setFormErrors(prev => ({
                ...prev,
                [field]: ''
            }));
        }
    }, [formErrors]);
    
    return {
        // Данные модели
        users: model.users,
        loading: model.loading,
        error: model.error,
        
        // Состояние формы
        showForm,
        editingUser,
        formData,
        formErrors,
        
        // Методы презентера
        handleLoadUsers,
        handleShowCreateForm,
        handleShowEditForm,
        handleHideForm,
        handleFormSubmit,
        handleDeleteUser,
        handleFormDataChange
    };
};

View (React Component)

// components/UserManagement.jsx
import React, { useEffect } from 'react';
import { useUserPresenter } from '../hooks/useUserPresenter';

const UserManagement = () => {
    const presenter = useUserPresenter();
    
    useEffect(() => {
        presenter.handleLoadUsers();
    }, [presenter]);
    
    return (
        <div className="user-management">
            <div className="header">
                <h1>Управление пользователями</h1>
                <button 
                    onClick={presenter.handleShowCreateForm}
                    className="btn btn-primary"
                >
                    Добавить пользователя
                </button>
            </div>
            
            {presenter.loading && (
                <div className="loading">Загрузка...</div>
            )}
            
            {presenter.error && (
                <div className="error">{presenter.error}</div>
            )}
            
            {!presenter.loading && presenter.users.length > 0 && (
                <div className="users-grid">
                    {presenter.users.map(user => (
                        <div key={user.id} className="user-card">
                            <h3>{user.name}</h3>
                            <p>{user.email}</p>
                            <span className={`role-badge ${user.role}`}>
                                {user.role}
                            </span>
                            <div className="user-actions">
                                <button 
                                    onClick={() => presenter.handleShowEditForm(user.id)}
                                    className="btn btn-secondary"
                                >
                                    Редактировать
                                </button>
                                <button 
                                    onClick={() => presenter.handleDeleteUser(user)}
                                    className="btn btn-danger"
                                >
                                    Удалить
                                </button>
                            </div>
                        </div>
                    ))}
                </div>
            )}
            
            {presenter.showForm && (
                <div className="form-modal">
                    <div className="form-content">
                        <h2>
                            {presenter.editingUser ? 'Редактирование' : 'Создание'} пользователя
                        </h2>
                        
                        <form onSubmit={(e) => {
                            e.preventDefault();
                            presenter.handleFormSubmit();
                        }}>
                            <div className="form-group">
                                <label htmlFor="name">Имя:</label>
                                <input
                                    id="name"
                                    type="text"
                                    value={presenter.formData.name}
                                    onChange={(e) => presenter.handleFormDataChange('name', e.target.value)}
                                    className={presenter.formErrors.name ? 'error' : ''}
                                    required
                                />
                                {presenter.formErrors.name && (
                                    <span className="error-message">
                                        {presenter.formErrors.name}
                                    </span>
                                )}
                            </div>
                            
                            <div className="form-group">
                                <label htmlFor="email">Email:</label>
                                <input
                                    id="email"
                                    type="email"
                                    value={presenter.formData.email}
                                    onChange={(e) => presenter.handleFormDataChange('email', e.target.value)}
                                    className={presenter.formErrors.email ? 'error' : ''}
                                    required
                                />
                                {presenter.formErrors.email && (
                                    <span className="error-message">
                                        {presenter.formErrors.email}
                                    </span>
                                )}
                            </div>
                            
                            <div className="form-group">
                                <label htmlFor="role">Роль:</label>
                                <select
                                    id="role"
                                    value={presenter.formData.role}
                                    onChange={(e) => presenter.handleFormDataChange('role', e.target.value)}
                                >
                                    <option value="user">Пользователь</option>
                                    <option value="admin">Администратор</option>
                                </select>
                            </div>
                            
                            <div className="form-group">
                                <label htmlFor="password">Пароль:</label>
                                <input
                                    id="password"
                                    type="password"
                                    value={presenter.formData.password}
                                    onChange={(e) => presenter.handleFormDataChange('password', e.target.value)}
                                    className={presenter.formErrors.password ? 'error' : ''}
                                    required={!presenter.editingUser}
                                />
                                {presenter.formErrors.password && (
                                    <span className="error-message">
                                        {presenter.formErrors.password}
                                    </span>
                                )}
                            </div>
                            
                            <div className="form-actions">
                                <button type="submit" className="btn btn-primary">
                                    {presenter.editingUser ? 'Обновить' : 'Создать'}
                                </button>
                                <button 
                                    type="button" 
                                    onClick={presenter.handleHideForm}
                                    className="btn btn-secondary"
                                >
                                    Отмена
                                </button>
                            </div>
                        </form>
                    </div>
                </div>
            )}
        </div>
    );
};

export default UserManagement;

Преимущества MVP

Плюсы:

  • Максимальная тестируемость — Presenter можно тестировать без UI
  • Четкое разделение ответственности — каждый компонент имеет свою роль
  • Пассивный View — представление не содержит логики
  • Легкость отладки — логика централизована в Presenter
  • Переиспользование — Presenter можно использовать с разными View

Минусы:

  • Больше кода — больше абстракций и слоев
  • Сложность — требует понимания паттерна
  • Производительность — дополнительные слои абстракции
  • Кривая обучения — сложнее для начинающих

Когда использовать MVP

Подходит для:

  • Сложные приложения — с множественной бизнес-логикой
  • Высокие требования к тестированию — нужна максимальная тестируемость
  • Командная разработка — четкое разделение ответственности
  • Долгосрочные проекты — нужна поддерживаемость

Не подходит для:

  • Простые приложения — избыточная сложность
  • Быстрые прототипы — слишком много кода
  • Высоконагруженные системы — производительность может страдать

Сравнение с другими паттернами

Вставить таблицу!


MVP — это идеальный выбор для приложений, где тестируемость и четкое разделение ответственности являются приоритетом!

Copyright © 2026