Паттерн состояний: как должны переходить состояния объекта, когда он участвует в сложных процессах? - PullRequest
10 голосов
/ 28 августа 2009

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

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

  • заявите: заказ новый, количество должно быть> 0 и должно иметь productId. Цена и поставщик еще не назначены.
  • состояние b: кто-то проверяет заказ. Его можно только отменить или назначить поставщика.
  • состояние c: поставщик может указать только цену, которая будет взиматься с клиента.
  • Состояние d: заказ отменен.

1) Order.isValid () изменяется между состояниями. То есть в состоянии некоторые операции не могут быть выполнены. Итак, они выглядят так:
void setQuantity (int q) {
if (_state.canChangeQuantity ()) this.quantity = q;
иначе выведите исключение.
}
Это правильно, или я должен получить каждое состояние для реализации операции setQuantity? В таком случае, где будет храниться значение? В порядке или состоянии? В последнем случае мне придется копировать данные при каждом переходе между состояниями?

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

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

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

Большое спасибо.

Ник

Ответы [ 5 ]

6 голосов
/ 11 декабря 2009

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

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

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

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

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

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

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

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

  • Если для перехода состояния требуется присвоение двух свойств, вы можете рассмотреть возможность использования одного метода, который принимает оба свойства по параметру (а не setQuantity, за которым следует set 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 и заставить каждый конкретный класс состояний возвращать свое имя.

0 голосов
/ 11 декабря 2009

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


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

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

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

0 голосов
/ 28 августа 2009

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

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

0 голосов
/ 08 декабря 2009

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

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

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

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

0 голосов
/ 28 августа 2009

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

BaseOrder {
    //  common getters
    // persistence capabilities
}

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...