Patrones de Comportamiento (V): Patrón State


Objetivo:

«Permitir que un objeto modifique su comportamiento cuando su estado interno cambie. Parecerá que el objeto cambia de clase».

DesignPatterns: Elements of Reusable Object-Oriented Software

El patrón State tiene la misión fundamental de encapsular el comportamiento de un objeto dependiendo del estado en el que éste se encuentre. ¿A qué nos referimos con estado? Estrictamente hablando, podemos definir el estado de un objeto como el conjunto actual de los valores de los atributos de un objeto. Desde un punto de vista más coloquial, y refiriéndonos al patrón que estamos presentando, podríamos definir un estado como un conjunto de características que harán que el objeto tenga unas características concretas. O hablando en plata: desde el punto de vista de la orientación a objetos, podemos decir que el estado de un vehículo es de velocidad instantánea igual a cero, sin consumo de combustible, a una distancia de entre cero y veinte centímetros de la acera y con el habitáculo vacío. Desde un punto de vista coloquial (que ahora mismo es el que nos interesa), podemos decir que la suma de esos atributos nos informa de que el vehículo se encuentra en estado aparcado.

Supongo que todos conocemos el concepto de «máquina de estados«. Si no es así, lo podemos resumir con aquella máxima de «una imagen vale más que mil palabras»:

Como podemos observar, contamos con una serie de círculos que representan los estados (Apagado, Parado, En Marcha). Cada uno de esos estados se caracteriza por los valores de una serie de atributos (por ejemplo, para que el vehículo se encuentre En Marcha, se debe dar el caso de que el vehículo esté arrancado y con una velocidad distinta de cero). ¿Cómo se producen los cambios de estado? A través de las llamadas transiciones. Estas transiciones podrían compararse con nuestros métodos y funciones, que actúan sobre los valores del objeto haciendo que éste cambie de estado. O no.

Por tanto, tenemos un conjunto limitado de estados (Apagado, Parado, En Marcha) y otro de transiciones (Contacto, Acelerar, Frenar). El factor determinante se encuentra en que dependiendo en qué estado nos encontremos, las transiciones actuarán de forma distinta, realizando ciertas operaciones, transiciones a otros estados… o no realizando absolutamente nada. Como ejemplo, pisar el acelerador con el vehículo apagado no realizará ningún cambio, mientras que si pisamos el acelerador con el vehículo arrancado, el coche se pondrá en movimiento, ganando velocidad (y realizando una transición del estado PARADO al estado EN_MARCHA).

Pongámosle código a nuestro ejemplo:

Un pequeño conjunto de estados

Modelaremos este ejemplo con una clase a la que llamaremos «VehiculoBasico». Su funcionamiento será sencillo: contará con una variable entera que almacenará el estado actual, otra que almacenará la velocidad, y un conjunto de métodos que serán los encargados de realizar las transiciones. ¿Cómo lo controlamos? Pues a priori, no tenemos mucho donde elegir: a través de una serie de sentencias switch o if…else, que comprueben el estado actual y actúen de forma distinta dependiendo del estado en el que nos encontremos. Comencemos con la declaración de la clase, un conjunto de enteros que modelen los posibles estados y sus atributos:

    public class VehiculoBasico
    {
        // Estados
        private const int APAGADO = 0;
        private const int PARADO = 1;
        private const int EN_MARCHA = 2;

        // Atributos
        private const int VELOCIDAD_MAXIMA = 200;
        private int estado = APAGADO;
        private int velocidadActual = 0;

    }

A continuación modelaremos cada uno de los métodos que pueden realizar transiciones. Comenzaremos con el método Contacto:

        public void Contacto()
        {
            switch (estado)
            {
                case APAGADO:
                    {
                        // El vehiculo arranca
                        estado = PARADO;
                        velocidadActual = 0;
                        Console.WriteLine("El vehiculo se encuentra ahora PARADO");
                        break;
                    }
                case PARADO:
                    {
                        // El vehiculo se apaga
                        estado = APAGADO;
                        Console.WriteLine("El vehiculo se encuentra ahora APAGADO");
                        break;
                    }
                case EN_MARCHA:
                    {
                        // No se puede detener el vehiculo en marcha!
                        Console.WriteLine("ERROR: No se puede cortar el contacto en marcha!");
                        break;
                    }
                default:
                    break;
            }
        } 

