javascript

Diablo II, римские стратегии и Prince of Persia: что фронтенду стоит перенять у геймдева

  • пятница, 18 сентября 2026 г. в 00:00:05
https://habr.com/ru/companies/kaspersky/articles/1078628/

Привет, Хабр! Меня зовут Павел Востриков. Я занимаюсь фронтенд-разработкой и пишу на JavaScript в «Лаборатории Касперского», а еще участвую в организации московского JS-сообщества.

В Diablo II характеристики навыков хранятся в обычных текстовых файлах, поэтому для изменения баланса игру не нужно пересобирать. В Prince of Persia 1989 года поведение персонажа описано через состояния и переходы, а в градостроительных стратегиях сборщик налогов сам выбирает следующее действие по набору правил.

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

Речь пойдет о контрактах и моках, конечных автоматах, событийной модели, pattern matching и деревьях поведения. Материала будет много, но основных мыслей три:

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

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

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

Зачем фронтендеру опыт геймдева в 2026 году

Сразу сниму одно возражение. Геймдев нельзя свести к C# и Unity или считать отдельным узким ремеслом. Это огромная область на стыке computer science, искусства и нескольких смежных дисциплин, и решения там принимались раньше, чем во фронтенде появились соответствующие задачи.

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

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

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

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

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

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

Урок Diablo II: движок отдельно, данные отдельно

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

Разница хорошо видна на двух вопросах. Первый: «Что нужно сделать?» Например: пройди два шага вперед, поверни за угол, выполни следующее действие. Второй вопрос: «Какой результат мы хотим получить?» Например, как управлять NPC, не прописывая заранее весь алгоритм его поведения.

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

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

Первый пример — Diablo II, на ее примере удобно рассмотреть, что веб-разработка может позаимствовать у RPG.

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

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

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

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

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

Моки, заложенные на этапе проектирования

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

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

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

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

Декларативные подходы: от генерации карт до микрофронтендов

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

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

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

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

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

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

Римская империя и экономика ресурсов

Теперь поговорим о Римской империи. Точнее, о градостроительных и экономических стратегиях: именно в них декларативная логика раскрывается особенно хорошо.

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

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

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

Сборщик налогов не выполняет жестко заданную функцию «сначала зайти в район патрициев, собрать там все подати, затем отправиться в квартал бедняков». Вместо такой последовательности действий игра использует набор правил, приоритетов и механизмов ранжирования. Агент оценивает доступные варианты и выбирает следующее действие в зависимости от текущего состояния города. Ресурсы, поведение агентов и порядок их действий описываются декларативными правилами.

Prince of Persia: от if-ов к конечным автоматам

Перейдем к третьей игре и посмотрим на упрощенных примерах кода, как подобные механики можно перенести во фронтенд. Речь пойдет о Prince of Persia, причем об оригинальной игре 1989 года, вышедшей во времена, когда усы среди программистов были не шуткой, а настоящей модой.

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

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

Отступление про скриншотные тесты

Этот опыт можно перенести во фронтенд не только на уровень реализации, но и в тестирование интерфейсов. Один из подходов — скриншотные тесты, которые позволяют сравнивать внешний вид приложения с эталоном и замечать визуальные регрессии. Здесь я рекомендую доклад Ромы Дворнового о запуске скриншотного тестирования в продакшене. Он вышел еще в 2018 году, но хорошо состарился и по-прежнему остается актуальным.

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

Первая сложность возникает при любом заметном обновлении интерфейса. Изменился компонент, шрифт или расположение элементов — эталонные изображения перестали совпадать, и их приходится обновлять. Чем больше экранов покрыто тестами, тем заметнее объем этой работы.

Со второй проблемой мы столкнулись на практике. Эталонные скриншоты генерировались в контейнере внутри CI-пайплайна, а при локальном запуске на macOS результаты уже не совпадали. Причиной могли стать различия в окружении, рендеринге и доступных шрифтах. Решается это унификацией среды: скриншоты нужно создавать и проверять в одинаковых контейнерах. Тогда результат меньше зависит от операционной системы и локальной конфигурации разработчика.

Прыжок принца: путь от условий к состояниям

Один из ключевых вопросов при использовании декларативных правил — управление поведением системы. Здесь стоит перейти к конечным автоматам, или state machines. Но перед этим пройдем путь от простейших if-условий к более структурированным способам описания состояний. Рассматривать этот путь будем на примере прыжка персонажа в Prince of Persia.

