asyncink.comasyncink

Основные вопросы разработки современных интерфейсов от Дэна Абрамова

Перевод статьи Дэна Абрамова «The Elements of UI Engineering» о сложностях, которые должны быть решены в хорошем интерфейсе. В статье рассмотрены фундаментальные проблемы, осмысление и решение которых самостоятельно – без использования готовых библиотек и фреймворков – способно дать глубинное понимание существующих на рынке решений в области frontend-разработки.


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

Я всегда проявлял наибольший интерес к созданию пользовательских интерфейсов. Так в чем же я разбираюсь на самом деле?

Конечно, мне хорошо знакомы JavaScript и React. Но дело не в них. Важные для понимания вещи приходят с опытом, они неуловимы и обычно ускользают при попытках точно их сформулировать. Это моя первая попытка систематизировать и описать некоторые из них.

Сегодня существует поразительное разнообразие технологий. На какую библиотеку сделать ставку в 2019 году? В 2020? Стоит изучать Vue или React? Или Angular? Что насчет Redux или Rx? Нужно ли изучать Apollo? REST или GraphQL? Здесь легко потеряться! К тому же любой советчик тоже может ошибаться.

Мои наибольшие достижения не связаны с какой-то конкретной технологией. Понимание приходит со временем, когда занимаешься решением конкретной проблемы. При этом иногда я находил чужие библиотеки, которые помогали мне решить задачу. А иногда писал собственные решения (и хорошие, и ужасные). Эта комбинация — состоящая из проблемы, экспериментов с инструментарием и различными решениями — давала мне наиболее ценный опыт и навыки.

В этой статье я остановлюсь только на проблемах, с которому столкнулся в ходе создания пользовательских интерфейсов.

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

Согласованность (Consistency)

Представьте, вы лайкнули пост и появилась надпись: “Вам и еще 3 вашим друзьям понравилась эта запись”. Вы нажали на кнопку лайка еще раз — и надпись исчезла. Звучит легко! Но что, если такая надпись присутствует в нескольких местах на экране одновременно. Или существует какая-то дополнительная визуальная индикация для лайка (например, цвет фона у кнопки), которая тоже должна изменяться при вашем клике. А список “лайкеров”, который был предварительно получен с сервера и отображается при наведении мышкой, теперь должен включать ваше имя. Если вы перешли в другой раздел или нажали кнопку Назад, пост не должен “забыть”, что у него есть ваш лайк. Как видите, даже локальная согласованность для всего лишь одного пользователя создает ряд непростых задач. При этом другие пользователи тоже могут взаимодействовать с данными, которые находятся у вас на экране (например, лайкнуть пост, который вы просматриваете). Как поддерживать данные в актуальном состоянии в разных частях экрана? Как и когда сверять локальные данные с сервером и получать/отсылать изменения?

Отзывчивость (Responsiveness)

Люди допускают отсутствие визуальной обратной связи для своих действий в течение ограниченного (и очень короткого) времени. Для непрерывных действий, таких как скролл, отсутствие реакции приложения незаметно только в течение кратчайшего периода — даже пропуск одного кадра в 16 миллисекунд уже будет заметен человеческому глазу и выглядеть, как микро глюк. Для дискретных действий, таких как клик, по данным некоторых исследований пользователи нормально воспринимают задержки в отлике не более 100 миллисекунд. Если же действие занимает больше времени, то необходимо показывать визуальный индикатор. Однако, тут есть несколько контринтуитивных моментов. Индикаторы, которые вызывают сдвиг в шаблоне страницы или которые проходят через несколько сменяющихся этапов, могут сделать действие дольше “по ощущениям”, чем оно было на самом деле. Аналогично, отклик приложения в течение 20 миллисекунд с пропуском одного кадра анимации может “ощущаться” медленнее, чем полная анимация в течение 30 миллисекунд. Наше сознание не работает по бенчмаркам. Как поддерживать приложения отзывчивыми для человеческого восприятия?

Задержка (Latency)

