Перейти к содержимому

Лекция 9. Архитектурные принципы и презентационные паттерны

Введение

На этой паре мы поднимаемся над уровнем «как написать экран» и спрашиваем: как организовать код так, чтобы приложение можно было развивать месяцами и годами, не утонув в собственной сложности. Это и есть архитектура.

Архитектура — это не библиотека и не фреймворк. Это набор решений о том, какие части системы существуют, кто за что отвечает и как части общаются друг с другом. В Expo/React Native у нас есть компоненты, хуки, навигация, запросы к API — и от того, как мы их раскладываем по «полочкам», зависит судьба проекта.

Сегодня разберём:

  • зачем нужна архитектура и какова цена её отсутствия;
  • базовые принципы: SoC, SOLID, DRY/KISS/YAGNI, связанность и связность;
  • презентационные паттерны MVC, MVP, MVVM — детально, с потоками данных;
  • что обрабатывать в представлении, а что — в логике;
  • антипаттерны и «запахи», по которым видно, что архитектура поехала.

1. Зачем нужна архитектура

1.1. Что такое архитектура (формально)

По стандарту ISO/IEC/IEEE 42010, архитектура — это фундаментальная организация системы: её компоненты, их взаимосвязи между собой и со средой, а также принципы проектирования и эволюции. Проще говоря — это «скелет» приложения и правила, по которым он растёт.

Важно: архитектура существует всегда. Вопрос только в том, спроектирована она осознанно или сложилась стихийно. «У нас нет архитектуры» означает «у нас плохая, неконтролируемая архитектура».

1.2. Зачем она нужна

Хорошая архитектура обеспечивает архитектурные качества (quality attributes):

  • Модифицируемость — новую фичу можно добавить, не переписывая половину кода.
  • Тестируемость — логику можно проверить без запуска UI и реальной сети.
  • Понятность — новый разработчик находит нужное место за минуты, а не за дни.
  • Переносимость и заменяемость — можно сменить источник данных (REST → GraphQL) или библиотеку состояния, не трогая бизнес-правила.
  • Надёжность — ошибки локализуются и не «растекаются» по всему приложению.

1.3. Цена отсутствия архитектуры

Когда правил нет, проект деградирует предсказуемо:

  • Хрупкость: правка в одном экране ломает другой, не связанный с ним напрямую.
  • Жёсткость: любое изменение тянет за собой каскад правок в десятках файлов.
  • Невозможность тестов: бизнес-логика прибита к UI и сети, её нельзя изолировать.
  • Дублирование: одни и те же форматирования/валидации копируются по экранам.
  • Замедление команды: чем больше код, тем медленнее идёт разработка (вместо ускорения).
  • Рост «bus factor»: систему понимает один человек, и его уход — катастрофа.

Эти проблемы накапливаются как технический долг. Архитектурные «запахи» — это сигналы долга, который придётся обслуживать; чем позже, тем дороже.

Метафора: писать без архитектуры — как строить дом без чертежа. Первый этаж получится быстро, но к третьему стены начнут расходиться.


2. Архитектурные принципы

2.1. Разделение ответственностей (Separation of Concerns, SoC)

Главный принцип: каждый «концерн» (забота) живёт в своём месте. Классическое разделение на три зоны:

  • Представление (UI) — рисует интерфейс и ловит ввод пользователя. Не знает, откуда берутся данные и по каким правилам считаются.
  • Логика (домен) — формулирует правила предметной области: что значит «оформить заказ», «можно ли удалить задачу». Независима от платформы.
  • Данные — доступ к источникам (HTTP, БД, кеш). Скрывает конкретику: компонент не должен знать URL эндпоинта.

Если эти зоны перемешаны — нарушено SoC, и начинаются проблемы из раздела 1.3.

2.2. SOLID применительно к RN/Expo

