Перейти к содержанию

State (Состояние)

Категория: паттерн поведения.

Проблема

Поведение объекта должно меняться в зависимости от его внутреннего состояния (заказ: новый/оплачен/отгружен/отменён; соединение: открыто/закрыто/переподключение). Наивная реализация - хранить состояние как enum-поле и на каждую операцию писать switch по этому полю:

public void Ship()
{
    switch (_status)
    {
        case OrderStatus.New: /* нельзя */ break;
        case OrderStatus.Paid: _status = OrderStatus.Shipped; break;
        case OrderStatus.Shipped: /* уже отгружен */ break;
        // ...
    }
}

При росте числа состояний и операций такие switch дублируются в каждом методе, разрастаются и их легко забыть обновить согласованно при добавлении нового состояния.

Решение

  1. Для каждого состояния выделяется отдельный класс, реализующий общий интерфейс с одним методом на каждую операцию контекста.
  2. Контекст хранит ссылку на объект текущего состояния (а не просто enum) и делегирует ему вызовы операций.
  3. Само состояние решает, что должно произойти при вызове операции в этом состоянии - в том числе может переключить контекст на другое состояние (context.TransitionTo(new NextState())).
  4. Контекст больше не содержит условной логики "что делать в каком состоянии" - она распределена по классам состояний.

Структура

  • 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.

Пример реализации на C#

State.cs
using System;

namespace DesignPatterns.Behavioral.State
{
    // Контекст хранит ссылку на текущее состояние и делегирует ему операции.
    // Сам контекст не содержит условной логики "если статус такой-то, то...".
    public sealed class OrderContext
    {
        public IOrderState CurrentState { get; private set; }

        public OrderContext()
        {
            CurrentState = new NewOrderState();
        }

        // Позволяет состояниям переключать контекст на следующее состояние
        public void TransitionTo(IOrderState state)
        {
            Console.WriteLine($"Переход: {CurrentState.GetType().Name} -> {state.GetType().Name}");
            CurrentState = state;
        }

        public void Pay() => CurrentState.Pay(this);
        public void Ship() => CurrentState.Ship(this);
        public void Cancel() => CurrentState.Cancel(this);
    }

    // Общий интерфейс состояния - один метод на каждую возможную операцию контекста.
    // Не все операции имеют смысл в каждом состоянии - об этом сообщает сама реализация.
    public interface IOrderState
    {
        void Pay(OrderContext context);
        void Ship(OrderContext context);
        void Cancel(OrderContext context);
    }

    public sealed class NewOrderState : IOrderState
    {
        public void Pay(OrderContext context) => context.TransitionTo(new PaidOrderState());

        public void Ship(OrderContext context) =>
            Console.WriteLine("Нельзя отгрузить неоплаченный заказ");

        public void Cancel(OrderContext context) => context.TransitionTo(new CancelledOrderState());
    }

    public sealed class PaidOrderState : IOrderState
    {
        public void Pay(OrderContext context) =>
            Console.WriteLine("Заказ уже оплачен");

        public void Ship(OrderContext context) => context.TransitionTo(new ShippedOrderState());

        public void Cancel(OrderContext context) => context.TransitionTo(new CancelledOrderState());
    }

    public sealed class ShippedOrderState : IOrderState
    {
        public void Pay(OrderContext context) =>
            Console.WriteLine("Заказ уже отгружен, повторная оплата невозможна");

        public void Ship(OrderContext context) =>
            Console.WriteLine("Заказ уже отгружен");

        public void Cancel(OrderContext context) =>
            Console.WriteLine("Отгруженный заказ отменить нельзя, оформите возврат");
    }

    public sealed class CancelledOrderState : IOrderState
    {
        public void Pay(OrderContext context) =>
            Console.WriteLine("Заказ отменён, оплата невозможна");

        public void Ship(OrderContext context) =>
            Console.WriteLine("Заказ отменён, отгрузка невозможна");

        public void Cancel(OrderContext context) =>
            Console.WriteLine("Заказ уже отменён");
    }

    public static class Demo
    {
        public static void Run()
        {
            var order = new OrderContext();

            order.Ship();  // нельзя - не оплачен
            order.Pay();   // New -> Paid
            order.Ship();  // Paid -> Shipped
            order.Cancel(); // нельзя - уже отгружен
        }
    }
}

Открыть State.cs отдельно Скачать State.cs