Como vemos, lo primero que haremos en el método será comprobar el estado actual y/o otros atributos. Acorde a esta información, realizaremos las operaciones oportunas. Seguiremos con el método Acelerar

        public void Acelerar()
        {
            switch (estado)
            {
                case APAGADO:
                    {
                        // Acelerar con el vehiculo apagado no sirve de mucho 🙂
                        Console.WriteLine("ERROR: El vehiculo esta apagado. Efectue el contacto para iniciar");
                        break;
                    }
                case PARADO:
                    {
                        // El vehiculo se pone en marcha. Aumenta la velocidad y cambiamos de estado
                        velocidadActual += 10;
                        estado = EN_MARCHA;
                        Console.WriteLine("El vehiculo se encuentra ahora EN_MARCHA");
                        Console.WriteLine("Velocidad actual: " + velocidadActual);
                        break;
                    }
                case EN_MARCHA:
                    {
                        // Aumentamos la velocidad, permaneciendo en el mismo estado
                        if (velocidadActual >= VELOCIDAD_MAXIMA)
                            Console.WriteLine("ERROR: El coche ha alcanzado su velocidad maxima");
                        else
                            velocidadActual += 10;
                        break;
                    }
                default:
                    break;
            }
        }

Finalmente, codificaremos el último de los métodos: Frenar.

        public void Frenar()
        {
            switch (estado)
            {
                case APAGADO:
                    {
                        // Frenar con el vehiculo parado tampoco sirve de mucho...
                        Console.WriteLine("ERROR: El vehiculo esta apagado. Efectue el contacto para iniciar");
                        break;
                    }
                case PARADO:
                    {
                        // No ocurre nada. Si el vehiculo ya se encuentra detenido, no habra efecto alguno
                        Console.WriteLine("ERROR: El vehiculo ya se encuentra detenido");
                        break;
                    }
                case EN_MARCHA:
                    {
                        // Reducimos la velocidad. Si esta llega a 0, cambiaremos a estado "PARADO"
                        velocidadActual -= 10;
                        if (velocidadActual <= 0)
                        {
                            estado = PARADO;
                            Console.WriteLine("El vehiculo se encuentra ahora PARADO");
                        }
                        Console.WriteLine("Velocidad actual: " + velocidadActual);
                        break;
                    }
                default:
                    break;
            }
        }

Ahora codificaremos un pequeño programita que haga uso de nuestra clase:

            VehiculoBasico vb = new VehiculoBasico();

            vb.Acelerar();
            vb.Contacto();
            vb.Acelerar();
            vb.Acelerar();
            vb.Acelerar();
            vb.Frenar();
            vb.Frenar();
            vb.Frenar();
            vb.Frenar();

El resultado es bastante previsible: al acelerar sin arrancar obtenemos un error. Damos contacto, aceleramos un poco y vamos frenando hasta que el vehículo se detiene. E incluso pisamos el freno estando ya parados, por lo que el vehículo nos informa de ello.

Encapsulando el estado

Todo esto resulta muy sencillo, pero ¿qué ocurriría si, por ejemplo, los requisitos de nuestro programa nos exigieran añadir un nuevo estado? Como podemos imaginar, cada uno de los métodos tendrá que añadir una nueva línea a su switch para contemplar esta posibilidad, amén de que seguramente tengamos que modificar el código ya existente para «encajar» nuestro nuevo estado dentro de la funcionalidad del programa. Por ejemplo, si imaginamos un estado «Sin Gasolina» que indique que nos hemos quedado sin combustible, implicaría:

  • Añadir una nueva constante SIN_COMBUSTIBLE para modelar el estado.
  • Añadir un nuevo case que contemple este estado en cada uno de los métodos existentes (Acelerar, Frenar y Contacto)
  • Modificar los métodos para contemplar que el vehículo no arrancará sin combustible, y que no podrá aumentar su velocidad si nos encontramos en marcha.

¿Cómo evitar este quebradero de cabeza? Siguiendo el principio de encapsular lo que cambia que tan buenos resultados nos ha dado hasta el momento. Y como lo que cambia parecen ser los estados, ¡vamos a encapsularlos!

Lo primero que deberemos hacer será crear una interfaz común que todos los estados deberán implementar. Esta interfaz, por lo tanto, deberá exponer los métodos Acelerar, Frenar y Contacto.

    public interface State
    {
        void Acelerar();
        void Frenar();
        void Contacto();
    }