В упрощенном сценарии у игрока есть два состояния: он либо стоит на земле, либо находится в воздухе. Самая очевидная реализация — обычный императивный код. На вход поступает действие пользователя, например нажатие пробела, после чего система проверяет текущее положение персонажа. Если герой стоит на земле, нажатие пробела запускает прыжок. Если он уже находится в воздухе, команда игнорируется либо запускает повторный прыжок, если в игре предусмотрена такая механика.

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

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

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

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

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

Здесь возникает закономерный вопрос: не достаточно ли использовать привычный менеджер состояния? С данными фронтенд уже научился работать через state management, с бизнес-логикой ситуация сложнее. Мы можем вынести действия в контроллеры, подключить роутер и разложить интерфейс по компонентам. Формально структура приложения станет аккуратнее, однако сама бизнес-логика нередко останется той же лапшой из условий, обработчиков и связанных друг с другом действий.

Событийно-ориентированный подход

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

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

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

Главное — всегда думать головой и не воспринимать ни один инструмент как универсальное решение. Не нужно использовать технологию только потому, что она популярна или хорошо показала себя в чужом проекте.

Конечный автомат в деле

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

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

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

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

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

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

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

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

Pattern matching: логика как набор случаев

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

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

Если сервер вернул статус 200, мы обрабатываем полученные данные. При статусе 404 показываем пользователю понятное сообщение и, например, предлагаем перейти к поиску. Для остальных ожидаемых ответов из диапазонов 4xx и 5xx запускаем обработку ошибки. Если же произошло нечто, что не укладывается ни в один из предусмотренных вариантов, выбрасываем исключение.

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

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

Бой с NPC: правила вместо последовательности команд

Посмотрим на простом игровом примере, как поведение системы можно описывать декларативными правилами.

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

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

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

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

Деревья поведения

Одна из моих задач — постоянно расширять собственный профессиональный контекст. Этими подходами я хочу поделиться и с вами, чтобы мы воспринимали себя не просто как JavaScript-разработчиков, а как инженеров, способных проектировать системы и проверять результаты их работы.

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

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

Пример из информационной безопасности

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

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

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

Главная задача нашей бизнес-логики — сделать так, чтобы система могла автоматически реагировать на подобные атаки в будущем. Для этого специалист по информационной безопасности задает набор правил.

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

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

Server-Driven UI: две тысячи экранов по общим правилам

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

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

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

На самом деле похожий паттерн уже используется в индустрии. Его называют Backend-Driven UI или Server-Driven UI. Во многом он пришел из мобильной разработки и решал практическую задачу: позволял изменять часть интерфейса без выпуска новой версии приложения через App Store или Google Play. Само приложение содержит движок и набор поддерживаемых компонентов, а структура конкретного экрана и необходимые данные приходят с сервера. Для веб-интерфейсов этот подход особенно полезен там, где требуется создавать много однотипных экранов по общим правилам.

Что попробовать на практике

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

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

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

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

Третье касается вероятностной логики. Представим рекомендательную механику: если пользователю нравится определенный тип контента, система предлагает ему похожие материалы. Это упрощенный пример, но он показывает важную особенность бизнес-логики. Она редко бывает полностью детерминированной, независимо от того, разрабатываем мы банковское приложение, интернет-магазин или сервис с контентом.

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

Теперь соберем все вместе. Первое и главное правило — разделять данные и логику. Эта мысль может показаться давно известной. Но почему тогда в собственных проектах, в коде коллег и в open source по-прежнему встречаются компоненты и административные интерфейсы, в которых данные, представление и бизнес-правила жестко связаны друг с другом?

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

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

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

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

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

Последний игровой пример снова касается отделения представления от логики. Возьмем одну из моих любимых игр детства Small Soldiers, вышедшую в 1998 году. Когда-то я играл в нее сам, а теперь показываю своим детям.

У нас есть одна консоль и общее игровое ядро, однако на телевизоре одновременно отображаются две независимые области экрана. Представления разные, а правила игры и состояние системы остаются общими.

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

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

В заключение

Пять выводов

  • Ядро бизнес-логики не должно зависеть от JSX и компонентов. Если внутри механики пользовательского сценария появляется компонент, это повод проверить границы слоев. Бизнес-логика должна существовать отдельно от конкретного способа ее отображения.

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

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

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

  • Непрерывно расширяйте свой профессиональный контекст. Не поддавайтесь ажиотажу и не пугайтесь заявлений людей, которые рассказывают: «Раньше я был 10×-инженером, а с агентами стал 100×-инженером и теперь все делаю автономно». И не бойтесь снова и снова оказываться джуниорами. Индустрия в очередной раз изменилась. Чтобы оставаться востребованными, продолжать зарабатывать разработкой и получать от нее удовольствие, нам придется постоянно осваивать новые области.

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