Продолжаем обсуждать код из прошлого урока.

Когда разработчики начали применять MVC в настольных и мобильных приложениях, довольно быстро стало ясно: идея разделения слоёв отличная, но классический вариант начинает «хромать» по мере роста интерфейса. Давайте соберём ключевые проблемы вместе, но коротко и по делу.

View всё ещё было слишком «умным».

Мы вынесли бизнес-логику в модель, контроллер стал посредником, но сама форма продолжала знать о модели, подписываться на её события, решать, какие цвета выбрать и что показать. В больших интерфейсах это приводило к тому, что View превращалось в смешение визуальных правил, условий и части поведения.

Контроллеры разрастались и привязывались к конкретным формам.

Пока экран один — всё выглядит аккуратно. Но как только экранов становится много, контроллер начинает обрастать условиями, исключениями и особенностями конкретного UI. Он превращается в тяжёлый одноразовый класс, который сложно переиспользовать и ещё сложнее поддерживать.

View обновлялось сразу из двух мест.

Модель толкала форму через событие StateChanged, а контроллер — через вызовы вроде ShowValidationError(). Управление интерфейсом раздваивалось, и становилось неясно, кто в итоге главный: модель или контроллер. Это создавало путаницу и нарушало чистое разделение ролей.

Именно здесь у разработчиков и возникла мысль: раз контроллер всё равно частично управляет View, почему бы не сделать его единым центром управления интерфейсом? Пусть он сам слушает модель, сам принимает решения, и сам вызывает нужные методы View — без смешивания источников обновления. Именно из-за этих накопившихся проблем — перегруженных View и стремительно разрастающихся контроллеров — разработчики начали искать более удобную и управляемую архитектуру для приложений с пользовательским интерфейсом.

MVC постоянно дорабатывался и модернизировался, появлялись его вариации, и одним из этапов этой эволюции стал паттерн MVP (Model–View–Presenter).

Он был предложен в начале 90-х, когда индустрия уже активно работала с оконными интерфейсами. К тому моменту стало очевидно, что классический MVC тяжело переносится на большие и сложные UI-системы: логика расползается по разным слоям, View остаётся слишком объёмной, контроллеры быстро превращаются в неподъёмные конструкции.

MVP стал ответом на эту проблему. Его цель — сделать View максимально тонкой, а логику управления интерфейсом — чище, предсказуемее и полностью сконцентрированной в одном месте.

Теперь давайте посмотрим: что же изменилось?

Уже из названия видно, что вместо привычного Controller появился новый участник...

P — это Presenter, и он полностью заменил собой контроллер.

По сути, Presenter — это контроллер, который забрал у View ту часть логики, которая ей вообще не положена. Мы уже говорили, что в MVC представление всё ещё оставалось слишком «умным»: оно знало о модели, подписывалось на её события, решало, какие цвета выбрать, как отрисовывать состояние и что делать после пересчёта.

Presenter полностью меняет эту картину.

Теперь он становится единственным центром управления интерфейсом.

Именно Presenter решает:

что показывать (раньше это делал View),

когда обновлять экран (раньше это делал View),

как реагировать на изменения модели (раньше это делал View),

какие визуальные действия нужно выполнить (раньше это делал View),

и в каком порядке должен происходить сценарий (раньше это делал View).

И вот здесь появляется ключевое отличие.

Раньше, в MVC, схема была такой: View получает действие от пользователя → вызывает контроллер → тот обновляет модель → модель поднимает событие → View сама решает, как обновиться. То есть View активно участвует в логике поведения и подписан на модель.

Кроме того, часть управления всё равно приходила и от контроллера — вспомните вызов ShowValidationError().

В MVP это устраняется.

Теперь View обновляется только по команде Presenter.

View больше не подписывается на модель, не отслеживает её состояние и не принимает решения самостоятельно. Она превращается в максимально простой, "глупый" слой: выполни то, что сказал Presenter.

И Presenter действительно становится хозяином всего интерфейса: он слушает модель, он принимает решения, он вызывает методы View, он контролирует весь сценарий сверху донизу.

Так MVP делает структуру экрана более чистой, а логику — более предсказуемой и управляемой.

Но это ещё не всё. По мере развития проектов стало проявляться ещё одно важное преимущество MVP — гибкость при работе с множеством экранов и их заменой.

Вспомните, как это выглядело в MVC.

Если у вас менялось представление — например, один экран заменялся другим, появлялся новый вариант отображения или вы хотите использовать другую форму для той же логики — то простой и безболезненной замены не получалось. Контроллер был жёстко привязан к конкретному View: он знал его поля, его контролы, его события, его структуру.

