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

Чему веб-разработка может поучиться у геймдева: контракты, декларативные правила и автоматы

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

Контракты и ранние моки: опыт Diablo II

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

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

// Строгий контракт состояния задачи, согласованный до начала работы над API
export interface UserTaskContract {
  taskId: string;
  stage: 'idle' | 'in_progress' | 'verifying' | 'completed';
  experiencePoints: number;
  isRequirementFulfilled: boolean;
}

// Декларативное правило завершения вместо цепочки случайных условий в UI
export const canCompleteTask = (task: UserTaskContract): boolean => {
  return (
    task.stage === 'verifying' &&
    task.isRequirementFulfilled &&
    task.experiencePoints >= 500
  );
};

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

Конечные автоматы: избавление от комбинаторного взрыва

В классических платформерах, таких как Prince of Persia, физика движений персонажа строго детерминирована: герой не может внезапно перейти из падения в бег, минуя фазу приземления. В веб-интерфейсах ситуация сплошь и рядом противоположна: разработчик может одновременно отобразить спиннер загрузки, модальное окно ошибки и старые данные из кэша из-за несогласованных булевых флагов (isLoading, isError, isSuccess).

Архитектурным спасением здесь выступает паттерн конечного автомата (Finite State Machine, FSM). Развивая наш контракт задачи, оформим переходы между состояниями через детерминированный автомат:

// Развитие контракта задачи в строгий конечный автомат (FSM)
export type TaskAction =
  | { type: 'START' }
  | { type: 'SUBMIT_FOR_REVIEW'; points: number; fulfilled: boolean }
  | { type: 'APPROVE' }
  | { type: 'REJECT' };

export class TaskStateMachine {
  constructor(private current: UserTaskContract) {}

  public getState(): UserTaskContract {
    return { ...this.current };
  }

  public transition(action: TaskAction): TaskStateMachine {
    const { stage, experiencePoints } = this.current;

    switch (stage) {
      case 'idle':
        if (action.type === 'START') {
          return new TaskStateMachine({ ...this.current, stage: 'in_progress' });
        }
        break;

      case 'in_progress':
        if (action.type === 'SUBMIT_FOR_REVIEW') {
          return new TaskStateMachine({
            ...this.current,
            stage: 'verifying',
            experiencePoints: experiencePoints + action.points,
            isRequirementFulfilled: action.fulfilled,
          });
        }
        break;

      case 'verifying':
        if (action.type === 'APPROVE' && canCompleteTask(this.current)) {
          return new TaskStateMachine({ ...this.current, stage: 'completed' });
        }
        if (action.type === 'REJECT') {
          return new TaskStateMachine({ ...this.current, stage: 'in_progress' });
        }
        break;

      case 'completed':
        // Завершенное состояние терминально: дальнейшие переходы запрещены
        break;
    }

    return this;
  }
}
Архитектурный эффект

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

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