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

6.11. Проектирование функциональных UI

Разработчику Архитектору Аналитику

Проектирование функциональных UI

1. Функциональный UI и визуальный дизайн

Прежде чем перейти к проектированию, необходимо чётко развести две задачи:

  • Визуальный дизайн — отвечает на вопрос «как это выглядит?»: композиция, цвет, типографика, анимации, эмоциональное восприятие. Его цель — сделать интерфейс привлекательным, интуитивным и доступным.
  • Функциональное проектирование UI — отвечает на вопрос «как это работает?»: какие действия доступны пользователю, в какой последовательности, при каких условиях, как состояние системы отражается в интерфейсе, как обрабатываются ошибки. Его цель — сделать взаимодействие корректным, предсказуемым и безопасным.

Разработчик, проектирующий функциональный UI, работает с моделями состояний, контрактами API, ограничениями домена и сценариями использования. Художник создаёт макет кнопки «Оплатить»; инженер определяет, когда она должна быть активна, что происходит при нажатии, как обрабатывается таймаут, как отображается прогресс, как восстанавливается состояние после обновления страницы.

Если визуальный дизайн нарушается — страдает удобство.
Если функциональное проектирование нарушается — страдает корректность.
Например: кнопка «Оплатить» серая и некрасивая — плохо. Кнопка «Оплатить» активна при нулевой сумме — критическая ошибка.

2. UI как отражение бизнес-состояния

Фундаментальный принцип: пользовательский интерфейс не управляет системой — он отражает её состояние и предоставляет канал для воздействия.

Это означает:

  • Любое действие пользователя — это запрос на изменение состояния, а не прямая команда.
  • Любое отображаемое значение — это проекция текущего состояния, а не кэшированная копия.
  • Любое ограничение интерфейса (недоступная кнопка, скрытое поле) — это выражение бизнес-правила, а не произвольное решение дизайнера.

Пример:
В системе управления заказами статус «Оплачен» означает:

  • Нельзя изменить состав заказа,
  • Можно инициировать отгрузку,
  • Нельзя отменить без возврата средств.

Функциональный UI должен:

  • Отключать элементы редактирования при статусе «Оплачен»,
  • Делать доступной кнопку «Отгрузить»,
  • Скрывать или переключать кнопку «Отменить» на «Запросить возврат».

Если эти правила реализованы только на фронтенде, возможна рассогласованность: пользователь может отправить запрос на изменение состава через Postman, и бэкенд его примет (если там нет проверки).
Если реализованы только на бэкенде — UI будет выглядеть «мертвым»: кнопки активны, но при нажатии возвращается ошибка 403.

Правильный подход:

  1. Бизнес-правила декларируются в домене (например, Order.CanModify()bool).
  2. Бэкенд предоставляет метаданные о доступных действиях (например, GET /orders/123 возвращает данные и availableActions: ["ship", "requestRefund"]).
  3. Фронтенд строит UI на основе этих метаданных, а не на основе локальной логики.

Такой подход называется HATEOAS (Hypermedia as the Engine of Application State) — один из принципов REST, редко реализуемый полностью, но крайне полезный как идеал.

3. Алгоритм проектирования функционального UI

Процесс начинается с анализа сценариев взаимодействия.

Шаг 1. Выявление акторов и целей

Кто использует интерфейс и зачем?

  • Администратор хочет массово изменить статусы заказов.
  • Покупатель хочет проследить этапы доставки.
  • Оператор колл-центра хочет быстро найти заказ по телефону и продиктовать статус.

У каждого — разные цели, следовательно, разные приоритеты в интерфейсе:

  • Для администратора — таблица с чекбоксами и bulk-действиями,
  • Для покупателя — линейный прогресс-бар с пояснениями,
  • Для оператора — поле поиска с автодополнением и крупным отображением статуса.

Проектирование начинается с пользовательских историй, но как источника для выявления ограничений и инвариантов.

Шаг 2. Моделирование состояний и переходов

Каждый UI-экран — это проекция состояния агрегата или процесса. Необходимо явно описать:

  • Какие состояния возможны (например, Draft → Confirmed → Paid → Shipped → Delivered),
  • Какие переходы разрешены,
  • Какие условия необходимы для перехода (например, «только после оплаты можно перейти в Shipped»),
  • Какие побочные эффекты сопровождают переход (email, webhook, изменение складских остатков).

Это часто оформляется как диаграмма состояний (state machine) для согласования между разработчиками, тестировщиками и аналитиками.

Если переход не моделируется явно, он реализуется императивно в коде (if (order.Status == "Paid") ...), что ведёт к:

  • Рассогласованности между UI и бэкендом,
  • Ошибкам при параллельных изменениях,
  • Сложности добавления новых состояний.

Шаг 3. Определение контракта данных