Хотите заменить одну форму на другую? Приходилось переписывать контроллер или создавать новый.

Хотите, чтобы одна и та же логика работала на нескольких экранах? Приходилось копировать код или создавать почти такие же контроллеры.

И это становилось особенно болезненно, когда приложение росло: появлялись десятки экранов, сложные переходы между ними, разные варианты отображения одного и того же сценария. Классический MVC просто не давал гибкого способа переключаться между различными View.

MVP решает эту проблему очень аккуратно и элегантно. Презентер работает не с конкретной формой, а с интерфейсом View.

То есть вместо того чтобы зависеть от MainForm, он зависит от, например, IView. Под которую Вы можете подложить любой экран, какой только захотите. Но давайте к практике. Модель остаётся почти такой же, только теперь не поднимает StateChanged, потому что View её больше не будет слушать.

Presenter сам будет слушать модель.

public class CustomerModel
{
    public string Name { get; private set; } = string.Empty;
    public int Age { get; private set; }
    public bool IsStudent { get; private set; }

    public string Category { get; private set; } = string.Empty;
    public int Discount { get; private set; }

    public void Update(string name, int age, bool isStudent)
    {
        Name = name?.Trim() ?? string.Empty;
        Age = age;
        IsStudent = isStudent;
    }

    public string? Validate()
    {
        if (string.IsNullOrWhiteSpace(Name))
            return "Имя клиента не должно быть пустым.";

        if (Age < 0 || Age > 120)
            return "Возраст клиента задан некорректно.";

        return null;
    }

    public void Recalculate()
    {
        Category = CalculateCategory();
        Discount = CalculateDiscount();
    }

    private string CalculateCategory()
    {
        if (Age < 18)
            return "Несовершеннолетний";
        if (Age <= 24)
            return "Молодой клиент";
        if (Age <= 59)
            return "Взрослый клиент";
        return "Пожилой клиент";
    }

    private int CalculateDiscount()
    {
        int discount = 0;

        if (IsStudent)
            discount += 10;

        if (Age < 18)
            discount += 5;
        else if (Age > 60)
            discount += 7;

        if (discount > 30)
            discount = 30;

        return discount;
    }
}

Важно: мы убрали событие StateChanged. Теперь модель никому не «кричит» об изменениях. Её никто не слушает напрямую — все взаимодействие с внешним миром идёт через Presenter.

Дальше мы делаем ключевой шаг MVP — вводим абстракцию представления. Мы больше не работаем с конкретной формой MainForm. Нам нужен любой экран, который соответствует простому контракту:

умеет предоставить имя, возраст и статус студента,

умеет сообщить, что пользователь нажал кнопку «пересчитать»,

умеет показать ошибку,

умеет показать результат.

То есть нам уже неважно, как именно экран нарисован. Это может быть WinForms-форма, WPF-окно, мобильный экран, страница сайта — главное, чтобы он умел делать вот это:

public interface ICustomerView
{
    // Ввод пользователя
    string CustomerName { get; }
    int CustomerAge { get; }
    bool IsStudent { get; }

    // Событие: пользователь нажал кнопку
    event EventHandler CalculateRequested;

    // Методы обновления UI
    void ShowValidationError(string message);
    void ShowResult(string category, int discount, string title);
}

То есть мы описали не форму, а ролевую модель экрана: что он должен уметь, чтобы с ним мог работать презентер.

Теперь посмотрим на Presenter.

Он действительно похож на контроллер из нашего MVC-варианта, но теперь играет гораздо более мощную роль.

Он больше не работает с конкретной формой, только с ICustomerView.

Он сам забирает данные у View.

Он также сам заставляет модель обновиться.

И именно он теперь и слушает модель (логически), а не View.

public class CustomerPresenter
{
    private readonly CustomerModel _model;
    private readonly ICustomerView _view;

    public CustomerPresenter(CustomerModel model, ICustomerView view)
    {
        _model = model;
        _view = view;

        // Подписываемся на событие View
        _view.CalculateRequested += OnCalculateRequested;
    }

    private void OnCalculateRequested(object? sender, EventArgs e)
    {
        // 1. Берём данные из View
        _model.Update(_view.CustomerName, _view.CustomerAge, _view.IsStudent);

        // 2. Проверяем корректность
        var error = _model.Validate();
        if (error != null)
        {
            _view.ShowValidationError(error);
            return;
        }

        // 3. Считаем
        _model.Recalculate();

        // 4. Обновляем View напрямую
        string title = $"Клиент: {_model.Name} ({_model.Age} лет)";
        _view.ShowResult(_model.Category, _model.Discount, title);
    }
}

