Навигация

MVC, MVP и MVVM в React и Vue

MVC, MVP и MVVM — три фундаментальных архитектурных паттерна, определяющих, как разделяется ответственность между данными, отображением и логикой. В статье разберём каждый из них, покажем классические примеры и посмотрим, как современные фронтенд-фреймворки — React и Vue — вписываются в эти модели или сознательно отходят от них.

Содержание

  1. MVC (Model-View-Controller)
  2. MVP (Model-View-Presenter)
  3. MVVM (Model-View-ViewModel)
  4. React и архитектурные паттерны
  5. Vue и архитектурные паттерны
  6. Сравнение паттернов в контексте современных фреймворков
  7. Практические рекомендации
  8. Ключевые тезисы для интервью
  9. Заключение
  10. Полезные ссылки

MVC (Model-View-Controller)

Основная концепция

MVC — это классический архитектурный паттерн, который разделяет приложение на три взаимосвязанных компонента:

Model (Модель) — отвечает за данные и бизнес-логику приложения. Модель не знает ничего о представлении и контроллере, она просто управляет данными и правилами их изменения.

View (Представление) — отвечает за отображение данных пользователю. Представление получает данные от модели и отображает их, но не содержит бизнес-логики.

Controller (Контроллер) — выступает посредником между моделью и представлением. Контроллер принимает пользовательский ввод, обрабатывает его, обновляет модель и выбирает представление для отображения.

Как работает MVC

Пользователь → Controller → Model → View → Пользователь
  1. Пользователь взаимодействует с представлением (например, нажимает кнопку)
  2. Представление передает событие контроллеру
  3. Контроллер обрабатывает событие и обновляет модель
  4. Модель уведомляет представление об изменении данных
  5. Представление обновляется и показывает новые данные пользователю

Пример MVC в классическом веб-приложении

// Model
class UserModel {
  constructor() {
    this.users = [];
  }
  
  addUser(user) {
    this.users.push(user);
    this.notify();
  }
  
  getUsers() {
    return this.users;
  }
  
  notify() {
    // Уведомление наблюдателей
  }
}

// View
class UserView {
  render(users) {
    const html = users.map(u => `<li>${u.name}</li>`).join('');
    document.getElementById('user-list').innerHTML = html;
  }
}

// Controller
class UserController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
  }
  
  addUser(name) {
    this.model.addUser({ name });
  }
  
  updateView() {
    const users = this.model.getUsers();
    this.view.render(users);
  }
}

Преимущества MVC

  • Четкое разделение ответственности
  • Упрощение тестирования компонентов
  • Возможность параллельной разработки разных частей
  • Повторное использование кода

Недостатки MVC

  • Сложность отладки из-за множества уровней абстракции
  • Контроллер может стать слишком большим (проблема “Massive View Controller”)
  • Необходимость синхронизации между компонентами

MVP (Model-View-Presenter)

Основная концепция

MVP — это эволюция MVC, где Presenter заменяет Controller. Ключевое отличие — полное разделение Model и View через Presenter.

Model (Модель) — хранит данные и бизнес-логику, не зависит от View и Presenter.

View (Представление) — пассивный компонент, который только отображает данные и передает пользовательские действия Presenter. View не обращается к Model напрямую.

Presenter (Презентер) — содержит всю логику представления, получает данные от Model и форматирует их для отображения во View. Presenter полностью контролирует View.

Как работает MVP

View ↔ Presenter ↔ Model
  1. Пользователь взаимодействует с View
  2. View передает действие Presenter
  3. Presenter обрабатывает действие, обращается к Model
  4. Model возвращает данные Presenter
  5. Presenter форматирует данные и обновляет View

Пример MVP

// Model
class UserModel {
  constructor() {
    this.users = [];
  }
  
  addUser(user) {
    this.users.push(user);
  }
  
  getUsers() {
    return this.users;
  }
}

// View Interface
class IUserView {
  renderUsers(users) {
    throw new Error('Method not implemented');
  }
}

// View
class UserView extends IUserView {
  constructor(presenter) {
    super();
    this.presenter = presenter;
    this.addButton = document.getElementById('add-btn');
    this.addButton.addEventListener('click', () => {
      const name = document.getElementById('name-input').value;
      this.presenter.addUser(name);
    });
  }
  
  renderUsers(users) {
    const html = users.map(u => `<li>${u.name}</li>`).join('');
    document.getElementById('user-list').innerHTML = html;
  }
}

// Presenter
class UserPresenter {
  constructor(model, view) {
    this.model = model;
    this.view = view;
  }
  
