Паттерн State: Избавляемся от лабиринтов `if/else` в управлении поведением

Введение
В проектах любого масштаба часто встречаются объекты, меняющие поведение в зависимости от внутреннего состояния: UI-компоненты (загрузка/успех/ошибка), платежные шлюзы (инициализация/подтверждение/отмена) или игровые персонажи (ходьба/прыжок/атака). Классический подход через if/else или switch быстро превращает код в хрупкие конструкции, сложные для расширения. Рассмотрим, как паттерн State решает эту проблему, повышая читаемость и снижая цикломатическую сложность.


Проблема: «Состояние» превращается в лабиринт условий

Представьте компонент файлового загрузчика с состояниями idle, uploading, success, error. Наивная реализация:

typescript
class FileUploader {
  state: string = 'idle';

  handleAction(action: string) {
    if (this.state === 'idle' && action === 'SUBMIT') {
      this.state = 'uploading';
      this.startUpload();
    } else if (this.state === 'uploading' && action === 'SUCCESS') {
      this.state = 'success';
      this.showSuccess();
    } else if (this.state === 'uploading' && action === 'ERROR') {
      this.state = 'error';
      this.showError();
    } else if (this.state === 'success' && action === 'RESET') {
      this.state = 'idle';
      this.reset();
    }
    // ... +20 строк для обработки всех кейсов
  }
}

Проблемы:

  • При добавлении нового состояния (retrying) нужно изменять все методы.
  • Высокая цикломатическая сложность: каждое условие — ветвь логики.
  • Нарушение SRP: один метод отвечает за всю логику переходов.

Решение: Инкапсуляция поведения в классах состояний

Паттерн State предлагает:

  1. Интерфейс состояния с методами для возможных действий.
  2. Конкретные состояния, реализующие поведение для этого состояния.
  3. Контекст (исходный класс), делегирующий вызовы текущему состоянию.

Реализация: От условий — к полиморфизму

Шаг 1. Интерфейс состояния
typescript
interface UploadState {
  handleAction(context: FileUploader, action: string): void;
}
Шаг 2. Конкретные состояния
typescript
class IdleState implements UploadState {
  handleAction(uploader: FileUploader, action: string) {
    if (action === 'SUBMIT') {
      uploader.setState(new UploadingState());
      uploader.startUpload();
    }
  }
}

class UploadingState implements UploadState {
  handleAction(uploader: FileUploader, action: string) {
    if (action === 'SUCCESS') {
      uploader.setState(new SuccessState());
      uploader.showSuccess();
    } else if (action === 'ERROR') {
      uploader.setState(new ErrorState());
      uploader.showError();
    }
  }
}
Шаг 3. Контекст с делегированием
typescript
class FileUploader {
  private state: UploadState = new IdleState();

  setState(state: UploadState) {
    this.state = state;
  }

  handleAction(action: string) {
    this.state.handleAction(this, action);
  }

  // Бизнес-логика вынесена "наружу" для ясности:
  startUpload() { /* ... */ }
  showSuccess() { /* ... */ }
}

Финал: Гибкость и расширяемость

Добавим состояние retrying без переписывания существующей логики:

typescript
class RetryingState implements UploadState {
  handleAction(uploader: FileUploader, action: string) {
    if (action === 'SUBMIT') {
      uploader.startUpload();
    } else if (action === 'SUCCESS') {
      uploader.setState(new SuccessState());
    } else if (action === 'CANCEL') {
      uploader.setState(new IdleState());
    }
  }
}

// В ErrorState добавляем переход в RetryingState:
class ErrorState implements UploadState {
  handleAction(uploader: FileUploader, action: string) {
    if (action === 'RETRY') {
      uploader.setState(new RetryingState());
    }
  }
}

Преимущества:

  • Изоляция логики состояний. При изменении RetryingState не нужно анализировать всю кодовую базу.
  • Упрощенные тесты: каждый класс состояния тестируется независимо.
  • Прозрачность переходов: взаимодействие состояний явно описано в коде.

Когда State не подойдет

  • Простой сценарий. Если состояний 2–3, а переходы тривиальны — if/else может быть проще.
  • Неизменяемые состояния. В функциональных подходах используют конечные автоматы (XState) или алгебраические типы.
  • Высокая частота переходов: паттерн создаёт объекты при каждом изменении, что может повлиять на производительность.

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

Следующие шаги:

  • Поэкспериментируйте с State в кодовой базе, начиная с модуля, где уже есть признаки «состоятельного хаоса».
  • Изучите библиотеки для FSM (конечных автоматов) — они предлагают продвинутые инструменты для сложных сценариев (guard conditions, history и др).
  • Для функционального подхода: адаптируйте паттерн через набор функций-состояний и объект-контекст.

Пример из практики: В системе управления заказами переход с условных операторов (1200 строк) на State сократил количество багов при добавлении статуса «частичная доставка» на 40% — за счёт изоляции логики изменений.