...

суббота, 20 ноября 2021 г.

Rockstar Games начала исполнять обещания после провала релиза и выпустила первый патч для Grand Theft Auto: The Trilogy

20 ноября 2021 года Rockstar Games начала исполнять свои обещания после провального релиза и выпустила патч 1.02 для Grand Theft Auto: The Trilogy. Пока что обновление доступно для консолей PS5, PS4, Xbox Series X|S и Xbox One, но скоро выйдет и для версии на ПК.
Вчера Rockstar заявила, что этот проект не соответствует высоким стандартам качества студии и требует доработки в ближайшее время. Компания пояснила, что первый большой патч выйдет для трилогии выйдет через несколько дней. Разработчики пообещали, что постараются исправить все проблемы ремастера в нескольких обновлениях.

Патч 1.02 содержит 61 исправление — 10 фиксов для всех игр, 14 изменений для GTA III, 12 исправлений для Vice City и 25 корректировок для San Andreas. В трилогии частично исправлены основные ошибки и баги игрового мира, на которые жаловались игроки, но многие другие ошибки и неточности в игровом процессе, функционировании оружия и транспортных средств, в механике персонажей и действиях NPC, работе квестов и оформлении карт и локаций в играх еще остались. Примечательно, что часть багов и ошибок моддеры исправили сами ранее, не дожидаясь реакции на проблемы со стороны Rockstar.

19 ноября, спустя 7 суток после релиза, команда Rockstar Games публично извинилась за провальный старт и «неожиданные» технические проблемы с Grand Theft Auto: The Trilogy. Компания вернет в продажу оригинальные версии игр трилогии и подарит их всем, кто купил ремастер или купит его до 30 июня 2022 года. Rockstar попросила игроков не ругаться и не вымещать на форумах и в соцсетях злость и негодование на разработчиков трилогии, а принять эту неприятную ситуацию и подождать исправлений.

Разработкой GTA: The Trilogy занималась студии Groove Street Game. Она вела этот проект 2 года.

Adblock test (Why?)

Senior Java Developer — как проходят собеседования

Я прошел за свою деятельность больше 100 собеседований на позиции Senior Java Developer, и скажу я вам, накопилось много интересных моментов, которыми и хочется поделиться.

К слову, я не махровый кодер, в разработке около 5 лет, на позиции Senior всего 2,5 года. Понятие о Senior у всех разное, но в моей конторе, я именно Senior и в предыдущей тоже таковым обзывался. Но это не исключает того, что я могу быть неопытным мидл разработчиком с ЧСВ от архитектора, который накидал однобокий и токсичный текст 😊

Прежде всего, цель статьи выплеснуть накопившиеся эмоции. Да вот так просто.

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

Давайте ближе к делу.

Хотел бы разбить по категориям наших любимых интервьюеров (сортировка от отстоя к классным).

1. Ленивец

Их метод прост как пробка - берем список вопросов и ответов из различных статей (как подготовится к собеседованию по Java). Обычно у таких проходишь собес на раз два. Однако, есть и подводные камни - некоторые такие деревянные, что могут, сверяя ваш ответ с тем, что на бумажке, не понять, что это одно и тоже. Попросту они не всегда могут смапить ответ на бумажке и ваш ответ. Редко, но все таки.

У такого подхода есть только один плюс для интервьюера - не надо долго готовиться и вырабатывать свою методу. В остальном - печаль.

2. Теоретик

Этот класс особо любим, но встречается редко. Здесь во главе угла чистейшая теория по Java Core, причем до каких то извращенных деталей, которые, по моему мнению, ну совсем не нужны в моей памяти.

Вопросы могут быть такими: - назовите все примитивные типы в Java, сколько каждый тип занимает памяти, если не помнишь, то посчитай; - какие методы класса Object, стандартный вопрос (многие задают), но в данном случае нужно назвать все до одного, т.к. собеседующий реально за это запаривается и сидит считает.

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

Здесь тоже плюсов особо нет, они ушли не далеко от ленивцев.

3. Мистер алгоритм

Ну из названия почти все понятно. Некоторые насмотрелись бест оф зе бест практис и решили перенять их 😊

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

Сюда же отнесу, да простит меня Java сообщество, "зануд" или "ярых программеров". Это люди увлеченные функциями, алгоритмами, формулами, в целом математикой, и прочими университетскими штучками. Такие люди готовы искать лучшее решение задачек в свое свободное время, вместо развлечения или отдыха.

Плюсов сильно много не вижу у таких собеседований, кроме как, если реально потребуется писать кучу своих реализаций с алгоритмами, вместо уже имеющихся джавовых, ибо они слишком медленные и вообще посредственны, ну не то что вы пишите... чистый идеал 😊

4. Лайв кодер

Здесь часто могут попросить написать тот или иной код в блокнотике расшаренном. Например: давай напиши синглетон, напиши какой-нибудь класс с функцией такой то и такой, напиши апи без логики, именно контракт и прочие.

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

Что же касается интервьюеров, то у них могут быть разные заскоки, которые не вяжутся с адекватностью.

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

Кто-то будет докапываться до реализации, мол подразумевалась доп. логика, на что ты аргументированно ставишь человека на место, он соглашается, но осадок остался о тебе как о неприятной личности (тоже провал).

Плюсы есть: реально посмотреть на то как пишешь код, как логику строишь, как проектируешь (используешь или нет паттерны). Главное, ребята - без фанатизма. Не надо докапываться до ерунды.

5. Крутые ребята

Начинают с основ, быстро проходят за 5 мин, понимают, что не джун.

Далее GC, память JVM. Потом вопросики поинтереснее про Spring и его работу, паттерны, SOLID. Потом БД. Понимают что точняк мидл, авось и более.

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

Тут прямо сам прокачиваешь от таких вопросов. Начинаешь рассуждать, думать, в общем превращаться в архитектора. Это топовый класс интервьюеров, с которыми приятно проходить собеседования. И при рассуждениях и беседе понимаешь, что с той стороны, действительно, бывалый разработчик с хорошими знаниями и набитыми шишками.

Итого для собеседующихся

  1. Не переживайте, если вас недооценили - это нормально. Главное чувствовать свой уровень. На отказы реагируйте спокойно, просите пояснить чем не подошли, делайте выводы.

  2. В целом любой собес - это прокачка узких мест, понимание своих пробелов. А это значит, что есть куда расти и вам указали точки роста.

  3. При поиске работы, общении с эйчарами и собеседованиях, вы оттачиваете способность себя продавать на рынке труда. Рынок есть рынок. Нужно знать свою цену на текущий момент. Периодически похаживайте по собесам - будьте в тонусе и в цене 😊

  4. Не скромничайте по цене. Если не хотите долго искать - просите 300К. Если есть месяц походить на собеседования, то берите 350К - 400К (правда некоторые захотят, чтобы за эти деньги вы умели летать и стрелять лазером из глаз). В Java на ноябрь 2021 года по Москве(или удаленка) вилка для Senior 300К - 400К руб. К чему я это, некоторые достойные люди, хорошие разработчики, просят по 250К - 300К, хотя рынок поменялся, инфляция колоссальная, недвижка выросла в 1.5-2 раза... ну вы поняли - выросли цены, и ЗП тоже должна подрасти (все по честному).

Итого для интервьюеров

  1. Посмотрите немного со стороны на ситуацию, вспомните свои собеседования, где вы были собеседующимися.

  2. Определите для себя, что критически важно именно для ваших задач:

    • знание инфраструктуры и девопсерство

    • алгоритмы

    • жестокая многопоточка, которая у вас используется на каждом шагу

    • понимание архитектуры как приложения, взаимодействия между системами

      Можете об этом сразу сказать собеседнику при начале собеседования, что именно вот это у вас критически важно.

  3. Не запаривайтесь за мелочевку, особенно если при дополнительном вопросе человек вам ответил как надо. Вы же не робота собеседуете, каждый может опечататься, забыть что-то, просто растеряться.

  4. Если человек не подошел по цене, так и скажите честно: "Все норм, но за эти деньги мы хотели больше опыта". Возможно предложить чуть поменьше, т.к. собеседующемуся может понравился ваш проект или команда.

Пишите интересные истории из своих собеседований в комментариях.

Adblock test (Why?)

Twitter сворачивает поддержку AMP и больше не перенаправляет на эти страницы

Twitter для Android и iOS больше не будет поддерживать ускоренные мобильные страницы (Accelerated Mobile Pages, AMP). Теперь ссылки из Twitter будут направлять пользователей на обычную веб-страницу, а не на доступную версию AMP. 

Twitter обновила страницу раздела для разработчиков, посвященную AMP, опубликовав уведомление о прекращении поддержки функции к концу года. Согласно данным SearchEngineLand, этот процесс уже завершился. 

