Шаблон состояний: как должны меняться состояния объекта, когда он задействован в сложных процессах?

У меня есть некоторые сомнения по поводу следующей реализации паттерна состояний:

У меня есть объект заказа. Для простоты предположим, что у него есть количество, productId, цена и поставщик. Также существует набор известных состояний, в которые порядок может переходить:

  • укажите: заказ новый, количество должно быть ›0 и иметь productId. Цена и поставщик пока не определены.
  • состояние b: кто-то проверяет заказ. Его можно только отменить или назначить поставщика.
  • состояние c: поставщик может указать только цену, взимаемую с клиента.
  • Состояние d: заказ отменен.
  1. Order.isValid () изменяется между состояниями. Т.е. в состоянии a некоторые операции не могут быть выполнены. Итак, они выглядят так:
    void setQuantity (int q) {
    if (_state.canChangeQuantity ()) this.quantity = q;
    else вызывает исключение.
    } < br /> Это правильно, или я должен заставить каждое состояние реализовать операцию setQuantity? В таком случае, где будет храниться значение? В порядке или в состоянии? В последнем случае мне придется копировать данные при каждом переходе между состояниями?

  2. orderProcessor.process (order) - это объект, который проверяет order.IsValid, переводит порядок в какое-то состояние, сохраняет его в базу данных и выполняет некоторые настраиваемые действия (в некоторых состояниях уведомляется администратор, в других - клиент и т. д.). У меня есть по одному для каждого состояния.
    В StateAOrderProcessor человек, который проверяет порядок, получает уведомление по электронной почте, и порядок переводится в состояние b.
    Теперь это выталкивает переходы состояний за пределы Класс заказа. Это означает, что у Order есть метод setState, поэтому каждый процессор может его изменить. Эта штука для изменения состояния извне звучит не очень красиво. Верно?

  3. Другой вариант - переместить всю логику проверки на процессор каждого состояния, но теперь мне нужно отслеживать, когда было изменено количество заказа, чтобы увидеть, действительна ли эта операция в текущем состоянии. Это оставляет мне некоторую анемию.

Что вы думаете, ребята? Можете ли вы дать мне какой-нибудь совет, как лучше спроектировать эту вещь?


person nick2083    schedule 28.08.2009    source источник


Ответы (5)


Это идеальный сценарий для паттерна состояний.

В шаблоне состояния ваши классы состояния должны нести ответственность за переходное состояние, а не только за проверку действительности перехода. Кроме того, перемещение переходов между состояниями за пределы класса порядка не является хорошей идеей и противоречит шаблону, но вы все равно можете работать с классом OrderProcessor.

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

Нет необходимости в таких методах, как canChangeQuantity () и isValid () - классы состояния гарантируют, что экземпляры вашего заказа всегда находятся в допустимом состоянии, потому что любая операция, недопустимая для текущего состояния, сработает, если вы ее попробуете.

Свойства вашего класса Order хранятся вместе с порядком, а не с состоянием. В .Net вы могли бы выполнить эту работу, вложив свои классы состояния в класс Order и предоставив ссылку на порядок при выполнении вызовов - тогда класс состояния будет иметь доступ к закрытым членам заказа. Если вы не работаете в .Net, вам необходимо найти аналогичный механизм для вашего языка - например, классы друзей в C ++.

Несколько комментариев по поводу ваших состояний и переходов:

  • Состояние A отмечает, что заказ новый, количество> 0 и у него есть идентификатор продукта. Для меня это означает, что вы либо предоставляете оба этих значения в конструкторе (чтобы гарантировать, что ваш экземпляр запускается в допустимом состоянии, но вам не понадобится метод setQuantity), либо вам нужно начальное состояние, которое имеет assignProduct (Количество Int32, идентификатор продукта Int32), который перейдет из начального состояния в состояние A.

  • Точно так же вы можете рассмотреть конечное состояние для перехода из состояния C после того, как поставщик указал цену.

  • Если для перехода между состояниями требуется присвоение двух свойств, вы можете рассмотреть возможность использования одного метода, который принимает оба свойства по параметру (а не setQuantity, за которым следует setProductId), чтобы сделать переход явным.

  • Я бы также предложил более описательные названия состояний - например, вместо StateD назовите его CanceledOrder.

