Patrón de diseño para "operación en objeto permitido, solo si el objeto está en cierto estado"

8

Por ejemplo:

Solo se pueden actualizar las solicitudes de empleo que aún no están en revisión o aprobadas. En otras palabras, una persona puede actualizar su formulario de dispositivo de trabajo hasta que RR. HH. Comience a revisarlo, o ya sea aceptado.

Entonces, una solicitud de empleo puede estar en 4 estados:

APLICADO (estado inicial), EN REVISIÓN, APROBADO, RECHAZADO

¿Cómo logro tal comportamiento?

Seguramente puedo escribir un método update () en la clase de aplicación, verificar el estado de la aplicación y no hacer nada o lanzar una excepción si la aplicación no está en el estado requerido

Pero este tipo de código no hace obvio que tal regla existe, permite que cualquiera llame al método update (), y solo después de fallar un cliente sabe que tal operación no estaba permitida. Por lo tanto, el cliente debe ser consciente de que tal intento podría fallar, por lo tanto, tenga cuidado. El hecho de que el cliente sea consciente de tales cosas también significa que la lógica se está escapando.

Intenté crear diferentes clases para cada estado (ApprovedApplication, etc.) y puse las operaciones permitidas solo en las clases permitidas, pero este tipo de enfoque también se siente mal.

¿Existe un patrón de diseño oficial, o un simple fragmento de código, para implementar tal comportamiento?

uylmz
fuente
77
Estas cosas generalmente se llaman StateMachines, y su implementación variará un poco dependiendo de sus requisitos y del idioma con el que esté trabajando.
Telastyn
y, ¿cómo se asegura de que los métodos correctos estén disponibles en los estados correctos?
uylmz
1
Depende del idioma. Diferentes clases es una implementación común para los idiomas populares, aunque "lanzar si no está en el estado correcto" es probablemente el más común.
Telastyn
1
¿Dónde está el problema al incluir el método "canUpdate" y verificarlo antes de llamar a Update?
Eufórico
1
this kind of code does not make it obvious such a rule exists- Por eso el código tiene documentación. Los escritores de buen código seguirán los consejos de Euphoric y proporcionarán un método para que el exterior pruebe la regla antes de probar el hardware.
Blrfl

Respuestas:

4

Este tipo de situación aparece con bastante frecuencia. Por ejemplo, los archivos solo se pueden manipular mientras están abiertos, y si intenta hacer algo con un archivo después de que se haya cerrado, obtendrá una excepción de tiempo de ejecución.

Su deseo ( expresado en su pregunta anterior ) de utilizar el sistema de tipos del lenguaje para asegurarse de que no ocurra lo incorrecto es noble, ya que los errores en tiempo de compilación siempre son preferibles a los errores de tiempo de ejecución. Sin embargo, no conozco ningún patrón de diseño para este tipo de situación, probablemente porque terminaría causando más problemas de los que resolvería. (Sería poco práctico).

Lo más cercano a su situación que conozco es modelar diferentes estados de un objeto que corresponden a diferentes capacidades a través de interfaces adicionales, pero de esta manera solo está reduciendo el número de lugares en el código donde puede ocurrir un error de tiempo de ejecución, usted es no erradicando la posibilidad de un error de tiempo de ejecución.

Entonces, en su situación, declararía una serie de interfaces que describen lo que se puede hacer con su objeto en sus diversos estados, y su objeto devolvería una referencia a la interfaz correcta en una transición de estado.

Entonces, por ejemplo, el approve()método de su clase devolvería una ApprovedApplicationinterfaz. La interfaz se implementaría de forma privada (a través de una clase anidada), por lo que el código que solo tiene una referencia a un Applicationno puede invocar ninguno de los ApprovedApplicationmétodos. Luego, el código que manipula una aplicación aprobada declara explícitamente su intención de hacerlo en el momento de la compilación al requerir ApprovedApplicationque trabaje con. Pero, por supuesto, si almacena esta interfaz en algún lugar y luego la utiliza después de decline()que se haya invocado el método, aún obtendrá un error de tiempo de ejecución. No creo que haya una solución perfecta para su problema.