То есть Presenter забрал у View и логику работы с моделью, и решение, когда обновлять интерфейс.

View больше не подписывается ни на какие события модели, не знает о её существовании и не принимает решений о поведении — она только выполняет команды Presenter’а.

Теперь посмотрим на нашу форму.

MainForm реализует интерфейс ICustomerView и превращается в тонкий слой отображения.

public partial class MainForm : Form, ICustomerView
{
    private CustomerPresenter _presenter;

    public MainForm()
    {
        InitializeComponent();

        var model = new CustomerModel();
        _presenter = new CustomerPresenter(model, this);
    }

    // ----- ICustomerView -----

    public string CustomerName => txtName.Text;
    public int CustomerAge => (int)nudAge.Value;
    public bool IsStudent => chkStudent.Checked;

    public event EventHandler? CalculateRequested;

    public void ShowValidationError(string message)
    {
        MessageBox.Show(message, "Ошибка ввода",
            MessageBoxButtons.OK, MessageBoxIcon.Warning);
    }

    public void ShowResult(string category, int discount, string title)
    {
        lblCategory.Text = category;
        lblDiscount.Text = $"{discount}%";

        Color discountColor;
        if (discount == 0)
            discountColor = Color.Red;
        else if (discount < 15)
            discountColor = Color.Orange;
        else
            discountColor = Color.Green;

        lblDiscount.ForeColor = discountColor;
        Text = title;
    }

    // ----- Обработчик кнопки -----

    private void btnCalculate_Click(object sender, EventArgs e)
    {
        CalculateRequested?.Invoke(this, EventArgs.Empty);
    }
}

Обратите внимание на важную деталь.

Часть логики всё ещё остаётся во View — например, выбор цвета для скидки. Это нормально и правильно: такая логика относится не к поведению, а к визуальному оформлению.

И самое важное: теперь форма вообще ничего не знает о модели. Она знает только Presenter’а через интерфейс ICustomerView. Модель полностью скрыта за ним. Переход от классического MVC к MVP позволил нам полностью решить те проблемы, которые становились всё заметнее по мере роста интерфейса. Мы убрали перегруженность View, избавились от раздваивания управления интерфейсом и наконец-то получили действительно централизованную логику поведения.

В MVP Presenter взял на себя почти всё, что раньше было размазано между контроллером и представлением. Теперь именно он — главный координатор всего экрана. Если раньше View могло слушать модель напрямую и само решать, как именно обновляться, то теперь вся эта ответственность полностью находится у Presenter. Он принимает события от представления, обновляет модель, проверяет корректность данных, запускает пересчёт, затем анализирует результат и отдаёт View уже готовую информацию для отображения.

Получается, весь поток данных теперь проходит строго через Presenter. Он — единственный, кто знает, когда и как менять модель, что показывать пользователю и когда обновлять интерфейс. Поэтому Presenter действительно выглядит самым «загруженным» участником схемы, но это именно то, что даёт нам структурированность и предсказуемость поведения. А остальные два участника — Model и View — наоборот, становятся максимально независимыми. Модель отвечает только за данные и правила, не зная ничего ни про форму, ни про кнопку. View показывает то, что ей скажут, и не принимает решений о логике работы приложения.

И ещё одно важное следствие: теперь мы можем спокойно работать не с конкретным экраном, а с абстракцией. Presenter зависит только от интерфейса ICustomerView, и ему совершенно всё равно, какой именно экран его реализует. Это может быть текущая форма, новая форма, редизайн интерфейса или вообще другой UI-фреймворк. Если завтра Вы захотите переписать экран полностью — достаточно создать новую форму, реализовать тот же интерфейс, и Presenter будет продолжать работать без каких-либо изменений. Вся логика поведения останется нетронутой.

То есть MVP дал нам две ключевые вещи:

чистую, централизованную логику поведения через Presenter,

гибкость и независимость View благодаря интерфейсу ICustomerView.

Так мы ушли от минусов MVC и получили архитектуру, которая лучше подходит для больших, живых, постоянно меняющихся приложений.

Шло время, приложения продолжали расти, в них появлялись новые экраны, новые сценарии, всё больше условий и вариантов поведения. И очень скоро стало заметно, что у MVP тоже есть своя теневая сторона. Мы с Вами уже видим, что Presenter получился довольно «нагруженным», а по мере роста приложений это ощущение только усиливалось.

По сути, да, мы решали часть проблем MVC, но не столько устранили их, сколько сдвинули из одного места в другое. Это как при уборке: можно не разложить вещи по своим местам, а просто сгрести всё с пола и стола и запихнуть в один большой шкаф. Вроде бы комната стала чище, на первый взгляд всё аккуратно — но внутри шкафа полный хаос. Формально проблема «неприятного вида» решена, но по сути беспорядок никуда не делся, он просто переехал.

