Введение
В проектах любого масштаба часто встречаются объекты, меняющие поведение в зависимости от внутреннего состояния: UI-компоненты (загрузка/успех/ошибка), платежные шлюзы (инициализация/подтверждение/отмена) или игровые персонажи (ходьба/прыжок/атака). Классический подход через if/else
или switch
быстро превращает код в хрупкие конструкции, сложные для расширения. Рассмотрим, как паттерн State решает эту проблему, повышая читаемость и снижая цикломатическую сложность.
Проблема: «Состояние» превращается в лабиринт условий
Представьте компонент файлового загрузчика с состояниями idle
, uploading
, success
, error
. Наивная реализация:
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. Интерфейс состояния
interface UploadState {
handleAction(context: FileUploader, action: string): void;
}
Шаг 2. Конкретные состояния
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. Контекст с делегированием
class FileUploader {
private state: UploadState = new IdleState();
setState(state: UploadState) {
this.state = state;
}
handleAction(action: string) {
this.state.handleAction(this, action);
}
// Бизнес-логика вынесена "наружу" для ясности:
startUpload() { /* ... */ }
showSuccess() { /* ... */ }
}
Финал: Гибкость и расширяемость
Добавим состояние retrying
без переписывания существующей логики:
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% — за счёт изоляции логики изменений.