Lapa Knowledge Base
Поведенческие паттерны

State

Паттерн State

State — это поведенческий паттерн проектирования, который позволяет объектам изменять поведение при изменении внутреннего состояния. Объект будет казаться изменившим свой класс.

Проблема

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

  • В состоянии "воспроизведение" кнопка Play ничего не делает, а кнопка Pause ставит трек на паузу
  • В состоянии "пауза" кнопка Play возобновляет воспроизведение, а кнопка Stop останавливает трек
  • В состоянии "остановка" кнопка Play начинает воспроизведение с начала

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

Решение

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


Структура

State
└── handle(context: Context): void

ConcreteStateA
└── handle(context: Context): void

ConcreteStateB
└── handle(context: Context): void

Context
├── state: State
├── setState(state: State): void
└── request(): void

Пример реализации

JavaScript

// Интерфейс состояния
class State {
    handle(context) {
        throw new Error('Метод handle должен быть переопределен');
    }
}

// Контекст - музыкальный плеер
class MusicPlayer {
    constructor() {
        this.state = new StoppedState();
        this.currentTrack = null;
        this.playlist = [];
        this.currentIndex = 0;
    }
    
    setState(state) {
        console.log(`Изменение состояния: ${this.state.constructor.name} -> ${state.constructor.name}`);
        this.state = state;
    }
    
    play() {
        this.state.handle(this);
    }
    
    pause() {
        this.state.handle(this);
    }
    
    stop() {
        this.state.handle(this);
    }
    
    next() {
        this.state.handle(this);
    }
    
    previous() {
        this.state.handle(this);
    }
    
    setPlaylist(tracks) {
        this.playlist = tracks;
        this.currentIndex = 0;
        if (tracks.length > 0) {
            this.currentTrack = tracks[0];
        }
    }
    
    getCurrentTrack() {
        return this.currentTrack;
    }
    
    getCurrentIndex() {
        return this.currentIndex;
    }
}

// Конкретные состояния
class StoppedState extends State {
    handle(context) {
        const methodName = this.getCallerMethod();
        
        switch (methodName) {
            case 'play':
                if (context.getCurrentTrack()) {
                    console.log(`Воспроизведение: ${context.getCurrentTrack()}`);
                    context.setState(new PlayingState());
                } else {
                    console.log('Нет трека для воспроизведения');
                }
                break;
            case 'pause':
                console.log('Плеер остановлен, пауза недоступна');
                break;
            case 'stop':
                console.log('Плеер уже остановлен');
                break;
            case 'next':
                context.nextTrack();
                console.log(`Следующий трек: ${context.getCurrentTrack()}`);
                break;
            case 'previous':
                context.previousTrack();
                console.log(`Предыдущий трек: ${context.getCurrentTrack()}`);
                break;
        }
    }
    
    getCallerMethod() {
        const stack = new Error().stack;
        const caller = stack.split('\n')[3];
        return caller.match(/\.(\w+)/)[1];
    }
}

class PlayingState extends State {
    handle(context) {
        const methodName = this.getCallerMethod();
        
        switch (methodName) {
            case 'play':
                console.log('Трек уже воспроизводится');
                break;
            case 'pause':
                console.log(`Пауза: ${context.getCurrentTrack()}`);
                context.setState(new PausedState());
                break;
            case 'stop':
                console.log(`Остановка: ${context.getCurrentTrack()}`);
                context.setState(new StoppedState());
                break;
            case 'next':
                context.nextTrack();
                console.log(`Следующий трек: ${context.getCurrentTrack()}`);
                break;
            case 'previous':
                context.previousTrack();
                console.log(`Предыдущий трек: ${context.getCurrentTrack()}`);
                break;
        }
    }
    
    getCallerMethod() {
        const stack = new Error().stack;
        const caller = stack.split('\n')[3];
        return caller.match(/\.(\w+)/)[1];
    }
}