Компьютерные вычисления и передача данных по сети требуют времени. Мы можем игнорировать время вычислений, когда оно не влияет на отзывчивость на быстрых современных устройствах (но убедитесь, что вы протестировали свой код на старых и бюджетных девайсах). Однако, время передачи данных по сети игнорировать нельзя — оно может исчисляться секундами! Приложение не может просто «зависнуть», пока мы ждем загрузки данных или кода. Любое действие, требующее загрузки новых ресурсов, является потенциально асинхронным и должно обрабатывать состояние своей загрузки. Это верно для абсолютного большинства экранов в современном интерфейсе. Как же правильно обрабатывать задержку при передаче данных, не отображая при этом каскад крутящихся спиннеров или пустых “дыр” в интерфейсе? Как избежать сдвигов в шаблоне страницы? И как менять асинхронные зависимости без необходимости постоянно переписывать код?

Навигация (Navigation)

Мы ожидаем, что интерфейс будет стабильным при взаимодействии с ним. Элементы не должны внезапно исчезать. Навигация — как внутри приложения (ссылки), так и снаружи (кнопка Назад в браузере) — должна придерживаться этого принципа. Например, переключение между вкладками /profile/likes и /profile/follows в одном разделе не должно обнулять содержимое поля поиска за пределами этого раздела. Переключение на другой экран должно быть похоже на прогулку в другую комнату. Люди ожидают, что вернувшись назад, они найдут все вещи в точности там, где они их оставили (и, возможно, будут рады каким-то новым вещам). Если вы находились в середине вашей ленты, нажали на вкладку профиля, после чего вернулись обратно в ленту — то вы точно не хотите заново листать все с самого начала или ждать, пока прогрузится прошлое состояние ленты. Как нужно проектировать приложение, чтобы обрабатывать произвольную навигацию пользователя без потерь важного контекста?

Устаревание (Staleness)

Мы можем сделать реализацию кнопки Назад моментальной, добавив в приложение локальный кэш. Для этого мы будем хранить в кеше необходимые данные прошлого состояния. Мы можем даже теоретически обновлять кэш, чтобы поддерживать данные в актуальном состоянии. Однако, имплементация кэширования влечет за собой новые проблемы. Кэш может устаревать. Если я поменял аватар, то он должен обновиться в том числе и в кэше. Если я опубликовал новый пост, то он должен сразу появиться в кэше, в противном случае кэш станет устаревшим. Такой код в итоге может стать сложным и трудноподдерживаемым. Что делать, если процесс публикация каких-то данных завершится с ошибкой? Как долго кэш должен храниться в памяти? Когда мы заново получаем набор даных, то мы объединяем новые данные с закэшированными ранее или же избавляемся от старого кэша и кэшируем весь набор заново? Как пагинация и сортировки должны быть представлены в кэше?

Энтропия (Entropy)

Второй закон термодинамики гласит примерно следующее: «Со временем все превращается в полный бардак». Это верно и для пользовательских интерфейсов. Мы не можем предугадать действия конкретного пользователя и их последовательность. В любой момент времени приложение может находиться в одном из огромного числа различных состояний. Мы стараемся изо всех сил, чтобы сделать результат предсказуемым и ограниченным в соответствии с нашим дизайном. Мы не хотим смотреть на скриншот с багом и думать про себя: «Черт, что это вообще такое?» Для N возможных состояний существует N × (N – 1) возможных переходов между ними. Например, если для кнопки возможны пять различных состояний (нормальное, активное, ховер, фокус и отключенное), то код, отвечающий за изменение кнопки, должен быть корректен для 5 × 4 = 20 возможных переходов — или прямо запрещать некоторые из них. Как справляться с комбинаторным увеличением возможных состояний и создавать предсказуемый визуальный вывод?

Приоритеты (Priority)

Очевидно, что одни вещи важнее других. Интерфейс диалога, может быть, должен появляться строго над кнопкой, которой он был вызван. Или только что запланированная задача может быть важнее, чем продолжительная задача, выполнение которой началось когда-то в прошлом. С увеличением размеров приложения разные его части, написанные разными людьми или даже командами, начинают соревноваться за ограниченные ресурсы, такие как вычислительные мощности устройства, сетевой трафик, место на экране или размер бандла. Иногда вы можете распределить элементы по единой шкале “важности”, подобно CSS-правилу z-index. Но обычно это не заканчивается ничем хорошим. Любой разработчик искренне считает свой код важным. Но если все будет одинаково важным, то в результате получится хаос. Каким образом мы можем заставить независимые части приложения эффективно взаимодействовать, а не сражаться за ограниченные ресурсы?

Доступность (Accessibility)

