Практика 15. Практическая работа 15. MVC, MVP, MVVM
Цель
- Понять, как презентационные паттерны решают задачу разделения интерфейса и логики (Separation of Concerns) и делают View «тонкой».
- Реализовать одну и ту же фичу (счётчик и список дел) в трёх стилях — MVC, MVP, MVVM — консольно, без GUI.
- Прочувствовать разницу в роли посредника (Controller / Presenter / ViewModel) и в способе обновления View (сама / явно / через привязку данных).
- Научиться оценивать тестируемость представления и выбирать паттерн под задачу.
Краткая теория
- Все три паттерна делят приложение на Model (данные и бизнес-логика, общая часть), View (то, что видит пользователь) и посредник между ними.
- MVC. Controller принимает ввод и меняет Model; View активна — может сама читать Model и обновляться (часто через «Наблюдатель»). Прост, доминирует в вебе.
- MVP. View пассивна и скрыта за интерфейсом, о Model не знает. Presenter
явно дёргает Model и сам передаёт результат во View методами вроде
show_tasks. Высокая тестируемость: вместо реальной View подставляют заглушку (mock). - MVVM. ViewModel выставляет наблюдаемые свойства (Observable) и команды, о
View ничего не знает. View декларативно привязывается к свойствам и
обновляется автоматически (data binding). В чистом Python binding имитируют
собственным
Observable. - Ключевое различие посредников: Presenter сам вызывает методы View; ViewModel —
нет, связь идёт через привязку. Подробности, схемы потоков данных и примеры — в
lecture_15.md.
Задания
Задание 1. Общая Model для счётчика и списка дел
Сначала вынесите бизнес-логику в Model, не зависящую ни от какого паттерна.
Требования:
- Класс
CounterModelс полем_value: intи методамиincrement(),decrement()(не ниже нуля),reset(),get_value(). - Поддержка наблюдателей:
register_observer(callback)и приватный_notify()после каждого изменения. Model не знает о View/посреднике и не содержит ввода-вывода.
Скелет:
from typing import Callable, List
class CounterModel: def __init__(self) -> None: self._value = 0 self._observers: List[Callable[[], None]] = []
def increment(self) -> None: self._value += 1 self._notify()
def decrement(self) -> None: ... # не уходить ниже нуля
def register_observer(self, cb: Callable[[], None]) -> None: self._observers.append(cb)
def _notify(self) -> None: for cb in self._observers: cb()Проверьте: изменение значения вызывает всех зарегистрированных наблюдателей;
в Model нет ни одного print и ни одного input.
Задание 2. Счётчик в стиле MVC
Реализуйте счётчик на основе CounterModel в стиле MVC.
Требования:
CounterView— активна: методdisplay(value)печатает состояние,display_message(text)— служебные сообщения. View умеет форматировать данные.CounterControllerпринимаетmodelиview, в конструкторе подписывает обновление View на Model (наблюдатель). Методыon_increment(),on_decrement(),on_reset()дёргают Model.- Обновление View происходит через уведомление Model, а не вручную из каждого метода контроллера.
Скелет:
class CounterController: def __init__(self, model: CounterModel, view: CounterView) -> None: self.model = model self.view = view self.model.register_observer(self._render)
def on_increment(self) -> None: self.model.increment()
def _render(self) -> None: self.view.display(self.model.get_value())Проверьте: контроллер «тонкий»; после on_increment() View обновляется сама
по цепочке Model → наблюдатель → View. Опишите в комментарии поток данных.
Задание 3. Список дел в стиле MVP
Реализуйте список задач (add, toggle, показать) в стиле MVP с пассивной
View за интерфейсом.
Требования:
- Расширьте Model до
TaskModel(хранит задачи{"id", "title", "completed"}, методыadd_task(title),toggle_task(id),get_tasks()). - Абстрактный
TaskViewInterface(ABC)с методамиshow_tasks(tasks),show_message(text),get_input(prompt). РеализацияConsoleTaskView— только ввод-вывод, никакой логики. TaskPresenter(model, view: TaskViewInterface): методыon_add(),on_toggle(id),refresh(). Presenter сам забирает данные из Model и сам передаёт их во View; зависит от интерфейса, не от конкретного класса.
Скелет:
from abc import ABC, abstractmethod
class TaskViewInterface(ABC): @abstractmethod def show_tasks(self, tasks: list[dict]) -> None: ... @abstractmethod def show_message(self, text: str) -> None: ...
class TaskPresenter: def __init__(self, model, view: TaskViewInterface) -> None: self.model, self.view = model, view
def on_add(self, title: str) -> None: self.model.add_task(title) self.view.show_message("Задача добавлена") self.refresh()
def refresh(self) -> None: self.view.show_tasks(self.model.get_tasks())Проверьте: напишите FakeView(TaskViewInterface), которая запоминает
переданные задачи в список вместо печати, и тестом убедитесь, что после on_add()
Presenter передал во View нужные данные без реального ввода-вывода.
Задание 4. Тот же список дел в стиле MVVM
Реализуйте список задач в стиле MVVM с привязкой данных через Observable.
Требования:
- Класс
Observableсо свойствомvalue(геттер/сеттер) и методомsubscribe(callback); сеттер уведомляет подписчиков только при изменении значения. TaskViewModel(model)выставляет наблюдаемые свойстваtasksиmessage, подписывается на Model и обновляетtasks.valueпри изменениях. Методыadd(),toggle(id)дёргают Model и пишут вmessage. ViewModel не знает о View.TaskViewMVVM(view_model)в конструкторе подписывается на свойства ViewModel и печатает их при изменении. После настройки привязок View нигде явно не вызывается.
Скелет:
class Observable: def __init__(self, value=None) -> None: self._value = value self._subs: list = []
@property def value(self): return self._value
@value.setter def value(self, new) -> None: if self._value != new: # уведомлять только при изменении self._value = new for cb in self._subs: cb(new)Проверьте: в демонстрации вызываются только методы ViewModel
(view_model.add(...)), а вывод появляется сам. Объясните, почему ViewModel не
хранит ссылку на View.
Задание 5*. Сравнительная таблица и рефлексия (повышенной сложности)
- Оформите таблицу по реализованным фичам: для MVC, MVP, MVVM укажите — кто обновляет View, знает ли посредник о View, есть ли data binding, как тестировать.
- Кратко (5–8 строк) ответьте: какой из трёх паттернов для консольного приложения оказался самым избыточным и почему; где MVVM-привязка реально окупится.
Критерии оценки
| Критерий | Вес |
|---|---|
| Задание 1: чистая Model с наблюдателями, без ввода-вывода | 15% |
| Задание 2: MVC — активная View, тонкий Controller, обновление через наблюдатель | 20% |
| Задание 3: MVP — пассивная View за интерфейсом, Presenter явно управляет View | 25% |
Задание 4: MVVM — Observable, привязка, ViewModel не знает о View | 25% |
| Задание 5*: сравнительная таблица и обоснованная рефлексия | 5% |
| Качество кода: имена, docstrings, аннотации типов, отсутствие логики во View | 10% |
Штрафы: бизнес-логика или print/input внутри Model; логика во View (MVP/MVVM);
ссылка на View внутри ViewModel; Presenter, зависящий от конкретного класса View, а
не от интерфейса; ручное обновление View там, где должна работать привязка.
Вопросы для самопроверки
- Какую общую проблему решают MVC, MVP и MVVM? Сформулируйте через Separation of Concerns.
- Что входит в Model во всех трёх реализациях и почему она не зависит от паттерна?
- Почему в вашей MVC-реализации View считается «активной», а Controller «тонким»?
- Как пассивная View в MVP позволила протестировать Presenter без реального
ввода-вывода? Что вы подставили вместо
ConsoleTaskView? - Чем зависимость Presenter от
TaskViewInterfaceлучше зависимости от конкретного класса View? - Что такое data binding и как его сымитировал ваш класс
Observable? - Почему сеттер
Observable.valueуведомляет подписчиков только при изменении значения? - Почему ViewModel в MVVM не хранит ссылку на View, а Presenter в MVP — фактически хранит (через интерфейс)?
- Какой паттерн оказался избыточным для консольного приложения и в какой среде он окупается?
- Как паттерн «Наблюдатель» используется в ваших реализациях MVC и MVVM?