10

我对状态模式的以下实现有一些疑问:

我有一个 Order 对象。为简单起见,假设它有数量、productId、价格和供应商。此外,还有一组已知状态可以转换顺序:

  • 状态 a:订单是新的,数量必须 > 0 并且必须有 productId。价格和供应商尚未指定。
  • 状态b:有人检查订单。只能取消,或指定供应商。
  • 状态c:供应商只能填写要向客户收取的价格。
  • 状态d:订单被取消。
  1. Order.isValid() 状态之间的变化。即,在状态 a 中某些操作无法完成。因此,它们看起来像:
    void setQuantity(int q) {
    if (_state.canChangeQuantity()) this.quantity = q;
    否则抛出异常。
    这是对的,还是我应该让每个状态都实现setQuantity
    操作?在这种情况下,值将存储在哪里?在顺序还是状态?在后一种情况下,我将不得不在每个状态转换中复制数据?

  2. orderProcessor.process(order) 是一个检查 order.IsValid 的对象,将订单转换为某种状态,将其保存到数据库并执行一些自定义操作(在某些状态下,通知管理员,在其他状态下通知客户端等)。每个州我都有一个。
    在 StateAOrderProcessor 中,检查订单的人会通过电子邮件收到通知,并且订单会转换到状态 b。
    现在,这将状态转换推到了 Order 类之外。这意味着 Order 有一个“setState”方法,因此每个处理器都可以更改它。从外部改变状态的这个东西听起来不太好。那正确吗?

  3. 另一种选择是将所有验证逻辑移动到每个状态的处理器,但现在我必须跟踪订单数量何时更改,以查看该操作在当前状态下是否有效。 这让我觉得订单有点乏味。

你们觉得怎么样?你能给我一些建议来更好地设计这个东西吗?

4

5 回答 5

6

这是状态模式的理想场景。

在状态模式中,您的状态类应该负责转换状态,而不仅仅是检查转换的有效性。此外,将状态转换推送到订单类之外不是一个好主意,并且违反了模式,但您仍然可以使用 OrderProcessor 类。

您应该让每个状态类实现 setQuantity 操作。状态类应该实现所有可能在某些状态下有效但在其他状态下无效的方法,无论它是否涉及状态更改。

不需要诸如 canChangeQuantity() 和 isValid() 之类的方法 - 状态类确保您的订单实例始终处于有效状态,因为如果您尝试,任何对当前状态无效的操作都会抛出。

您的 Order 类的属性与订单一起存储,而不是与状态一起存储。在 .Net 中,您可以通过将状态类嵌套在 Order 类中并在进行调用时提供对订单的引用来完成这项工作 - 然后状态类将有权访问订单的私有成员。如果您不在.Net 中工作,则需要为您的语言找到类似的机制——例如,C++ 中的友元类。

关于您的状态和转换的一些评论:

  • 状态 A 指出该订单是新订单,数量 >0 并且具有产品 ID。对我来说,这意味着您要么在构造函数中提供这两个值(以确保您的实例以有效状态开始,但您不需要 setQuantity 方法),或者您需要一个具有 assignProduct 的初始状态(Int32 quantity, 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
  {
  }
 }

您可以使用订单处理器类,但它们使用订单类上的公共方法,并让订单的状态类负责转换状态。如果您需要知道您当前处于什么状态(以允许订单处理器确定要做什么),您可以在订单类和 BaseState 上添加一个字符串状态属性,并让每个具体的状态类返回其名称。

于 2009-12-11T02:40:06.743 回答
0

为了使状态模式起作用,上下文对象必须公开状态类可以使用的接口。至少,这必须包括一个changeState(State)方法。恐怕这只是模式的限制之一,也是它并不总是有用的一个可能原因。使用状态模式的秘诀是保持状态所需的接口尽可能小,并限制在一个紧凑的范围内。

(1) 拥有一个canChangeQuantity方法可能比让所有状态都实现一个更好setQuantity。如果某些州正在做一些比抛出异常更复杂的事情,则可能不会遵循此建议。

(2)setState方法不可避免。然而,它应该尽可能地保持在范围内。在 Java 中,这可能是 Package 范围,在 .Net 中,它可能是 Assembly(内部)范围。

(3) 关于验证的观点提出了你何时进行验证的问题。在某些情况下,允许客户端将属性设置为无效值并仅在您进行某些处理时验证它们是明智的。在这种情况下,每个状态都有一个验证整个上下文的“isValid()”方法是有意义的。在其他情况下,您想要一个更直接的错误,在这种情况下,我将创建一个isQuantityValid(qty)并且isPriceValid(price)在更改值之前由 set 方法调用,如果它们返回 false 则抛出异常。我一直将这两个称为 Early Validation 和 Late Validation,如果不知道更多关于你在做什么,就很难说出你需要哪个。

于 2009-12-08T12:55:24.280 回答
0

更改当前状态对象可以直接从状态对象、订单甚至从外部源(处理器)中完成,尽管不常见。

根据状态模式,Order 对象将所有请求委托给当前的 OrderState 对象。如果 setQuantity() 是特定于状态的操作(在您的示例中),则每个 OrderState 对象都应实现它。

于 2009-08-28T14:39:09.800 回答
0

我会将信息存储在 Order 类中,并将指向 Order 实例的指针传递给状态。像这样的东西:


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

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

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

于 2009-12-10T22:29:37.117 回答
-1

你有几个不同的班级,每个州一个。

BaseOrder {
    //  common getters
    // persistence capabilities
}

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

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

等等。这个想法是在特定状态下需要命令的代码只获取正确类的对象,因此不需要状态检查。只想在任何状态下对订单进行操作的代码使用 BaseClass。

于 2009-08-28T04:42:30.090 回答