Lo siguiente que haremos será crear una clase que se denominará clase de contexto, encargada de albergar el estado actual junto al resto de los atributos que se consideren oportunos. En nuestro caso concreto, será el vehículo:

Vehiculo.cs:

    public class Vehiculo
    {
        #region Atributos

        private State estado;                 // Estado actual del vehiculo (apagado, parado, en marcha, sin combustible)
        private int velocidadActual = 0;      // Velocidad actual del vehiculo
        private int combustibleActual = 0;    // Cantidad de combustible restante

        #endregion

        #region Properties

        // Asigna o recupera el estado del vehiculo
        public State Estado
        {
            get { return estado; }
            set { estado = value; }
        }

        // Asigna o recupera la velocidad actual del vehiculo
        public int VelocidadActual
        {
            get { return velocidadActual; }
            set { velocidadActual = value; }
        }

        // Obtiene la cantidad de combustible actual
        public int CombustibleActual
        {
            get { return combustibleActual; }
        }

        #endregion

        #region Constructores

        // El constructor inserta el combustible del que dispondra el vehiculo
        public Vehiculo(int combustible)
        {
            this.combustibleActual = combustible;
            //TODO:
            //Indicar un estado inicial (Apagado)
        }

        #endregion

        #region Metodos relacionados con los estados

        // Los metodos del contexto invocaran el metodo de la interfaz State, delegando las operaciones
        // dependientes del estado en las clases que los implementen.
        public void Acelerar()
        {
            estado.Acelerar();
            Console.WriteLine("Velocidad actual: " + velocidadActual + ". Combustible restante: " + combustibleActual);
        }

        public void Frenar()
        {
            estado.Frenar();
        }

        public void Contacto()
        {
            estado.Contacto();
        }

        #endregion

        #region Otros metodos

        public void ModificarVelocidad(int kmh)
        {
            velocidadActual += kmh;
        }

        public void ModificarCombustible(int decilitros)
        {
            combustibleActual += decilitros;
        }

        #endregion
    }

Volveremos a esta clase dentro de un momento. Antes deberemos implementar la interfaz con una clase por cada estado existente en el sistema. Nuestra idea será crear una clase por cada uno de los estados, que implemente los métodos de la interfaz State. Además, añadiremos un nuevo estado Sin Combustible, que será un estado final (una vez alcanzado, no podrá cambiarse de estado).

State Pattern Class Diagram

La idea de este nuevo modelo es modificar el diagrama anterior para contemplar el estado SinCombustible, de forma similar a la siguiente:

Comencemos por el primero de ellos: ApagadoState.

    public class ApagadoState : State
    {
        // Referencia a la clase de contexto
        private Vehiculo v;

        // Constructor que inyecta la dependencia en la clase actual
        public ApagadoState(Vehiculo v)
        {
            this.v = v;
        }

        public void Acelerar()
        { }

        public void Frenar()
        { }

        public void Contacto()
        { }
    }

Crearemos otras tres clases similares que implementen State: ParadoState, EnMarchaState y SinCombustibleState. Las cuatro clases que implementan los estados tienen como característica común (además de los métodos de la interfaz) que su constructor recibe como parámetro una referencia al contexto, de modo que puedan utilizar sus métodos públicos y, sobre todo, para que puedan realizar la transición de estados inyectando ese objeto en los nuevos objetos que se creen.

Por lo tanto, una vez comprendido esto, procederemos a codificar la funcionalidad de los estados, incluyendo las transiciones:

ApagadoState.cs:

    public class ApagadoState : State
    {
        // Referencia a la clase de contexto
        private Vehiculo v;

        // Constructor que inyecta la dependencia en la clase actual
        public ApagadoState(Vehiculo v)
        {
            this.v = v;
        }

        public void Acelerar()
        {
            // Acelerar con el vehiculo apagado no sirve de mucho 🙂
            Console.WriteLine("ERROR: El vehiculo esta apagado. Efectue el contacto para iniciar");
        }

        public void Frenar()
        {
            // Frenar con el vehiculo parado tampoco sirve de mucho...
            Console.WriteLine("ERROR: El vehiculo esta apagado. Efectue el contacto para iniciar");
        }

        public void Contacto()
        {
            // Comprobamos que el vehiculo disponga de combustible
            if (v.CombustibleActual > 0)
            {
                // El vehiculo arranca -> Cambio de estado
                //estado = PARADO;
                v.Estado = new ParadoState(v);
                Console.WriteLine("El vehiculo se encuentra ahora PARADO");
                v.VelocidadActual = 0;
            }
            else
            {
                // El vehiculo no arranca -> Sin combustible
                //estado = SIN COMBUSTIBLE
                v.Estado = new SinCombustibleState(v);
                Console.WriteLine("El vehiculo se encuentra sin combustible");
            }
        }
    }

