🧠 State ≠ State Machine: разбираем поведенческий паттерн, который часто путают

🧠 State ≠ State Machine: разбираем поведенческий паттерн, который часто путают

Хотел написать короткий пост о своём любимом паттерне проектирования. Но пока разбирался — понял, что знаю о нём гораздо меньше, чем думал 😅. В итоге короткий пост превратился в полноценный лонгрид с примерами. Буду публиковать его по кусочкам. Рассказываю, что такое паттерн State, когда его использовать, как не перепутать его с State Machine. 👇

State паттерн ("Состояние") — это поведенческий паттерн проектирования, который позволяет объекту изменять своё поведение в зависимости от внутреннего состояния. Многие описывают этот паттерн в контексте паттерна State Machine ("Конечный автомат", "Машина состояний") и не рассматривают в отрыве от этой концепции. На самом деле State самостоятельный паттерн и часто используется независимо.

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

Паттерн State особенно полезен когда:

  • Логика поведения объекта зависит от его различных состояний.
  • Логика содержит множество условных операторов (if/else, switch), зависящих от текущего состояния или внутренних флагов.
  • Хотим убрать связь между состоянием и поведением.
  • Есть множество состояний и они могут меняться со временем.

Примеры:

  • Текстовый редактор с режимами: чтение, редактирование, навигация.
  • Состояния заказа / платежа.
  • Жизненный цикл кредита/займа.
  • Транзакции между счетами.
  • Активность пользовательского аккаунта.

✅ Преимущества, которые мы получим, когда используем State:

  • Более чистый и структурированный код - за счет применения Single Responsibility и Open-Closed принципов ООП.
  • Уменьшаем связанность между компонентами.
  • Расширяемость - добавление нового состояния требует минимального изменения существующих.
  • Инкапсуляция логики.

✍ Пример реализации на Java

Есть несколько вариантов реализации паттерна State, один из них нарушает Liskov Substitution Principle, но тем не менее он представлен на множестве ресурсов. Рассмотрим сначала более правильный вариант:

1. Определяем интерфейс состояния:

public interface EditorState { void handle(TextEditor context); }

2. Конкретные состояния реализуют интерфейс `EditorState`:

public class ReadingState implements EditorState { @Override public void handle(TextEditor context) { System.out.println("You are in Reading mode. View the document."); System.out.println("Switching to Editing mode."); context.setState(new ReadingState()); } } public class EditingState implements EditorState { @Override public void handle(TextEditor context) { System.out.println("You are in Editing mode. Make changes to the text."); System.out.println("Switching to Navigating mode."); context.setState(new NavigatingState()); } } public class NavigatingState implements EditorState { @Override public void handle(TextEditor context) { System.out.println("You are in Navigating mode. Browse through sections."); System.out.println("Switching to Reading mode."); context.setState(new ReadingState()); } }

3. Контекст, который хранит текущее состояние:

public class TextEditor { // Текущее состояние редактора private EditorState currentState; public TextEditor() { this.currentState = new EditingState(); // начальное состояние } public void setState(EditorState state) { this.currentState = state; } public void performAction() { currentState.handle(this); } }

Тестирование:

public class Main public static void main(String[] args) { TextEditor editor = new TextEditor(); // Цикл переключений состояний for (int i = 0; i < 3; i++) { editor.performAction(); System.out.println("----------------------------"); } } }

Результат:

Вы находитесь в режиме редактирования. Вносите изменения в текст. Переход в режим навигации. ---------------------------- Вы находитесь в режиме навигации. Просматривайте разделы. Переход в режим чтения. ---------------------------- Вы находитесь в режиме чтения. Просматривайте документ. Переход в режим редактирования. ----------------------------

А теперь реализация с нарушением LSP принципа. Иногда приходится использовать такое решение, исходя из потребностей системы и бизнес логики:

public class Main { public static interface DoorState { void open(Door context); void close(Door context); void lock(Door context); } // Открытое состояние public static class OpenState implements DoorState { @Override public void open(Door context) { System.out.println("Дверь уже открыта!"); } @Override public void close(Door context) { System.out.println("Дверь закрывается."); context.setState(new ClosedState()); } @Override public void lock(Door context) { System.out.println("Невозможно заблокировать открытую дверь!"); } } // Закрытое состояние public static class ClosedState implements DoorState { @Override public void open(Door context) { System.out.println("Дверь открывается."); context.setState(new OpenState()); } @Override public void close(Door context) { System.out.println("Дверь уже закрыта!"); } @Override public void lock(Door context) { System.out.println("Дверь блокируется."); context.setState(new LockedState()); } } // Заблокированное состояние public static class LockedState implements DoorState { @Override public void open(Door context) { System.out.println("Дверь заблокирована, сначала разблокируйте!"); } @Override public void close(Door context) { System.out.println("Дверь уже закрыта и заблокирована!"); } @Override public void lock(Door context) { System.out.println("Дверь уже заблокирована!"); } } public static class Door { private DoorState currentState; public Door() { this.currentState = new ClosedState(); // Начальное состояние } public void setState(DoorState state) { this.currentState = state; } public void open() { currentState.open(this); } public void close() { currentState.close(this); } public void lock() { currentState.lock(this); } } public static void main(String[] args) { Door door = new Door(); door.open(); // "Дверь открывается." → состояние Open door.close(); // "Дверь закрывается." → состояние Closed door.lock(); // "Дверь блокируется." → состояние Locked door.open(); // "Дверь заблокирована, сначала разблокируйте!" } }

📌 Вывод

Паттерн State — подходящий инструмент для управления состоянием объектов и организации чистого кода. Его стоит использовать, когда требуется более сложная система управления состояниями с поддержкой переходов, валидации и визуализации. Он полезен, когда поведение объекта сложно и зависит от множества состояний. Однако если нужно явно описывать все возможные переходы между состояниями и валидировать их, стоит посмотреть в сторону паттерна State Machine.

Использовали паттерн? Потом через полгода открывая проект, говорили: "Да откуда же это взялось???"

Подпишись на мой канал в telegram

1
Начать дискуссию