Mike Nakis
fuente
Como nota al margen, ¿debería ser application.approve (someoneWhoCanApprove) o someoneWhoCanApprove.approve (application)? Creo que debería ser el primero, ya que "alguien" puede no tener acceso a los campos de aplicación para hacer los ajustes necesarios
uylmz
No estoy seguro, pero también debe examinar la posibilidad de que ninguno de los dos sea correcto. es decir, if( someone.hasApprovalPermission( application ) ) { application.approve(); } el principio de Separación de Preocupaciones indica que ni a la aplicación, ni a nadie, debe preocuparse por tomar decisiones con respecto a los permisos y la seguridad.
Mike Nakis
3

Asiento con la cabeza en diferentes partes de las diversas respuestas, pero parece que el OP todavía tiene la preocupación del control de flujo. Hay demasiado para tratar de fusionarse en palabras. Solo voy a corregir un código: el patrón de estado.


Nombres de estado como tiempo pasado

"In_Review" no es un estado quizás sino una transición o proceso. De lo contrario, los nombres de sus estados deben ser coherentes: "Solicitud", "Aprobación", "Rechazo", etc. O también "Revisado". O no.

El estado Aplicado realiza una transición de revisión y establece el estado en Revisado. El estado revisado realiza una transición de aprobación y establece el estado en Aprobado (o Rechazado).


// Application class encapsulates state transition,
// the client is unable to directly set state.
public class Application {
    State currentState = null;

    State AppliedState    = new Applied(this);
    State DeclinedState   = new Declined(this);
    State ApprovedState   = new Approved(this);
    State ReviewedState   = new Reviewed(this);

    public class Application (ApplicationDocument myApplication) {
        if(myApplication != null && isComplete()) {
            currentState = AppliedState;
        } else {            
            throw new ArgumentNullException ("Your application is incomplete");
            // some kind of error communication would probably be better
        }
    }

    public apply()    { currentState.apply(); }
    public review()   { currentState.review(); }
    public approve()  { currentState.approve(); }
    public decline()  { currentState.decline(); }


    //These could be done via an enum. I like enums!
    protected void setSubmittingState() {}
    protected void setApproveState() {}
    // etc. ...
}

// could be an interface if we don't have any default or base behavior.
public abstract class State {   
    protected Application theApp;
    // maybe these return an object communicating errors / error state.
    public abstract void apply();
    public abstract void review();
    public abstract void accept();
    public abstract void decline();
}

public class Applied implements State {
    public Applied (Application newApp) {
        if(newApp != null)
            theApp = newApp;
        else
            throw new ArgumentNullException ("null application argument");
     }

    public override void apply() {
        // whatever is appropriate when already in "applied" state
        // do not do any work on behalf of other states!
        // throwing exceptions here is not appropriate, as others
        // have said.
      }

    public override void review() {
        if(recursiveBureaucracyBuckPassing())
            theApp.setReviewedState();
    }

