Что такое MVC: модель, представление и контроллер простыми словами

Что такое MVC: модель, представление и контроллер простыми словами Полезное

MVC (Model-View-Controller) — это архитектурный шаблон, который делит код приложения на три роли: модель хранит данные и правила предметной области, представление показывает эти данные пользователю, контроллер принимает действие пользователя (клик, HTTP-запрос) и решает, что сделать с моделью и какое представление вернуть.

Важно сразу развести две версии шаблона. В классическом MVC для настольных интерфейсов представление подписано на модель и само перерисовывается, когда модель меняется. В веб-MVC (так устроены Rails, Laravel, Spring MVC, ASP.NET Core MVC) связь идет через контроллер: он получает запрос, берет данные у модели и передает их в шаблон. Ниже в основном речь о веб-варианте — именно его чаще всего имеют в виду, когда спрашивают «MVC что такое».

Три роли на одном примере

Роль За что отвечает Чего делать не должна Пример в интернет-магазине
Model (модель) данные, их проверка, бизнес-правила, работа с БД формировать HTML, знать про URL «товар», «заказ», правило «нельзя заказать больше, чем есть на складе»
View (представление) вывод готовых данных: HTML-шаблон, экран, JSON ходить в базу, принимать решения страница карточки товара
Controller (контроллер) разбор запроса, вызов модели, выбор ответа и кода статуса хранить бизнес-правила обработчик GET /products/42

Аналогия-якорь: кафе. Официант (контроллер) принимает заказ и передает его на кухню, кухня (модель) знает рецепты и продукты и готовит блюдо, тарелка на столе (представление) — то, что видит гость. Официант не жарит котлеты, повар не общается с гостем.

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

Как проходит запрос в веб-MVC

Пошагово на запросе GET /courses/2 (открыть страницу курса):

  1. Роутер фреймворка сопоставляет метод и URL с действием контроллера: GET /courses/<id> -> CourseController.detail.
  2. Контроллер проверяет входные данные: id должен быть числом.
  3. Контроллер просит модель: «дай курс с id = 2».
  4. Модель выполняет запрос к БД и возвращает курс или «не найдено».
  5. Контроллер выбирает ответ: курс есть — представление карточки и статус 200, курса нет — страница ошибки и 404.
  6. Представление превращает данные в HTML, экранируя пользовательский текст. Готовый ответ уходит браузеру.

Упрощение: в реальных фреймворках между шагами есть middleware (аутентификация, CSRF, логирование), а работу с БД часто выносят из модели в отдельный слой репозиториев или сервисов. Суть от этого не меняется: данные и правила отдельно, отображение отдельно, управление запросом отдельно.

Минимальный рабочий пример на Python

Чтобы увидеть роли без магии фреймворка, соберем MVC на стандартной библиотеке Python: sqlite3 для данных, html для экранирования, re для маршрутов. Проверено на Python 3.14.7 (официальный образ python:3.14-slim).

import html
import re
import sqlite3


# ---------- Model: данные и правила предметной области ----------
class CourseModel:
    def __init__(self, conn):
        self.conn = conn
        conn.execute(
            "CREATE TABLE courses ("
            " id INTEGER PRIMARY KEY,"
            " title TEXT NOT NULL CHECK (length(title) BETWEEN 3 AND 80))"
        )

    def add(self, title):
        if not isinstance(title, str):
            raise ValueError("название должно быть строкой")
        title = title.strip()
        if not 3 <= len(title) <= 80:
            raise ValueError("название: от 3 до 80 символов")
        cur = self.conn.execute("INSERT INTO courses (title) VALUES (?)", (title,))
        return cur.lastrowid

    def all(self):
        return self.conn.execute("SELECT id, title FROM courses ORDER BY id").fetchall()

    def get(self, course_id):
        return self.conn.execute(
            "SELECT id, title FROM courses WHERE id = ?", (course_id,)
        ).fetchone()


# ---------- View: только отображение готовых данных ----------
def render_list(courses):
    items = "".join(
        f'<li><a href="/courses/{cid}">{html.escape(title)}</a></li>'
        for cid, title in courses
    )
    return f"<h1>Курсы</h1><ul>{items}</ul>"


def render_detail(course):
    cid, title = course
    return f"<h1>{html.escape(title)}</h1><p>id = {cid}</p>"


def render_error(status, text):
    return f"<h1>{status}</h1><p>{html.escape(text)}</p>"


# ---------- Controller: разбор запроса и выбор ответа ----------
class CourseController:
    def __init__(self, model):
        self.model = model

    def list(self):
        return 200, render_list(self.model.all())

    def detail(self, raw_id):
        course = self.model.get(int(raw_id))
        if course is None:
            return 404, render_error(404, "Курс не найден")
        return 200, render_detail(course)

    def create(self, form):
        try:
            self.model.add(form.get("title"))
        except ValueError as err:
            return 400, render_error(400, str(err))
        return 303, "/courses"


ROUTES = [
    ("GET", re.compile(r"/courses"), "list"),
    ("GET", re.compile(r"/courses/([0-9]{1,9})"), "detail"),
    ("POST", re.compile(r"/courses"), "create"),
]


