В последнее время набирает популярность концепция Optimistic UI. На мой взгляд, ее достоинства сильно переоценены, а недостатки замалчиваются. В этой статье я хочу более явно продемонстрировать недостатки, а также предложить достойную альтернативу, которую назвал Realistic UI.
TL;DR
> Демонстрация концепции Realistic UI
> Репозиторий с исходным кодом
История вопроса
Помимо Realistic UI я смог вспомнить три других способа взаимодействия с бэкендом в вебе и мобайле.
1. "Традиционный" UI
До распространения AJAX для выполнения почти любого действия требовалось перезагрузить страницу, поэтому UI выглядел примерно так:
Такой подход и сейчас достаточно распространен, но очевидно, что современные технологии позволяют достигнуть куда более приятного UX.
2. "Block-the-world" UI
После распространения AJAX разработчики смогли сделать шаг вперед и внедрить возможность выполнения удаленных запросов без необходимости перезагрузки страницы в браузере для отдельных операций, как это было в "Традиционном UI". Однако, такие веб-приложения все еще оставались с точки зрения архитектуры "тонкими" клиентами и не имели возможности управлять своим состоянием. Таким образом, переход на другую страницу мог прервать выполнение удаленной операции, и лучшим решением этой проблемы были блокировка интерфейса и демонстрация индикатора загрузки на стороне клиента.
3. Optimistic UI
Далее, с появлением и распространением концепции Single Page Application появилась возможность разрабатывать "толстый" клиент и управлять состоянием приложения. Optimistic UI предполагает, что мы, разработчики, знаем, что должно случиться в результате выполнения операции, и можем сразу обновить интерфейс так, как если бы операция выполнилась успешно, а в это время в фоновом режиме выполнить соответствующий AJAX запрос. В 1 – 3 % случаев операция закончится неудачей, и мы просто покажем сообщение об ошибке. Optimistic UI активно продвигается Facebook'ом, и их решения вроде Relay используют этот механизм по умолчанию.
Критика Optimistic UI
Optimistic UI может показаться хорошей и свежей идеей, но при ближайшем рассмотрении становится понятно, что ее применение если и оправдано, то в весьма редких случаях. Я могу назвать следующий проблемы:
1. Разделение операций на "важные" и "неважные"
Optimistic UI не подходит для "важных" операций, например, если вы покупаете билеты в кино, то вам необходимо знать на 100%, получилось ли это сделать, или случилась ошибка. Также Optimistic UI не подходит для тех операций, для которых сервер формирует часть данных, которые мы не можем предугадать на стороне клиента, таких как уникальные номера транзакций.
Это создает сразу несколько проблем. Во-первых, сервис почти всегда содержит несколько "важных" операций, иначе не очень ясно, зачем он в принципе существует. А это означает, что разработчикам придется реализовывать и поддерживать два разных способа взаимодействия с бэкендом: для "важных" и "неважных" операций, что потребует больше времени и усилий. С другой стороны, пользователям тоже нужно привыкнуть, что часть операций выполняется одним образом, а часть — другим. Это может стать источником недоразумений и проблем, что в свою очередь может увеличить затраты на поддержку пользователей и привести к ухудшению UX соответственно.
Во-вторых, должен быть человек, который решает, какие операции "важные", а какие — "неважные". Любое решение будет всегда субъективным. Например, "лайк" может показаться "неважной" операцией, но на самом деле для отдельных пользователей сервиса отсутствие лайка может испортить настроение, а кому-то и хороший вечер. Сама необходимость принимать подобные решения отнимает время и силы, поэтому было бы лучше как для разработчиков, так и для пользователей иметь универсальное решение, которое можно использовать для всех типов операций.
2. Проблемы синхронизации данных, транзакционности и разрешения конфликтов
Optimistic UI в теории хорошо работает с offline режимом. На практике же существует много проблем, хорошо знакомых мобильным разработчикам.
Во-первых, транзакционность.
Да, для мессенджера порядок выполнения операций может не иметь решающего значения, но для других типов приложений зачастую это не так. А это значит, что необходимо как на стороне клиента, так и на сервере придумывать механизмы, которые бы обеспечивали правильный порядок выполнения операций, что потребует дополнительных усилий и времени.
Во-вторых, допустим, клиент выполнил пять различных операций в offline и теперь пытается их синхронизировать с сервером. Первые две выполнились успешно, а третья завершилась ошибкой. Что следует сделать в этой ситуации? Остановить выполнение? Откатить первые две успешно выполнившиеся операции? Постараться выполнить четвертую и пятую? А если они зависят от выполнения третьей? А как сообщить о проблеме пользователю?
Как мы видим, появляется множество вопросов, каждый из которых требует раздумий и зачастую индивидуального подхода. Такие решения очень плохо поддаются поддержке и неважно масштабируются, так как являются в большей степени state of art, нежели тиражируемым решеним или набором лучших практик. Также, существует огромное количество возможных последовательностей действий пользователя, что превращает процесс тестирования различных комбинаций в ад.
3. Обман пользователя
Когда мы демонстрируем пользователю, что его действие выполнилось успешно, хотя на самом деле оно еще только выполняется, мы создаем ощущение, что нашей системе нельзя доверять в случае неудачи.
Realistic UI
Disclaimer
Хотя я придумал и определил концепцию Realistic UI, я не претендую на лавры первооткрывателя. Как и принципы REST были определены более, чем через 10 лет после широкого распространения Интернета в его современном виде, так и идеи Realistic UI можно встретить в различных проектах уже сейчас. В частности, панели управления Azure и Google Cloud Platform реализованы в соответствии с принципами Realistic UI.
Принципы Realistic UI
Блокируется только часть пользовательского интерфейса. Например, если пользователь заполняет форму и нажимает кнопку “Отправить”, никто не будет ему мешать перейти на другую страницу или выполнить другое независимое действие при условии, что изменение данных формы будет невозможно до завершения выполнения удаленной операции.
UI содержит виджет, который отображает все активные операции. С помощью этого виджета пользователь может отслеживать текущий статус выполнения операций, а также переходить на соответствующую страницу и / или часть страницы при желании.
- UI содержит еще один виджет, который отображает операции, завершившиеся неудачно. Из этого виджета пользователь может повторно запустить выполнение неудачной операции, перейти к соответствующей странице и / или части страницы или проигнорировать ошибку.
Realistic UI vs. Optimistic UI
Realistic UI предполагает единый механизм для всех типов операций.
Realistic UI не обманывает пользователя.
Realistic UI позволяет выполнять несколько операций одновременно, но гарантирует, что их выполнение произойдет независимо друг от друга, что существенно упрощает процесс разрешения конфликтов.
Realistic UI дает пользователю полное представление о выполняемых и выполненных операциях.
Таким образом, Realistic UI берет лучшие черты от всех подходов взаимодействия с бэкендами, но лишен большинства недостатков.
Заключение
Меня очень волнует существующая волна хайпа вокруг концепции Optimistic UI, и я надеюсь, что у меня получится вызвать дискуссию, которая бы смогла более объективно подойти к ее оценке.
С целью демонстрации идей Realistic UI на практике я реализовал небольшой Proof Of Concept, который можно посмотреть, перейдя по ссылке.
Исходный код демки я выложил на GitHub.
Если мои доводы показались вам убедительными, я буду очень благодарен за ретвит и звездочку. Я считаю, что крайне важно донести эти идеи до коллег, так как существующие проблемы Optimistic UI могут принести множество вреда тем командам, которые решатся их внедрять, не осознавая всех возможных трудностей.
Разумеется, я буду очень рад как возражениям, так и критике в комментариях, что позволит сделать концепцию еще лучше.
Комментарии (35)
Scf
12.01.2017 16:49+4hypers gonna hype.
Оба подхода имеют право на жизнь, но, имхо, лучше писать быстрые бэкенды.yury-dymov
12.01.2017 16:53+4Безусловно, но быстрые бэкенды бессильны перед плохим или медленным интернетом.
AnotherAnkor
12.01.2017 17:41+1Обычно всё как раз иначе. Для того же мессенджера удобнее отправлять аяксом, как это и делается. Представьте, что вы пишите в каком-нибудь телеграмме долгую и подробную вещь и отправляете. А пока получатель это читает и осмысливает, вы уже пишите дополнение. Но если заблокировать кнопку «отправить», то вам придётся ждать и терпеть «медленный интернет».
Очевидно, что для банковских форм это приемлемо. Для важных данных. Но для всего остального нужно делать так, как оно вам не нравится.yury-dymov
12.01.2017 17:45Вот именно для мессенджера, особенно если очередность сообщений не важна, Optimistic UI хорошо подходит. Но это как раз то исключение, которое подтверждает правило
apollonin
12.01.2017 18:32+1https://console.cloud.google.com работает по Realistic UI, по сути. Должен сказать что это по началу очень непривычно, когда система принимает мой запрос, но при этом позволяет заниматься другими делами, пока он выполняется. При этом визуально только небольшой лоадер в шапке страницы и на кнопке…
yury-dymov
12.01.2017 18:35Спасибо, хороший пример, добавил в статью. Я согласен с тем, что это может быть не очень привычно, но мне кажется, что будущее как раз за подобными интерфейсами.
Dreyk
12.01.2017 19:02+1ммм… а если на странице есть зависимые операции? Их мы получается тоже не должны дать редактировать?
yury-dymov
12.01.2017 19:06В общем случае да, иначе у нас могут возникнуть проблемы, если первое действие окажется неудачным. В отдельных частных случаях, можно не блокировать, если есть веские причины
softaria
13.01.2017 19:22Даже если оно окажется удачным, всё равно надо блокировать. Зависимая операция может использовать результат первого действия.
bano-notit
12.01.2017 19:18+1Логично. Только скорее не запретить редактирование, а запретить отправку на сервер. Тогда логично будет. Пользователь отправил одно, и пока оно отправляется он уже заполняет другое. А когда первое отправится ему даётся возможность отправить второе.
yury-dymov
12.01.2017 19:28Так даже лучше. Единственное, надо как-то отобразить пользователю информацию, что кнопка "отправить" заблокирована, потому что связанное действие все еще выполняется.
bano-notit
12.01.2017 19:33+1Ну заблокировать её нетрудно, а вот как показать связность действий тут уже немного другой вопрос.
Можно формы поставить друг под другом и стрелочками или циферками показывать последовательность, но это 100% не то. Слишком муторно и выглядеть будет не так эстетично. Ещё вариант написать на кнопке отправки "Ждём результатов предыдущего действия...". Ну или короче как-то.Deosis
13.01.2017 07:24Можно оставить только одну кнопку «Отправить», когда отправка предыдущих данных завершится, переместить эту кнопку под новые данные и разблокировать.
vintage
12.01.2017 20:44+1Порядок сообщений в мессенджере ещё как важен. Это бывает по круче, чем расположение запятой.
- Не надо блокировать интерфейс. У пользователя должна быть возможность отменить лайк или исправить данные в форме не дожидаясь ответа от сервера.
yury-dymov
12.01.2017 21:15По первому пункту воздержусь от комментария.
Концепция блокировки части UI не является синонимом отсутствия возможности отмена запросов — достаточно добавить в UI рядом с заблокированной кнопкой "Отправить" кнопку "Отмена".
vintage
12.01.2017 21:51+1да не надо никаких "отмен" — если пользователь решил что-то изменить — отменять можно автоматом. https://jsbin.com/fifoxusozo/edit?output
bano-notit
12.01.2017 21:54+1Сколько раз я так могу отправить на сервер? Дофигищу. При этом какой-то может недоставиться и в конце концов результат может быть неудовлетворительный. Да и вопрос, зачем нам нужно несколько запросов, когда можно сделать 1?
vintage
12.01.2017 22:02+1Если пользователь поставил лайк, а потом его решил снять, то вам в любом случае нужно сделать 2 запроса. Блокировка инетрфейса внесёт лишь лишнее раздражение у пользователя, которому приходится сидеть и ждать пока обработается установка лайка, чтобы появилась возможность его снять. Вы поиграйтесь с примером — результат всегда ожидаемый. Кроме, разумеется, случая, когда сервер упал с ошибкой.
bano-notit
12.01.2017 22:15+1Упасть может не только сервер, а ещё и клиент, а точнее его соединение с интернетом.
В вашем примере нету механизма, который делает только 2 запроса, в случае дислайка. Как и говорится в статье, для того, чтобы реализовать это нужен отдельный механизм, который будет убивать один запрос и зарождать другой. Не очень удобно, согласитесь. Причём надо пришибить запрос так, чтобы он не успел отправить данные на сервер. А если долго тупит не транспорт меж юзером и сервером, а сервер? Тогда не получается, потому что будет столько запросов, сколько "закажет" юзер. А если какой-то последний не дойдёт, тогда будет интересно, потому что конечная задача не выполнена, лайк не убран, зато отправлено over 1000 запросов.vintage
12.01.2017 23:04+2в приведённом примере первый запрос отменяется. и совершенно не важно, дошёл ли он до сервера. всё равно второй затрёт его изменения. а если не затрёт, то пользователь увидит ошибку и попробует снова. Постарайтесь думать не в терминах деиствий, а в терминах состояний. А чтобы было наглядней — предтавьте, что пользователь не лайки ставит, а наносит ядерные удары, на каждый из которых требуется по 1 часу.
bano-notit
12.01.2017 23:23+1Отстранение от юзера никогда не приносило успехов. Относительно состояний согласен. Относительно количества запросов нет, ибо это потенциальная лазейка для DoS атаки. Нельзя, чтобы на какой-то метод можно было отправлять немереное количество запросов и обрывать их посреди пути. Относительно состояний тут я согласен. Но всё же, если сервер приложения не такой быстрый или ширина канала клиента мала (а это вероятней), то это проблема. Поэтому блокировка тут имеет плюс в безопасности и логичности.
Вообще нельзя заменять действия. Так же как отстраняться от юзера, а то он в конце концов может быть в шоке от того, что такое поведение он совершенно не предполагал.
Вообще по идее можно комбинировать элементы, как вы показали, но нельзя же давать юзеру возможность постоянно вызывать какие-то функции и заниматься хренью.
У юзера всегда есть особенность: он не понимает того, что перед ним, а точнее не хочет понимать, поэтому мы должны подумать за него.vintage
13.01.2017 04:16+1Никакой клиентской логикой вы не защититесь от дос атаки. Банально потому, что злоумышленнику не составит труда её изменить. А пользователей не стоит считать за имбицилов.
bano-notit
14.01.2017 11:52Конечно. Клиентаская логика не защитит от дос атаки, потому что это нормальная работа, она не должна делать слишком много запросов. Досы будут пострашнее и почаще.
Пользователей за имбицилов я не считаю. Просто давайте сами подумаем. Коли к нам пришёл заказчик с просьбой сделать программу, значит он не понимает как это работает. Значит мы должны понять как это работает и сделать так, чтобы ему было не трудно работать с той громадиной, с которой он хочет. Именно отсюда взялось моё заявление, что пользователь не понимает, что он делает, и не хочет понимать. Если бы он понимал, то он бы не заказывал программу. Ему было бы достаточно обычных инструментов. Но он захотел сделать это удобнее, поэтому он заказал абстракцию. А абстракция подразумевает, что мы скрываем часть айсберга, и показываем только ту верхушку, которую юзер не боится. А остальное мы сами тихонечко обплываем.
bano-notit
14.01.2017 11:53Кстати, как вам ui хабра? Он ведь по сути сделан на realistic. Тут и кнопочки всякие блокируются, и сообщения не прилитают, пока они на сервере не созданы.
vintage
14.01.2017 13:15Бывает сбойнёт и кнопка не разблокируется. В итоге приходится перезагружать страницу, чтобы разблокировать кнопку.
bano-notit
12.01.2017 21:50+1А если не блокировать интерфейс, то может получиться примерно так:
Лайкнул… Хм, не не хочу, дислайкнул… Что-то не дизлайкается, надо бы ещё раз, и ещё раз {over 30} Блин, у меня же интернет сдох!
В общем, блокировка как раз таки является одним из ограничителей юзера, которые он должен видеть. Конечно же, мы хотим огородить пользователя от всей этой лабуды с запросами и ответами, но какие-то вещи от нас не зависят и их скрыть просто невозможно, и эта вещь — время отклика сервера. Как бы мы не кочевряжились, если данные не пришли, то мы не можем сказать, что они пришли. Юзер будет думать, что его документ уже отправлен и люди уже смотрят его и вникают, а он уже упал при запросе отправки на сервер.
Просто диалог с оптимистичным получается такой:
- Я хочу отметить вот эту галку
- Окей, уже отмечено
- А теперь вот эту
- Извини, но прошлая не отметилась, так что тебе придётся её отметить заново.
Weks
13.01.2017 22:37К сожалению, даже Realistic UI оказался не застрахованным от странных переключателей вместо понятных и привычных чекбоксов.
yury-dymov
13.01.2017 22:42Этот контрол к Realistic UI явного отношения не имеет :) Обратите внимание на 3 принципа, указанных в статье; все остальное — вторично
foxmuldercp
16.01.2017 16:52Лично для себя после использования смартфонов, и разных UI фреймворков резкой разницы между чекбоксами и такими вот приятными переключателями.
Единственный плюс что такой вот переключатель удобнее располагается в UI и имеет соотнесенную к нему иконку и подпись, как, например, это сделано в интерфейсах настроек яблочной продукции.
А с чекбоксами я еще во время twitter bootstrap возился, пытаясь их хоть как-то привести в ту позицию, в которой хочу их видеть кроме штатного чекбокса "Запомнить меня" в окне ввода емейла/пароля
VitaZheltyakov
Самой большой проблемой обоих подходов (UI) является страх и предубеждение российских программистов перед произвольным кодом выполняемым на клиенте… мол, как это так на клиенте мы выполняем любой код, который присылаем с сервера.
Да, да именно так. Обоим подходам уже больше 20 лет, но наши программисты до сих пор «боятся» их использовать.
raveclassic
Вы, наверное, хотели сказать «бэкэнд боится»?
Недавно пытался продать бэкэнду связку couchdb/pouchdb. На меня посмотрели как на сумасшедшего, мол, как так, доступ в базу с клиента.
napa3um
И правильно смотрели. Доступ с клиента напрямую в БД может быть обоснован только в случае, если это изначально приложение для системной работы с базами данных (тулза разработчика). В «реальном мире» веб-приложений (и вообще клиент-серверных приложений) не принято делать бизнес-логику средствами СУБД, для этого обычно заводят отдельное звено (сервер приложений). Триггеры и хранимые процедуры правильно использовать только для поддержки консистентности персистентных моделей, а не для слоя бизнес-логики.
raveclassic
В том то и дело, что как такого «прямого» доступа там нет. Там отличная фоновая синхронизация с (разумеющейся) проверкой всех прав на стороне couchdb. Бизнес-логики там никакой, триггеров и хранимок никаких, это обычный key/value. Представьте, что это не «база», а «транспорт» до бэка.
Взгляните
napa3um
Я видел и даже использовал. Уверен, что можно найти условия, в которых такой «транспорт» будет полезен — если и на сервере, и на клиенте это будет заинкапсулировано в бибилотеках синхронизации клиентских и серверных моделей, а логика синхронизации будет гарантировать отсутствие противоречий. Но в общем случае это не очень полезная архитектура, т.к. синхронизация моделей происходит без контроля слоя бизнес-логики, который лишь по факту УЖЕ наличия отсинхронизированных неконсистентных состояний на сервере должен будет как-то оповестить клиента и откатить транзакции и на сервере, и на клиенте.