    public override void decline() { // ditto  }
}

public class Reviewed implements State {}
public class Approved implements State {}
public class Declined implements State {}

Editar - Comentarios de manejo de errores

Un comentario reciente:

... si está intentando prestar un libro que ya ha sido emitido a otra persona, el modelo del Libro contendrá la lógica para evitar que cambie su estado. Esto podría ser a través de un valor de retorno (por ejemplo, un booleano exitoso sí / no, o código de estado) o una excepción (por ejemplo, IllegalStateChangeException) o algún otro medio. Independientemente de los medios elegidos, este aspecto no está cubierto como parte de esta (o ninguna) respuesta.

Y de la pregunta original:

Pero este tipo de código no hace obvio que tal regla existe, permite que cualquiera llame al método update (), y solo después de fallar un cliente sabe que tal operación no estaba permitida.

Hay más trabajo de diseño que hacer. No existe Unified Field Theory Pattern. La confusión proviene de suponer que el marco de transición de estado realizará funciones generales de aplicación y manejo de errores. Eso se siente mal porque lo es. La respuesta que se muestra está diseñada para controlar el cambio de estado.


Seguramente puedo escribir un método update () en la clase de aplicación, verificar el estado de la aplicación y no hacer nada o lanzar una excepción si la aplicación no está en el estado requerido

Esto sugiere que hay tres funcionalidades trabajando aquí: el estado, la actualización y la interacción de las dos. En este caso Applicationno es el código que he escrito. Puede usarlo para determinar el estado actual. ApplicationNo es el applicationPaperworktampoco. ApplicationNo es la interacción de los dos, pero podría ser una StateContextEvaluatorclase general . Ahora Applicationorquestará estas interacciones de componentes y luego actuará en consecuencia, como emitir un mensaje de error.

Fin de edición

radarbob
fuente
¿Me estoy perdiendo de algo? Esto parece permitir llamar a los cuatro métodos, independientemente del estado, sin una pista de cómo se utilizará esta configuración para comunicar a los métodos de llamada que la llamada apply () no tuvo éxito debido a que ya se ha aplicado, por ejemplo.
kwah
1
permitir llamar a los cuatro métodos, independientemente del estado Sí. Debería. sin una pista de cómo se utilizará esta configuración para comunicarse con los métodos de llamada. Vea el comentario en el Applicationconstructor donde se produce la excepción. Tal vez la llamada AppliedState.Approve()podría dar como resultado un mensaje de usuario "La solicitud debe revisarse antes de que pueda aprobarse".
radarbob
1
... la llamada a apply () no tuvo éxito debido a que ya se había aplicado, por ejemplo . Eso es un pensamiento equivocado. La llamada es exitosa. Pero hay diferentes comportamientos para diferentes estados. Ese es el patrón de estado ... Sin embargo, el programador debe decidir qué comportamiento es apropiado. Pero está mal pensar que "¡Dios mío, un error! ¡Tenemos que volvernos apopléticos y abortar el programa!" Espero AppliedState.apply()recordarle suavemente al usuario que la solicitud ya se ha enviado y está esperando su revisión. Y el programa continúa.
radarbob
Suponiendo que el patrón de estado se esté utilizando como modelo, el "fallo" debe comunicarse a la interfaz de usuario. Por ejemplo, si está intentando prestar un libro que ya ha sido emitido a otra persona, el modelo del Libro contendrá la lógica para evitar que cambie su estado. Esto puede ser a través de un valor de retorno (por ejemplo, un booleano exitoso sí / no, o código de estado) o una excepción (por ejemplo, IllegalStateChangeException) o algún otro medio. Independientemente de los medios elegidos, este aspecto no está cubierto como parte de esta (o ninguna) respuesta.
kwah
Gracias a Dios que alguien lo dijo. "Necesito un comportamiento diferente en función del estado de un objeto ... Sí, sí. Desea el patrón de estado ". ++ frijol viejo.
RubberDuck
1

En general, lo que está describiendo es un flujo de trabajo. Más específicamente, las funciones comerciales que están encarnadas por estados como REVISADO APROBADO o DECLINADO se encuadran bajo el título de "reglas de negocios" o "lógica de negocios".

Pero para ser claros, las reglas comerciales no deben codificarse en excepciones. Hacerlo sería usar excepciones para el control de flujo del programa, y ​​hay muchas buenas razones por las que no debe hacer eso. Las excepciones deben usarse para condiciones excepcionales, y el estado NO VÁLIDO de una aplicación es completamente excepcional desde el punto de vista comercial.

Use excepciones en los casos en que el programa no pueda recuperarse de una condición de error sin la intervención del usuario ("archivo no encontrado", por ejemplo).

No existe un patrón específico para escribir lógica empresarial, aparte de las técnicas habituales para organizar sistemas de procesamiento de datos comerciales y escribir código para implementar sus procesos. Si las reglas de negocios y el flujo de trabajo son elaborados, considere usar algún tipo de servidor de flujo de trabajo o motor de reglas de negocios.

En cualquier caso, los estados REVISAR, APROBADO, RECHAZADO, etc. se pueden representar mediante una variable privada de tipo enum en su clase. Si usa métodos getter / setter, puede controlar si los setters permitirán cambios o no al examinar primero el valor de la variable enum. Si alguien intenta escribir en un regulador cuando el valor de enumeración está en un estado incorrecto, entonces se puede lanzar una excepción.

Robert Harvey
fuente
Hay un objeto, llamado "Aplicación", sus propiedades solo se pueden cambiar si su "Estado" es "INICIAL". Este no es un gran flujo de trabajo, como documentos que fluyen de un departamento a otro. Lo que no puedo hacer es reflejar este comportamiento en un sentido orientado a objetos.
uylmz
La aplicación @Reek debería exponer la interfaz de lectura / escritura, y la lógica de iteración debería tener lugar en un nivel superior. Tanto el solicitante como el departamento de recursos humanos usan el mismo objeto, pero tienen privilegios diferentes: el objeto de la aplicación no debería preocuparse por ello. Se pueden usar excepciones internas para proteger la integración del sistema, pero no me pondré a la defensiva (la edición de la información de contacto puede ser necesaria incluso para aplicaciones aprobadas, solo necesito un mayor nivel de acceso).
estremecimiento
1

Applicationpodría ser una interfaz y podría tener una implementación para cada uno de los estados. La interfaz podría tener un moveToNextState()método, y esto ocultaría toda la lógica del flujo de trabajo.

Para las necesidades del cliente, también podría haber un método que devuelva directamente lo que puede hacer y no (es decir, un conjunto de valores booleanos), en lugar de solo el estado, para que no necesite una "lista de verificación" en el cliente (supongo el cliente para ser un controlador MVC o UI de todos modos).

Sin embargo, en lugar de lanzar una excepción, simplemente no puede hacer nada y registrar el intento. Esto es seguro en tiempo de ejecución, se aplicaron reglas y el cliente tenía formas de ocultar los controles de "actualización".

piedras grandes
fuente
1

Un enfoque para este problema que ha sido extremadamente exitoso en la naturaleza es hipermedia: la representación del estado de la entidad se acompaña de controles hipermedia que describen los tipos de transiciones que actualmente están permitidas. El consumidor consulta los controles para descubrir qué se puede hacer.

Es una máquina de estado, con una consulta en su interfaz que le permite descubrir qué eventos puede activar.

En otras palabras: estamos describiendo la web (REST).

Otro enfoque es tomar su idea de diferentes interfaces para diferentes estados y proporcionar una consulta que le permita detectar qué interfaces están disponibles actualmente. Piense en IUnknown :: QueryInterface, o en la conversión. El código del cliente juega Mother May I con el estado para averiguar qué está permitido.

Es esencialmente el mismo patrón: solo usar una interfaz para representar los controles hipermedia.

VoiceOfUnreason
fuente
Me gusta esto. Se podría combinar con el patrón de Estado para devolver una colección de Estados válidos a los que se podría hacer la transición. Cadena de mando viene a la mente de alguna manera.
RubberDuck
1
Supongo que no quieres "colección de estados válidos" sino "colección de acciones válidas". Piense en el gráfico: desea el nodo actual (estado) y la lista de aristas (acciones). Descubrirá el siguiente estado cuando elija su acción.
VoiceOfUnreason
Si. Estás en lo correcto. Una colección de acciones válidas donde esa acción es en realidad una transición de estado (o algo que desencadena una).
RubberDuck
1

Aquí hay un ejemplo de cómo puede abordar esto desde una perspectiva funcional, y cómo ayuda a evitar las posibles dificultades. Estoy trabajando en Haskell, lo que supongo que no sabes, así que lo explicaré en detalle a medida que avance.

data Application = Applied ApplicationDetails |
                   InReview ApplicationDetails |
                   Approved ApplicationDetails |
                   Declined ApplicationDetails

Esto define un tipo de datos que puede estar en uno de los cuatro estados que corresponden a los estados de su aplicación. ApplicationDetailsse supone que es un tipo existente que contiene la información detallada.

newtype UpdatableApplication = UpdatableApplication Application

Un alias de tipo que necesita conversión explícita hacia y desde Application. Esto significa que si definimos la siguiente función que acepta y desenvuelve UpdatableApplicationy hace algo útil con ella,

updateApplication :: UpdatableApplication -> ApplicationDetails -> Application
updateApplication (UpdatableApplication app) details = ...

entonces tenemos que convertir explícitamente la Aplicación a una Aplicación Actualizable antes de que podamos usarla. Esto se hace usando esta función:

findUpdatableApplication :: Application -> Maybe UpdatableApplication
findUpdatableApplication app@(Applied _) = Just (UpdatableApplication app)
findUpdatableApplication _               = Nothing

Aquí hacemos tres cosas interesantes:

  • Verificamos el estado de la aplicación (usando la coincidencia de patrones, que es realmente útil para este tipo de código), y
  • si puede actualizarse, lo envolvemos en un UpdatableApplication(que solo involucra una nota de compilación de tipo del cambio de tipo que se agrega, ya que Haskell tiene una característica específica para hacer este tipo de truco de nivel de tipo, no cuesta nada en tiempo de ejecución) y
  • devolvemos el resultado en un "Quizás" (similar a Optionen C # o Optionalen Java; es un objeto que envuelve un resultado que podría faltar).

Ahora, para realmente armar esto, necesitamos llamar a esta función y, si el resultado es exitoso, pasarlo a la función de actualización ...

case findUpdatableApplication application of
    Just updatableApplication -> do
        storeApplicationInDatabase (updateApplication updatableApplication)
        showConfirmationPage
    Nothing -> do
        showErrorPage

Como la updateApplicationfunción necesita el objeto envuelto, no podemos olvidar comprobar las condiciones previas. Y debido a que la función de verificación de precondición devuelve el objeto envuelto dentro de un Maybeobjeto, tampoco podemos olvidar verificar el resultado y responder en consecuencia si falla.

Ahora ... podrías hacer esto en un lenguaje orientado a objetos. Pero es menos conveniente:

  • Ninguno de los lenguajes OO que he probado tiene una sintaxis simple para hacer un tipo de contenedor seguro, por lo que eso es repetitivo.
  • También será menos eficiente, porque al menos para la mayoría de los idiomas no podrán eliminar el tipo de envoltura, ya que será necesario que exista y sea detectable en tiempo de ejecución (Haskell no tiene verificación de tipo de tiempo de ejecución, todas las verificaciones de tipo son realizado en tiempo de compilación).
  • Si bien algunos lenguajes OO tienen tipos equivalentes a Maybeellos, generalmente no tienen una forma tan conveniente de extraer los datos y elegir la ruta a seguir al mismo tiempo. La coincidencia de patrones también es realmente útil aquí.
Jules
fuente
1

Puede usar el patrón «comando» y luego pedirle al Invoker que proporcione una lista de funciones válidas de acuerdo con el estado de la clase de receptor.

Usé lo mismo para proporcionar funcionalidad a las diferentes interfaces que se suponía que iban a llamar a mi código, algunas de las opciones no estaban disponibles dependiendo del estado actual del registro, por lo que mi invocador actualizó la lista y de esa manera cada GUI le preguntó al Invoker qué opciones estaban disponibles y se pintaron en consecuencia.

bns
fuente