Como observamos, cada método es independiente e implementa su propia funcionalidad, incluyendo la transición de estados. Para ello realiza un new pasando como parámetro al constructor la referencia al contexto (el objeto de la clase Vehiculo). El resto de estados seguirán un patrón similar:

ParadoState.cs:

    public class ParadoState :State
    {
        // Referencia a la clase de contexto
        private Vehiculo v;

        // Constructor que inyecta la dependencia en la clase actual
        public ParadoState(Vehiculo v)
        {
            this.v = v;
        }

        public void Acelerar()
        {
            // Comprobamos que el vehiculo disponga de combustible
            if (v.CombustibleActual > 0)
            {
                // El vehiculo se pone en marcha. Aumenta la velocidad y cambiamos de estado
                //estado = EN_MARCHA;
                v.Estado = new EnMarchaState(v);
                Console.WriteLine("El vehiculo se encuentra ahora EN MARCHA");
                v.ModificarVelocidad(10);
                v.ModificarCombustible(-10);
            }
            else
            {
                //estado = SIN COMBUSTIBLE
                v.Estado = new SinCombustibleState(v);
                Console.WriteLine("El vehiculo se encuentra ahora SIN COMBUSTIBLE");
            }
        }

        public void Frenar()
        {
            // No ocurre nada. Si el vehiculo ya se encuentra detenido, no habra efecto alguno
            Console.WriteLine("ERROR: El vehiculo ya se encuentra detenido");
        }

        public void Contacto()
        {
            // El vehiculo se apaga
            // estado = APAGADO;
            v.Estado = new ApagadoState(v);
            Console.WriteLine("El vehiculo se encuentra ahora APAGADO");
        }
    }

EnMarchaState.cs:

    public class EnMarchaState : State
    {
        private const int VELOCIDAD_MAXIMA = 200;

        // Referencia a la clase de contexto
        private Vehiculo v;

        // Constructor que inyecta la dependencia en la clase actual
        public EnMarchaState(Vehiculo v)
        {
            this.v = v;
        }

        public void Acelerar()
        {
            if (v.CombustibleActual > 0)
            {
                // Aumentamos la velocidad, permaneciendo en el mismo estado
                if (v.VelocidadActual >= VELOCIDAD_MAXIMA)
                {
                    Console.WriteLine("ERROR: El coche ha alcanzado su velocidad maxima");
                    v.ModificarCombustible(-10);
                }
                else
                {
                    v.ModificarVelocidad(10);
                    v.ModificarCombustible(-10);
                }
            }
            else
            {
                //estado = SIN COMBUSTIBLE
                v.Estado = new SinCombustibleState(v);
                Console.WriteLine("El vehiculo se ha quedado sin combustible");
            }
        }

        public void Frenar()
        {
            // Reducimos la velocidad. Si esta llega a 0, cambiaremos a estado "PARADO"
            v.ModificarVelocidad(-10);
            if (v.VelocidadActual <= 0)
            {
                //estado = PARADO;
                v.Estado = new ParadoState(v);
                Console.WriteLine("El vehiculo se encuentra ahora PARADO");
            }
        }

        public void Contacto()
        {
            // No se puede detener el vehiculo en marcha!
            Console.WriteLine("ERROR: No se puede cortar el contacto en marcha!");
        }
    }
<

SinCombustibleState.cs:

    public class SinCombustibleState : State
    {
        // Referencia a la clase de contexto
        private Vehiculo v;

        // Constructor que inyecta la dependencia en la clase actual
        public SinCombustibleState(Vehiculo v)
        {
            this.v = v;
        }

        public void Acelerar()
        {
            Console.WriteLine("ERROR: El vehiculo se encuentra sin combustible");
        }

        public void Frenar()
        {
            Console.WriteLine("ERROR: El vehiculo se encuentra sin combustible");
        }

        public void Contacto()
        {
            Console.WriteLine("ERROR: El vehiculo se encuentra sin combustible");
        }
    }

