状态模式:当一个对象参与复杂过程时,它们的状态应如何转换?

时间:2009-08-28 04:27:26

标签: design-patterns state

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

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

  • 陈述a:订单是新的,数量必须是> 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)Ahother选项是将所有验证逻辑移动到每个州的处理器,但现在我必须跟踪订单数量的变化,以查看该操作在当前状态下是否有效。 这让我的订单变得贫血。

你觉得伙计们怎么样?你能给我一些建议来更好地设计这件事吗?

非常感谢。

尼克

5 个答案:

答案 0 :(得分:6)

这是状态模式的理想情况。

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

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

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

您的订单类的属性与订单一起存储,而不是状态。在.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上添加String Status属性,并让每个具体的状态类返回其名称。

答案 1 :(得分:0)

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

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

答案 2 :(得分:0)

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

(1)使用canChangeQuantity方法可能比让所有州实现setQuantity更好。如果某些州正在做一些比抛出异常更复杂的事情,那么这个建议可能不会出现。

(2)setState方法是不可避免的。但是,它应尽可能保持严格的范围。在Java中,这可能是Package范围,在.Net中它将是Assembly(内部)范围。

(3)关于验证的观点提出了何时进行验证的问题。在某些情况下,允许客户端将属性设置为无效值并仅在执行某些处理时验证它们是明智的。在这种情况下,具有验证整个上下文的'isValid()'方法的每个状态都是有意义的。在其他情况下,您需要更直接的错误,在这种情况下,我会创建一个isQuantityValid(qty)isPriceValid(price),在更改值之前将由set方法调用,如果它们返回false则抛出异常。我总是称这两个早期和晚期验证,如果不了解更多关于你的要求,就不容易说出你需要的东西。

答案 3 :(得分:0)

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


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

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

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

答案 4 :(得分:-1)

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

BaseOrder {
    //  common getters
    // persistence capabilities
}

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

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

等等。我们的想法是,在特定状态下需要订单的代码只能获取正确类的对象,因此不需要进行状态检查。只想在任何州情况下对订单进行操作的代码都使用BaseClass。