SOLID — пять принципов объектно-ориентированного дизайна, которые отлично работают и в мире хуков/модулей.

  • S — Single Responsibility (единственная ответственность). У модуля одна причина для изменения. Экран отвечает за отображение, репозиторий — за доступ к данным, сервис — за конкретную операцию. Не сваливаем всё в один компонент.
  • O — Open/Closed (открыт для расширения, закрыт для изменения). Новую функциональность добавляем новой реализацией интерфейса/нового хука, а не правкой существующего рабочего кода.
  • L — Liskov Substitution (подстановка). Mock-репозиторий и реальный репозиторий взаимозаменяемы: код, работающий с интерфейсом, не должен «догадываться», какая реализация под ним. Это и делает возможным тестирование.
  • I — Interface Segregation (разделение интерфейсов). Маленькие интерфейсы под конкретный сценарий лучше одного «толстого». Компоненту-списку нужен getTodos(), а не весь API-клиент целиком.
  • D — Dependency Inversion (инверсия зависимостей). Верхние уровни зависят от абстракций, а не от деталей. ViewModel зависит от интерфейса TodoRepository, а не от fetch/axios напрямую.

2.3. DRY, KISS, YAGNI

  • DRY (Don’t Repeat Yourself) — не дублируйте знание. Логика форматирования цены или валидации email должна жить в одном месте. Но осторожно: преждевременное «обобщение» двух случайно похожих кусков — тоже зло.
  • KISS (Keep It Simple, Stupid) — простейшее решение, которое работает, обычно лучшее. Не вводите пять слоёв абстракции там, где хватает функции.
  • YAGNI (You Aren’t Gonna Need It) — не пишите функциональность «на будущее», которую сейчас никто не просит. Спекулятивная гибкость = лишняя сложность сегодня.

KISS и YAGNI уравновешивают SOLID и DRY: цель не «максимум абстракций», а достаточно абстракций под реальные потребности.

2.4. Низкая связанность и высокая связность

Два ключевых свойства хорошей структуры:

  • Связанность (coupling) — степень зависимости модулей друг от друга. Стремимся к низкой: изменение одного модуля не должно требовать правок в куче других.
  • Связность (cohesion) — логическая целостность модуля, насколько его части служат одной цели. Стремимся к высокой: всё, что относится к «корзине», лежит вместе, а не размазано по проекту.

Идеал: модули внутри плотные (высокая связность), а между собой соединены тонкими, явными контрактами (низкая связанность). Тогда систему легко менять по частям.

2.5. Чистые функции и изоляция эффектов

  • Бизнес-логику предпочтительно оформлять чистыми функциями: одни и те же входные данные → один и тот же результат, без побочных эффектов. Их тривиально тестировать.
  • Побочные эффекты (сеть, хранилище, время, случайность) изолируем в адаптерах/сервисах на краю системы. Тогда «ядро» остаётся детерминированным.

3. Презентационные паттерны: MVC, MVP, MVVM

Презентационные паттерны описывают, как разделить отображение и логику представления. Все три разделяют систему на «модель» (данные/правила) и «вид» (UI), но по-разному организуют посредника между ними.

3.1. MVC — Model-View-Controller

Участники:

  • Model — данные и бизнес-правила.
  • View — отображение, показывает состояние модели.
  • Controller — принимает ввод пользователя, обновляет модель, выбирает View.

Поток данных:

Пользователь → View → Controller → Model
↑ |
└──── обновление ─────┘
(View читает Model, Controller их связывает)

Плюсы: простота, хорошо для несложных экранов; разделение данных и отображения. Минусы: Controller быстро разрастается — собирает на себя всю логику. На iOS это вошло в фольклор как «Massive View Controller». Границы между View и Controller размыты.

3.2. MVP — Model-View-Presenter

Участники:

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

Поток данных:

Пользователь → View → Presenter → Model
↑ |
└─ команды ┘
(Presenter явно вызывает методы View: view.showLoading(),
view.renderList(items), view.showError(msg))

Плюсы: View максимально «глупая», поэтому Presenter тестируется как обычный класс/функции, без UI. Чёткое разделение. Минусы: много «ручной» связки (boilerplate) — Presenter вызывает методы View по одному; интерфейс View разрастается.

3.3. MVVM — Model-View-ViewModel

