[Из песочницы] Основные проблемы разработки современных интерфейсов
Привет, Хабр! Представляю вашему вниманию перевод поста Дэна Абрамова «The Elements of UI Engineering» о современных проблемах и задачах, которые должны быть решены в хорошем интерфейсе. Автор разбирает фундаментальные проблемы при разработке интерфейсов, осмысление и решение которых самостоятельно — без использования готовых библиотек и фреймворков — способно дать глубинное понимание существующих на рынке решений в области frontend-разработки.
В моей прошлой публикации я писал про то, как важно уметь признавать пробелы в собственных знаниях. Могло показаться, будто я предлагаю вам отговорки для того, чтобы быть посредственностью. Вовсе нет! Но на самом деле наши знания — это обширная тема для разговора.
Я убежден, что вы можете начать свое познание «с места в карьер» и нет необходимости изучать технологии (технологический стек для веб-разработки — прим. переводчика) в определенном порядке. Но я так же считаю, что имеет огромное значение накопление опыта и профессиональных навыков в выбранной области. Лично я всегда испытывал наибольший интерес к созданию пользовательских интерфейсов.
И я раздумывал — в чем же я разбираюсь и что нахожу важным? Конечно, я хорошо знаком с такими технологиями, как Javascript и React. Однако, самые важные вещи, которые приходят с опытом, неуловимы и обычно ускользают при попытках точно их сформулировать. Я никогда не пытался выразить их словами. Это моя первая попытка систематизировать и описать некоторые из них.
Сегодня есть много разных путей при изучения технологий. На какую библиотеку сделать ставку в 2019 году? А в 2020? Стоит изучать Vue или React? Или Angular? Что насчет Redux или Rx? Нужно ли изучать Apollo? REST или GraphQL? Здесь легко потеряться! К тому же автор тоже может ошибаться.
Мои наибольшие достижении в познании не связаны с какой-то конкретной технологией. Я начинал понимать больше, когда занимался решением конкретной UI (User Interface — прим. переводчика) проблемы. При этом иногда я находил чужие библиотеки и паттерны, которые помогали мне решить задачу. А иногда писал собственные решения (и хорошие, и ужасные).
Эта комбинация — состоящая из осмысления проблемы, экспериментов с инструментарием и применения различных путей решения — давала мне наиболее ценный опыт и навыки. Этот пост сфокусирован только на проблемах, осмыслением которых я занимался в ходе создания пользовательских интерфейсов.
Если вы занимались разработкой пользовательских интерфейсов, то вы, скорее всего, сталкивались с некоторыми из этих проблем — напрямую или при использовании библиотек. В обоих случаях, я рекомендую вам поработать над простым приложением без библиотек вообще, попытаться воспроизвести и решить эти проблемы. Не существует единственно верного решения ни для одной из них. Опыт приходит со знакомством с этими проблемами и изучением возможных решений, учитывая сильные и слабые стороны каждого.
Целостность (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)
Мы должны доставить код приложения до компьютере конечного пользователя. Какой способ передачи и формат мы будем использовать? В этом вопросе каждый ответ будет компромиссом со своим набором сильных и слабых сторон. Например, нативным приложениям приходится загружать весь свой код заранее из-за своего огромного размера. В то время, как веб-приложения обычно имеют гораздо меньшее время начальной загрузки, но вынуждены обрабатывать множество задержке и загрузок во время использования. Как нам решить, какой тип задержки выбрать из этих двух вариантов? Как нам оптимизировать время отклика исходя из статистики использования приложения пользователями? Какими данными нам нужно располагать для принятия оптимального решения?
Гибкость (Resilience)
Никто не любит встречать баги в собственных программах. Тем не менее, некоторые баги неизбежно доберутся до продакшена. И очень важно — что произойдет тогда. Некоторые баги вызывают неправильное, но строго определенное и заранее заданное поведение. Например, ваш код показывает не подходящее состояние для заданного условия. Но что если в результате бага приложение полностью прекратило отрисовку? В этом случае мы не сможем продолжить осмысленное выполнение программы, так как визуальный вывод будет не определен. Ошибка при отрисовке одного поста из ленты не должна «ломать» отрисовку всей ленты или ввести приложение в нестабильное состояние, которое приведет к дальнейшим ошибкам. Как нам писать код, который изолирует ошибки при отрисовке или получении данных в одной из частей и продолжит корректную работу остального приложения? Что означает отказоустойчивость при создании пользовательских интерфейсов?
Абстрактность (Abstraction)
В небольшом приложении мы можем хардкодить и решать все вышеперечисленные проблемы в лоб. Но приложениям свойственно расти. Мы хотим иметь возможность переиспользовать, разветвлять и объединять различные части приложения и делать это совместно с другими людьми. Мы хотим определить понятные границы между частями одного целого, которые будут приняты разными людьми и при этом избежать слишком жесткой логики, так как она часто изменяется и эволюционирует в процессе работы. Как нам создавать абстракции, которые позволят скрыть детали имплементации UI? Как нам избежать повторного появления перечисленных проблем с ростом приложения?
Конечно, есть еще много проблем, которые я не упомянул. Этот список не является ни в какой мере полным или исчерпывающим. Например, я вообще не затронул тему совместной работы дизайна и разработки, тему отладки или тестирования. Возможно, мы вернемся к этому в другой раз.
Заманчиво читать про эти проблемы, держа в голове в качестве решения конкретный фреймворк для отображения данных или библиотеку для получения данных. Но я рекомендую вам представить, что этих решений не существует и попробовать прочитать еще раз. Как бы вы попытались решить эти вопросы? Попробуйте реализовать свои идеи на простом приложении. Я буду рад увидеть результаты ваших экспериментов на Github. (Просто отмечайте Дэна Абрамова в Твиттере и прикрепляйте ссылки на репозитории — прим. переводчика).
Что особенно интересно в этих проблемах — большинство из них проявляют себя на любом масштабе. Вы можете столкнуться с ними при работе над маленьким виджетом, типа всплывающей подсказки, и в огромных приложениях, таких как Twitter или Facebook.
Подумайте про нетривиальные элементы пользовательского интерфейса из приложения, которым вам нравится пользоваться, и пробегите вновь по списку вышеперечисленных проблем. Вы можете описать компромиссы, на которые пошли разработчики? Попробуйте воспроизвести похожее поведение с нуля!
Я многое осознал про разработку хороших пользовательских интерфейсов, экспериментирую над этими проблемами в маленьких приложениях без использования сторонних библиотек и фреймворков. Я рекомендую это всем, кто хочет обрести глубокое понимание решений и компромисов при разработке сложных интерфейсов.