【问题标题】:State Pattern: How the states of an object should transition when they're involved in complex processes?状态模式:当对象涉及复杂过程时,它们的状态应该如何转换?
【发布时间】:2020-08-08 08:59:44
【问题描述】:

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

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

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

【问题讨论】:

    标签: design-patterns state-pattern


    【解决方案1】:

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

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

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

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

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

    关于你的状态和转换的一些 cmet:

    • 状态 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 上添加一个字符串状态属性,并让每个具体的状态类返回其名称。

    【讨论】:

    • 这对我来说似乎是一种明智的做法。一个问题是,作为默认访问的状态类使得为它们编写单元测试变得困难。你是怎么处理的?
    • 单元测试:设置初始状态,运行公共 API 检查行为,确认结果状态符合预期。 (在这里忽略主要作用是促进协作的类。)以此为例,我认为 Order 类是我正在测试的单元的一部分 - 状态类提供的行为是订单的组成部分。所以我将它们作为一个整体进行测试。
    • 状态类本身本质上是从 Order 类中提取一个不能被认为与 Order 类隔离的行为——状态类是 Order 的一个行为,它会得到什么操作后从初始状态到结果状态的顺序。
    【解决方案2】:

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

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

    【讨论】:

    • 所以,如果 setQuantity 是状态特定的操作,数量会保存在当前状态。当订单移动到新状态时,我不仅需要上下文(订单),还需要旧状态来获取数量。那正确吗?还有其他方法可以做到这一点吗?谢谢。
    • 我会说数量是订单本身的一个属性。 OrderState 类封装了特定于状态的行为。您将拥有以下类:Order、OrderStateA、OrderStateB、OrderStateC、OrderStateD。在你的一个。和 c。状态下订单数量可以更改,其他状态下不能。在这些情况下,您可以从 setQuantity() 方法中抛出异常。
    • 这就是我的问题所在。如果在每个 OrderState 中我都有一个 setQuantity 方法,我将在每个状态中存储数据。现在,我可以将数量存储在每个状态(数据)中,或者我可以检查我是否可以在订单中设置数量(来自状态、行为)并引发异常。正确的方法是什么? (对不起,如果我的评论不清楚)
    • 我会将数量存储在订单中,如果调用者想要在不允许的状态下更改数量,则会引发异常。
    【解决方案3】:

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

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

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

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

    【讨论】:

      【解决方案4】:

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

      class Order { setQuantity(q) { _state.setQuantity(q); } } StateA { setQuantity(q) { _order.q = q; } } StateB { setQuantity(q) { throw exception; } }

      【讨论】:

        【解决方案5】:

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

        BaseOrder {
            //  common getters
            // persistence capabilities
        }
        
        NewOrder extends BaseOrder {
            // setters
            CheckingOrder placeOrder();
        } 
        
        CheckingOrder extends BaseOrder {
             CancelledOrder cancel();
             PricingOrder assignSupplier();
        }
        

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

        【讨论】:

        • 所以你不会使用状态模式?您将如何完成所描述的一些示例要求?我看不到图片。谢谢。
        • 状态模式的本质(至少在我阅读en.wikipedia.org/wiki/State_pattern 时)是每个状态都可以使用相同的功能,并且对不同状态使用继承具有完美的多态意义。您还需要特定于每个州的额外行为。将它们添加到这些子类中是有意义的。在我描述的方法中,我没有看到任何违反状态概念的行为。
        • @djna:正如您所描述的,您似乎错过了上下文对象。状态模式的要点是它允许您将活动对象(上下文)从一种状态转换到另一种状态,而无需将所有数据复制到新对象。为此,您将数据放在上下文类中,然后将操作委托给状态类。这允许您在对象创建后更改状态。如果没有上下文类,您将只有一个普通的旧继承层次结构,而不是状态模式。
        • @Martin Brown 是的,你是对的,谢谢。我忙于关注客户的观点,我认为使用单独的状态类是合理的。我展示的伪代码丢失了上下文,从而避免了在状态转换时复制数据。
        猜你喜欢
        • 2011-01-04
        • 2010-09-16
        • 1970-01-01
        • 2013-01-11
        • 2021-12-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-16
        相关资源
        最近更新 更多