  addUser(name) {
    this.model.addUser({ name });
    this.updateView();
  }
  
  updateView() {
    const users = this.model.getUsers();
    this.view.renderUsers(users);
  }
  
  init() {
    this.updateView();
  }
}

// Использование
const model = new UserModel();
const view = new UserView();
const presenter = new UserPresenter(model, view);
view.presenter = presenter;
presenter.init();

Преимущества MVP

  • Полное разделение Model и View
  • Легко тестировать Presenter (можно мокать View)
  • Четкая ответственность каждого компонента
  • View становится максимально простым

Недостатки MVP

  • Много boilerplate кода
  • Presenter может стать очень большим
  • Необходимость создавать интерфейсы для View

MVVM (Model-View-ViewModel)

Основная концепция

MVVM — это паттерн, который использует двустороннее связывание данных (data binding) для автоматической синхронизации между View и ViewModel.

Model (Модель) — данные и бизнес-логика, не зависит от View и ViewModel.

View (Представление) — активный компонент, который наблюдает за изменениями в ViewModel и автоматически обновляется при их возникновении.

ViewModel (Модель представления) — абстракция View, которая содержит данные и логику для представления. ViewModel использует observable свойства, которые автоматически уведомляют View об изменениях.

Как работает MVVM

View ←→ ViewModel ←→ Model
  1. View связывается с ViewModel через data binding
  2. Пользователь взаимодействует с View
  3. View автоматически обновляет свойства ViewModel
  4. ViewModel обрабатывает изменения и обновляет Model
  5. Model возвращает данные ViewModel
  6. ViewModel обновляет observable свойства
  7. View автоматически обновляется через механизм binding

Пример MVVM

// Model
class UserModel {
  constructor() {
    this.users = [];
  }
  
  addUser(user) {
    this.users.push(user);
  }
  
  getUsers() {
    return this.users;
  }
}

// ViewModel с Observable
class UserViewModel {
  constructor(model) {
    this.model = model;
    this.users = [];
    this.newUserName = '';
    
    // Наблюдатели
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
  }
  
  notify() {
    this.observers.forEach(obs => obs.update());
  }
  
  setNewUserName(name) {
    this.newUserName = name;
  }
  
  addUser() {
    if (this.newUserName) {
      this.model.addUser({ name: this.newUserName });
      this.users = this.model.getUsers();
      this.newUserName = '';
      this.notify();
    }
  }
}

// View с Data Binding
class UserView {
  constructor(viewModel) {
    this.viewModel = viewModel;
    this.viewModel.subscribe(this);
    
    this.input = document.getElementById('name-input');
    this.addButton = document.getElementById('add-btn');
    this.list = document.getElementById('user-list');
    
    this.input.addEventListener('input', (e) => {
      this.viewModel.setNewUserName(e.target.value);
    });
    
    this.addButton.addEventListener('click', () => {
      this.viewModel.addUser();
    });
  }
  
  update() {
    // Автоматическое обновление при изменении ViewModel
    const html = this.viewModel.users.map(u => `<li>${u.name}</li>`).join('');
    this.list.innerHTML = html;
    this.input.value = this.viewModel.newUserName;
  }
}

Преимущества MVVM

  • Автоматическая синхронизация View и ViewModel
  • Минимум boilerplate кода
  • Легко тестировать ViewModel
  • Декларативный стиль программирования

Недостатки MVVM

  • Сложность отладки из-за автоматического binding
  • Производительность при большом количестве bindings
  • Скрытие логики в механизме binding

React и архитектурные паттерны

React: не MVC, не MVVM, а компонентный подход

React официально не следует ни одному из классических паттернов. Вместо этого React использует компонентный подход с однонаправленным потоком данных (unidirectional data flow).

Как React соотносится с MVC

React можно рассматривать как упрощенную версию MVC, где:

  • Model — это состояние приложения (state) и контекст (context)
  • View — это JSX-разметка компонентов
  • Controller — это методы компонентов (event handlers, lifecycle methods)

Однако в React все три части объединены в одном компоненте:

