Паттерн «Состояние» (State)
Описание
Паттерн «Состояние» — это поведенческий паттерн проектирования, который позволяет объекту изменять своё поведение в зависимости от текущего состояния. Этот паттерн инкапсулирует поведения, соответствующие различным состояниям объекта, в отдельные классы. Такой подход помогает избежать громоздких конструкций if-else и switch, делая код более чистым, гибким и удобным для расширения.
Имеет схожую структуру с паттерном «Стратегия». Построен на принципе «композиции», то есть на делегировании работы другим объектам.
«Стратегия» vs «Состояние»
В паттерне «Стратегия» различные стратегии работают независимо друг от друга и не взаимодействуют между собой.
В паттерне «Состояние» состояния могут менять друг друга, поскольку они связаны с общим объектом и управляют его переходами из одного состояния в другое.
Основные элементы паттерна «Состояние»:
- Контекст (Context) — основной объект, состояние которого может изменяться в зависимости от вызова различных методов. Контекст управляет переключением между состояниями и предоставляет общий интерфейс для пользователя.
- Интерфейс состояния (State) — интерфейс, который определяет общие методы для различных состояний. Эти методы должны быть реализованы во всех конкретных состояниях.
- Конкретные состояния (Concrete State) — классы, которые реализуют конкретное поведение для каждого состояния.
Пример: Банкомат
пример с банкоматом, который может находиться в различных состояниях:
- Ожидание карты (
WaitingCardState): банкомат ожидает, когда пользователь вставит карту. - Вставка карты (
CheckCardState): банкомат проверяет пароль от карты. - Выдача наличных (
WithdrawCashState): банкомат готов к выдаче наличных после успешного ввода пароля.
Каждое состояние инкапсулирует свою логику, и банкомат переключается между ними в зависимости от пользовательских действий.
Интерфейс состояния
Интерфейс State определяет методы для различных действий, которые пользователь может выполнить в банкомате, таких как вставка карты, ввод пароля, снятие наличных и получение статуса (нужен просто для проверки текущего состояния).
interface State {
insertCard(): void;
withdrawCash(sum: number): void;
checkPassword(passwd: string): void;
getStatus(): string;
}Конкретные состояния
Каждое состояние реализует поведение для всех методов, определённых в интерфейсе State. Например:
- Состояние ожидания карты (
WaitingCardState): банкомат ожидает вставки карты. Если пользователь пытается ввести пароль или снять наличные, банкомат уведомляет о необходимости вставить карту.
class WaitingCardState implements State {
constructor(private cashMachine: CashMachine) {}
public withdrawCash(sum: number): void {
console.log('Вставьте карту в банкомат');
}
public insertCard(): void {
console.log('**Банкомат принимает карту**');
this.cashMachine.setState(this.cashMachine.getCheckCardState());
}
public checkPassword(passwd: string): void {
console.log('Вставьте карту в банкомат');
}
public getStatus(): string {
return 'Ожидание карты';
}
}- Состояние вставки карты (
CheckCardState): банкомат ожидает ввода пароля. При успешном вводе банкомат переходит в состояние выдачи наличных, при неудачном — возвращает карту пользователю.
class CheckCardState implements State {
private attemptCount = 0;
private readonly maxAttempts = 3;
constructor(private cashMachine: CashMachine) {}
public withdrawCash(sum: number): void {
console.log('Карта активна. Введите пароль');
}
public insertCard(): void {
console.log('Карта активна. Введите пароль');
}
public checkPassword(passwd: string): void {
if (passwd === '1234') {
console.log('**Введен верный пароль**');
this.cashMachine.setState(this.cashMachine.getWithdrawCashState());
this.attemptCount = 0;
} else {
this.attemptCount++;
console.log(`Неверный пароль. Попытка ${this.attemptCount}/${this.maxAttempts}.`);
if (this.attemptCount >= this.maxAttempts) {
console.log('**Банкомат вернул карту**');
this.cashMachine.setState(this.cashMachine.getWaitingCardState());
this.attemptCount = 0;
}
}
}
public getStatus(): string {
return 'Карта вставлена в банкомат';
}
}- Состояние выдачи наличных (
WithdrawCashState): банкомат позволяет пользователю снять наличные, после чего возвращает карту и переходит в состояние ожидания карты.
class WithdrawCashState implements State {
constructor(private cashMachine: CashMachine) {}
public withdrawCash(sum: number): void {
if (this.cashMachine.cash < sum) {
console.log('В банкомате недостаточно средств!');
} else {
this.cashMachine.cash -= sum;
console.log(`Наличные выданы. В банкомате осталось: ${this.cashMachine.cash} рублей`);
console.log('**Банкомат вернул карту**');
this.cashMachine.setState(this.cashMachine.getWaitingCardState());
}
}
public insertCard(): void {
console.log('Ваша карта уже активна. Введите сумму для снятия');
}
public checkPassword(passwd: string): void {
console.log('**Происходит выдача наличных**');
}
public getStatus(): string {
return 'Выдача наличных';
}
}Контекст (класс CashMachine)
Контекст управляет текущим состоянием и предоставляет методы для вызова действий, которые делегируются текущему состоянию. В зависимости от состояния банкомат выполняет соответствующие действия.
class CashMachine {
private state: State;
private waitingCardState: State;
private CheckCardState: State;
private withdrawCashState: State;
constructor(public cash: number) {
this.waitingCardState = new WaitingCardState(this);
this.CheckCardState = new CheckCardState(this);
this.withdrawCashState = new WithdrawCashState(this);
this.state = this.waitingCardState;
}
public setState(state: State): void {
this.state = state;
}
public withdrawCash(sum: number): void {
this.state.withdrawCash(sum);
}
public insertCard(): void {
this.state.insertCard();
}
public checkPassword(passwd: string): void {
this.state.checkPassword(passwd);
}
public getWithdrawCashState(): State {
return this.withdrawCashState;
}
public getWaitingCardState(): State {
return this.waitingCardState;
}
public getCheckCardState(): State {
return this.CheckCardState;
}
public showStatus(): void {
console.log(`Текущий статус: ${this.state.getStatus()}`);
}
public showAmountOfCash(): void {
console.log(`В банкомате находятся ${this.cash} рублей`);
}
}Преимущества
- Упрощение кода: логика, связанная с различными состояниями, разделена на отдельные классы, что делает код более понятным и лёгким для поддержки.
- Расширяемость: новые состояния можно добавлять без изменения существующего кода контекста, что соответствует принципу открытости/закрытости (OCP).
- Инкапсуляция поведения: каждый класс состояния инкапсулирует логику, что позволяет объекту (банкомату) менять своё поведение в зависимости от состояния, не меняя структуры самого объекта.
Заключение
Паттерн «Состояние» — инструмент для управления поведением объектов с различными состояниями. Он позволяет отделить логику каждого состояния в отдельные классы, упростить код, сделать его гибким и поддерживаемым, что особенно полезно при большом числе взаимосвязанных состояний и сложных переходах между ними.