2009-08-28 5 views
9

Tengo algunas dudas sobre la siguiente implementación del patrón de estado:Patrón de estado: ¿Cómo deberían pasar los estados de un objeto cuando están involucrados en procesos complejos?

Tengo un objeto Order. Para simplificar, supongamos que tiene una cantidad, idProducto, precio y proveedor. Además, hay un conjunto de estados conocidos en los que la orden puede realizar la transición:

  • estado a: el pedido es nuevo, la cantidad debe ser> 0 y debe tener idProducto. El precio y el proveedor aún no están asignados.
  • estado b: alguien verifica la orden. Solo se puede cancelar o se puede asignar el proveedor.
  • estado c: el proveedor solo puede completar el precio que se cobrará al cliente.
  • Estado d: la orden se cancela.

1) Order.isValid() cambia entre estados. Es decir, en estado algunas operaciones no se pueden hacer. Por lo tanto, se ven como:
void setQuantity (int q) {
if (_state.canChangeQuantity()) this.quantity = q;
else arroja una excepción.
}
¿Es correcto o debo hacer que cada estado implemente la operación setQuantity? En ese caso, ¿dónde se almacenará el valor? En el orden, o el estado? En este último caso, tendré que copiar los datos en cada transición de estado.

2) orderProcessor.process (orden) es un objeto que comprueba order.IsValid, transiciona la orden a algún estado, la guarda en la base de datos y realiza algunas acciones personalizadas (en algunos estados, se notifica al administrador, en otros el cliente, etc.). Tengo uno para cada estado.
En StateAOrderProcessor, la persona que verifica el pedido recibe una notificación por correo electrónico y el pedido pasa al estado b.
Ahora, esto empuja las transiciones de estado fuera de la clase de orden. Eso significa que Order tiene un método 'setState', por lo que cada procesador puede cambiarlo. Esto de cambiar el estado desde el exterior no suena bien. ¿Tiene razón?

3) La opción Atherher es mover toda la lógica de validación al procesador de cada estado, pero ahora tengo que rastrear cuándo se cambió la cantidad de una orden para ver si esa operación es válida en el estado actual. Eso me deja el orden algo anémico.

¿Qué opinas chicos? ¿Puede darme algunos consejos para diseñar mejor esto?

Muchas gracias.

Nick

Respuesta

-1

Tiene varias clases diferentes, una por estado.

BaseOrder { 
    // common getters 
    // persistence capabilities 
} 

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

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

y así sucesivamente. La idea es que el código que necesita pedidos en un estado particular solo obtiene objetos de la clase correcta, por lo que no se necesitan verificaciones de estado. El código que solo quiere operar en pedidos en cualquier caso de estado utiliza la BaseClass.

+0

¿Entonces no usaría el patrón de estado? ¿Cómo cumpliría algunos de los requisitos de muestra descritos? No puedo ver la foto. Gracias. – nick2083

+0