Участники:

  • Model — данные и правила.
  • View — отображает состояние и подписывается на него; декларативна.
  • ViewModel — хранит наблюдаемое состояние (observable) и логику представления. Не знает о конкретной View.

Ключевое отличие: связывание данных (data binding). View не вызывают «в лоб» — она сама реагирует на изменение состояния ViewModel. Это идеально ложится на декларативный UI.

Поток данных:

Пользователь → View → (действие) → ViewModel → Model
↑ |
└── состояние (binding)─┘
(View автоматически перерисовывается при изменении state)

В RN/Expo MVVM почти «нативен»:

  • Функциональный компонент = View.
  • Кастомный хук со стейтом и эффектами = ViewModel.
  • Реактивность React (перерисовка при смене state) = механизм биндинга.
// ViewModel: вся логика экрана в хуке
function useTodosViewModel(repo) {
const [state, setState] = useState({ status: 'idle', items: [] });
async function load() {
setState({ status: 'loading', items: [] });
try {
const items = await repo.getTodos(); // repo — абстракция (D из SOLID)
setState({ status: 'success', items });
} catch (e) {
setState({ status: 'error', items: [] });
}
}
return { state, load };
}
// View: только отображение и проброс событий
function TodosScreen({ repo }) {
const { state, load } = useTodosViewModel(repo);
useEffect(() => { load(); }, []);
if (state.status === 'loading') return <Spinner />;
if (state.status === 'error') return <ErrorView onRetry={load} />;
return <TodoList items={state.items} />;
}

View здесь не содержит ни запросов, ни обработки ошибок — только «как нарисовать состояние». Вся логика — в ViewModel, и её можно протестировать отдельно от компонента.

3.4. Сравнительная таблица

КритерийMVCMVPMVVM
ПосредникControllerPresenterViewModel
Роль Viewактивная, читает Modelпассивная, «глупая»декларативная, подписана на state
Связь View↔логикапрямые вызовыPresenter командует Viewбиндинг (реакция на state)
Связь с Viewтеснаячерез интерфейс ViewViewModel не знает о View
Тестируемость логикинизкая (логика в Controller+UI)высокаявысокая
Boilerplateмаломного (ручная связка)мало (биндинг автоматический)
Риск разрастания«Massive Controller»толстый интерфейс View«God-хук» при недосмотре
Где уместенпростые экраныгде нужен строгий контроль и тестыдекларативный UI (React, SwiftUI)

3.5. Что выбирать в React Native

Рекомендация — MVVM на хуках. Декларативная природа React делает биндинг бесплатным: компонент автоматически перерисовывается при смене состояния. Достаточно вынести логику экрана в кастомный хук (useXxxViewModel) — и вы получаете MVVM без лишней церемонии.

  • Простой экран без логики — можно вообще без формального паттерна (всё в компоненте, это нормально для мелочей; помним KISS/YAGNI).
  • Экран с состоянием, загрузкой, ошибками — выносим логику в хук-ViewModel.
  • MVP/MVC в чистом виде в RN применяют редко: MVP даёт лишний boilerplate, классический MVC плохо ложится на функциональные компоненты.

4. Где что обрабатывать: представление vs логика

Граница между View и логикой — главный практический вопрос. Ориентир:

В представлении (View / компонент):

  • рендеринг разметки и стилей;
  • локальное UI-состояние (открыт ли дропдаун, фокус поля);
  • проброс событий пользователя «наверх» (в ViewModel);
  • отображение состояния, которое ему дали.

В логике (ViewModel / домен / данные):

  • загрузка и сохранение данных (через репозиторий);
  • бизнес-правила и валидация («сумма > 0», «email корректен»);
  • обработка ошибок и решения «повторить/показать дефолт»;
  • преобразование данных в форму, удобную для отображения (view-модель).

Простое правило-эвристика: если строку кода нужно протестировать без рендеринга UI — она не должна жить в JSX. Если внутри компонента появился fetch, if (response.status === 401) или формула расчёта скидки — логика «протекла» в представление.

Полезно мыслить UI как конечный автомат: Idle → Loading → Success/Empty → Error. ViewModel управляет переходами, View лишь рисует текущее состояние. Это убирает целый класс багов «забыли обработать ветку».