Сайты, не адаптированные для людей с ограниченным возможностями, не являются редкой проблемой. В Англии с этим сталкивается каждый пятый пользователь (наглядная инфографика). Я ощутил это и на себе. Несмотря на то, что мне всего 26, я с трудом пользуюсь сайтами с тонкими шрифтами и неконтрастной цветовой гаммой. Я стараюсь реже использовать трекпад, но боюсь того дня, когда мне придется воспользоваться с клавиатуры не адаптированным для этого сайтом. Мы обязаны при работе над нашими приложениями задумываться о людях с ограниченным возможностями — и хорошие новости заключаются в том, что это не так уж и сложно. Нужно начать с изучения решений и инструментов. К тому же мы должны сделать принятие правильных решений простым и понятным для дизайнеров и разработчиков. Что можно сделать для того, чтобы доступность современных приложений была включена по умолчанию, а не являлась запоздалой доработкой?

Интернационализация (Internationalization)

Наши приложения должны работать по всему миру. Да, люди говорят на разных языках. А еще используют разные системы письменности, по-разному считают время, записывают даты и единицы мер. Как поддержать подобное многообразие при минимальных усилиях со стороны разработчиков и не теряя при этом в отзывчивости приложения и времени отклика?

Доставка до пользователя (Delivery)

Мы должны доставить код приложения до компьютере конечного пользователя. Какой способ передачи и формат мы будем использовать? В этом вопросе каждый ответ будет компромиссом со своим набором сильных и слабых сторон. Например, нативным приложениям приходится загружать весь свой код заранее из-за огромного размера. Веб-приложения обычно имеют гораздо меньшее время первоначальной загрузки, но вынуждены обрабатывать множество задержек и загрузок во время использования. Как решить, какой тип задержки выбрать из этих двух вариантов? Как оптимизировать время отклика исходя из статистики использования приложения пользователями? Какими данными нужно располагать для принятия оптимального решения?

Абстракция (Abstraction)

В небольшом приложении мы можем идти напролом и решать все вышеперечисленные проблемы в лоб. Но приложениям свойственно расти. С ростом на первый план выходит возможность переиспользовать, разветвлять и объединять различные части приложения — и делать это совместно с другими людьми. Для решения этой задачи нужны понятные границы и разделение между частями целого. При этом нужно избежать слишком жесткой логики и большого количества правил, так как они часто изменяются и эволюционируют в процессе работы. Как создавать абстракции, которые позволят скрыть детали имплементации? Как избежать повторного появления перечисленных проблем с ростом приложения?

Гибкость (Resilience)

Никто не любит находить баги в собственных программах. Тем не менее, некоторые баги неизбежно доберутся до продакшена. И очень важно — что произойдет тогда. Некоторые баги вызывают неправильное, но строго определенное и заранее заданное поведение. Например, ваш код может по ошибке показывать неподходящее состояние для заданного условия. Но что, если в результате бага приложение полностью завершило свою работу? В этом случае мы не сможем продолжить осмысленное выполнение программы, так как визуальный вывод будет не определен. Ошибка при отрисовке одного поста из ленты не должна “крашить” отрисовку всей ленты или ввести приложение в нестабильное состояние, которое приведет к дальнейшим ошибкам. Как писать код, который изолирует ошибки при отрисовке или получении данных в одной из частей и продолжит корректную работу остального приложения? Что значит отказоустойчивость при создании пользовательских интерфейсов?


Конечно, есть еще много проблем, которые я не упомянул. Этот список не является ни в коей мере полным или исчерпывающим. Например, вообще не были затронуты темы совместной работы дизайна и разработки, темы отладки и тестирования. Возможно, мы вернемся к этому в другой раз.

Заманчиво читать про эти проблемы, держа в голове в качестве решения конкретный фреймворк для отрисовки данных или библиотеку для получения данных. Но я рекомендую вам представить, что этих решений не существует и попробовать прочитать еще раз. Как бы вы попытались решить эти вопросы? Попробуйте реализовать свои идеи на простом приложении.

Что особенно интересно в этих проблемах — большинство из них проявляют себя на любом масштабе. Вы можете столкнуться с ними при работе над маленьким виджетом, типа всплывающей подсказки, и в огромных приложениях, таких как Twitter или Facebook.

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

Я многое осознал про разработку хорошего UI, экспериментируя с этими вопросами в маленьких приложениях без использования сторонних библиотек и фреймворков. И рекомендую это всем, кто хочет обрести понимание решений и компромиссов при разработке сложных интерфейсов.

разработка