La esencia del patrón de estado (al menos cuando leo http://en.wikipedia.org/wiki/State_pattern) es que las mismas capacidades están disponibles en cada estado, y el uso de la herencia para los diferentes estados hace que la polimorfa perfecta sentido. También desea comportamientos adicionales específicos para cada estado. Tiene sentido agregarlos a esas subclases. No veo ninguna violación del concepto de estado en el enfoque que describo. – djna

+0

@djna: Al describirlo, parece que se está perdiendo el objeto de contexto. El objetivo del patrón de estado es que le permite hacer la transición de un objeto vivo (el contexto) de un estado a otro sin copiar todos los datos a un nuevo objeto. Para hacer esto, coloca los datos en la clase de contexto y luego delegue las operaciones en una clase de estado. Esto le permite cambiar el estado después de la creación del objeto. Sin la clase de contexto, solo tiene una jerarquía de herencia antigua simple en lugar del patrón de estado. –

0

El cambio del objeto de estado actual se puede realizar directamente desde un objeto de estado, desde el pedido e incluso desde una fuente externa (procesador), aunque es inusual.

Según el patrón de estado, el objeto Order delega todas las solicitudes al objeto OrderState actual. Si setQuantity() es una operación específica del estado (está en su ejemplo), cada objeto OrderState debe implementarlo.

+0

Por lo tanto, si setQuantity es una operación específica de estado, la cantidad se guardará en el estado actual. Cuando la orden pase a un nuevo estado, no solo necesitaré el contexto (orden) sino también el estado anterior para obtener la cantidad. ¿Está bien? ¿Hay otras maneras de hacer esto? Thankx. – nick2083

+0

Diría que la cantidad es un atributo del pedido en sí. Las clases OrderState encapsulan el comportamiento específico del estado. Tendrá las siguientes clases: Order, OrderStateA, OrderStateB, OrderStateC, OrderStateD. En su a. y C. indica que la cantidad de la orden se puede cambiar, en los otros estados no se puede. Puede lanzar una excepción del método setQuantity() en estos casos. – Karl

+0

Ahí va mi pregunta. Si en cada OrderState tengo un método setQuantity, estaré almacenando datos en cada estado correcto. Ahora, podría almacenar la cantidad en cada estado (datos) o podría verificar si puedo establecer la cantidad en el orden (desde el estado, el comportamiento) y generar una excepción. ¿Cuál es la forma correcta de ir? (lo siento si mi comentario no estaba claro) – nick2083

0

Para que el patrón de estado funcione, el objeto de contexto debe exponer una interfaz que las clases de estado pueden usar. Como mínimo, esto deberá incluir un método changeState(State). Me temo que esto es solo una de las limitaciones del patrón y es una posible razón por la cual no siempre es útil. El secreto para usar el patrón de estado es mantener la interfaz requerida por los estados lo más pequeña posible y restringida a un ámbito estricto.

(1) Tener un método canChangeQuantity es probablemente mejor que tener todos los estados implementando un setQuantity. Si algunos estados están haciendo algo más complejo que lanzar una excepción, este consejo puede no seguir.

(2) El método setState es inevitable. Sin embargo, debería mantenerse lo más cerrado posible. En Java, este sería probablemente el alcance del paquete, en .Net sería el alcance de la Asamblea (interna).

(3) El punto acerca de la validación plantea la cuestión de cuándo se realiza la validación. En algunos casos, es sensato permitir que el cliente establezca propiedades a valores no válidos y solo validarlas cuando se procesa. En este caso, cada estado que tenga un método 'isValid()' que valide todo el contexto tiene sentido. En otros casos, desea un error más inmediato, en cuyo caso crearía un isQuantityValid(qty) y isPriceValid(price) que llamarían los métodos establecidos antes de cambiar los valores, si devuelven falso arrojar una excepción. Siempre he llamado a estas dos validación temprana y tardía y no es fácil decir lo que necesita sin saber más acerca de lo que está haciendo.

0

Almacenaría información en la clase de orden y pasaría un puntero a la instancia de pedido al estado. Algo como esto:

 

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

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

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

Este es un escenario ideal para el patrón de estado.

En el patrón de estado, sus clases de estado deberían ser responsables de la transición de estado, no solo de verificar la validez de la transición. Además, presionar transiciones de estado fuera de la clase de orden no es una buena idea y va en contra del patrón, pero aún puede trabajar con una clase OrderProcessor.

Debe obtener cada clase de estado para implementar la operación setQuantity. La clase de estado debe implementar todos los métodos que pueden ser válidos en algunos estados pero no en otros, ya sea que implique o no un cambio de estado.

No hay necesidad de métodos como canChangeQuantity() e isValid(): las clases de estado garantizan que las instancias de orden estén siempre en un estado válido, ya que cualquier operación que no sea válida para el estado actual se lanzará eso.

Las propiedades de la clase de orden se almacenan con el orden, no con el estado. En .Net, usted haría que esto funcione anidando sus clases de estado dentro de la clase Order y proporcionando una referencia al pedido al hacer llamadas: la clase de estado tendrá acceso a los miembros privados de la orden. Si no está trabajando en .Net, necesita encontrar un mecanismo similar para su idioma, por ejemplo, clases de amigos en C++.

algunos comentarios sobre sus estados y transiciones:

  • Estado A señala que la orden es nueva, la cantidad es> 0 y tiene un identificador de producto. Para mí, esto significa que estás proporcionando ambos valores en el constructor (para asegurarte de que tu instancia comience en un estado válido, pero no necesitarías un método setQuantity), o necesitas un estado inicial que tenga un assignProduct (Int32 cantidad, Int32 productId) método que pasará del estado inicial al estado A.

  • De forma similar, es posible que desee considerar un estado final para pasar del estado C, una vez que el proveedor haya completado el precio.

  • Si su transición de estado requiere la asignación de dos propiedades, puede considerar utilizar un único método que acepte ambas propiedades por parámetro (y no setQuantity seguido por set setProductId), para hacer explícita la transición.

  • También sugeriría nombres de estado más descriptivos; por ejemplo, en lugar de StateD, llámelo CanceledOrder.

Aquí hay un ejemplo de cómo iba a implementar este patrón en C#, sin añadir nuevos estados:

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 
    { 
    } 
} 

Puede trabajar con sus clases de procesador de orden, pero con las que trabajan los métodos públicos en la clase de orden y deje que las clases de estado de la orden tengan toda la responsabilidad del estado de transición. Si necesita saber en qué estado se encuentra actualmente (para permitir que el procesador de órdenes determine qué hacer), puede agregar una propiedad de Estado de cadena en la clase de orden y en BaseState, y hacer que cada clase de estado concreta devuelva su nombre.

+0

esto parece una aproximación sensata para mí. El único problema es que las clases de estado como acceso predeterminado dificultan la escritura de pruebas unitarias para ellas. ¿Cómo te acercas a eso? –

+0

Pruebas unitarias: configure el estado inicial, ejecute la API pública para verificar el comportamiento, confirme que el estado resultante sea el esperado. (Ignorando las clases cuyo papel principal es facilitar la colaboración aquí.) Usando esto como ejemplo, considero que la clase de Orden es parte de la unidad que estoy probando - el comportamiento proporcionado por las clases de estado es parte integral de la orden. Entonces los pruebo como una sola unidad. –

+0

La clase de estado en sí misma está extrayendo un comportamiento de la clase Order que no puede considerarse aislado de la clase Order; la clase de estado es una parte del comportamiento de Order, es lo que hará que la orden entre su estado inicial y el estado resultante después de una operación. –

Cuestiones relacionadas