Вот пример того, как я бы реализовал этот шаблон на C #, без добавления каких-либо новых состояний:

 public class Order
 {
  private BaseState _currentState;

  public Order(
   Int32 quantity,
   Int32 prodId)
  {
   Quantity = quantity;
   ProductId = prodId;
   _currentState = new StateA();
  }

  public Int32 Quantity
  {
   get; private set;
  }

  public Int32 ProductId
  {
   get; private set;
  }

  public String Supplier
  {
   get; private set;
  }

  public Decimal Price
  {
   get; private set;
  }

  public void CancelOrder()
  {
   _currentState.CancelOrder(this);
  }

  public void AssignSupplier(
   String supplier)
  {
   _currentState.AssignSupplier(this, supplier);
  }

  public virtual void AssignPrice(
   Decimal price)
  {
   _currentState.AssignPrice(this, price);
  }


  abstract class BaseState
  {
   public virtual void CancelOrder(
    Order o)
   {
    throw new NotSupportedException(
     "Invalid operation for order state");
   }

   public virtual void AssignSupplier(
    Order o, 
    String supplier)
   {
    throw new NotSupportedException(
     "Invalid operation for order state");
   }

   public virtual void AssignPrice(
    Order o, 
    Decimal price)
   {
    throw new NotSupportedException(
     "Invalid operation for order state");
   }
  }

  class StateA : BaseState
  {
   public override void CancelOrder(
    Order o)
   {
    o._currentState = new StateD();
   }

   public override void AssignSupplier(
    Order o, 
    String supplier)
   {
    o.Supplier = supplier;
    o._currentState = new StateB();
   }
  }

  class StateB : BaseState
  {
   public virtual void AssignPrice(
    Order o, 
    Decimal price)
   {
    o.Price = price;
    o._currentState = new StateC();
   }
  }

  class StateC : BaseState
  {
  }

  class StateD : BaseState
  {
  }
 }

Вы можете работать со своими классами обработчика заказов, но они работают с общедоступными методами класса заказа и позволяют классам состояния заказа нести всю ответственность за переходное состояние. Если вам нужно знать, в каком состоянии вы находитесь в данный момент (чтобы обработчик заказов мог определять, что делать), вы можете добавить свойство String Status в класс заказа и в BaseState, чтобы каждый конкретный класс состояния возвращал свое имя.

person Remi Despres-Smyth    schedule 11.12.2009
comment
мне это кажется разумным подходом. Единственная проблема заключается в том, что классы состояния, являющиеся доступом по умолчанию, затрудняют написание для них модульных тестов. Как вы к этому подойдете? - person Mike C; 30.01.2015
comment
Модульные тесты: настройте начальное состояние, запустите общедоступный API для проверки поведения, убедитесь, что полученное состояние соответствует ожиданиям. (Игнорируя классы, основная роль которых заключается в содействии сотрудничеству.) Используя это в качестве примера, я считаю, что класс Order является частью модуля, который я тестирую - поведение, обеспечиваемое классами состояний, является неотъемлемой частью порядка. Поэтому я тестирую их как единое целое. - person Remi Despres-Smyth; 19.02.2015
comment
Сам класс состояния по сути извлекает часть поведения из класса Order, которую нельзя считать изолированной от класса Order - класс состояния - это часть поведения Order, это то, что получит порядок из начального состояния в результирующее. состояние после операции. - person Remi Despres-Smyth; 19.02.2015

Изменение объекта текущего состояния может быть выполнено непосредственно из объекта состояния, из Порядка и даже ОК из внешнего источника (процессора), хотя это необычно.

Согласно паттерну State объект Order делегирует все запросы текущему объекту OrderState. Если setQuantity () - это операция, зависящая от состояния (она есть в вашем примере), то каждый объект OrderState должен реализовать ее.

person Karl    schedule 28.08.2009
comment
Таким образом, если setQuantity является операцией, зависящей от состояния, количество будет сохранено в текущем состоянии. Когда Заказ переходит в новое состояние, мне может понадобиться не только Контекст (Заказ), но и старое состояние, чтобы получить количество. Это правильно? Есть ли другие способы сделать это? Спасибо. - person nick2083; 31.08.2009
comment
Я бы сказал, что количество - это атрибут самого Ордена. Классы OrderState инкапсулируют поведение, зависящее от состояния. У вас будут следующие классы: Order, OrderStateA, OrderStateB, OrderStateC, OrderStateD. В вашем файле. и c. заявляет, что количество заказа может быть изменено, в других состояниях не может. В этих случаях вы можете вызвать исключение из метода setQuantity (). - person Karl; 31.08.2009
comment
Вот к чему мой вопрос. Если в каждом OrderState у меня есть метод setQuantity, я буду хранить данные в каждом состоянии правильно. Теперь я мог бы сохранить количество в каждом состоянии (данных) или проверить, могу ли я установить количество в порядке (из состояния, поведения) и вызвать исключение. Какой правильный путь? (извините, если мой комментарий был непонятным) - person nick2083; 02.09.2009
comment
Я бы сохранил количество в Заказе и вызвал бы исключение, если вызывающий абонент хочет изменить количество в состоянии, когда ему это не разрешено. - person Karl; 02.09.2009