def dispatch(controller, method, path, form=None):
    allowed = []
    for route_method, pattern, action in ROUTES:
        match = pattern.fullmatch(path)
        if not match:
            continue
        if route_method != method:
            allowed.append(route_method)
            continue
        handler = getattr(controller, action)
        if action == "create":
            return handler(form or {})
        return handler(*match.groups())
    if allowed:
        return 405, render_error(405, "Метод не разрешен, допустимо: " + ", ".join(allowed))
    return 404, render_error(404, "Страница не найдена")


if __name__ == "__main__":
    app = CourseController(CourseModel(sqlite3.connect(":memory:")))
    app.model.add("Архитектура и шаблоны проектирования")
    app.model.add("Python <для начинающих>")

    requests = [
        ("GET", "/courses", None),
        ("GET", "/courses/2", None),
        ("GET", "/courses/99", None),
        ("GET", "/courses/abc", None),
        ("GET", "/courses/\u0661", None),
        ("DELETE", "/courses/2", None),
        ("POST", "/courses", {"title": "  Go  "}),
        ("POST", "/courses", {"title": ["список"]}),
        ("POST", "/courses", {"title": "SQL для аналитиков"}),
    ]
    for method, path, form in requests:
        status, body = dispatch(app, method, path, form)
        print(method, ascii(path), "->", status, body)

Сохраните код в mvc_demo.py и запустите python3 mvc_demo.py. В консоли появятся девять строк — по одной на каждый «запрос»:

GET '/courses' -> 200 <h1>Курсы</h1><ul><li><a href="/courses/1">Архитектура и шаблоны проектирования</a></li><li><a href="/courses/2">Python &lt;для начинающих&gt;</a></li></ul>
GET '/courses/2' -> 200 <h1>Python &lt;для начинающих&gt;</h1><p>id = 2</p>
GET '/courses/99' -> 404 <h1>404</h1><p>Курс не найден</p>
GET '/courses/abc' -> 404 <h1>404</h1><p>Страница не найдена</p>
GET '/courses/\u0661' -> 404 <h1>404</h1><p>Страница не найдена</p>
DELETE '/courses/2' -> 405 <h1>405</h1><p>Метод не разрешен, допустимо: GET</p>
POST '/courses' -> 400 <h1>400</h1><p>название: от 3 до 80 символов</p>
POST '/courses' -> 400 <h1>400</h1><p>название должно быть строкой</p>
POST '/courses' -> 303 /courses

Разбор: кто за что отвечает в примере

Модель CourseModel знает, как хранится курс и какое название допустимо. Правило «от 3 до 80 символов» живет именно здесь (и продублировано CHECK в таблице), поэтому оно сработает, откуда бы ни пришел вызов. SQL-запрос параметризован (?), значение не склеивается со строкой.

Представление — три функции render_*. Они получают готовые данные и ничего не решают. Единственная их забота кроме разметки — html.escape: название «Python <для начинающих>» вывелось как &lt;...&gt;, а не как тег.

Контроллер CourseController вместе с dispatch играет роль роутера и обработчиков. Обратите внимание на два разных 404: /courses/99 — маршрут верный, но модели нечего вернуть («Курс не найден»); /courses/abc — маршрут вообще не подошел («Страница не найдена»). Третий случай — DELETE /courses/2: адрес существует, но такой метод для него не предусмотрен, поэтому ответ 405, а не 404. В настоящем HTTP-ответе 405 сервер обязан добавить заголовок Allow со списком допустимых методов (RFC 9110); в учебном примере заголовков нет, и список выводится в тексте.

Граничные входы проверены прогоном. Маршрут использует [0-9] и fullmatch, поэтому арабско-индийская цифра \u0661 не проходит, хотя "\u0661".isdigit() в Python вернет True, а int() превратит ее в 1. Список вместо строки в форме дает 400, а не падение. Строка « Go » после strip() короче трех символов — тоже 400. Ответ 303 после успешного POST — типичный прием «redirect after POST», чтобы повторное обновление страницы не создало курс второй раз.

Типичная ошибка: логика и данные протекают в представление

Частый способ сломать MVC — позволить шаблону самому решать, как выводить сырые данные. Вот представление без экранирования:

def render_detail_bad(course):
    cid, title = course
    return f"<h1>{title}</h1><p>id = {cid}</p>"


print(render_detail_bad((3, "<script>alert(1)</script>")))

Фактический результат — скрипт попадает в страницу как есть, это XSS:

<h1><script>alert(1)</script></h1><p>id = 3</p>

Исправление — то, что уже сделано в render_detail: html.escape(title). В настоящих шаблонизаторах (Jinja2 с включенным autoescape, шаблоны Django, Blade в Laravel, Razor в ASP.NET Core) экранирование по умолчанию включено, и отключать его стоит только для заведомо доверенного HTML.