class PausedState extends State {
    handle(context) {
        const methodName = this.getCallerMethod();
        
        switch (methodName) {
            case 'play':
                console.log(`Возобновление: ${context.getCurrentTrack()}`);
                context.setState(new PlayingState());
                break;
            case 'pause':
                console.log('Трек уже на паузе');
                break;
            case 'stop':
                console.log(`Остановка: ${context.getCurrentTrack()}`);
                context.setState(new StoppedState());
                break;
            case 'next':
                context.nextTrack();
                console.log(`Следующий трек: ${context.getCurrentTrack()}`);
                break;
            case 'previous':
                context.previousTrack();
                console.log(`Предыдущий трек: ${context.getCurrentTrack()}`);
                break;
        }
    }
    
    getCallerMethod() {
        const stack = new Error().stack;
        const caller = stack.split('\n')[3];
        return caller.match(/\.(\w+)/)[1];
    }
}

// Дополнительные методы для плеера
MusicPlayer.prototype.nextTrack = function() {
    if (this.playlist.length > 0) {
        this.currentIndex = (this.currentIndex + 1) % this.playlist.length;
        this.currentTrack = this.playlist[this.currentIndex];
    }
};

MusicPlayer.prototype.previousTrack = function() {
    if (this.playlist.length > 0) {
        this.currentIndex = this.currentIndex === 0 ? this.playlist.length - 1 : this.currentIndex - 1;
        this.currentTrack = this.playlist[this.currentIndex];
    }
};

// Использование
const player = new MusicPlayer();

// Устанавливаем плейлист
player.setPlaylist(['Song 1', 'Song 2', 'Song 3']);

console.log('=== Тестирование музыкального плеера ===');

// Тестируем различные действия
player.play();    // Начинаем воспроизведение
player.pause();   // Ставим на паузу
player.play();    // Возобновляем
player.next();    // Следующий трек
player.stop();    // Останавливаем
player.play();    // Начинаем заново
player.previous(); // Предыдущий трек

TypeScript с интерфейсами

// Интерфейс состояния
interface State {
    handle(context: Context): void;
}

// Интерфейс контекста
interface Context {
    setState(state: State): void;
    request(): void;
}

// Контекст - торговый автомат
class VendingMachine implements Context {
    private state: State;
    private items: Map<string, number> = new Map();
    private selectedItem: string | null = null;
    private insertedMoney: number = 0;
    
    constructor() {
        this.state = new IdleState();
        this.items.set('Coke', 2.50);
        this.items.set('Pepsi', 2.50);
        this.items.set('Water', 1.00);
    }
    
    setState(state: State): void {
        console.log(`Состояние изменено: ${this.state.constructor.name} -> ${state.constructor.name}`);
        this.state = state;
    }
    
    request(): void {
        this.state.handle(this);
    }
    
    selectItem(item: string): void {
        this.selectedItem = item;
        this.request();
    }
    
    insertMoney(amount: number): void {
        this.insertedMoney += amount;
        this.request();
    }
    
    getSelectedItem(): string | null {
        return this.selectedItem;
    }
    
    getInsertedMoney(): number {
        return this.insertedMoney;
    }
    
    getItemPrice(item: string): number {
        return this.items.get(item) || 0;
    }
    
    dispenseItem(): void {
        if (this.selectedItem) {
            console.log(`Выдан товар: ${this.selectedItem}`);
            this.selectedItem = null;
            this.insertedMoney = 0;
        }
    }
    
    returnMoney(): void {
        console.log(`Возвращено денег: $${this.insertedMoney.toFixed(2)}`);
        this.insertedMoney = 0;
    }
}

// Конкретные состояния
class IdleState implements State {
    handle(context: VendingMachine): void {
        console.log('Автомат готов к работе. Выберите товар.');
    }
}

class ItemSelectedState implements State {
    handle(context: VendingMachine): void {
        const selectedItem = context.getSelectedItem();
        const price = context.getItemPrice(selectedItem!);
        
        console.log(`Выбран товар: ${selectedItem}, цена: $${price.toFixed(2)}`);
        console.log('Вставьте деньги для покупки.');
    }
}

class MoneyInsertedState implements State {
    handle(context: VendingMachine): void {
        const selectedItem = context.getSelectedItem();
        const price = context.getItemPrice(selectedItem!);
        const insertedMoney = context.getInsertedMoney();
        
        if (insertedMoney >= price) {
            console.log(`Достаточно денег! Выдаем товар.`);
            context.dispenseItem();
            context.setState(new IdleState());
        } else {
            const remaining = price - insertedMoney;
            console.log(`Недостаточно денег. Нужно еще: $${remaining.toFixed(2)}`);
        }
    }
}

