State (Состояние)¶
Категория: паттерн поведения.
Проблема¶
Поведение объекта должно меняться в зависимости от его внутреннего состояния (заказ: новый/оплачен/отгружен/отменён; соединение: открыто/закрыто/переподключение). Наивная реализация - хранить состояние как enum-поле и на каждую операцию писать switch по этому полю:
public void Ship()
{
switch (_status)
{
case OrderStatus.New: /* нельзя */ break;
case OrderStatus.Paid: _status = OrderStatus.Shipped; break;
case OrderStatus.Shipped: /* уже отгружен */ break;
// ...
}
}
При росте числа состояний и операций такие switch дублируются в каждом методе, разрастаются и их легко забыть обновить согласованно при добавлении нового состояния.
Решение¶
- Для каждого состояния выделяется отдельный класс, реализующий общий интерфейс с одним методом на каждую операцию контекста.
- Контекст хранит ссылку на объект текущего состояния (а не просто enum) и делегирует ему вызовы операций.
- Само состояние решает, что должно произойти при вызове операции в этом состоянии - в том числе может переключить контекст на другое состояние (
context.TransitionTo(new NextState())). - Контекст больше не содержит условной логики "что делать в каком состоянии" - она распределена по классам состояний.
Структура¶
Context- хранит ссылку на текущее состояние, делегирует ему операции, предоставляет способ смены состояния.State- общий интерфейс операций, зависящих от состояния.ConcreteState- реализует поведение операций для конкретного состояния, включая переходы в другие состояния.
Когда применять¶
- Поведение объекта существенно отличается в зависимости от значения одного поля-состояния, и это отражается в большом количестве условных операторов по всему классу.
- Список возможных состояний и переходов между ними достаточно стабилен и хорошо определён (конечный автомат).
- Нужно явно запретить некоторые операции в определённых состояниях, а не просто по-разному их выполнять.
Плюсы¶
- Устраняет разрастающиеся
switch/ifпо состоянию, разнося логику по отдельным, тестируемым в изоляции классам. - Добавление нового состояния - это новый класс, а не правка
switchв каждом методе (при условии, что не забыт метод в интерфейсе). - Переходы между состояниями становятся явными и локализованными в коде самих состояний.
Минусы¶
- Больше классов, чем при простом enum + switch - оправдано, когда состояний и операций действительно много и логика в каждом состоянии нетривиальна.
- Логика переходов оказывается "размазанной" по классам состояний - чтобы понять полную диаграмму переходов, нужно просмотреть все классы, а не одну таблицу/switch.
Отличие от Strategy¶
Технически State и Strategy устроены почти одинаково (контекст хранит ссылку на объект-стратегию/состояние и делегирует ему поведение). Разница - в намерении: - Strategy - реализации независимы друг от друга, клиент сам выбирает нужную стратегию снаружи, и они обычно не переключаются друг в друга. - State - конкретные состояния знают друг о друге и сами управляют переходами между собой в ответ на операции контекста; выбор следующего состояния - часть логики паттерна, а не решение внешнего кода.
Пример в .NET Framework / BCL¶
System.Net.Sockets.Socket/TcpClient- внутреннее поведение существенно зависит от состояния соединения (не подключен/подключается/подключен/закрывается).Taskи его состояния (Created,Running,RanToCompletion,Faulted,Canceled) - поведение операций над задачей (Wait,GetAwaiter) зависит от того, в каком она состоянии.- Конечные автоматы бизнес-процессов (workflow-движки, обработка заявок, платёжные статусы) - типичная предметная область для State.