Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «MVC (модель-подання-контролер) та MVP (модель-подання-презентер) патерни проектування». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**MVC vs MVP** - архітектурні патерни (architectural patterns), що відрізняються способом зв'язку View і Model. ```javascript // MVC: View звертається до Model напряму class View { render() { console.log(this.model.getData()); } } // MVP: View отримує дані тільки від Presenter class ViewMVP { show(data) { console.log(data); } } class Presenter { update() { this.view.show(this.model.getData()); } } ``` **Ключове:** у MVP Presenter повністю контролює взаємодію Model і View; у MVC View може самостійно запитувати Model. | | MVC | MVP | |---|---|---| | View-Model | Прямий | Через Presenter | | Тестування | Складніше | Простіше | | Де застосовується | Rails, ASP.NET MVC | Android, WinForms |Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**MVC (Model-View-Controller)** та **MVP (Model-View-Presenter)** - це архітектурні патерни, які розбивають код застосунку на чіткі ролі: одна управляє даними, інша відповідає за відображення, третя стоїть між ними. Головна різниця полягає в тому, чи може View напряму звертатись до Model. ## Теорія ### TL;DR - MVC: View може самостійно діставати дані з Model; Controller обробляє дії користувача - MVP: View пасивний і нічого не знає про Model; Presenter керує всією логікою і сам передає дані до View - Аналогія: MVC - ресторан, де відвідувачі (View) читають меню напряму; MVP ставить офіціанта (Presenter) єдиним посередником між відвідувачами і кухнею - Правило вибору: обирай MVP коли потрібно тестувати Views без реального UI (Android, десктоп); MVC підходить для веб-фреймворків, де прив'язка даних обробляється автоматично - Android перейшов від MVP до MVVM разом з Jetpack Compose, але питання про MVP досі зустрічаються на співбесідах ### Швидкий приклад Головна різниця в одному фрагменті коду: ```javascript // MVC: View напряму звертається до Model class Model { getData() { return 'user data'; } } class View { constructor(model) { this.model = model; } // View тримає посилання на Model render() { console.log(this.model.getData()); } // пряме звернення } const model = new Model(); new View(model).render(); // Output: user data // MVP: View нічого не знає про Model, спілкується тільки з Presenter class Presenter { constructor(model, view) { this.model = model; this.view = view; } updateView() { this.view.show(this.model.getData()); } // Presenter штовхає дані } class ViewMVP { show(data) { console.log(data); } // немає посилання на model } new Presenter(model, new ViewMVP()).updateView(); // Output: user data ``` Обидва варіанти дають однаковий результат. Різниця - хто тримає зв'язок. ### Головна різниця У MVC View може спостерігати за Model або запитувати в неї дані. Це спрощує реактивний UI, але прив'язує View до структури Model - тести без живої Model стають складнішими. MVP розриває цей зв'язок повністю. View надає тільки методи на кшталт `show(data)` або `showError(msg)`, і Presenter їх викликає. View ніколи не чіпає Model, а отже його легко замінити у тестах: передаєш підробний View у Presenter і перевіряєш, що він отримав. ### Коли використовувати - Веб-застосунки з data-binding фреймворками (React, Angular): MVC підходить, фреймворк синхронізує Controller і View автоматично - Android-застосунки (до Compose) де потрібні unit-тести Views: MVP, мокуємо View-інтерфейс без залежності від Model - TDD-проекти з жорсткою ізоляцією тестів: MVP, Presenter зосереджує всю логіку - Прості прототипи: MVC, менше файлів, швидша ітерація - Важка бізнес-логіка з частим рефакторингом: MVP, Presenter тримає все в одному місці ### Таблиця порівняння | Аспект | MVC | MVP | |---|---|---| | Зв'язок View-Model | Прямий (View може запитувати Model) | Відсутній (Presenter посередник) | | Потік даних | Користувач → View → Controller → Model → View | Користувач → View → Presenter → Model → Presenter → View | | Тестування | Складніше (View потребує живої Model) | Простіше (мокуємо View-інтерфейс) | | Зв'язаність | Середня (View знає структуру Model) | Низька (View пасивний) | | Boilerplate | Менше | Більше (Presenter для кожного View) | | Фреймворки | Ruby on Rails, ASP.NET MVC, Spring MVC | Android (до Compose), WinForms, GWT | | Коли застосовувати | Реактивний веб-UI, бекенд | Застосунки з великою кількістю тестів, Android | ### Як патерни (design patterns) працюють у фреймворках У Ruby on Rails роутер надсилає HTTP-запити до Controllers, ті запитують дані через ActiveRecord і рендерять Views (ERB-шаблони). View має доступ до даних, переданих Controller-ом, і може викликати методи Model напряму. В Android MVP Activity реалізує View-інтерфейс. Presenter тримає посилання на цей інтерфейс, а не на Activity безпосередньо. Dependency injection (Dagger, Hilt) пов'язує їх під час виконання. Саме це дає змогу підміняти реальний Activity на мок у тестах без жодних проблем. З власного досвіду: найгірші баги в MVC-проектах виникають не через архітектуру, а через Controller-и на 200 рядків, де зосередились валідація, запити до БД і відправка email одночасно. Дисципліна MVP з Presenter-ом допомагає цього уникнути. ### Типові помилки **Помилка: виклик Model напряму з MVP View** ```java // Неправильно: View чіпає Model class LoginActivity implements LoginView { void onLogin() { if (model.authenticate(email)) showSuccess(); // залежність від Model } } ``` Це руйнує ізоляцію тестів. View стає залежним від Model, і замінити його у тестах вже не вийде. Виправлення: делегувати все до Presenter. ```java // Правильно: View тільки делегує class LoginActivity implements LoginView { void onLogin() { presenter.login(email, pass); } public void showSuccess() { /* перехід на dashboard */ } public void showError(String msg) { /* показати toast */ } } ``` **Помилка: бізнес-логіка в MVC Controller** ```javascript // Неправильно: валідація і запит до БД прямо в Controller app.post('/order', (req, res) => { if (!req.body.item) return res.status(400).json({ error: 'missing item' }); const result = db.query('INSERT INTO orders ...'); // логіка Model в Controller res.json(result); }); ``` Переноси логіку в Model. Controller повинен тільки маршрутизувати запити і повертати відповіді. **Помилка: ігнорування подій lifecycle у MVP** Якщо користувач натискає "назад" або повертає телефон, Presenter має про це знати. Забутий виклик `presenter.onCancel()` або `presenter.onDestroy()` призводить до розсинхронізації стану і витоків пам'яті, бо Presenter тримає посилання на View, якого вже немає. **Помилка: дублювання запитів у різних MVC Controller-ах** ```javascript // Погано: один і той самий запит у двох місцях app.get('/users', (req, res) => { const users = db.query('SELECT * FROM users WHERE active=1'); // вбудований запит res.json(users); }); app.get('/user/:id', (req, res) => { const user = db.query('SELECT * FROM users WHERE id=? AND active=1', [req.params.id]); // дублювання res.json(user); }); // Виправлення: виносимо в UserModel class UserModel { async getActive() { return db.query('SELECT * FROM users WHERE active=1'); } async getById(id) { return db.query('SELECT * FROM users WHERE id=? AND active=1', [id]); } } ``` Дубльовані запити - одне з найпоширеніших джерел N+1-проблем в Express MVC-проектах. ### Де зустрічається у продакшені - Ruby on Rails: повноцінний MVC, Models через ActiveRecord, Views на ERB-шаблонах - ASP.NET MVC: Controllers обробляють HTTP, Models через Entity Framework Core, Views на Razor - Spring MVC: Controllers з анотацією `@Controller`, Views через Thymeleaf або JSP - Android (до Compose): MVP де Activity реалізує View-інтерфейс, Presenter переживає зміни конфігурації - GWT (Google Web Toolkit): MVP для компіляції Java в JS, Presenter управляє DOM-подіями - WinForms: варіант MVP для десктопних застосунків з тестованим UI ### Питання на співбесіді **Q:** Намалюй потік даних MVC та MVP на дошці. **A:** MVC: користувач → View → Controller → Model → View (або View сам опитує Model). MVP: користувач → View викликає метод Presenter → Presenter оновлює Model → Presenter викликає метод View з новими даними. **Q:** Як unit-тестувати MVP Presenter? **A:** Мокуєш View-інтерфейс через Mockito: `LoginView mockView = mock(LoginView.class)`. Передаєш у Presenter, викликаєш `presenter.login("bad@email.com", "wrong")`, перевіряєш `verify(mockView).showError("Invalid credentials")`. Без Activity, без UI, без емулятора. **Q:** Який реальний мінус додаткового шару в MVP? **A:** Boilerplate. Кожному View потрібен Presenter і зазвичай інтерфейс. У великому застосунку це сотні додаткових класів. Для невеликих фіч виглядає як оверінжиніринг. **Q:** Чому Android відмовився від MVP? **A:** Jetpack Compose змінив підхід до UI - декларативний рендеринг на основі стану усуває потребу в пасивному View. MVVM з LiveData або StateFlow підходить краще: ViewModel нативно переживає зміни конфігурації, а управління lifecycle Presenter-а завжди було ручним і схильним до помилок. **Q:** (Senior) Як масштабувати MVP, коли кілька Views ділять одну Model? **A:** Окремий Presenter для кожного View, кожен з власним View-інтерфейсом. Model ділиться через dependency injection (один екземпляр у DI-графі) або через pub-sub канал на кшталт RxJava subjects. Спільний змінний стан між Presenter-ами повертає той самий coupling, від якого ти тікав. ## Приклади ### Android: форма логіну (MVP) Реальний екран авторизації за MVP-підходом, що використовувався в Android до Compose: ```java // Model: чиста бізнес-логіка, без Android-імпортів class UserModel { boolean authenticate(String email, String pass) { return email.equals("test@test.com") && pass.equals("pass"); } } // View-інтерфейс: що Presenter може викликати в UI interface LoginView { void showSuccess(); void showError(String msg); } // Presenter: керує логікою, спілкується з Model і View class LoginPresenter { private UserModel model; private LoginView view; LoginPresenter(UserModel m, LoginView v) { model = m; view = v; } void login(String email, String pass) { if (model.authenticate(email, pass)) view.showSuccess(); else view.showError("Invalid credentials"); } } // Activity реалізує View-інтерфейс, делегує всі дії Presenter-у public class LoginActivity implements LoginView { LoginPresenter presenter = new LoginPresenter(new UserModel(), this); public void onLoginClick(String email, String pass) { presenter.login(email, pass); // Activity нічого не знає про Model } public void showSuccess() { /* перехід на dashboard */ } public void showError(String msg) { /* показати toast з msg */ } } ``` Тестування тривіальне: мокуємо `LoginView`, передаємо в `LoginPresenter`, викликаємо `presenter.login(...)`, перевіряємо який метод View було викликано. Без емулятора, без Activity. ### Express.js MVC - тонкі Controller-и Виправлена версія типової помилки - логіка Model прямо в обробниках маршрутів: ```javascript const express = require('express'); const app = express(); // Шар Model: визначення запитів тут, не в Controller-ах class UserModel { async getActiveUsers() { return db.query('SELECT * FROM users WHERE active=1'); } async getActiveUser(id) { return db.query('SELECT * FROM users WHERE id=? AND active=1', [id]); } } // Controller-и залишаються тонкими: тільки маршрутизація і відповідь app.get('/users', async (req, res) => { const model = new UserModel(); const users = await model.getActiveUsers(); // логіка в Model, не тут res.json(users); }); app.get('/user/:id', async (req, res) => { const model = new UserModel(); const user = await model.getActiveUser(req.params.id); res.json(user); }); ``` Визначення запиту живе в одному місці. Змінюєш умову `WHERE active=1` раз - обидва маршрути отримують виправлення автоматично.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.