Другие типичные нарушения ролей:

  • толстый контроллер — проверки, расчеты цены и SQL прямо в обработчике; правило приходится копировать в каждый контроллер, который работает с той же сущностью;
  • запросы к БД из шаблона — представление начинает зависеть от схемы, а число запросов незаметно растет с каждым элементом списка;
  • модель, которая знает про HTTPrequest, коды статусов и редиректы внутри модели не дают переиспользовать ее в фоновой задаче или API.

Зачем это разделение на практике

Главный выигрыш — изменения становятся локальными: поменять дизайн страницы можно в представлении, не трогая правила; сменить СУБД — в модели, не трогая шаблоны. Второй выигрыш — тестируемость. Контроллер из примера проверяется без базы данных, если подставить ему поддельную модель:

from mvc_demo import CourseController, dispatch


class FakeModel:
    def get(self, course_id):
        return (7, "Тестовый курс") if course_id == 7 else None


app = CourseController(FakeModel())
assert dispatch(app, "GET", "/courses/7")[0] == 200
assert dispatch(app, "GET", "/courses/8")[0] == 404
print("контроллер проверен без базы данных")

Файл кладется рядом с mvc_demo.py, запуск печатает контроллер проверен без базы данных.

Граница пользы: для скрипта на 50 строк или одностраничного прототипа три слоя — лишняя церемония. MVC окупается, когда страниц и сущностей становится много и над кодом работают несколько человек.

MVC во фреймворках

Фреймворк Как называются роли
Ruby on Rails модели ActiveRecord, контроллеры ApplicationController, представления ERB
Laravel (PHP) модели Eloquent, контроллеры, шаблоны Blade
Spring MVC (Java) @Controller, модель — объект Model плюс доменные классы, представление — Thymeleaf или JSP
ASP.NET Core MVC (C#) классы Controller, модели, представления Razor (.cshtml)
Django (Python) сам фреймворк называет схему MTV: Model, Template, View; здесь «view» — это аналог контроллера, а template — аналог представления

Путаница с Django — частый вопрос на собеседованиях: слово «view» в Django обозначает функцию-обработчик запроса, то есть роль контроллера, а за отображение отвечает шаблон.

MVC, MVP и MVVM: в чем разница

Все три шаблона отделяют данные от интерфейса, но по-разному связывают интерфейс с логикой.

MVC MVP MVVM
Кто принимает действие пользователя контроллер представление передает его презентеру представление через привязки (binding) к ViewModel
Знает ли представление о модели в классическом варианте да (подписка), в веб-MVC получает готовые данные нет, только о презентере нет, только о ViewModel
Как обновляется экран контроллер выбирает представление или представление реагирует на модель презентер явно вызывает методы представления автоматически через привязку данных
Где типично встречается серверные веб-фреймворки Android до Jetpack, WinForms, GWT WPF, .NET MAUI, Android Jetpack с ViewModel, Vue и Knockout по идее

Коротко: если фреймворк сам разбирает HTTP-запросы и рендерит шаблоны на сервере — перед вами почти наверняка MVC. Если интерфейс живет на клиенте и обновляется при изменении состояния без явных вызовов — ближе к MVVM.

Выводы

  • MVC делит приложение на модель (данные и правила), представление (вывод) и контроллер (обработка запроса и выбор ответа).
  • В веб-MVC представление не обращается к модели само: контроллер получает данные у модели и передает их в шаблон.
  • Бизнес-правила живут в модели, экранирование — в представлении, разбор и проверка входа — в контроллере; нарушение ролей дает толстые контроллеры и XSS в шаблонах.
  • Django называет ту же идею MTV, и его «view» соответствует контроллеру.
  • MVP и MVVM решают ту же задачу для клиентских интерфейсов: через презентер с явными вызовами и через привязку данных соответственно.

Где применяется / связь с практикой

Освойте тему на практике

MVC — первый архитектурный шаблон, с которым сталкивается веб-разработчик, и хорошая точка входа в более широкую тему: слоистая архитектура, разделение на сервисы и репозитории, шаблоны GoF внутри каждого слоя, выбор между MVC, MVVM и чистой архитектурой под конкретную задачу. Системно эти решения разбирают на курсе «Архитектура и шаблоны проектирования».

Освойте тему на практике

Чтобы посмотреть, как преподаватели объясняют архитектурные шаблоны на живом коде, можно заглянуть на бесплатные открытые уроки Otus.

FAQ

Кто придумал MVC?
Шаблон описал Трюгве Реенскауг в конце 1970-х, работая в Xerox PARC над интерфейсами для Smalltalk. Веб-вариант с контроллером-посредником сложился позже, с приходом серверных фреймворков.

Можно ли использовать MVC во фронтенде?
Можно, но современные клиентские фреймворки (React, Vue, Angular) строятся вокруг компонентов и состояния, поэтому их чаще описывают через MVVM или однонаправленный поток данных, а не классический MVC.

MVC — это паттерн проектирования или архитектура?
Это архитектурный шаблон: он задает крупное разделение приложения на роли. Внутри него используются обычные паттерны проектирования, например наблюдатель (представление подписано на модель) и стратегия (контроллер как сменяемая стратегия реакции представления на ввод) в классическом MVC.

OTUS Журнал