Хотя Twitter отмечала, что AMP обеспечивает «быструю загрузку, красивый, высокопроизводительный мобильный веб-интерфейс», эта технология вызвала споры с момента ее анонса в 2015 году. Большая часть претензий была связана с предполагаемым контролем над проектом со стороны Google. Кроме того, у AMP были проблемы с пользовательским интерфейсом Twitter, хотя соцсеть не указала причину изменения политики. 

Google представила проект AMP 7 октября 2015 года. Более 30 новостных изданий и несколько технологических компаний, включая Twitter, Pinterest, LinkedIn и WordPress, были анонсированы заранее в качестве партнеров проекта AMP. В феврале 2017 года, через год после публичного запуска проекта, на AMP-страницы приходилось 7% всего веб-трафика для ведущих изданий в США. В мае 2017 года Google сообщила, что глобальный охват пользования технологией быстрой загрузки страниц составляет более 2 миллиардов AMP-страниц, опубликованных во всем мире.

Adblock test (Why?)

Сотрудница Tesla обвинила работодателя в создании враждебной рабочей среды

Работница калифорнийского производства Tesla Джессика Барраза рассказала о сексуальных домогательствах на рабочем месте. Об этом сотрудница сообщила в судебном иске, который она подала против компании на этой неделе. 

Барраза заявила, что она и другие сотрудницы Tesla в течение трех лет подвергались объективизации. Им угрожали и делали гнусные предложения. Она говорит, что хотела выполнять свою работу, не подвергаясь сексуальным домогательствам, которые заставляли ее чувствовать себя униженной, оскорбленной и травмированной.

В поданном в Верховный суд Калифорнии иске Барраза утверждает, что менеджеры и руководители производства в Фримонте не только знали о масштабе сексуальных домогательств на заводе, но и сами проявляли оскорбительное поведение. Сотрудница рассказала, что отдел кадров Tesla не смог ее защитить после жалобы.

Завод во Фримонте производит Tesla Model S, Model 3, Model Y и Model X.

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

Обстановка на рабочем месте привела к появлению у Барразы панических атак. Сейчас она находится на больничном, проходя лечение от стрессового расстройства и тревоги.

Работница производства сравнивает корпоративную культуру на заводе Tesla с архаичными стройплощадками и студенческими домами. Сотрудница отмечает немногочисленность исков против компании тем, что работники подписывают соглашение, в котором отказываются от своего права участвовать в судебных заседаниях.

В 2017 году инженер Tesla Эй Джей Вандермейден подала в суд на работодателя, обвинив его в том, что компания отказывала в повышении сотрудникам-женщинам, платила им меньше, чем мужчинам, а также преследовала в случаях обращений с жалобами. Спустя несколько месяцев она была уволена.

Барраза требует от компании компенсацию, а также запрет на преследования на заводе в Фримонте. Адвокаты подадут иск от имени Барразы и других сотрудниц Tesla, которые подвергались сексуальным домогательствам на рабочем месте. Представитель работницы Дэвид Лоу говорит, что компания несет ответственность за систематические сексуальные домогательства на производстве.

В начале октября суд обязал Tesla выплатить $137 млн лифтеру Оуэну Диасу за расистские оскорбления, которым он подвергся во время его работы в компании. Дело получило ход, поскольку Диас отказался подписывать документы обязательного арбитража.

На этой неделе Tesla попросила суд отменить выплату компенсации $137 млн и назначить новое разбирательство. Компания подчеркивает, что денежное возмещение «просто невозможно выплатить».

Adblock test (Why?)

Зловещая долина: terra incognita, в которой расставлены нейронные сети

Не припомню, чтобы в детстве я боялся клоунов. За все детство я побывал в цирке-шапито, может быть, два раза. Зато я определенно испытывал отвращение и настороженную злость к деду Морозу,  поскольку примерно в семь лет прочел сказку Евгения Шварца «Два брата», а также был впечатлен завязкой фильма «Сказка странствий» (примерно 4.30 – 8.00). Много позже я стал понимать, что ощущение жуткой фальшивости деда Мороза было настоящим проявлением эффекта «зловещей долины». Этот эффект, получивший широкую известность в трактовке Масахиро Мори (род. 1927) в 1970 году, в дальнейшем стал предметом серьезных исследований и моделирования. В сегодняшней статье будет рассмотрено, как был обнаружен и как изучался этот феномен. Постараемся поговорить о нем с точки зрения психологии, распознавания образов и соотношения гармонии и уродства.

Статья написана в соавторстве с Екатериной Черских @MarkOcean аспиранткой Санкт-Петербургского ФИЦ РАН.

Актуальность и история понятия

Феномен «зловещей долины» до сих пор обладает скорее эмпирическим, чем научным обоснованием. По-английски он называется «Uncanny valley» и под «valley»понимается «яма» на графике, характеризующем степень воспринимаемой «жуткости» наблюдаемых антропоморфных объектов, в особенности масок, кукол и роботов.

В самой общей форме такой феномен «жуткости» отмечал еще в начале XX века немецкий психиатр Эрнст Йенч (1867-1919). В 1906 году он писал, что «жуткость» как психологическое явление может возникать из-за наблюдения чего-то очень знакомого в тревожно-непривычном контексте. Он связывал такие переживания с неуверенностью, «является ли некоторая фигура человеком или, допустим, автоматом», либо «является ли безжизненный объект одушевленным».   

Именно в контексте робототехники и искусственного интеллекта ведутся наиболее интересные исследования «зловещей долины» в наше время. Человеко-машинные взаимодействия развиваются уже не один век, машины освобождают человека от тяжелой, опасной или рутинной работы. В начале-середине XX века считалось, что для улучшения взаимодействий между человеком и роботом логично приближать внешний вид роботов к человеческому, получая андроидов. Предполагалось, что постепенно роботы станут практически неотличимы от людей, настолько, что даже будут вызывать у человека симпатию. В научной кинофантастике именно в таком духе выдержаны образы андроидов из фильмов о «Чужих» - таковы, в частности, Эш (1979), Бишоп (1986) и Дэвид (2012). В том же ряду можно поставить Калдера из фильма «Дознание пилота Пиркса» (1978). Все эти образы соскальзывают в «зловещую долину» как по сути самих персонажей, так и по оказываемому ими художественному эффекту.  Использование зловещей долины в качестве художественного метода будет рассмотрено ниже в этой публикации.

Итак, Масахиро Мори обратил внимание, что в робототехнике робот-машина воспринимается пользователем настороженно и враждебно, но для снижения такой тревожности и отторжения достаточно повысить сходство робота с человеком. Подобная «гуманизация» робота плавно улучшает восприятие робота человеком, пока не приводит к резкому спаду на этапе чрезмерно реалистичной имитации человечности. Мори также впервые отметил, что данный эффект интенсифицируется, если «страшный» объект движется. Сам Мори следующим образом описывал, как пришел к этой идее:

Я заметил, что, стремясь добиться максимальной человекоподобности роботов, мы постепенно начинаем ощущать все большее «сродство» с ними, пока их возрастающая реалистичность не приводит нас к своеобразной «долине» [на графике], которую я назову «зловещей». 

Далее он упоминает, что в его детские годы как раз разрабатывались первые реалистичные протезы, которые во многом и навели его на такие мысли:

Кто-то может сказать, что рука-протез во многом сближается по внешнему виду с человеческой рукой, подобно тому, как вставные зубы похожи на настоящие. Но на самом деле, когда мы догадываемся, что перед нами протез, а только что нам казалось, что эта рука настоящая – нас охватывает жуткое чувство. Например, если при рукопожатии мы ощутим протез вместо руки, то сразу отметим, что он бескостный, холодный и по текстуре отличается от кожи. «Сходство» протеза с рукой улетучивается, и искусственная рука становится жуткой.

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

График демонстрирует, что, в сущности, роботы и андроиды не попадают в «зловещую долину». То есть, большинство роботов все-таки находятся левее нее, а андроиды (из фантастических произведений), отдельно на этом графике не указанные – вероятно, правее, ближе к здоровому человеку. К 2009 году робототехника, вероятно, прочно обосновалась на правом склоне долины.

Туда ее вывел Хироси Исигуро (род. 1968), профессор Осакского университета, сконструировавший целую серию гуманоидных роботов; первых из них он назвал «геминоидами». Геминоид, выполненный в мужском облике, является приблизительным двойником самого Исигуро:

Этот робот обладает 50 степенями свободы (каждая степень свободы – это независимая координата перемещения или вращения; совокупность степеней свободы определяет возможные положения механизма в пространстве). Он настолько похож на человека, что даже читает в аудитории лекции самого Исигуро. Обратите внимание: даже состарившийся примерно на 10 лет профессор все равно очень похож на робота, а в 2009 году был от него практически неотличим.