// Использование
const vendingMachine = new VendingMachine();

console.log('=== Тестирование торгового автомата ===');

// Тестируем различные сценарии
vendingMachine.request(); // Начальное состояние

vendingMachine.selectItem('Coke'); // Выбираем товар
vendingMachine.insertMoney(1.00); // Вставляем недостаточно денег
vendingMachine.insertMoney(1.50); // Добавляем еще денег

console.log('\n--- Новый товар ---');
vendingMachine.selectItem('Water'); // Выбираем другой товар
vendingMachine.insertMoney(1.00); // Вставляем достаточно денег

Когда использовать

Используйте State, когда:

  • Объект ведет себя по-разному в зависимости от состояния
  • Много условных операторов, зависящих от состояния объекта
  • Состояния и переходы между ними четко определены
  • Нужно избежать дублирования кода для разных состояний

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

  • Принцип единственной ответственности: каждое состояние в отдельном классе
  • Принцип открытости/закрытости: легко добавлять новые состояния
  • Устраняет условные операторы для состояний
  • Делает переходы между состояниями явными

Недостатки

  • Может быть избыточным для объектов с небольшим количеством состояний
  • Увеличивает количество классов
  • Может усложнить код, если состояния редко изменяются

Отличия от других паттернов

  • Strategy — выбирает алгоритм во время выполнения
  • Command — инкапсулирует запросы как объекты
  • Template Method — определяет скелет алгоритма

Реальный пример

// State для системы заказов
class OrderState {
    handle(order) {
        throw new Error('Метод handle должен быть переопределен');
    }
}

class PendingState extends OrderState {
    handle(order) {
        console.log('Заказ ожидает подтверждения');
        // Логика для ожидающего заказа
    }
    
    confirm(order) {
        console.log('Заказ подтвержден');
        order.setState(new ConfirmedState());
    }
    
    cancel(order) {
        console.log('Заказ отменен');
        order.setState(new CancelledState());
    }
}

class ConfirmedState extends OrderState {
    handle(order) {
        console.log('Заказ подтвержден, готовится к отправке');
    }
    
    ship(order) {
        console.log('Заказ отправлен');
        order.setState(new ShippedState());
    }
    
    cancel(order) {
        console.log('Заказ отменен');
        order.setState(new CancelledState());
    }
}

class ShippedState extends OrderState {
    handle(order) {
        console.log('Заказ в пути');
    }
    
    deliver(order) {
        console.log('Заказ доставлен');
        order.setState(new DeliveredState());
    }
}

class DeliveredState extends OrderState {
    handle(order) {
        console.log('Заказ доставлен');
    }
    
    return(order) {
        console.log('Заказ возвращен');
        order.setState(new ReturnedState());
    }
}

class CancelledState extends OrderState {
    handle(order) {
        console.log('Заказ отменен');
    }
}

class ReturnedState extends OrderState {
    handle(order) {
        console.log('Заказ возвращен');
    }
}

class Order {
    constructor(id, items) {
        this.id = id;
        this.items = items;
        this.state = new PendingState();
    }
    
    setState(state) {
        this.state = state;
    }
    
    getStatus() {
        return this.state.constructor.name;
    }
    
    // Делегируем методы состоянию
    confirm() {
        if (this.state.confirm) {
            this.state.confirm(this);
        }
    }
    
    ship() {
        if (this.state.ship) {
            this.state.ship(this);
        }
    }
    
    deliver() {
        if (this.state.deliver) {
            this.state.deliver(this);
        }
    }
    
    cancel() {
        if (this.state.cancel) {
            this.state.cancel(this);
        }
    }
    
    return() {
        if (this.state.return) {
            this.state.return(this);
        }
    }
}

// Использование
const order = new Order('ORD-001', ['Laptop', 'Mouse']);

console.log(`Статус заказа: ${order.getStatus()}`);
order.confirm();
console.log(`Статус заказа: ${order.getStatus()}`);
order.ship();
console.log(`Статус заказа: ${order.getStatus()}`);
order.deliver();
console.log(`Статус заказа: ${order.getStatus()}`);

State — это изменение поведения при изменении состояния!

Copyright © 2026