Поведенческие паттерны
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 — это изменение поведения при изменении состояния!