Женская модификация геминоида движется как человек, сидя без движения она может вполне сойти за человека:

Вернемся, однако, к левому краю долины, чтобы конкретизировать, с чего она начинается. Вот, например, киновоплощение робота C-3PiO из «Звездных войн». Он подчеркнуто механистичен, и при этом вполне антропоморфный. C-3PiO в большей степени похож на игрушку, чем на человека. Очевидно, что с человеком его не перепутать, и страха он, как правило, не вызывает:

Зловещая долина как художественный метод

С распространением анимационных технологий кинофантастика преобразилась. Уже никого не удивишь воссозданием лиц и аватаров, неотличимых не то что от персонажей «Final Fantasy», но и от настоящих людей. Писатели-фантасты и киносценаристы сыграли немалую роль в развитии технологий, после чего некоторые из этих разработок, воплощенные и обкатанные в реальном мире, вернулись в кино. Гиперреализм, который стал гораздо более достижим благодаря компьютерной графике, быстро взяли на вооружение режиссеры-авангардисты, естественно, сразу оказавшиеся в «зловещей долине».

Так, в фильме «Еда» чешский режиссер Ян Шваркмайер использует собственный стиль монтажа и анимации для вывода зрителя на грань любопытства, страха и отвращения. Здесь показано обращение людей в машины. В одном из эпизодов этой картины человек заходит в помещение, посреди которого стоит обеденный стол, а за столом сидит мужчина. Но сидящий будто погружен в сон, а на его шее висит инструкция по применению. Человек читает ее и при этом не воспринимает «сонного» как человека – вскоре становится понятно, почему. Оказывается, это машина. У нее раскрывается грудная клетка, представляющая собой шахту лифта, через которую доставляют еду. Руки, глаза и голова – это кнопки, «пользовательский интерфейс». Пообедав, герой занимает место «сонного», а недавний «манекен» оживает и уходит. Эффект зловещей долины здесь особенно нарочит, так как человек и машина смешиваются в одном образе. Зритель уверен в том, что в кадре живой человек, но человек этот вынужден стать машиной и выполнить свою функцию доставки еды для следующего посетителя ресторана.

Насильственное превращение людей в роботов, чьи корпуса и лица повторяют анатомию человеческого тела используется и в сериале «Доктор Кто». Казалось бы, внешний вид механистических персонажей далек от человеческого; тем не менее эффект долины присутствует из-за подобия: черные дыры на месте глаз, механическое звучание голоса, механические внутренности, но живой мозг. Этот случай отличен от механизации Шваркмайера тем, что здесь человек искореняется и заменяется машиной, в то время как в «Еде» человеческое все еще составляет значительную часть робота. В обоих случаях граница восприятия размывается, наблюдатель не знает робот ли перед ним или же человек, что вызывает у него чувство ужаса, отвращения. В более жутких и гротескных формах подобные сочетания организма и механизма выведены в книгах Чайны Мьевиля о Нью-Кробюзоне. «Переделанные» в его романах являются целой кастой; «переделке» подвергают как по решению суда, так и для занятия экзотическими профессиями, малопригодными для человека, а также просто ради забавы.

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

Главный герой фильм «Ex Machina» 2014 испытывает подобное чувство, обнаруживая себя в окружении человекоподобных роботов. В какой-то момент он, смотря на себя в зеркало, исследует черты своего лица в попытке доказать себе то, что он человек. Но прежде герой восхищен демонстрируемой ему машиной, он говорит с роботом, обучает его и в какой-то мере очеловечивает безупречную машину. В картине зрителю представлены люди, создающие образ роботов посредством актерской игры и использования технологий компьютерной графики. Постепенно можно заметить, как идеальные лица начинают кровоточить мелкими недочетами, которые пугают как главного героя, так как зрителя. Все в порядке, но что-то не так. Что же именно не так? Вы приблизились к зловещей долине.

В аниме «Trinity Blood» роботы представлены практически живыми людьми, с эмоциями, кровью, потребностью в еде – но это роботы, поскольку они являются искусственными. Люди, выращенные в лаборатории, вводят в свои организмы наномашины «крусники», чтобы выжить, в результате превращаясь в киборгов. Крусники-люди, совершенно не пугают нас до того момента пока не превращаются в киборгов-убийц. «Наномашины, Крусник 02!» – вызывает главный герой свою вторую ипостась и на наших глазах преображается в робота, чьи действия – уже в пределах зловещей долины. Несмотря на то, что визуальный ряд как таковой страха не вызывает, зритель сознает, что перед ним искусственно выращенный человек, в какой-то степени управляемый наномашинами. Авторы подчеркивают различия в поведении, внешнем виде и действиях людей и крусников-людей в результате чего образ машины закрепляется в сознании зрителя, вызывая отторжение.

В компьютерной игре «Detroit: become human» (2018) машины также очеловечиваются, но их образы постоянно возвращают наблюдателя к тому, что перед ним не человек. Главные герои-роботы проявляют эмоции: они смеются над шутками, беспокоятся, проявляют интерес. Все эмоции объясняются как результат чисто статистического анализа: ИИ изучает разнообразные ситуации и реагирует на них. Но суть игры неотделима от очеловечивания машины. Здесь игрок получил возможность управлять роботом, влиять на ход событий и «вырастить» своего героя в машину или человека. Но каким бы ни стал робот, роботом он остается: застывшее лицо, рваные движения, моменты неуправляемого холодного расчета при принятии решений, появление копии машины при «смерти» напоминают игроку об этом. В этом мире люди свободно общаются с человекоподобными роботами, но существует и оппозиция, выступающая против появления новых машин (эта позиция в игре также обосновывается). Но важно заметить, что таких людей-диссидентов значительно меньше тех, кто пользуется роботами так же буднично, как мы пользуемся смартфонами.

Машины такого типа, как роботы из «Detroit become human» и «Ex Machina», до сих пор не сошли в реальный мир с киноэкранов и книжных страниц; тем не менее их физическое воплощение прокрадывается в мир реальный. Человек уже в состоянии проектировать лица и тела роботов с такой степенью антропоморфизма, что неосведомленный наблюдатель не сразу обнаружит «подмену». Все это – уже правый склон «зловещей долины», заметными ранними представителями которого в кинофантастике были, вероятно, вышеупомянутые андроиды из «Чужих», пилот Калдер, а также терминатор Т-800 и Санни из фильма «Я, робот».  2004, C-3PO из «Звездных войн», но искусство продолжает совершенствовать образ робота и способы воздействия на зрителя, пользуясь зловещей долиной для провоцирования чувства страха у наблюдателя.

Оно больное или притворяется

В статье Кристины Лузер и Талии Уитли делается интересное обобщение тех «неправильных» черт, которые могут вызывать эффект «зловещей долины». В первую очередь, человек реагирует на несоответствие мимики ожиданиям.

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

Человек отлично приспособился не только различать лица, но и считывать их выражение: это ключевой навык, необходимый уже ребенку для распознавания родных, а взрослому человеку – для отличия «своих» от «чужих» и «друзей» от «врагов». Именно поэтому эффект зловещей долины может быть более выраженным при рассматривании лиц, чем, например, рук-протезов.

Минимальные и трудноуловимые отличия андроида от человека также могут восприниматься как нездоровье, что вызывает инстинктивное отторжение.  Исторически человек не всегда отличал инфекционные заболевания от психических. Поэтому «неадекватность» реакций робота также может восприниматься как заразная.

Эволюционное объяснение данного сценария позволяет предположить, что отбор поддерживал развитие отвращения к нездорово выглядящему человеку. Чем более по-человечески выглядит робот, тем сильнее выделяются его «нечеловеческие» детали: например, белизна покровов может восприниматься как бледность кожи. Кроме того, учитывая вышеуказанную склонность человека к распознаванию лиц, синтетическое лицо может вызывать отвращение из-за любого, даже минимального, нарушения пропорций, а также из-за плохого «качества кожи». Вполне возможно, что здесь также задействованы байесовские механизмы оценки правдоподобия. Лицо или тело, отдающее зловещей долиной, не оправдывает наших ожиданий.  

Учитывая, что в самой нижней точке зловещей долины находится «зомби» (и находится он там, поскольку является подвижным; прямо над ним на схеме располагается «труп»), отторжение может быть связано не столько со страхом заражения, сколько со страхом смерти. Подобно зомби, андроид может казаться «ожившим» мертвецом, который, однако, неуклюже движется, не контролирует или не имеет эмоций, а его лицо проявляет симптомы, напоминающие инсульт.