Чтобы шаблон состояния работал, объект контекста должен предоставлять интерфейс, который могут использовать классы состояния. Как минимум, это должно включать changeState(State) метод. Боюсь, что это всего лишь одно из ограничений шаблона и возможная причина, по которой он не всегда полезен. Секрет использования паттерна состояний заключается в том, чтобы интерфейс, требуемый состояниями, был как можно меньше и ограничен узкими рамками.

(1) Вероятно, лучше иметь метод canChangeQuantity, чем все состояния реализовать setQuantity. Если в некоторых состояниях выполняется что-то более сложное, чем создание исключения, этому совету может не следовать.

(2) Метод setState неизбежен. Тем не менее, он должен быть максимально ограничен. В Java это, вероятно, будет область действия пакета, в .Net это будет область сборки (внутренняя).

(3) Вопрос о валидации поднимает вопрос о том, когда вы проводите валидацию. В некоторых случаях имеет смысл разрешить клиенту устанавливать для свойств недопустимые значения и проверять их только при выполнении некоторой обработки. В этом случае имеет смысл каждое состояние, имеющее метод isValid (), который проверяет весь контекст. В других случаях вам нужна более немедленная ошибка, и в этом случае я бы создал isQuantityValid(qty) и isPriceValid(price), которые будут вызываться методами set перед изменением значений, если они возвращают false, генерируют исключение. Я всегда называл эти две ранней и поздней проверкой, и нелегко сказать, что вам нужно, не зная больше о том, чем вы занимаетесь.

person Martin Brown    schedule 08.12.2009

Я бы сохранил информацию в классе Order и передал бы указатель на экземпляр Order в состояние. Что-то вроде этого:


class Order {
  setQuantity(q) {
    _state.setQuantity(q);
  } 
}

StateA {
  setQuantity(q) {
    _order.q = q;
  }
}

StateB {
  setQuantity(q) {
    throw exception;
  }
}

person Aurélien Gâteau    schedule 10.12.2009

У вас есть несколько разных классов, по одному на штат.

BaseOrder {
    //  common getters
    // persistence capabilities
}

NewOrder extends BaseOrder {
    // setters
    CheckingOrder placeOrder();
} 

CheckingOrder extends BaseOrder {
     CancelledOrder cancel();
     PricingOrder assignSupplier();
}

и так далее. Идея состоит в том, что код, которому нужны заказы в определенном состоянии, получает только объекты правильного класса, поэтому проверки состояния не требуются. Код, который просто хочет работать с заказами в любом случае состояния, использует BaseClass.

person djna    schedule 28.08.2009
comment
Значит, вы не стали бы использовать паттерн состояний? Как бы вы выполнили некоторые из описанных требований к образцу? Я не вижу изображения. Спасибо. - person nick2083; 28.08.2009
comment
Суть шаблона состояния (по крайней мере, как я читал en.wikipedia.org/wiki/State_pattern) заключается в том, что одни и те же возможности доступны в каждом состоянии, а использование наследования для разных состояний имеет прекрасный полиморфный смысл. Вам также нужно дополнительное поведение, специфичное для каждого состояния. Имеет смысл добавить их к этим подклассам. Я не вижу нарушения концепции государства в описываемом мной подходе. - person djna; 28.08.2009
comment
@djna: Как вы это описываете, вам кажется, что вы упускаете из виду объект контекста. Суть шаблона состояния в том, что он позволяет вам переводить живой объект (контекст) из одного состояния в другое без копирования всех данных в новый объект. Для этого вы помещаете данные в класс контекста, а затем делегируете операции классу состояния. Это позволяет изменять состояние после создания объекта. Без класса контекста у вас будет просто старая простая иерархия наследования, а не шаблон состояния. - person Martin Brown; 08.12.2009
comment
@ Мартин Браун: да, ты прав, спасибо. Я был занят, сосредотачиваясь на представлении клиента, где я считаю разумным использование отдельных классов состояний. Псевдокод в том виде, в каком я его представляю, теряет контекст, что позволяет избежать копирования данных при переходах между состояниями. - person djna; 08.12.2009