Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке архітектурний патерн MVC?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**MVC (Model-View-Controller)** - архітектурний патерн, що ділить додаток на три частини: Model (дані і бізнес-логіка), View (UI), Controller (обробляє ввід, з'єднує перших двох). ```javascript // Користувач натискає кнопку UserController.load(1); // Controller запитує Model, отримує user, передає View // View рендерить: <h1>Alice</h1> ``` **Ключове:** Controller посередник між усіма. View ніколи не звертається до Model напряму.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**MVC (Model-View-Controller)** - архітектурний патерн, що ділить додаток на три компоненти: Model відповідає за дані і бізнес-логіку, View за інтерфейс, Controller стоїть між ними і обробляє ввід користувача. ## Теорія ### TL;DR - Model = шар даних (запити, валідація, стан); View = те що бачить користувач; Controller = диспетчер між ними - Аналогія: ресторан. Кухня (Model) готує їжу. Тарілка з їжею (View) потрапляє до гостя. Офіціант (Controller) приймає замовлення і з'єднує кухню з гостем. - Потік: користувач діє у View, Controller оновлює Model, Model сповіщає View - Використовуй MVC коли UI-логіка і логіка даних мають рости незалежно - Пропускай для скриптів до ~500 рядків або чистих API без View ### Швидкий приклад ```javascript // Model: тільки логіка даних const UserModel = { users: [{ id: 1, name: 'Alice' }], getUser(id) { return this.users.find(u => u.id === id); } }; // View: тільки рендеринг const UserView = { render(user) { document.getElementById('app').innerHTML = `<h1>${user.name}</h1>`; } }; // Controller: обробляє ввід, з'єднує інших двох const UserController = { load(id) { const user = UserModel.getUser(id); UserView.render(user); // Результат: <h1>Alice</h1> } }; UserController.load(1); ``` Кожна частина робить одне. Model не знає як виглядає UI. View не знає звідки дані. Controller знає обох, але тільки щоб їх з'єднати. ### Як три компоненти взаємодіють насправді Model зберігає стан і надає методи для читання або зміни даних. Вона нічого не знає ні про View, ні про Controller. View отримує дані і рендерить їх, без жодного уявлення про їхнє джерело. Controller слухає події користувача, викликає методи Model, потім передає результат у View. Напрямок завжди один: дія користувача потрапляє в Controller, Controller звертається до Model, дані повернулись, Controller передає їх у View. Ніяких скорочень. View ніколи не викликає Model напряму. Ця однонаправлена дисципліна і відрізняє MVC від старого підходу де UI і дані були перемішані. Щойно ти ігноруєш це правило, компоненти починають самостійно тягнути дані, робити побічні ефекти при рендері і ламатися при кожній зміні шару даних. ### Коли використовувати MVC - Веб-додаток з реальним UI і бекендом: MVC (Rails, ASP.NET MVC, Spring MVC) - Express.js API з шаблонами: підходить чудово (роути діють як Controllers, EJS або Handlebars як Views) - React SPA зі складним станом: краще Flux або Redux (MVC незручний з двонаправленим станом React) - CLI-інструмент або невеликий скрипт: пропусти патерн, звичайні функції справляться - Команда з 5+ розробників: MVC дозволяє паралельно працювати на окремих шарах без конфліктів - Мікросервіс без UI: тільки Model і Controller, шар View не потрібен ### Порівняння: MVC, моноліт і Flux | Аспект | MVC | Моноліт | Flux/Redux | |--------|-----|---------|------------| | Потік даних | Controller → Model → View | Часто двонаправлений | Строго однонаправлений | | Тестування | Шари ізолюються незалежно | Потрібні моки всього додатку | Unit-тести actions і reducers | | Паралельна розробка | Команди на окремих шарах | Один розробник, вузьке місце | Глобальний стан, складніше ділити | | Найкраще для | Традиційні веб-додатки (Rails, ASP.NET) | Прототипи до 1k рядків | React зі складним клієнтським станом | ### Як це працює під капотом У браузері клік потрапляє до Controller першим через `addEventListener`. Controller викликає метод Model, який може виконати асинхронний запит до бази через `fetch` або ORM на зразок Sequelize. Коли дані готові, Model може видати кастомний event або використати pub/sub. View слухає і перерендерює. У Rails це все відбувається на рівні HTTP: запит потрапляє до Controller-дії, яка викликає методи Model, а результат передається у View-шаблон (ERB, Handlebars, Thymeleaf). Той самий патерн, тільки через HTTP замість DOM-подій. ### Типові помилки **1. View самостійно отримує дані** ```javascript // Погано: View прив'язаний до Model function UserView() { const user = UserModel.getUser(1); // зламається коли Model стане async return `<div>${user.name}</div>`; } // Добре: Controller посередник UserController.load(1); // Controller викликає Model, передає результат у View ``` View що самостійно тягне дані здається зручним, поки джерело даних не зміниться. Тоді редагуєш кожен View замість одного методу Model. **2. Товстий Controller (God object)** Controller в підсумку робить запити до бази, валідацію, бізнес-логіку і рендеринг. 500+ рядків, неможливо тестувати. Рішення просте: Controller тільки викликає методи Model і методи View. Вся логіка залишається в Model. **3. Застарілий View після оновлення Model** ```javascript // Погано: Model оновилась, View не знає Model.updateUser(id, data); View.render(null); // користувач бачить застарілі дані // Добре: Model видає event, View перерендерює Model.updateUser(id, data); // спрацьовує 'change' event model.addEventListener('change', (e) => View.render(e.detail)); ``` Це найпоширеніша проблема "MVC View not updating" на Stack Overflow. Model оновилась, але ніхто не сповістив View. **4. Синхронний Model у Node.js** Блокування event loop синхронними запитами до бази зупиняє весь сервер. Завжди `async/await` для методів Model. Controller перехоплює помилки відхилених Promise і передає стан помилки у View. ### Де зустрічається в реальних проєктах - Ruby on Rails 7: `app/models/user.rb`, `app/views/users/show.html.erb`, `app/controllers/users_controller.rb` - ASP.NET Core 8: `Models/User.cs`, `Views/User/Index.cshtml`, `Controllers/UserController.cs` - Spring MVC (Java): `@Service` для логіки Model, Thymeleaf для View, `@Controller` для роутингу - Backbone.js 1.4: побудований навколо MVC, `model.bind()` з'єднує з `view.render()` напряму - Django використовує варіант MVT (Model-View-Template), де "View" поводиться ближче до Controller ### Питання на співбесіді **Q:** Опиши MVC-потік для форми редагування користувача. **A:** Користувач заповнює і відправляє форму (View). Controller перехоплює `onSubmit`, валідує дані, викликає `Model.save()`. Model зберігає і видає подію `change`. Controller отримує подію і просить View відрендерити повідомлення про успіх. **Q:** Як тестувати кожен шар MVC незалежно? **A:** Для Model: тестова база або мок ORM. Для Controller: заглушка Model через sinon.js і перевірка що викликані потрібні методи View. Для View: передаєш фіктивні дані напряму і знімаєш snapshot. **Q:** У чому різниця між MVC і MVVM? **A:** У MVVM (Vue.js, Angular) ViewModel прив'язується до View двонаправлено. Зміни у View автоматично оновлюють ViewModel і навпаки. У MVC Controller не прив'язаний до даних View, він передає дані явно через виклики методів. **Q:** Як обробляти помилки Model у View? **A:** Controller огортає виклик Model у try/catch, або обробляє відхилення Promise, потім викликає `View.renderError(error)` замість `View.render(data)`. View отримує об'єкт стану помилки, а не сирий exception. **Q:** Як масштабувати MVC на micro-frontends? **A:** Кожен мікрофронтенд отримує свій Controller і View. Спільний стан Model передається через event bus (RxJS Subject або власний EventEmitter). Shadow DOM ізолює View від витоку стилів і подій між мікрододатками. **Q:** Чи актуальний MVC у 2024 з React hooks? **A:** Hooks по суті є логікою Controller, винесеною у функції. `useEffect` обробляє сайд-ефекти так само як Controller. Next.js App Router має структуру близьку до MVC: серверні компоненти як Model і Controller, клієнтські як View. Патерн живий, просто не завжди так і називається. ## Приклади ### Базовий MVC: завантаження користувача на vanilla JS ```javascript // Model const UserModel = { users: [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ], getUser(id) { return this.users.find(u => u.id === id) || null; } }; // View const UserView = { render(user) { const app = document.getElementById('app'); if (!user) { app.innerHTML = '<p>Користувача не знайдено</p>'; return; } app.innerHTML = `<h1>${user.name}</h1><p>${user.email}</p>`; } }; // Controller const UserController = { init() { document.getElementById('load-btn').addEventListener('click', () => { const user = UserModel.getUser(1); UserView.render(user); // Результат: Alice / alice@example.com }); } }; UserController.init(); ``` Model не знає що є кнопка або DOM. View не знає звідки дані. В цьому і весь сенс розділення. ### Середній рівень: Express.js REST Controller ```javascript const express = require('express'); const Database = require('better-sqlite3'); const db = new Database('app.db'); const app = express(); app.use(express.json()); // Model: тільки робота з базою даних const UserModel = { getUser(id) { return db.prepare('SELECT * FROM users WHERE id = ?').get(id); }, updateUser(id, data) { db.prepare('UPDATE users SET name = ? WHERE id = ?').run(data.name, id); return this.getUser(id); } }; // Controller: Express route handlers і є шар Controller app.get('/users/:id', (req, res) => { const user = UserModel.getUser(req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); res.json(user); // Результат: { id: 1, name: 'Alice' } }); app.put('/users/:id', (req, res) => { const updated = UserModel.updateUser(req.params.id, req.body); res.json(updated); // Результат: { id: 1, name: 'Bob' } }); // View: JSON-відповідь і є View у REST API ``` У REST API View це JSON-відповідь. Розділення залишається: Model розмовляє з базою, Controller обробляє запит і відповідь, View складається з даних які Controller надає. ### Просунутий рівень: Model з pub/sub, View підписується напряму ```javascript // Model з нотифікацією через events (не знає хто слухає) class UserModel extends EventTarget { constructor() { super(); this.data = { id: 1, name: 'Alice' }; } update(name) { this.data.name = name; this.dispatchEvent(new CustomEvent('change', { detail: { ...this.data } })); } } const model = new UserModel(); // View підписується на події Model (Controller налаштовує це при ініціалізації) model.addEventListener('change', (e) => { document.getElementById('username').textContent = e.detail.name; // Результат: DOM оновлюється миттєво без повного перерендеру }); // Controller document.getElementById('save-btn').addEventListener('click', () => { const newName = document.getElementById('name-input').value; model.update(newName); // спрацьовує 'change' event вище }); ``` Одне спостереження з практики: View що підписується на Model events виглядає чисто у vanilla JS, але в React 18+ обходить reconciler. Оберни оновлення від Model у `useEffect`, або використовуй Zustand, щоб залишитись у циклі рендерингу React.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.