MVC — Model-View-Controller
Зачем нужно
MVC — архитектурный паттерн, разделяющий приложение на три слоя: Model (данные и логика), View (отображение) и Controller (обработка ввода). Цель — разделение ответственности: изменение интерфейса не затрагивает бизнес-логику и наоборот.
Где используется
- Серверные фреймворки (Express.js, Ruby on Rails, Django, Spring)
- Классические фронтенд-фреймворки (Backbone.js, Ember.js)
- Мобильная разработка (UIKit/iOS, Android Activities)
- Любое приложение, где нужно разделить данные и представление
Компоненты MVC
Пользователь
│
▼
Controller ──── обрабатывает ввод, вызывает Model
│
▼
Model ────────── данные, бизнес-логика, валидация
│
▼
View ─────────── отображает данные из Model
│
▼
Пользователь видит результат
Model — данные и бизнес-логика
class TodoModel {
constructor {
this.todos = ;
this.listeners = ;
}
// Подписка на изменения
subscribe(listener) {
this.listeners.push(listener);
}
notify {
this.listeners.forEach(fn => fn(this.todos));
}
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
done: false,
});
this.notify;
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.done = !todo.done;
this.notify;
}
}
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.notify;
}
getAll {
return [...this.todos];
}
getActive {
return this.todos.filter(t => !t.done);
}
}
View — отображение
class TodoView {
constructor(container) {
this.container = container;
this.onAdd = null; // Колбэки для Controller
this.onToggle = null;
this.onRemove = null;
}
render(todos) {
this.container.innerHTML = `
<div class="todo-app">
<div class="input-group">
<input type="text" id="todo-input" placeholder="Новая задача">
<button id="add-btn">Добавить</button>
</div>
<ul class="todo-list">
${todos.map(todo => `
<li class="${todo.done ? 'done' : ''}">
<input type="checkbox" data-id="${todo.id}"
${todo.done ? 'checked' : ''}>
<span>${todo.text()}</span>
<button data-remove="${todo.id}">×</button>
</li>
`).join('')}
</ul>
<p>Осталось: ${todos.filter(t => !t.done).length}</p>
</div>
`;
this.bindEvents;
}
bindEvents {
const input = this.container.querySelector('#todo-input');
const addBtn = this.container.querySelector('#add-btn');
addBtn?.addEventListener('click', () => {
if (input.value.trim()) {
this.onAdd?.(input.value.trim());
input.value = '';
}
});
input?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
this.onAdd?.(input.value.trim());
input.value = '';
}
});
this.container.querySelectorAll('[data-id]').forEach(cb => {
cb.addEventListener('change', () => {
this.onToggle?.(Number(cb.dataset.id));
});
});
this.container.querySelectorAll('[data-remove]').forEach(btn => {
btn.addEventListener('click', () => {
this.onRemove?.(Number(btn.dataset.remove()));
});
});
}
}
Controller — связующее звено
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
// View уведомляет Controller о действиях пользователя
this.view.onAdd = (text) => this.model.addTodo(text);
this.view.onToggle = (id) => this.model.toggleTodo(id);
this.view.onRemove = (id) => this.model.removeTodo(id);
// Model уведомляет View об изменениях данных
this.model.subscribe((todos) => this.view.render(todos));
// Первый рендер
this.view.render(this.model.getAll);
}
}
// Запуск
const model = new TodoModel();
const view = new TodoView(document.getElementById('app'));
const controller = new TodoController(model, view);
Вариации MVC
MVP — Model-View-Presenter
View ←──→ Presenter ←──→ Model
Отличие от MVC:
- View НЕ знает о Model
- Presenter получает данные из Model и передаёт в View
- View — «тупой», только отображает то, что скажет Presenter
class TodoPresenter {
constructor(model, view) {
this.model = model;
this.view = view;
this.view.onAdd = (text) => {
this.model.addTodo(text);
this.updateView;
};
this.view.onToggle = (id) => {
this.model.toggleTodo(id);
this.updateView;
};
this.updateView;
}
updateView {
// Presenter полностью контролирует что видит View
const todos = this.model.getAll;
const activeCount = this.model.getActive.length;
this.view.render({ todos, activeCount });
}
}
MVVM — Model-View-ViewModel
View ←──binding──→ ViewModel ←──→ Model
Отличие:
- ViewModel содержит данные для отображения
- Two-way data binding: изменение в View → ViewModel → View
- Vue.js, Angular, Knockout.js используют MVVM
// ViewModel — реактивный объект, связанный с View
class TodoViewModel {
constructor {
this.todos = ; // Данные для View
this.newTodoText = ''; // Связано с input
this.filter = 'all'; // Связано с radio-buttons
}
// Computed property
get filteredTodos {
switch (this.filter) {
case 'active': return this.todos.filter(t => !t.done);
case 'done': return this.todos.filter(t => t.done);
default: return this.todos;
}
}
// Команды (methods)
addTodo {
if (this.newTodoText.trim()) {
this.todos.push({
id: Date.now(),
text: this.newTodoText,
done: false,
});
this.newTodoText = '';
}
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
}
// Во Vue это выглядит нативно:
// const vm = new Vue({
// data { return { todos: , newTodoText: '' } },
// computed: { filteredTodos { ... } },
// methods: { addTodo { ... } }
// })
Сравнение MVC / MVP / MVVM
| Аспект | MVC | MVP | MVVM |
|---|---|---|---|
| View знает о Model | Да | Нет | Нет |
| Посредник | Controller | Presenter | ViewModel |
| Связь View↔Model | Через Observer | Через Presenter | Data binding |
| Тестируемость | Средняя | Высокая | Высокая |
| Сложность | Низкая | Средняя | Средняя |
| Примеры | Express, Rails | Android MVP | Vue, Angular |
MVC в Express.js (серверный)
// Model — models/User.js
class User {
static async findAll {
return db.query('SELECT * FROM users');
}
static async findById(id) {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
}
static async create(data) {
return db.query('INSERT INTO users SET ?', data);
}
}
// Controller — controllers/userController.js
const User = require('../models/User');
exports.getUsers = async (req, res) => {
const users = await User.findAll;
res.json(users);
};
exports.getUser = async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
};
exports.createUser = async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
};
// Routes (часть Controller) — routes/users.js
const express = require('express');
const router = express.Router;
const userCtrl = require('../controllers/userController');
router.get('/', userCtrl.getUsers);
router.get('/:id', userCtrl.getUser);
router.post('/', userCtrl.createUser);
module.exports = router;
Частые ошибки
- Бизнес-логика в Controller — Controller должен быть тонким, логика в Model
- Бизнес-логика в View — View только отображает, не вычисляет
- Model зависит от View — Model не должен знать как данные отображаются
- Жирный Controller — превращается в God Object, нужно выделять Service-слой
- Путаница паттернов — смешивают MVC, MVP, MVVM в одном проекте
Практика
- Реализовать TODO-приложение по паттерну MVC (Model + View + Controller)
- Переделать в MVP — View не знает о Model
- Добавить серверный MVC на Express (Model → DB, Controller → Routes)
- Сравнить: написать тесты для Model — они не зависят от View
Связанные темы
- Component architecture — компонентная архитектура как альтернатива
- Управление состоянием — state management развивает идеи MVC
- Компонентный подход — компоненты совмещают V+C
- Что такое Express — серверный MVC