Все эти допущения приводят нас к следующим промежуточным выводам, которые отражаются на практике:

  1. Если эффект зловещей долины имеет эволюционные корни, то он может наблюдаться у обезьян

  2. Если эффект зловещей долины связан с тем, что робот или андроид воспринимается как психически (неврологически) нездоровая особь, то этот эффект должен быть сильнее выражен у взрослых, чем у детей

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

Опыты по изучению эффекта зловещей долины как у обезьян, так и у детей, уже проводились. В Тюбингенском университете группа под руководством Питера Тира проверяла наличие этого эффекта у макак-резусов. О данном исследовании есть хорошая статья на сайте N+1. При помощи компьютерного моделирования были получены каркасные модели обезьяньих лиц, а также анимированные движущиеся лица разной степени реалистичности. Мимика моделировалась с применением МРТ, смоделированное лицо выражало разные эмоции, в частности, удивление, агрессию или оставалось нейтральным.

Оказалось, что резусы более дружелюбно реагируют на наиболее реалистичные лица, видимо, относя их к «сородичам». Авторы исследования полагают, что повышенная реалистичность не вызывает эффект «зловещей долины» как таковой, а снижает толерантность к аномалиям в мимике. Компьютерная модель не может передать всех тонких движений лицевых мышц, поэтому реалистичные лица скорее приводили резусов в замешательство, тогда как безжизненные каркасные модели, вероятно, воспринимались как более неприятные, обезьяна рассматривала динамическое лицо дольше, чем статическое. Имея дело с синтетической мимикой, обезьянам сложно распознать «намерения» этого лица, но по-настоящему неприятными им кажутся иные составляющие: (безжизненная) текстура кожи и вообще отсутствие шерсти на лице, непривычная компоновка черт лица, размеры тех или иных черт лица.  

Замешательство, проявленное обезьянами при разглядывании динамичных реалистичных лиц, согласуется с предположением об «обманутых ожиданиях». Человек может бессознательно (статистически) оценивать мимику и язык тела, опираясь на имеющийся опыт, делая это практически по байесовскому принципу оценки правдоподобия, о чем я упоминал выше. Наблюдая хаотичную или противоречивую мимику, как обезьяна, так и человек затрудняется интерпретировать намерения «существа», а также категоризировать его по принципу «свой-чужой», «живой-неживой» и пр.

В 2017 году опыт по оценке эффекта «зловещей долины» также был проведен на детях; его поставили Генри Уэллман и Кимберли Бринк из Мичиганского университета.

В рамках исследования был проведен опрос 240 детей и подростков (в возрасте от 3 до 18 лет), участникам которого предлагалось оценить «настроение» трех разных роботов и прокомментировать, как им эти роботы. Детям показывали видео с роботом, очень похожим на человека, машинно-подобным роботом и еще одним человекоподобным роботом, сочетавшим черты Baymax из «Города героев» и EVE из «ВАЛЛ-И». Детей спрашивали, могут ли на их взгляд такие роботы подумать о себе, совершать целенаправленные действия, а также понимают ли, что такое хорошо и что такое плохо. Кроме того, детей спрашивали, может ли робот проголодаться, если пропустит завтрак, испугаться, если увидит змею, либо будет ли ему больно, если его ущипнуть.

Также был и заданы вопросы о том, страшные ли эти роботы, и вызывают ли они неловкость.

Это исследование продемонстрировало ярко выраженные отличия между детьми до девяти и старше девяти лет. Дети до девяти лет сообщили, что роботы очень похожи на людей и совсем не страшные. Эти данные – не в пользу о том, что зловещая долина может быть связана с «болезненностью» внешнего вида, поскольку подобный страх должен был эволюционно закрепляться уже у маленьких детей. С другой стороны, дети старше девяти лет говорили, что роботы, похожие на людей, гораздо неприятнее роботов, похожих на машины. Вероятно, это косвенно подкрепляет именно «мимическую» составляющую феномена. Маленькие дети более доверчивы и не так хорошо считывают социальные сигналы, возможно, плохо категоризируют их. Взрослея, дети осваивают принцип «свой-чужой», начинают более настороженно относиться к обману и лицемерию, развивают эмоциональный интеллект – и этот опыт входит в противоречие с действиями или бездействием человекоподобных роботов.

Авторы исследования предполагают, что «зловещий» компонент коррелирует с тем, насколько «разумным» кажется робот. Отношение к такой разумности у маленьких и у взрослеющих детей (а тем более у взрослых) прямо противоположное. Маленькие дети предпочитают играть с тем роботом, которого считают более разумным. Но взрослеющих детей «разумность» робота начинает напрягать и пугать, поскольку, с одной стороны, человекоподобному роботу проще приписать человеческие мысли и чувства, а с другой стороны – их сложнее «распознать» и «классифицировать». Очеловечиваясь, робот начинает восприниматься как неискреннее, но при этом достаточно умное и коварное существо, поступки которого сложно прогнозировать, а намерения – угадывать. Итак, избыточная разумность игрушки – как правило, хороша, а избыточная разумность машины – неприятна и опасна. Эти идеи пересекаются с тестом Тьюринга и проблемой его прохождения (о чем я собираюсь написать отдельную статью) и являются спекулятивными, но вполне убедительно свидетельствуют, что эффект зловещей долины сильно обусловлен социальными факторами и жизненным опытом, а значит – развивается с возрастом

Зловещая долина и GAN

Генеративно-состязательные нейронные сети и лежащие в их основе алгоритмы глубокого обучения в последние 3-4 года пользуются огромной популярностью благодаря тому, как здорово с их помощью получаются реалистичные изображения, тексты и музыка. Обзор GAN выходит за рамки этой статьи, но желающим рекомендую посмотреть курс Александра Дьяконова, выложенный здесь, а также уже ставшую знаменитой книгу Сергея Николенко, Артура Кадурина и Екатерины Архангельской.

Я впервые познакомился с GAN, залипая на сайте «ThisPersonDoesNotExist». Обновляя страницу, мы видим лица, конструируемые нейронкой по результатам изучения реальных фотографий, изученных ею в результате анализа соцсетей. На мой взгляд, эта GAN – настоящее произведение искусства, но я сразу пытался подловить ее на фальшивости и ошибках, присматриваясь, в частности, не фонит ли зловещей долиной от каких-то сделанных ею фотографий. Отчасти эти поиски также были связаны с интересом к технологии deepfake, связанной с наложением синтетических лиц на движущегося агента с далеко идущими последствиями – в частности, для искусственного создания компромата.

Просматривая выборку, я находил минимум изъянов в предлагаемых фотографиях, но обращал внимание на:

  1. Слишком старую кожу у детей

  2. Неправильную форму улыбки, напоминающую легкие последствия инсульта

  3. Чрезмерно массивную нижнюю челюсть, а также слишком крупные зубы

Совсем недавно, в октябре 2021 года, вышла интересная статья американских ученых, демонстрирующая, что GAN по-прежнему регулярно ошибается в воспроизведении зрачков. На приведенных в этой статье фотографиях слева изображена реальная девочка, а справа – несуществующая:

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

Я попробовал поэкспериментировать с сайтом «ThisPersonDoesNotExist» и уже после восьмого кряду обновления страницы получил вот такое женское лицо:

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

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

Состязательность GAN заключается в одновременном стремлении к обманчивости результата и обнаружению этой обманчивости, поэтому сеть максимизирует функцию правдоподобия, подгоняя генерируемые образцы под то множество данных, на котором училась. Можно обратить этот процесс: начать обучение с реалистичных образцов, но затем на каждой итерации корректировать работу сети, заставляя ее заострять те наиболее характерные черты, которые она классифицирует как фальшь. В таком случае эти черты постепенно выпячиваются, что приводит к выраженному соскальзыванию в зловещую долину:

Здесь показаны результаты работы сети после 0, 250, 500, 750, 1000 и 1500 итераций. Вот как выглядит один из откорректированных «портретов», полученных после 500 итераций:

Авторы расценивают это лицо как пик неправдоподобия, при котором лицо еще воспринимается как человеческое – и, соответственно, приближается к самой низкой точке зловещей долины. Мне представляется, что этот портрет ярко демонстрирует как неискренность, так и болезненность, которые я кажутся ключевыми составляющими эффекта зловещей долины.

Заключение

С эмпирической точки зрения эффект «зловещей долины» несомненно существует, но является скорее субъективным результатом высшей нервной деятельности, чем объективным набором характеристик. Поскольку фальшь накапливается незаметно, соскользнуть в долину с левого края можно совершенно случайно. По-видимому, мы настолько тонко обучились распознавать такую фальшь в ходе биологической эволюции, что «зловещая долина» фактически превращается в набор антипаттернов проектирования роботов. Чтобы получить робота или аватара, находящегося на вершине ее правого склона, в нее все равно придется зайти слева, полностью преодолеть, а затем благополучно выйти – что отнюдь не гарантировано. Поэтому робота желательно оставлять дружелюбной машиной, которая выглядит как машина и не дает повода усомниться, что она является машиной. Но, кто знает, может быть, подросший AGI решит иначе.