Ahora modificaremos el constructor del contexto para que disponga de un estado inicial (por ejemplo, ApagadoState)

        // El constructor inserta el combustible del que dispondra el vehiculo e instancia el
        // estado inicial (apagado)
        public Vehiculo(int combustible)
        {
            this.combustibleActual = combustible;

            //Indicar un estado inicial (Apagado)
            estado = new ApagadoState(this);
        }

Finalmente, disponemos de un pequeño fragmento de código que haga uso de nuestro patrón:

            Vehiculo v = new Vehiculo(20);

            v.Acelerar();
            v.Contacto();
            v.Acelerar();
            v.Acelerar();
            v.Acelerar();
            v.Frenar();
            v.Frenar();
            v.Frenar();
            v.Frenar();

El resultado de nuestra ejecución será el siguiente:

Resultado

¿Cuándo utilizar este patrón? Ejemplos reales

Se suele decir que los patrones Strategy y State son hermanos que fueron separados al nacer. Esto se debe a que sus razones de ser son muy parecidas: modificar el funcionamiento de un objeto en tiempo de ejecución de forma transparente a través de un proceso de composición. La diferencia fundamental, sin embargo, se encuentra en que el patrón Strategy tiene como objetivo proporcionar alternativas para realizar una misma tarea, aunque ésta sea de alto nivel, es decir: el patrón Strategy es adecuado cuando queremos, por ejemplo, serializar un objeto. Cada una de las implementaciones encapsulará una forma distinta de serialización: binaria, Base64, como un fichero XML, JSON, etc. State, por su parte, tiene su razón de ser en proporcionar comportamientos distintos cuando la aplicación se encuentra en estados distintos. Con este patrón, la clase que implemente nuestro State realizará operaciones distintas dependiendo de su estado, no proporcionará «varias formas de hacer lo mismo». Por ello, Strategy y State son patrones distintos, aunque muy similares: Strategy encapsula el algoritmo, mientras que State encapsulará un comportamiento que variará dependiendo del estado en el que se encuentre el programa.

Por lo tanto, la aplicación de este patrón será adecuada en entornos en los que se identifiquen «estados» con operaciones comunes que varíen su comportamiento dependiendo de en cuál de ellos se encuentren activos. Un buen ejemplo de este comportamiento sería la implementación de un Workflow de un gestor de defectos en el que un elemento de trabajo pueda encontrarse en distintos estados (nuevo, en curso, corregido…) y en el que cada uno de los estados implica un conjunto de posibles operaciones y transiciones que difieren entre sí.

Una posible mejora de este patrón consiste en extraer cualquier tipo de información variable (en este ejemplo, la velocidad y el combustible) y situarla en el contexto o en otra clase externa, permitiendo de este modo modelar cada estado de forma estática o a través de un patrón Singleton. Así, en lugar de instanciar un estado cada vez que se necesite realizar un cambio, bastará con referenciar la instancia ya existente, que únicamente incluirá el comportamiento (el código mostrado en este artículo sería un buen ejemplo en el que podría aplicarse esta alternativa).

Fuentes:

8 comentarios

  1. Excelente explicación. Pero esta implementación se hace casi imposible si el número de «Eventos» de entrada es muy alto y cada estado reacciona a unos pocos. Dado este caso, que es bastante real, la interfaz base se hace intratable y se sobrecarga de manera exagerada cada clase estado. Para esta situación, considero que lo recomendable es NO definir cada método en la interfaz base, sino definir uno (Fire) y con parámetro (o instancia mejor y no usar tipaje) de evento de entrada. Así, cada estado interpreta el evento de entrada y lo trata si llo cree oportuno.
    Repito, esta situación es real, una máquina de estados con un número de estados no muy alto pero sí de eventos que mueve la máquina.

    1. Además, la teoría dice que hay que implementar las «transiciones». Pero eso, lo aseguro, sólo es viable si el grado de dependencias es mínimo. Y desgraciadamente, eso es uno de los grandes quebraderos de cabeza del desarrollo de software.

Deja un comentario