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

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

Разработчику Архитектору Аналитику
Теория данных (раздел 3)

Проектирование функциональных 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 для диагностики.

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

Код ITЗагрузка примера кода…


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.


6. Практический сценарий "Оплата заказа"

Шаблон поведения для критической кнопки:

  1. Пользователь нажимает "Оплатить".
  2. UI переводит кнопку в состояние loading и блокирует повторное нажатие.
  3. Запрос отправляется с Idempotency-Key.
  4. При сетевой ошибке пользователь видит промежуточный статус "Платёж обрабатывается", а не мгновенный повтор без контекста.
  5. Итоговый статус подтверждается повторным чтением состояния заказа из API.

Так интерфейс остаётся предсказуемым, даже когда сеть и интеграции работают нестабильно.


7. Частые ошибки в функциональном UI

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

Эти ошибки снижают доверие пользователя и увеличивают нагрузку на поддержку.


8. Мини-чек-лист перед релизом

  • Для каждого экрана зафиксированы состояния idle/loading/success/error.
  • Для каждой команды определен механизм защиты от повторов.
  • Ошибки маппятся на понятные сообщения и action-подсказки.
  • Доступность проверена клавиатурной навигацией и скринридером.
  • UI отражает правила домена через контракт API, а не через "магические if".

Смежные материалы