5. Антипаттерны и «запахи»

«Запах» (code smell) — не баг, но признак, что структура портится. Узнавать их — половина дела.

5.1. God-компонент (God-хук / God-module)

Один файл/компонент/хук делает всё: рендер, состояние, сетевые запросы, навигацию, форматирование. Признаки: 500+ строк, десятки useState, всё связано со всем. Почему плохо: невозможно тестировать, переиспользовать, понять. Нарушает SRP и высокую связность. Лечение: выделить ViewModel-хук, разбить на под-компоненты, вынести данные в репозиторий.

5.2. Бизнес-логика в UI

Расчёт цены, правила доступа, валидация прямо в JSX или в обработчике onPress компонента. Почему плохо: логику нельзя протестировать без UI, она дублируется между экранами, меняется вместе с вёрсткой. Лечение: вынести в чистые функции / домен / ViewModel.

5.3. Прямые запросы из экрана

Компонент сам зовёт fetch('https://api...'), знает URL, заголовки, формат ответа. Почему плохо: жёсткая связь UI↔HTTP. Нельзя сменить источник, нельзя подставить mock для теста, дублирование сетевого кода. Лечение: ввести репозиторий/сервис как фасад; компонент зависит от абстракции (Dependency Inversion).

5.4. «Процедурная куча»

Слоёв нет вовсе: всё свалено в utils и компоненты, абстракций нет, форматы данных дублируются. Лечение: ввести явные слои (presentation/domain/data) хотя бы концептуально.

5.5. Скрытые сайд-эффекты

Функция незаметно меняет глобальное состояние, пишет в хранилище, дёргает аналитику. Почему плохо: поведение непредсказуемо, тесты «плавают». Лечение: делать эффекты явными, изолировать на краю системы (раздел 2.5).

5.6. Жёсткая связь UI ↔ конкретная библиотека

UI напрямую завязан на конкретный стор/HTTP-клиент по всему коду. Почему плохо: смена библиотеки = переписать всё. Лечение: изолировать за тонким адаптером/хуком.

Запахи — повод для рефакторинга, а не для паники. Фиксируйте их и обслуживайте планово, иначе технический долг превратится в «архитектурное банкротство».


Краткие итоги

  • Архитектура есть всегда — выбор лишь в том, осознанная она или стихийная. Цена отсутствия: хрупкость, жёсткость, невозможность тестов, замедление команды.
  • Базовые принципы: SoC (разделить UI/домен/данные), SOLID (особенно SRP и Dependency Inversion), DRY/KISS/YAGNI как баланс, низкая связанность + высокая связность.
  • Презентационные паттерны разделяют отображение и логику представления: MVC (простой, риск «Massive Controller»), MVP (пассивная View, много boilerplate, хорошо тестируется), MVVM (биндинг состояния, минимум связки).
  • В React Native выбирают MVVM на хуках: компонент = View, кастомный хук = ViewModel, реактивность React = биндинг.
  • Граница View/логика: в UI — рендер и локальное UI-состояние; в логике — данные, бизнес-правила, ошибки. Эвристика: тестируется без UI → не место в JSX.
  • Антипаттерны: God-компонент, бизнес-логика в UI, прямые запросы из экрана, скрытые эффекты. Лечатся выделением слоёв, ViewModel и репозиториев.

Вопросы для самопроверки

  1. (Вопрос 21) Что такое архитектура приложения и какова цена её отсутствия? Перечислите принципы SoC, SOLID, DRY/KISS/YAGNI и объясните, что означают низкая связанность и высокая связность.
  2. (Вопрос 22) Сравните презентационные паттерны MVC, MVP и MVVM: роли участников, потоки данных, плюсы и минусы. Какой паттерн обычно выбирают в React Native и почему?
  3. (Вопрос 29) Какие архитектурные антипаттерны и «запахи» вы знаете (God-компонент, бизнес-логика в UI, прямые запросы из экрана)? Чем они опасны и как их лечить? Где должна жить бизнес-логика, а что остаётся в представлении?