Adblock test (Why?)

AMD может стать первым покупателем 3-нанометровых чипов от Samsung

По слухам, разработчик процессоров AMD станет первым заказчиком 3-нанометровых чипов Samsung. Помимо AMD, в контракте с корейской компанией может быть заинтересована Qualcomm.

По сведениям тайваньского издания DigiTimes, тесное сотрудничество TSMC с Apple заставило AMD рассмотреть Samsung в качестве альтернативы для своих заказов на техпроцесс 3 нм. Источники DigiTimes утверждают, что начальные мощности 3 нм TSMC уже зарезервированы Apple. 

Samsung, как ожидается, начнет поставки 3-нанометровых чипов в первой половине следующего года. TSMC немного отстает, поскольку она начнет производство чипов по техпроцессу 3 нм во второй половине следующего года, а продукты с использованием новейших чипов появятся в 2023 году. 

Технология TSMC N3 обещает повышение производительности на 10%-15% при меньшем энергопотреблении (до -30%) по сравнению с передовыми современными чипами. Процесс изготовления будет основан на литографии в глубоком ультрафиолете (EUV), и хотя точное количество слоев EUV неизвестно, их будет больше 14-ти (как в N5). Сложность технологии вынудит компанию увеличить число этапов производственного процесса, что, соответственно, увеличит и время цикла.

Заключив контракты с Samsung вместо TSMC, AMD и Qualcomm, вероятно, смогут получить 3-нанометровые чипы даже быстрее, не конкурируя за них с Apple.

Adblock test (Why?)

Alphabet задействовала прототипы своих роботов для уборки офисов Google

Alphabet объявила, что ее проект Everyday Robots Project развернет первые прототипы «универсального обучающегося робота» в кампусах Google Bay Area для уборки.

Парк роботов Google насчитывает более 100 прототипов, которые автономно выполняют ряд задач.

Глава Everyday Robot Ханс Петер Брондмо говорит, что «тот же робот, который сортирует мусор, теперь может быть оснащен ракелем, чтобы протирать столы, и использовать тот же захват, которым берет чашки, чтобы открывать двери».

Прототипы Google представляют собой робозахват, прикрепленной к башне на колесах. На вершине башни есть также «голова» с камерами и датчиками машинного зрения, а сбоку установлен прибор, напоминающий вращающийся лидар, предположительно, для навигации.

Как указывает Брондмо, в 2019 году Alphabet уже задействовала Everyday Robot в сортировке отходов. Компания обещает, что в будущем они смогут работать в «неструктурированной» среде, например, в домах и офисах.

Запуская прототипы в свои офисы, Alphabet пытается решить задачу, которая стоит перед ведущими разработчиками робототехники: как научить роботов работать в новой среде и выполнять разнообразные задачи. 

Ранее Google заявила о начале работы над проектом комплексных нейросетей. Они, по словам разработчиков, смогут одновременно работать над решением тысяч задач. Проект получил название Pathways. Команда попробует скомбинировать опыт, полученный нейросетью в ходе решения одной из задач, с опытом решения другой. По мнению разработчиков, такой комплексный подход даст возможность решить третью, более сложную или специфическую задачу.

Adblock test (Why?)

Нужен ли Mockito, если у вас Kotlin?

Салют, коллеги.

В рамках пятничной статьи предлагаю посмотреть на интересный способ создания моков в Kotlin, без использования сторонних библиотек.

Я занимаюсь разработкой аддонов для Atlassian-стека в компании Stiltsoft и, из-за технических ограничений, до сих пор (да в 2021 году и, скорее всего, в ближайшие пару лет) вынужден использовать Java 8. Но, чтоб не отставать от прогрессивного человечества, внутри компании мы пробуем Kotlin, пишем на нем тесты и разные экспериментальные продукты.

Однако, вернемся к тестам. Часто у нас есть интерфейс из предметной области, нам не принадлежащий, но который активно используется нашим кодом. Причем у самого интерфейса много разных методов, но в каждом сценарии используем их буквально по паре штук. Например, интерфейс ApplicationUser.

public interface ApplicationUser {
    String getKey();
 
    String getUsername();          
     
    String getEmailAddress();
 
    String getDisplayName();       
     
    long getDirectoryId();
 
    boolean isActive();
}

В разных тестах нам нужен объект типа ApplicationUser с разным набором предустановленных полей, где-то надо displayName и emailAddress, где-то только username и так далее. 

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

Самое простое решение - анонимные классы

ApplicationUser user = new ApplicationUser() {
    @Override
    public String getDisplayName() {
        return "John Doe";
    }
 
    @Override
    public String getEmailAddress() {
        return "jdoe@example.com";
    }
 
    @Override
    public String toString() {
        return getDisplayName() + " <" + getEmailAddress() + ">";
    }
 
    @Override
    public String getKey() {
        return null;
    }
 
    @Override
    public String getUsername() {
        return null;
    }
 
    @Override
    public long getDirectoryId() {
        return 0;
    }
 
    @Override
    public boolean isActive() {
        return false;
    }
};

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

public abstract class AbstractApplicationUser implements ApplicationUser {
    @Override
    public String getKey() {
        return null;
    }
 
    @Override
    public String getUsername() {
        return null;
    }
 
    @Override
    public long getDirectoryId() {
        return 0;
    }
 
    @Override
    public boolean isActive() {
        return false;
    }
 
    @Override
    public String getEmailAddress() {
        return null;
    }
 
    @Override
    public String getDisplayName() {
        return null;
    }
}

и потом использовать его.

ApplicationUser user = new AbstractApplicationUser() {
    @Override
    public String getDisplayName() {
        return "John Doe";
    }
 
    @Override
    public String getEmailAddress() {
        return "jdoe@example.com";
    }
 
    @Override
    public String toString() {
        return getDisplayName() + " <" + getEmailAddress() + ">";
    }
};

Это улучшит ситуацию со строками, но класс-обертку придется написать на каждую сущность такого плана.

Более продвинутый вариант - использовать специализированную библиотеку.

ApplicationUser user = mock(ApplicationUser.class);
when(user.getDisplayName()).thenReturn("John Doe");
when(user.getEmailAddress()).thenReturn("jdoe@example.com");
 
String toString = user.getDisplayName() + " <" + user.getEmailAddress() + ">";
when(user.toString()).thenReturn(toString);

C количеством строк тут уже порядок, но код стал более "тяжелым" для восприятия и, на мой вкус, не очень красивым.

Я предлагаю альтернативный план: собрать решение из существующих фич Kotlin. Но сначала, небольшое теоретическое отступление про делегаты.

Один из юзкейсов делегирования, навесить какой-то дополнительный функционал на "чужой" объект, причем незаметным для конечного пользователя способом.