// React компонент сочетает Model, View и Controller
function UserComponent() {
  // Model (состояние)
  const [users, setUsers] = useState([]);
  const [newUserName, setNewUserName] = useState('');
  
  // Controller (обработчики)
  const addUser = () => {
    if (newUserName) {
      setUsers([...users, { name: newUserName }]);
      setNewUserName('');
    }
  };
  
  // View (рендер)
  return (
    <div>
      <input 
        value={newUserName}
        onChange={(e) => setNewUserName(e.target.value)}
      />
      <button onClick={addUser}>Добавить</button>
      <ul>
        {users.map((user, i) => (
          <li key={i}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

React как MVVM?

Некоторые разработчики проводят параллели между React и MVVM:

  • ViewModel — это состояние компонента (useState, useReducer)
  • View — это JSX-разметка
  • Data Binding — однонаправленный поток данных через props

Однако это не совсем MVVM, потому что:

  • В React нет автоматического двустороннего binding
  • Компоненты React не являются пассивными, они активно управляют своим состоянием
  • Поток данных в React однонаправленный (от родителя к ребенку)

React и Flux/Redux

Для управления состоянием в React часто используют Flux или Redux, которые добавляют дополнительную архитектуру:

// Redux Store (Model)
const userReducer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_USER':
      return [...state, action.payload];
    default:
      return state;
  }
};

// Component (View + Controller)
function UserComponent() {
  const users = useSelector(state => state.users);
  const dispatch = useDispatch();
  
  const addUser = (name) => {
    dispatch({ type: 'ADD_USER', payload: { name } });
  };
  
  return (
    <div>
      {/* View */}
      <ul>
        {users.map((user, i) => <li key={i}>{user.name}</li>)}
      </ul>
      <button onClick={() => addUser('John')}>
        Добавить
      </button>
    </div>
  );
}

В этой модели:

  • Redux Store — это Model
  • Component — это View + Controller
  • Actions — это способ взаимодействия с Model

Vue и архитектурные паттерны

Vue: ближе всего к MVVM

Vue официально позиционируется как MVVM-фреймворк. Это наиболее близкое соответствие классическому MVVM среди современных фреймворков.

Как Vue реализует MVVM

<template>
  <!-- View -->
  <div>
    <input v-model="newUserName" placeholder="Имя пользователя" />
    <button @click="addUser">Добавить</button>
    <ul>
      <li v-for="(user, index) in users" :key="index">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // ViewModel
      users: [],
      newUserName: ''
    };
  },
  
  methods: {
    // Controller логика
    addUser() {
      if (this.newUserName) {
        this.users.push({ name: this.newUserName });
        this.newUserName = '';
      }
    }
  }
}
</script>

Vue 3 Composition API и MVVM

Vue 3 с Composition API предоставляет более гибкий подход:

<template>
  <div>
    <input v-model="newUserName" />
    <button @click="addUser">Добавить</button>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// ViewModel
const users = ref([]);
const newUserName = ref('');

const canAddUser = computed(() => newUserName.value.trim() !== '');

// Controller
const addUser = () => {
  if (canAddUser.value) {
    users.value.push({ 
      id: Date.now(),
      name: newUserName.value 
    });
    newUserName.value = '';
  }
};
</script>

Vue и реактивность

Vue использует систему реактивности для автоматической синхронизации:

// Vue 3 реактивность
import { reactive, watch } from 'vue';

// Model
const userModel = reactive({
  users: [],
  addUser(user) {
    this.users.push(user);
  }
});

// ViewModel
const userViewModel = reactive({
  newUserName: '',
  
  get users() {
    return userModel.users;
  },
  
  addUser() {
    if (this.newUserName) {
      userModel.addUser({ name: this.newUserName });
      this.newUserName = '';
    }
  }
});

// Автоматическое отслеживание изменений
watch(
  () => userViewModel.users,
  (newUsers) => {
    console.log('Users updated:', newUsers);
  }
);

Vue vs React: различия в подходе

АспектReactVue
ПаттернКомпонентный + Flux/ReduxMVVM
Data BindingОднонаправленныйДвусторонний (v-model)
СостояниеState + PropsReactive data
Обновление ViewVirtual DOM + reconciliationReactive system + Virtual DOM
ПодходИмперативныйДекларативный

Сравнение паттернов в контексте современных фреймворков

Когда использовать какой паттерн

MVC подходит для:

  • Серверных приложений (Express.js, Django, Ruby on Rails)
  • Приложений с четким разделением между данными, отображением и управлением
  • Команд с опытом работы с классическими паттернами

MVP подходит для:

  • Приложений, где важно тестирование UI
  • Проектов с сложной бизнес-логикой представления
  • Когда нужно полное разделение Model и View

MVVM подходит для:

  • Клиентских SPA-приложений
  • Приложений с сложным UI и множеством форм
  • Когда важна автоматическая синхронизация данных

React vs Vue: выбор подхода

Выбирайте React, если:

  • Вам нужна гибкость в выборе архитектуры
  • Вы предпочитаете функциональный подход
  • Вам нужна большая экосистема и сообщество
  • Вы хотите контролировать поток данных