UI требует структурированный контекст:

  • Текущее состояние объекта,
  • Возможные действия,
  • Ограничения ввода (валидация в реальном времени),
  • Справочники и enum’ы (со значениями и локализацией),
  • История изменений (если нужна аудиторская трассировка).

Контракт должен быть самодостаточным. Пример плохого API:

{ "status": "paid", "canShip": true }

— откуда UI знает, что canShip означает? Что будет, если завтра добавится canCancel?

Хороший API:

{
"status": "paid",
"transitions": [
{ "action": "ship", "label": "Отгрузить", "requires": ["warehouseNote"] },
{ "action": "requestRefund", "label": "Запросить возврат" }
],
"validationRules": {
"warehouseNote": { "required": true, "maxLength": 255 }
}
}

Такой подход:

  • Снижает связность UI и бэкенда,
  • Позволяет динамически менять логику без деплоя фронтенда,
  • Упрощает локализацию и A/B-тестирование интерфейсов.

Шаг 4. Управление асинхронностью и частичными состояниями

В распределённых системах UI часто работает с неполным или устаревшим состоянием. Проектирование должно учитывать:

  • Что показывать во время выполнения операции? (индикатор, блокировка кнопки),
  • Что делать при таймауте? (повтор, отмена, сохранение черновика),
  • Как синхронизировать состояние после переподключения? (оптимистичные обновления, операционные трансформации),
  • Как обрабатывать конфликты? («другой пользователь изменил этот заказ»).

Пример: редактирование заказа.

  • Пользователь меняет адрес,
  • В это время оператор отменяет заказ,
  • Пользователь нажимает «Сохранить».

Если UI не проверяет условие If-Match (ETag) или не получает события OrderCancelled, сохранение пройдёт — и отменённый заказ снова станет активным.

Решения:

  • Использовать оптимистичную блокировку (ETag в заголовках),
  • Подписываться на события домена через SSE или WebSocket,
  • Вводить локальные сайд-эффекты (например, отмена сохранения при получении события OrderCancelled).

Шаг 5. Валидация

Валидация в UI — ускорение проверок на бэкенде.

УровеньЦельПример
UI-валидация (мгновенная)Улучшить UX, снизить нагрузкуПроверка формата email при потере фокуса (/^[^@]+@[^@]+\.[^@]+$/)
Фронтенд-валидация (перед отправкой)Предотвратить заведомо невалидные запросыПроверка общей суммы ≥ минимальной корзины
Бэкенд-валидация (на входе)Гарантия целостностиПовторная проверка формата, бизнес-правил, доступа
Доменная валидация (в сущностях)Защита инвариантовOrder.AddLine() проверяет, что товар активен и в наличии

Важно: никакая валидация на фронтенде не отменяет проверки на бэкенде. UI можно обойти; бэкенд — нет.

Шаг 6. Отображение ошибок

Ошибка — часть взаимодействия. UX ошибки должен быть:

  • Конкретным: «Срок действия карты истёк» вместо «Ошибка оплаты»,
  • Действенным: предложить решение — «Введите новую карту» или «Свяжитесь с банком»,
  • Локализованным: код ошибки (CARD_EXPIRED) + сообщение на языке пользователя,
  • Логируемым: уникальный correlation ID для диагностики.

Идеальный контракт ошибки:

{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "На счёте недостаточно средств",
"details": {
"balance": 1200,
"required": 1500,
"currency": "RUB"
},
"resolution": {
"actions": ["topUp", "useBonus"],
"links": {
"topUp": "/wallet/top-up",
"bonus": "/profile/bonus"
}
}
}
}

4. Разделение ответственности между UI и бизнес-логикой

Последствия дублирования логики:

  • UI содержит if (total < 1000) shipping = 300 else shipping = 0,
  • Бэкенд содержит ту же логику,
  • Правило меняется: доставка бесплатна от 800 ₽,
  • Забыли обновить фронтенд — пользователь видит 300 ₽, но платит 0,
  • Пользователь путается, поддержка получает жалобы.

Как избежать:

  1. Вынос правил в shared-библиотеку (если стек позволяет — например, TypeScript + .NET через генерацию DTO и валидаторов),
  2. Предоставление правил через API (например, GET /pricing/rules возвращает { "freeShippingThreshold": 800 }),
  3. Использование декларативных форм (JSON Schema, form.io), где схема генерируется на бэкенде и интерпретируется на фронтенде.

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

5. Доступность и инклюзивность как требования проектирования

Функциональное проектирование UI включает обеспечение доступности (accessibility) — как функциональное требование.

Примеры:

  • Кнопка без aria-labelнедоступна для 1% пользователей, что нарушает функциональность.
  • Цветовая индикация статуса (красный = ошибка) без текста — некорректное отображение состояния.
  • Отсутствие tabindexблокировка работы через клавиатуру.

Стандарты (WCAG 2.1, Section 508) содержат конкретные критерии, которые можно тестировать автоматически (axe, Lighthouse) и вручную. Их соблюдение — часть контракта UI.