Например, мы отдаем объект ApplicationUser`a наружу, но хотим отправлять какое-то событие, каждый раз как у него вызовут метод getEmailAddress(). Для этого делаем свой объект, реализующий интерфейс ApplicationUser

public class EventApplicationUser implements ApplicationUser {
 
    private ApplicationUser delegate;
 
    public EventApplicationUser(ApplicationUser delegate) {
        this.delegate = delegate;
    }
 
    @Override
    public String getEmailAddress() {
        System.out.println("send event");
        return delegate.getEmailAddress();
    }
 
    @Override
    public String getDisplayName() {
        return delegate.getDisplayName();
    }
 
    @Override
    public String getKey() {
        return delegate.getKey();
    }
 
    @Override
    public String getUsername() {
        return delegate.getUsername();
    }
 
    @Override
    public long getDirectoryId() {
        return delegate.getDirectoryId();
    }
 
    @Override
    public boolean isActive() {
        return delegate.isActive();
    }
}

Используется такая конструкция следующим образом

public ApplicationUser method() {
    ApplicationUser user = getUser();
    return new EventApplicationUser(user);
}

Так вот, в Kotlin есть встроенная поддержка для такого использования делегата. И вместо простыни кода в стиле 

@Override
public String someMethod() {
    return delegate.someMethod();
}

Можно сделать так

class EventApplicationUser(private val user: ApplicationUser) : ApplicationUser by user {
    override fun getEmailAddress(): String {
        println("send event")
        return user.emailAddress
    }
}

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

val user = object : ApplicationUser by originalUser {
    override fun getEmailAddress(): String {
        println("send event")
        return originalUser.emailAddress
    }
}

Теперь надо лишь как-то подготовить объект originalUser, реализующий дефолтное поведение. Тут нам пригодится возможность создать динамический прокси

Написав простую инлайн функцию 

inline fun <reified T> proxy() = Proxy.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java), { _, _, _ -> null }) as T

мы получаем возможность писать так 

val user1 = proxy<ApplicationUser>()
val user2: ApplicationUser = proxy()

Обе строки делают одно и то же, создают динамический прокси для интерфейса ApplicationUser.

Разница, чисто синтаксическая, в первом случае мы явно параметризуем нашу функцию proxy() и компилятор понимает, что результат будет типа ApplicationUser, во втором случае мы откровенно говорим, что хотим переменную типа ApplicationUser и компилятор понимает чем надо параметризовать функцию proxy().

Остается только свести все вместе

val user = object : ApplicationUser by proxy() {
    override fun getDisplayName() = "John Doe"
    override fun getEmailAddress() = "jdoe@example.com"
    override fun toString() = "$displayName <$emailAddress>"
}

Здесь мы создаем анонимный объект с интерфейсом ApplicationUser, тут же все методы делегируем в свежесозданный мок и переопределяем только нужное, без всяких оберток/заготовок под каждую сущность, естественным образом. 

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

val user = proxy<ApplicationUser>() {
    override fun getDisplayName() = "John Doe"
    override fun getEmailAddress() = "jdoe@example.com"
    override fun toString() = "$displayName <$emailAddress>"
}

Adblock test (Why?)

Фронтенд на рельсах (почти) без JS

Вопреки слухам на пространствах девелоперских комьюнити, Rails не становится устаревшей технологией, он не собирается умирать, и остается отличным инструментом для разработки вашего нового проекта. И одна из причин заключается в том, что у Rails имеется достаточно инструментов, чтобы покрыть базовый функционал типичного веб-приложения. Вам не нужно думать о том, как обрабатывать НТТР запросы, что использовать для ввода и получения данных из базы, как отрисовать HTML, который пользователи увидят в своих браузерах, и даже как "вдохнуть жизнь" в пользовательский интерфейс.

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

То, что есть из коробки: rails-ujs, turbolinks

Rails UJS

Давным давно, когда я только пытался сверстать свою первую HTML страничку, у Rails уже был крутой инструмент jquery-ujs (unobtrusive javascript), который теперь называется rails-ujs. Он отлично работает с рельсовым бэкендом, когда вам нужно добавить парочку AJAX запросов малой ценой.

Можете попробовать сделать что-то вроде этого:

app/controllers/money_controller.rb

class MoneyController < ApplicationController
  def show
    @money = GetAllMoney.call
  end

  def destroy
    SpendAllMoney.call
  end
end

views/money/show.html.erb

<div class="money">
  <h3>Your money</h3>
  <span id="money-amount"><%= @money %></span>
  <span>$</span>

  <%= link_to 'Spend all money',
              money_path,
              method: 'delete',
              remote: true,
              data: { confirm: 'Do you want to spend all money?' },
              class: 'spend-money-button' %>
</div>

views/money/destroy.js

document.querySelector('#money-amount').innerHTML = 0
Новый сайт со ставками на спорт. Лучшие коэффициенты xD
Новый сайт со ставками на спорт. Лучшие коэффициенты xD

Итак, вы сделали AJAX запрос, используя всего несколько HTML атрибутов и один JS файл с одной строчкой кода. Круто, правда?

Turbolinks

Еще один старожил в мире Rails - Turbolinks. Эта библиотека не находится в стадии активной разработки, но о ее преемнике мы поговорим немного позже. В двух словах, Turbolinks приносит вам SPA опыт почти без клиентского кода. Если подробно, то эта библиотека:

  • загружает содержимое новых страниц с помощью JS и заменяет его на странице без перезагрузки браузера; 

  • она кэширует страницы, чтобы повторные посещения казались мгновенными;

  • позволяет сохранять элементы на странице неизменными во время навигации.

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

app/helpers/application_helper.rb

module ApplicationHelper
  def notifications_count
    sleep 3 # emulate some calculations

    10
  end

  def articles
    Article.last(5)
  end
end

app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
<head>
  <title>Turbolinks</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>

  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body>
<div class="container">
  <nav class="navigation">
    <ul>
      <%- articles.each do |article| %>
        <li>
          <%= link_to article.title, article_path(article.id) %>
        </li>
      <% end %>
    </ul>
    <div class="notifications">
      <div class="notifications-badge">
        <%= notifications_count %>
      </div>
    </div>
  </nav>
  <section class="content">
    <%= yield %>
  </section>
</div>
</body>
</html>

Подсчет количества уведомлений может занять некоторое время, но это цена, которую вы платите за поддержку актуальности данных. 

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

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

Таким образом, мы можем просто не выполнять код, если страница запрашивалась Turbolinks и и запретить Turbolinks обновлять часть страницы. Вот как это выглядит:

app/helpers/application_helper.rb

module ApplicationHelper
  def notifications_count
+   return nil if request.headers['Turbolinks-Referrer'].present?
+
    sleep 3 # emulate some calculations

    10
  end

  def articles
    Article.last(5)
  end
end

app/views/layouts/application.html.erb

<div class="notifications">
-   <div class="notifications-badge" id="notifications-badge">
+   <div class="notifications-badge" id="notifications-badge" data-turbolinks-permanent>
    <%= notifications_count %>
  </div>
</div>

Чего для нас не достаточно

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

Новые инструменты от команды Rails

В начале 2021 года DHH объявил о появлении альтернативного подхода Hotwire, нового способа Rails для построения пользовательских интерфейсов. Несмотря на то, что Hotwire является собирательным названием для семейства библиотек, эта семья довольно мала. По состоянию на октябрь 2021 года было всего две библиотеки: Turbo и Stimulus.

Они обе разработаны командой Rails и могут без проблем интегрироваться в ваш величественный монолит. Я расскажу больше о Turbo, так как эта библиотека относительно новая и заменит уже существующую Turbolinks.

Turbo

Если вы думали, что Turbolinks потеряли свою часть "links", потому что это теперь больше чем навигация, вы на 100% правы. Библиотека Turbo разделена на несколько частей, где каждая служит единой цели - доставить в ваше приложение HTML, отрисованный на сервере, с разницей в том, когда и как это делается:

  • Turbo Drive - тот старый добрый Turbolinks, с которым мы знакомы. 

  • Turbo Frames - “отдельные” фреймы, которые могут быть загружены асинхронно и обновлены, когда сервер возвращает фрейм с тем же id. 

  • Turbo Streams - другой тип фреймов, который обновляется в результате HTTP запроса или с помощью сервера через Websocket. 

  • Turbo Native - обёртка вашего “турбированного” веб-приложения, которая интегрирует его в мобильное приложение.

Итак, теперь обо всем по порядку.

Turbo Drive

Как упоминалось ранее, Turbo Drive просто заменяет Turbolinks и берет на себя навигацию между страницами. Поскольку почти ничего не изменилось, миграция довольно проста.

Вам нужно просто добавить пакет npm

yarn add @hotwired/turbo

Заменить Turbolinks на Turbo в вашем javascript коде

app/javascript/packs/application.js

  import Rails from "@rails/ujs"
- import Turbolinks from "turbolinks"
+ import * as Turbo from "@hotwired/turbo"
 
  Rails.start()
- Turbolinks.start()

Зменить data-turbolinks... атрибуты с data-turbo…

app/views/layouts/application.html.erb


-    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
-    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
+    <%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>

Важный момент, на который нужно обратить внимание -  заполнение формы. Turbo drive берет на себя и это. Прежде всего, он ожидает, что redirect после отправки формы будет со статусом 303, чтобы позволить Fetch API автоматически следовать за редиректом. Это правильный статус НТТР для неиндемпотентных (умное слово для описания HTTP методов помимо GET и HEAD 🤓) запросов, если вы хотите, чтобы переадресация осуществлялась с помощью метода GET. В противном случае правильно перенаправлены будут лишь POST запросы, поскольку они также предусматривают статусы 301 и 302. Так что вам следует явно указать код статуса для редиректа. И вот как это сделать:

app/controllers/any_controller.rb

-    redirect_to money_path
+    redirect_to money_path, status: :see_other

Так или иначе в рельсовых формах все равно используется метод POST и добавляется <input type="hidden" name="_method" value="patch">, чтобы определить какое действие контроллера использовать. Это означает, что ваши формы все еще будут работать, а о необходимости правильного кода статуса уже велись бурные дискуссии

Следующее, на что стоит обратить внимание это то, что Turbo не поддерживает параметр local: true, который вы могли использовать для отключения JS-контроля над формой. Если это ваш случай, необходимо внести еще одно небольшое изменение:

app/views/_any_form.html.erb

- <%= form_with(url: money_path, local: true) do |f| %>
+ <%= form_with(url: money_path, data: { turbo: false }) do |f| %>

Turbo Frames

Наконец мы подобрались к чему-то новенькому в рельсе. Turbo Frame - это простой инструмент для создания контейнера с контентом, который может загружаться и обновляться отдельно. Так же, как и в геме render_async или .ejs от Rails, но с меньшим количеством кода. 

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

Ваш новый интернет магазин
Ваш новый интернет магазин

Я пропущу часть примера, относящуюся к настройке моделей, маршрутов, установке Bootstrap и добавлению CSS. Полагаю, вас не интересуют такие базовые вещи.

Вот так выглядит наш app/views/products/show.html.erb

<div class="product">
  <ul class="nav nav-tabs" id="product-tab" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">General</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="properties-tab" data-bs-toggle="tab" data-bs-target="#properties" type="button" role="tab" aria-controls="properties" aria-selected="false">Properties</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="reviews-tab" data-bs-toggle="tab" data-bs-target="#reviews" type="button" role="tab" aria-controls="reviews" aria-selected="false">Reviews</button>
    </li>
  </ul>

  <div class="tab-content">
    <div class="tab-pane active p-3" id="general" role="tabpanel" aria-labelledby="general-tab">
      <turbo-frame id="<%= dom_id(@product, 'general') %>" loading="lazy" src="<%= general_product_path %>">
        <%= render('common/spinner') %>
      </turbo-frame>
    </div>
    <div class="tab-pane p-3" id="properties" role="tabpanel" aria-labelledby="properties-tab">
      <turbo-frame id="<%= dom_id(@product, 'properties') %>" loading="lazy" src="<%= properties_product_path %>">
        <%= render('common/spinner') %>
      </turbo-frame>
    </div>
    <div class="tab-pane p-3" id="reviews" role="tabpanel" aria-labelledby="reviews-tab">
      <turbo-frame id="<%= dom_id(@product, 'reviews') %>" loading="lazy" src="<%= reviews_product_path %>">
        <%= render('common/spinner') %>
      </turbo-frame>
    </div>
  </div>
</div>

Это обычные табы из Bootstrap. Но самое интересное в элементах .tab-page. Мы добавили тег turbo-frame, который является нашим контейнером для загрузки и обновления. У каждого фрейма должен быть свой собственный атрибут идентификатора (id), а хелпер dom_id будет хорошим инструментом, чтобы освободить нас от необходимости думать над именами. Для асинхронной загрузки фрейма мы должны добавить атрибут src, и ответ из этого пути должен вернуть фрейм с таким же идентификатором (id).

Поскольку мы хотим загружать только видимую часть, мы добавляем loading="lazy" и фрейм будет загружаться только тогда, когда этот элемент появится на странице. Обратите внимание, что не важно, как этот элемент стал видимым. Пользователь может просто проскролить страницу к этому тегу и его содержимое загрузится, стили родительского элемента могут измениться с display: none на display: block, приложение может вставить этот тег на страницу с помощью Javascript или вы даже можете рекурсивно рендерить один фрейм из другого (но не забудьте как-нибудь выйти из рекурсии).

Спиннер в примере - это просто div с CSS анимацией. Вам ничего не нужно с этим делать. Он просто будет вращаться, пока содержимое фрейма не загрузится и появится на странице.

app/views/common/_spinner.html.erb

<div class="text-center mt-5">
  <div class="spinner-grow text-secondary" role="status">
    <span class="visually-hidden">Loading...</span>
  </div>
</div>

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

Наш контроллер довольно простой:

app/controllers/products_controller.rb

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end

  def general
    @product = Product.find(params[:id])

    render partial: 'products/general'
  end

  def properties
    @product = Product.find(params[:id])

    render partial: 'products/properties'
  end

  def reviews
    @product = Product.find(params[:id])

    render partial: 'products/reviews'
  end
end

app/views/products/_general.html.erb

<turbo-frame id="<%= dom_id(@product, 'general') %>" loading="lazy" src="<%= general_product_path(product_id: @product) %>">
  <div class="product--general">
    <h1>
      <%= @product.title %>
    </h1>
    <div class="row mt-4">
      <div class="col">
        <div class="product--image">
          <%= image_tag @product.image %>
        </div>

      </div>
      <div class="col">
        <h3>
          <%= @product.price %>
        </h3>

        <%= @product.content %>
      </div>
    </div>
  </div>
</turbo-frame>

app/views/products/_properties.html.erb

<turbo-frame id="<%= dom_id(@product, 'properties') %>">
  <h1>
    <%= @product.title %> properties
  </h1>
  <dl class="row mt-4">
    <%- @product.properties.each do |name, value| %>
      <dt class="col-sm-3"><%= name.to_s.titleize %></dt>
      <dd class="col-sm-9"><%= value %></dd>
    <% end %>
  </dl>
</turbo-frame>

app/views/products/_review.html.erb

<turbo-frame id="<%= dom_id(@product, 'reviews') %>">
  <%- @product.reviews.each do |review| %>
    <div class="card mb-3">
      <div class="card-body">
        <div class="card-title">
          <%= review.author %>
        </div>
        <div class="card-text">
          <%= review.content %>
        </div>
      </div>
    </div>
  <% end %>
</turbo-frame>

Тут мы рендерим отдельные фрагменты, но это может быть и вся страница с макетом. Главное - отрендерить тег turbo-frame с тем же идентификатором, что и у тега куда контент будет вставлен.

В целом, это все, что вам нужно, чтобы получить “лениво загруженную страницу”. К сожалению, я не смог найти удобный способ обработки ошибок при работе с turbo frames, но набросал решение, которое может быть вам в помощь:

app/controllers/any_controller.rb

def general
  @product = Product.find(params[:id])

  raise StandardError, 'Some error'

  render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) }
rescue StandardError
  render partial: 'common/turbo_error',
         locals: { id: dom_id(@product, 'general'), error_message: 'Oops. Something went wrong' }
end

app/views/common/_turbo_error.html.erb

<turbo-frame id="<%= id %>">
  <%= error_message %>
</turbo-frame>

Ещё одна замечательная вещь, которую мы можем сделать с turbo frames, это заменять части страницы в ответ на заполнение формы. Идея очень похожая. Действие контроллера должно вернуть тег turbo-frame и Turbo заменит его на странице. Давайте расширим предыдущий пример, чтобы получить возможность добавлять и удалять товары в корзине.

app/controllers/products_controller.rb

class ProductsController < ApplicationController
  include ActionView::RecordIdentifier

  def show
    @product = Product.find(params[:id])
  end

  def general
    @product = Product.find(params[:id])

-   render partial: 'products/general'
+   render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) }
  end

  def properties
    @product = Product.find(params[:id])

    render partial: 'products/properties'
  end

  def reviews
    @product = Product.find(params[:id])

    render partial: 'products/reviews'
  end

+  def add_to_cart
+    @product = Product.find(params[:id])
+
+    session[:cart] = (session[:cart] || []) << @product.id
+
+    render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) }
+  end
+
+  def remove_from_cart
+    @product = Product.find(params[:id])
+
+    session[:cart] = (session[:cart] || []).reject { |id| @product.id == id }
+
+    render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) }
+  end
+
+  private
+
+  def product_in_cart?(product)
+    return false unless product && session[:cart]
+
+    session[:cart].include?(product.id)
+  end
end

app/views/products/_general.html.erb

<turbo-frame id="<%= dom_id(@product, 'general') %>" loading="lazy" src="<%= general_product_path(product_id: @product) %>">
  <div class="product--general">
    <h1>
      <%= @product.title %>
    </h1>
    <div class="row mt-4">
      <div class="col">
        <div class="product--image">
          <%= image_tag @product.image %>
        </div>

      </div>
      <div class="col">
        <h3>
          <%= @product.price %>
        </h3>

+        <%- if in_cart %>
+          <%= form_with(url: remove_from_cart_product_path) do |f| %>
+            <%= f.submit 'Remove from cart', class: 'my-3 btn btn-danger' %>
+          <%- end %>
+        <%- else %>
+          <%= form_with(url: add_to_cart_product_path) do |f| %>
+            <%= f.submit 'Add to cart', class: 'my-3 btn btn-success' %>
+          <%- end %>
+        <% end %>
        <%= @product.content %>
      </div>
    </div>
  </div>
</turbo-frame>

Вы видите, что теперь у контроллера есть экшены для добавления и удаления товаров. Оба этих метода просто рендерят фрагмент general и он волшебным образом обновляется на странице. Это очень похоже на то, что обычно делается в шаблонах .js.erb. Тем не менее, turbo - более предпочтительный вариант, чтобы избежать дополнительного JS кода, который, к тому же, лежит в папке views.

Turbo Streams

Turbo дал нам еще один интересный инструмент для изменения HTML на странице - Turbo Streams. Он дает больше возможностей для обновления интерфейса DOM и вы не ограничены заменой только одного фрейма, как это происходит с Turbo frames. Этa манипуляция с DOM называются action и она должна выполняться на элементах targets, полученных из какого-либо селектора. Turbo streams дает вам 7 действий для выполнения:

  • append - добавить html в начало цели. 

  • prepend - добавить html в конец цели. 

  • replace - заменить всю цель на html. 

  • update - обновить html внутри цели. 

  • remove - удалить всю цель. 

  • before - добавить html после цели. 

  • after - добавить html перед целью

Вы обычно можете услышать/прочитать о Turbo Streams, когда речь заходит про обновления в режиме реального времени и создание еще одного приложения для чата. Но мы можем начать с примера попроще и посмотреть, как Turbo Streams помогает отобразить заполнение формы в пользовательском интерфейсе. Давайте продолжим с предыдущим примером: добавим возможность разместить новый отзыв и показать общее количество отзывов. В начале я просто добавлю количество отзывов и форму для добавления нового отзыва:

app/views/products/show.html.erb

<ul class="nav nav-tabs" id="product-tab" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">General</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="properties-tab" data-bs-toggle="tab" data-bs-target="#properties" type="button" role="tab" aria-controls="properties" aria-selected="false">Properties</button>
    </li>
    <li class="nav-item" role="presentation">
-      <button class="nav-link" id="reviews-tab" data-bs-toggle="tab" data-bs-target="#reviews" type="button" role="tab" aria-controls="reviews" aria-selected="false">Reviews</button>
+      <button
+        class="nav-link"
+        id="reviews-tab"
+        data-bs-toggle="tab"
+        data-bs-target="#reviews"
+        type="button"
+        role="tab"
+        aria-controls="reviews"
+        aria-selected="false"
+      >
+        Reviews
+        <span id=<%= dom_id(@product, 'reviews_count') %>>
+          <%= render(partial: 'products/reviews/count_badge', locals: { count: @product.reviews.count }) %>
+        </span>
+      </button>
    </li>
  </ul>

app/views/products/_reviews.html.erb

<turbo-frame id="<%= dom_id(@product, 'reviews') %>">
  <%= render(partial: 'products/reviews/form', locals: { product: @product }) %>

  <div id="<%= dom_id(@product, 'reviews_list') %>">
    <%- @product.reviews.each do |review| %>
      <%= render(partial: 'products/reviews/card', locals: { review: review }) %>
    <% end %>
  </div>
</turbo-frame>

app/views/products/reviews/_count_badge.html.erb

<span class="badge bg-primary">
  <%= count %>
</span>

app/views/products/reviews/_form.html.erb

<%= form_with(url: add_review_product_path(id: product.id), class: 'mb-4', id: dom_id(product, 'reviews_form')) do |f| %>
  <%= f.text_area :review, class: "form-control mb-1" %>
  <%= f.submit 'Add a review', class: 'btn btn-primary' %>
<% end %>

app/views/products/reviews/_card.html.erb

<%= form_with(url: add_review_product_path(id: product.id), class: 'mb-4', id: dom_id(product, 'reviews_form')) do |f| %>
  <%= f.text_area :review, class: "form-control mb-1" %>
  <%= f.submit 'Add a review', class: 'btn btn-primary' %>
<% end %>

И вот как это будет выглядеть:

Людям нравится
Людям нравится

Теперь мы можем добавить немного интерактивности, используя Turbo Streams:

app/controllers/products_controller.rb

+ def add_review
+    @product = Product.find(params[:id])
+    @review = @product.add_review(author: 'You', content: params[:review])
+ end

app/views/products/add_review.turbo_stream.erb

<%= turbo_stream.update(dom_id(@product, 'reviews_count')) do %>
  <%= render(partial: 'products/reviews/count_badge', locals: { count: @product.reviews.count }) %>
<% end %>

<%= turbo_stream.replace(dom_id(@product, 'reviews_form')) do %>
  <%= render(partial: 'products/reviews/form', locals: { product: @product }) %>
<% end %>

<%= turbo_stream.prepend(dom_id(@product, 'reviews_list')) do %>
  <%= render(partial: 'products/reviews/card', locals: { review: @review }) %>
<% end %>

Самый интересный файл вот здесь _add_review.turbo_stream.erb. Формат turbo_stream может быть новым для вас, если вы впервые сталкиваетесь с Turbo Streams. Turbo требует, чтобы HTTP ответ имел контент-тип text/vnd.turbo-stream.html, поэтому вы должны либо передать content_type: "text/vnd.turbo-stream.html" в метод render  в действии контроллера, либо добавить расширения .turbo_stream.erb для вашего шаблона. Второй вариант мне кажется более практичным. Главный субъект в _add_review.turbo_stream.erb это хэлпер turbo_stream. Мы используем его для вызова ранее упомянутых действий. А если точнее, он генерирует XML теги, которые описывают, какие манипуляции DOM должны быть сделаны. Этот файл делает три вещи:

  • Обновляет счетчик отзывов - обновляет содержимое тега с идентификатором dom_id(@product, 'reviews_count') 

  • Сбрасывает форму обзора - заменяет весь тег на id dom_id(@product, 'reviews_count'). 

Показывает новый обзор на странице - добавляет контент в начало тега с id dom_id(@product, 'reviews_list')

Еще один довольный пользователь
Еще один довольный пользователь

Это все, что вам нужно для создания действительно интерактивного веб-приложения. Без единой строки JS кода! И этого будет достаточно для большинства приложений. 

С Turbo Streams вы также можете изменять содержимое страницы с помощью WebSocket. Это не потребует много действий с нашей стороны. Предлагаю вернуться к нашему примеру и обновить отзывы во всех открытых браузерах, когда будет добавлен новый отзыв.

Перед тем, как мы начнем, вы должны добавить в свой Gemfile гем turbo-rails и запустить эту команду bundle exec rails turbo:install

Он установит @hotwired/turbo-rails и заменит адаптер Action Cable с async(по умолчанию) на redis. Теперь мы готовы к работе в режиме реального времени.

Первое, что нам нужно сделать, это подписаться на обновления продукта. Это очень просто благодаря хэлперу  turbo_stream_from. Вот как это выглядит:

app/views/products/show.html.erb

<div class="product">
+  <%= turbo_stream_from @product %>

  <ul class="nav nav-tabs" id="product-tab" role="tablist">

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

app/controllers/products_controller.rb

def add_review
  @product = Product.find(params[:id])
  @review = @product.add_review(author: 'You', content: params[:review])

+  Turbo::StreamsChannel.broadcast_update_to(
+    @product,
+    target: ActionView::RecordIdentifier.dom_id(@product, 'reviews_count'),
+    partial: 'products/reviews/count_badge',
+    locals: { count: @product.reviews.count }
+  )
+
+  Turbo::StreamsChannel.broadcast_prepend_to(
+    @product,
+    target: ActionView::RecordIdentifier.dom_id(@product, 'reviews_list'),
+    partial: 'products/reviews/card',
+    locals: { review: @review }
+  )
end

И чтобы не выполнять некоторые действия дважды, удалим их из НТТР ответа

app/views/products/add_review.turbo_stream.erb

- <%= turbo_stream.update(dom_id(@product, 'reviews_count')) do %>
-   <%= render(partial: 'products/reviews/count_badge', locals: { count: @product.reviews.count }) %>
- <% end %>

<%= turbo_stream.replace(dom_id(@product, 'reviews_form')) do %>
  <%= render(partial: 'products/reviews/form', locals: { product: @product }) %>
<% end %>

- <%= turbo_stream.prepend(dom_id(@product, 'reviews_list')) do %>
-   <%= render(partial: 'products/reviews/card', locals: { review: @review }) %>
- <% end %>

Обновление формы все еще останется в HTTP ответе, так как форма должна быть очищена после отправки, и мы не хотим очищать ее для всех пользователей.

Вот и всё, что вам нужно, если вы хотите добавить в Rails приложение немного коммуникации в режиме реального времени. Магия рельсов во всей её красе!

Команда Rails проделала большую работу, чтобы свести к минимуму взаимодействие со всем этим большим и страшным миром JS, оставив фреймворк отличным инструментом для создания современных веб-приложений. Конечно, реальный мир может (и, скорее всего, будет) требовать больше, чем может дать Turbo. И команда Rails разработала Stimulus и request.js, чтобы сделать вашу жизнь легче, когда вам все-таки придется писать JS код в Rails приложении. Впрочем, это уже совсем другая история.

Adblock test (Why?)