Вот с Presenter произошло примерно то же самое. Во View стало меньше логики, контроллер исчез, но весь объём сценариев, условий, реакций на разные ситуации и веток поведения теперь начал скапливаться внутри одного класса — презентера.

Формально терминов вроде SOLID тогда ещё не было, но о разделении обязанностей уже задумывались. И довольно быстро стало понятно: Presenter берёт на себя чересчур много ответственности. Один класс начинает управлять вообще всем — и это снова делает систему хрупкой, трудной для сопровождения и развития. То есть MVP стал шагом вперёд по сравнению с классическим MVC, но и у него нашлись свои пределы, особенно когда речь заходила о действительно больших и сложных приложениях.

Со временем обнаружилась и другая проблема: Presenter неизбежно начинал «знать» слишком много о логике конкретного экрана. Он знал, какие данные показывать, когда их обновлять, какие ветки сценариев запускать. Но если экранов становилось десятки, а логика — сложнее, Presenter превращался в длинный файл с сотнями строк условий. Любое изменение в интерфейсе или поведении требовало вмешательства в Presenter, и порой одно изменение тянуло за собой цепочку других. Это нарушало принцип изоляции изменений и делало систему более хрупкой.

Кроме того, Presenter становился своеобразным «бутылочным горлышком». Поскольку весь поток управления теперь шёл строго через него, любое усложнение интерфейса увеличивало нагрузку именно на Presenter. Добавление таймеров, фоновая загрузка данных, сложные переходы между состояниями → всё это попадало внутрь одного класса. Он становился не просто «центром принятия решений», а центром всего, что только можно, — и это делало его трудно тестируемым, трудно расширяемым и тяжело поддерживаемым.

Так что, как только приложения стали ещё на порядок сложнее, стало понятно: чтобы двигаться дальше, нужна структура, где логика поведения не замыкается на одном объекте, а может распределяться, комбинироваться, отделяться и переиспользоваться. Это и привело архитектуру к следующему шагу эволюции, от MVP — к тем паттернам, которые мы используем сегодня. Когда мы говорили про MVC, мы отметили, что, несмотря на его возраст, своё место он нашёл именно в вебе.

ASP.NET MVC, Ruby on Rails, Django — все эти фреймворки используют MVC потому, что его структура естественным образом совпадает с природой веб-запроса: пришёл запрос → его обработал контроллер → модель подготовила данные → представление отрисовало HTML. Такая последовательность идеально ложится на классическую схему MVC, поэтому этот паттерн там живёт до сих пор и чувствует себя вполне уверенно.

С MVP ситуация получилась совсем другая. Он изначально создавался именно для настольных приложений, когда интерфейсы становились сложнее, а классический MVC уже не справлялся с бурно растущим UI. На тот момент MVP был великолепным шагом вперёд: он сделал View тоньше, логику поведения чище, а Presenter стал единым центром управления экраном.

Но когда интерфейсы продолжили усложняться — появились анимации, состояния, фоновая работа, цепочки действий, асинхронность, динамические компоненты — MVP тоже начал «трещать». Presenter разрастался, становился тяжёлым и малоуправляемым. Чтобы двигаться дальше, архитектуре нужен был новый шаг, новое разделение обязанностей, новая форма взаимодействия между слоями.

И в этот момент MVP эволюционировал в следующую архитектурную модель, которую мы будем разбирать в следующем уроке. Это была логичная преемственность: MVP стал таким же важным этапом, как и MVC, — но в чистом виде сегодня его практически нигде не используют. Он выполнил свою роль, помог уйти от слабых мест MVC, но и сам оказался временным звеном в цепочке развития UI-архитектур.

Тем не менее знать о нём полезно. Во-первых, потому что многие современные паттерны выросли именно из MVP. Во-вторых, потому что понимание его ограничений отлично раскрывает, почему возникли последующие подходы и что именно они исправляют. Можно сказать, что MVP — это «отец» следующего паттерна, к которому мы теперь и перейдём.

MVP (Model-View-Presenter) — архитектурный паттерн, в котором Presenter является единственным посредником между View и Model. View максимально «тупое» — оно только отображает данные и делегирует все действия Presenter-у.

Presenter — посредник, который полностью управляет View: слушает модель, принимает решения и вызывает методы View. В отличие от Controller в MVC, Presenter — единственный центр управления интерфейсом.

Passive View — подход, при котором View не содержит никакой логики — только отображение и делегирование событий.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.