Выбирайте Vue, если:

  • Вам нужен готовый MVVM-фреймворк
  • Вы предпочитаете декларативный стиль
  • Вам важна простота изучения
  • Вы хотите встроенную реактивность из коробки

Практические рекомендации

Для React-приложений

  1. Используйте Context API для глобального состояния:
const UserContext = createContext();

function UserProvider({ children }) {
  const [users, setUsers] = useState([]);
  
  const addUser = (user) => {
    setUsers([...users, user]);
  };
  
  return (
    <UserContext.Provider value={{ users, addUser }}>
      {children}
    </UserContext.Provider>
  );
}
  1. Разделяйте логику и представление с помощью custom hooks:
// Custom hook (ViewModel логика)
function useUsers() {
  const [users, setUsers] = useState([]);
  const [newUserName, setNewUserName] = useState('');
  
  const addUser = () => {
    if (newUserName) {
      setUsers([...users, { name: newUserName }]);
      setNewUserName('');
    }
  };
  
  return { users, newUserName, setNewUserName, addUser };
}

// Component (View)
function UserComponent() {
  const { users, newUserName, setNewUserName, addUser } = useUsers();
  
  return (
    <div>
      <input 
        value={newUserName}
        onChange={(e) => setNewUserName(e.target.value)}
      />
      <button onClick={addUser}>Добавить</button>
      <ul>
        {users.map((user, i) => <li key={i}>{user.name}</li>)}
      </ul>
    </div>
  );
}

Для Vue-приложений

  1. Используйте Composition API для сложной логики:
<script setup>
import { ref, computed } from 'vue';

// Логика в composables
function useUserManagement() {
  const users = ref([]);
  const newUserName = ref('');
  
  const canAdd = computed(() => newUserName.value.trim() !== '');
  
  const addUser = () => {
    if (canAdd.value) {
      users.value.push({ name: newUserName.value });
      newUserName.value = '';
    }
  };
  
  return { users, newUserName, canAdd, addUser };
}

export default {
  setup() {
    return useUserManagement();
  }
}
</script>
  1. Используйте Pinia для управления состоянием:
// stores/user.js
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    newUserName: ''
  }),
  
  getters: {
    canAdd: (state) => state.newUserName.trim() !== ''
  },
  
  actions: {
    addUser() {
      if (this.canAdd) {
        this.users.push({ name: this.newUserName });
        this.newUserName = '';
      }
    }
  }
});

Ключевые тезисы для интервью

  • MVC разделяет приложение на Model (данные), View (отображение) и Controller (посредник); MVP заменяет Controller на Presenter, полностью изолируя View от Model; MVVM использует двустороннее связывание, автоматически синхронизируя ViewModel и View через реактивные свойства.
  • Основные недостатки: MVC превращается в «Massive View Controller», MVP требует много boilerplate, MVVM усложняет отладку из-за скрытых binding.
  • React не следует ни одному классическому паттерну — использует компонентный подход с однонаправленным потоком данных, где компонент объединяет Model (state), View (JSX) и Controller (event handlers).
  • Vue официально позиционируется как MVVM-фреймворк: data/ref — ViewModel, шаблон — View, v-model — двустороннее связывание.
  • Custom hooks в React позволяют вынести ViewModel-логику (состояние и бизнес-правила) из компонентов, приблизив архитектуру к MVVM.
  • Redux/Pinia играют роль внешнего Model, разгружая компоненты до View + Controller.
  • MVC чаще встречается в серверных фреймворках (Express, Rails, Django), MVVM — во фронтенде с реактивностью (Vue, Knockout, Angular).

Заключение

Понимание MVC, MVP и MVVM помогает лучше организовать код и выбрать правильный подход для вашего проекта:

  • React не следует строго одному паттерну, но предлагает гибкий компонентный подход с однонаправленным потоком данных. Вы можете построить архитектуру, похожую на MVVM, используя custom hooks и Context API.

  • Vue наиболее близок к классическому MVVM с встроенной системой реактивности и двусторонним связыванием данных. Это делает его отличным выбором для приложений с сложным UI.

Выбор между React и Vue зависит от ваших предпочтений в архитектуре:

  • Если вам нужна гибкость и контроль — выбирайте React
  • Если вам нужен готовый MVVM-фреймворк — выбирайте Vue

Оба подхода имеют свои преимущества, и понимание классических паттернов поможет вам принимать более обоснованные архитектурные решения в любом фреймворке.

Полезные ссылки