Patrones de Comportamiento (III): Template Method


Objetivo:

«Permitir que ciertos pasos de un algoritmo definidos en un método de una clase sean redefinidos en sus clases derivadas sin necesidad de sobrecargar la operación entera».

Design Patterns: Elements of Reusable Object-Oriented Software

Si el patrón Command nos permite encapsular una invocación a un método, el patrón Template Method o Método Modelo establece una forma de encapsular algoritmos. Este patrón se basa en un principio muy sencillo: si un algoritmo puede aplicarse a varios supuestos en los que únicamente cambie un pequeño número de operaciones, la idea será utilizar una clase para modelarlo a través de sus operaciones. Esta clase base se encargará de definir los pasos comunes del algoritmo, mientras que las clases que hereden de ella implementarán los detalles propios de cada caso concreto, es decir, el código específico para cada caso.

El procedimiento es sencillo:

  • Se declara una clase abstracta, que será la plantilla o modelo. Esta clase definirá una serie de funciones y métodos. Aquellas que sean comunes estarán implementadas. Aquellas que dependan de cada caso concreto, se declararán como abstractas, obligando a las clases hijas a implementarlas.
  • Cada clase derivada implementará los métodos específicos, acudiendo a la clase base para ejecutar el código común.
  • La clase base también se encargará de la lógica del algoritmo, ejecutando los pasos en un orden preestablecido (las clases hijas no deberían poder modificar el algoritmo, únicamente definir la funcionalidad específica que tienen que implementar).

Dado que la clase padre es la que se encarga de llamar los métodos de las clases derivadas (los pasos del algoritmo estarán implementado en la clase base), se trata de una aplicación manifiesta del principio de inversión de dependencias: la clase base no tiene por qué saber nada acerca de sus hijas, pero aún así, se encargará de invocar su funcionalidad cuando sea necesario. El principio de Hollywood («no nos llames, nosotros te llamaremos») vuelve a entrar en escena.

Implementando el patrón

En anteriores artículos hemos hecho uso de este patrón sin ser conscientes de ello al definir el funcionamiento de un motor de cuatro tiempos. Veíamos que el funcionamiento de un motor de gasolina se basaba en el siguiente algoritmo:

  • Admisión: el descenso del pistón crea un vacío que aspira la mezcla de aire y combustible de la válvula de admisión. La válvula de escape permanece cerrada.
  • Compresión: una vez que el pistón ha bajado hasta el final, se cierra la válvula de admisión. El pistón asciende, comprimiendo la mezcla y aumentando la presión.
  • Explosión: el pistón alcanza la parte superior y la bujía produce una chispa que hace explotar la mezcla de aire y combustible, haciendo que el pistón vuelva a descender.
  • Escape: la válvula de escape se abre. El pistón asciende nuevamente, empujando los gases resultantes de la explosión y comenzando un nuevo ciclo.

Ahora veamos cómo funciona, a grandes rasgos, un motor diesel de cuatro tiempos:

  • Admisión: el descenso del pistón crea un vacío que aspira aire desde la válvula de admisión. La válvula de escape permanece cerrada.
  • Compresión: una vez que el pistón ha bajado hasta el final, se cierra la válvula de admisión. El pistón asciende, comprimiendo el aire y aumentando la presión.
  • Combustión: los inyectores pulverizan el combustible, haciendo que la presión se encargue de aumentar la temperatura, haciendo que se produzca la combustión y la expansión de los gases que fuerzan el descenso del pistón.
  • Escape: la válvula de escape se abre. El pistón asciende nuevamente, empujando los gases resultantes de la explosión y comenzando un nuevo ciclo.

Tal y como observamos, ambos motores tienen un funcionamiento muy similar. Las fases 2 y 4 (compresión y escape) son idénticas, la fase 1 (admisión) varía ligeramente, mientras que la fase 3 (explosión en el motor de gasolina, combustión en el motor diesel) tiene un comportamiento diferente. ¿Cómo encaja aquí el patrón Template Method?

Dado que el algoritmo para realizar el ciclo del motor tiene los mismos pasos efectuados en el mismo orden, es el contexto adecuado para utilizar este patrón. Podemos crear una superclase Motor que implemente las fases comunes a ambos motores (Compresión, Escape) más el algoritmo RealizarFaseMotor, que será el encargado de invocar los métodos en un orden fijo. Esta ejecución será invariable, por lo que las clases derivadas únicamente podrán (y deberán) implementar las partes específicas de cada motor.

Dado que la fase Admisión varía ligeramente (inyección de mezcla de combustible y gas en el motor de gasolina frente a inyección de gas en el motor diesel), es posible implementar la parte común en el propio método de la superclase y encapsular únicamente la parte que varía en otro método que será invocado desde Compresión. Así respetaremos otro de los principios de la orientación a objetos: encapsular aquello que es susceptible de cambiar.

Comencemos creando la clase abstracta Motor. Si bien en otros casos hemos hablado de utilizar una interfaz o una clase abstracta para implementar una abstracción, en este caso será necesario utilizar una clase abstracta, no una interfaz. ¿Por qué? Porque si utilizamos una interfaz únicamente podremos definir la firma de los métodos a utilizar, pero no podremos codificar una funcionalidad común, que es lo que pretende este patrón. Por lo tanto, Motor será una clase abstracta que implementará de forma explícita la parte común y declarará como abstractos los métodos que las clases derivadas estarán obligadas a implementar. Comenzaremos declarando unas cuantas variables: dos para modelar el estado de las válvulas y otras dos para modelar el ángulo actual del cigüeñal y del árbol de levas. También crearemos un método que «reinicie» el ángulo de estos dos elementos, manteniéndolos siempre entre 0 y 359 grados:

    public abstract class Motor
    {
        // Estado de las válvulas
        private bool valvulaAdmisionAbierta = false;
        private bool valvulaEscapeAbierta = false;

        // Ángulos del cigueñal y del árbol de levas
        protected int anguloCiguenal = 0;
        protected int anguloArbolLevas = 0;


        // Método que mantendrá el ángulo entre 0 y 359 grados
        protected int SumarAngulo(int anguloActual, int cantidad)
        {
            if (anguloActual + cantidad >= 360)
                return anguloActual + cantidad - 360;
            else
                return anguloActual + cantidad;
        }

    }

A continuación modelaremos los dos métodos que implementará de forma explícita la clase base: compresión y escape:

        // Segunda Fase: Compresión
        protected void Compresion()
        {
            Console.WriteLine("COMENZANDO FASE DE COMPRESION");

            // Se cierra la válvula de admisión
            valvulaAdmisionAbierta = false;

            // Giros del cigueñal y del árbol de levas
            anguloCiguenal = SumarAngulo(anguloCiguenal, 360);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 180);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);

            Console.WriteLine("Valvula de admision abierta: " + valvulaAdmisionAbierta);
            Console.WriteLine("Valvula de escape abierta: " + valvulaEscapeAbierta + "\n");
        }
        // Cuarta Fase: Escape
        protected void Escape()
        {
            Console.WriteLine("COMENZANDO FASE DE ESCAPE");

            // Se abre la válvula de escape
            valvulaEscapeAbierta = true;

            // Giros del cigueñal y del árbol de levas
            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);

            Console.WriteLine("Gases expulsados. Fin de ciclo");
        }

    }

Hemos dicho que la tercera fase depende exclusivamente de las clases derivadas. Por lo tanto, nuestra clase motor se limitará a declarar el método como abstracto y obligar a las clases derivadas a implementar la funcionalidad específica de cada motor:

        // Tercera Fase: Consumo del combustible. Dado que depende del motor concreto,
        // este método será abstracto y deberá ser implementado por la clase derivada.
        protected abstract void ConsumirCombustible();

Nos queda la primera fase: admisión. Hemos dicho que parte de la funcionalidad es común (bajada del pistón, apertura de la válvula de admisión), pero otra parte depende de si el motor es de gasolina (inyección de combustible y aire) o diesel (inyección de aire). Por tanto, creamos otro método abstracto que las clases hija tendrán que codificar (BajarPiston()) y se invoca desde el método Admision(), que codificará la parte común:

        // La bajada del pistón depende del motor concreto, por lo que deberá ser implementada
        // por la clase hija.
        protected abstract void BajarPiston();

        // Primera Fase: Admisión
        protected void Admision()
        {
            Console.WriteLine("COMENZANDO FASE DE ADMISION");

            // Se abre la válvula de admisión y se cierra la válvula de escape
            valvulaAdmisionAbierta = true;
            valvulaEscapeAbierta = false;

            // Se baja el pistón. Esta operación será distinta en el motor diesel (que
            // inyectará aire) o gasolina (que inyectará una mezcla de aire y combustible)
            BajarPiston();

            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);

            Console.WriteLine("Valvula de admision abierta: " + valvulaAdmisionAbierta);
            Console.WriteLine("Valvula de escape abierta: " + valvulaEscapeAbierta + "\n");
        } 

Finalmente, codificamos el propio algoritmo, de carácter público, que se encargará de invocar todos los pasos en un orden determinado:

        // Método público que ejecutará el algoritmo completo
        public void RealizarFaseMotor()
        {
            Admision();             // Parcialmente implementado en la clase base
            Compresion();           // Implementado en la clase base
            ConsumirCombustible();  // Delegado en las clases hijas
            Escape();               // Implementado en la clase base
        }

La clase completa, por lo tanto, tendrá el siguiente aspecto:

Motor.cs

    public abstract class Motor
    {
        // Estado de las válvulas
        private bool valvulaAdmisionAbierta = false;
        private bool valvulaEscapeAbierta = false;

        // Ángulos del cigueñal y del árbol de levas
        protected int anguloCiguenal = 0;
        protected int anguloArbolLevas = 0;

        // Método público que ejecutará el algoritmo completo
        public void RealizarFaseMotor()
        {
            Admision();             // Parcialmente implementado en la clase base
            Compresion();           // Implementado en la clase base
            ConsumirCombustible();  // Delegado en las clases hijas
            Escape();               // Implementado en la clase base
        }

        // Primera Fase: Admisión
        protected void Admision()
        {
            Console.WriteLine("COMENZANDO FASE DE ADMISION");

            // Se abre la válvula de admisión y se cierra la válvula de escape
            valvulaAdmisionAbierta = true;
            valvulaEscapeAbierta = false;

            // Se baja el pistón. Esta operación será distinta en el motor diesel (que
            // inyectará aire) o gasolina (que inyectará una mezcla de aire y combustible)
            BajarPiston();

            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);

            Console.WriteLine("Valvula de admision abierta: " + valvulaAdmisionAbierta);
            Console.WriteLine("Valvula de escape abierta: " + valvulaEscapeAbierta + "\n");
        }

        // La bajada del pistón depende del motor concreto, por lo que deberá ser implementada
        // por la clase hija.
        protected abstract void BajarPiston();

        // Segunda Fase: Compresión
        protected void Compresion()
        {
            Console.WriteLine("COMENZANDO FASE DE COMPRESION");

            // Se cierra la válvula de admisión
            valvulaAdmisionAbierta = false;

            // Giros del cigueñal y del árbol de levas
            anguloCiguenal = SumarAngulo(anguloCiguenal, 360);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 180);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);

            Console.WriteLine("Valvula de admision abierta: " + valvulaAdmisionAbierta);
            Console.WriteLine("Valvula de escape abierta: " + valvulaEscapeAbierta + "\n");
        }

        // Tercera Fase: Consumo del combustible. Dado que depende del motor concreto,
        // este método será abstracto y deberá ser implementado por la clase derivada.
        protected abstract void ConsumirCombustible();

        // Cuarta Fase: Escape
        protected void Escape()
        {
            Console.WriteLine("COMENZANDO FASE DE ESCAPE");

            // Se abre la válvula de escape
            valvulaEscapeAbierta = true;

            // Giros del cigueñal y del árbol de levas
            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);

            Console.WriteLine("Gases expulsados. Fin de ciclo");
        }

        // Método que mantendrá el ángulo entre 0 y 359 grados
        protected int SumarAngulo(int anguloActual, int cantidad)
        {
            if (anguloActual + cantidad >= 360)
                return anguloActual + cantidad - 360;
            else
                return anguloActual + cantidad;
        }
    }

Una vez que tenemos la clase padre, implementaremos las clases derivadas: MotorGasolina y MotorDiesel. Únicamente tendrán que implementar las partes específicas, es decir, los métodos BajarPiston() y ConsumirCombustible():

MotorGasolina.cs

    public class MotorGasolina : Motor
    {
        protected override void BajarPiston()
        {
            Console.WriteLine("Inyectando aire y combustible en el motor");
        }

        public override void ConsumirCombustible()
        {
            Console.WriteLine("COMENZANDO FASE DE EXPLOSIÓN");

            Console.WriteLine("Iniciando chispa en la bujía");
            Console.WriteLine("La explosión provoca el movimiento del pistón");

            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas + "\n");  
        }
    }

MotorDiesel.cs:

    public class MotorDiesel : Motor
    {
        protected override void BajarPiston()
        {
            Console.WriteLine("Inyectando aire en el motor");
        }

        protected override void ConsumirCombustible()
        {
            Console.WriteLine("COMENZANDO FASE DE COMBUSTIÓN");

            Console.WriteLine("Inyectando combustible pulverizado en el motor");
            Console.WriteLine("La presión provoca el movimiento del pistón");

            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas + "\n");            
        }
    }

Tal y como hemos dicho, las dos diferencias entre ambos radican en la compresión (inyección de mezcla en el motor gasolina por inyección de aire en el motor diesel) y en el consumo de combustible (explosión en el motor gasolina por combustión en el motor diesel). Salvando los detalles concretos que hemos codificado, el algoritmo del motor es exactamente el mismo, y viene determinado por el método público codificado en la clase base RealizarFaseMotor(). De hecho, si usamos el método para comprobar el funcionamiento de ambos motores, veremos el resultado:

            Motor mGasolina = new MotorGasolina();
            Motor mDiesel = new MotorDiesel();

            mGasolina.RealizarFaseMotor();
            Console.WriteLine("----------------------------------------------------");
            mDiesel.RealizarFaseMotor();

Algoritmos con gancho

Lo que hemos visto hasta ahora es sencillo de entender y de implementar, pero probablemente nos hayamos dado cuenta de un pequeño detalle: es demasiado rígido. La idea de mantener encapsulado e inmutable el cuerpo del algoritmo evita efectos indeseados, pero limita muchísimo la posibilidad de añadir pasos intermedios. Por ejemplo, ¿qué ocurriría si nuestro motor fuera turbo? El turbo realiza una compresión del aire antes de introducirlo en la cámara de explosión, proporcionando una mayor potencia sin modificar la cilindrada del motor. Sin embargo, este paso estaría (que me disculpen los profesionales del motor por lo que voy a decir) justo antes de la admisión. Sin embargo… habrá motores que implementen turbo y que no lo implementen. ¿Creamos por tanto una clase MotorTurboGasolina y MotorTurboDiesel? ¿Hacemos también abstracto el método Admisiónpara que este funcionamiento pueda personalizarse en las clases hijas? No es necesario. Para este tipo de situaciones, el patrón proporciona lo que se conocen como Hooks, métodos de enganche o ganchos.

La filosofía de estos métodos es sencilla: se implementan en la clase base con un comportamiento por defecto, que puede ser sobrecargado en las clases derivadas o ejecutado como está. De este modo, se proporcionan pasos opcionales, haciendo que las clases derivadas enganchen funcionalidad opcional en ciertos puntos del algoritmo. Estos hooks suelen utilizarse normalmente con cláusulas condicionales, de forma que se ejecuten en ciertas condiciones concretas.

En nuestro caso, añadiríamos un nuevo paso a nuestro algoritmo, al que llamaríamos ComprimirTurbo(), que implementaríamos en la clase con un comportamiento por defecto:

        protected virtual void ComprimirTurbo()
        {
            Console.WriteLine("Turbo no presente");
        }
        // Método público que ejecutará el algoritmo completo
        public void RealizarFaseMotor()
        {
            ComprimirTurbo();       // Hook (método opcional)
            Admision();             // Parcialmente implementado en la clase base
            Compresion();           // Implementado en la clase base
            ConsumirCombustible();  // Delegado en las clases hijas
            Escape();               // Implementado en la clase base
        }

Al ser declarado como virtual, el método puede ser sobrecargado en las clases derivadas. Así, modificaremos nuestro motor diesel para que permita instanciar un motor turbo de la siguiente manera:

    public class MotorDiesel : Motor
    {
        private bool turbo = false;

        public MotorDiesel(bool turbo)
        {
            this.turbo = turbo;
        }

        protected override void ComprimirTurbo()
        {
            // Si el coche es turbo, ejecutará su propio código. En caso contrario, efectuará
            // la operación por defecto
            if (turbo)
                Console.WriteLine("Comprimiendo aire en el turbo antes de la admisión");
            else
                base.ComprimirTurbo();
        }

        protected override void BajarPiston()
        {
            Console.WriteLine("Inyectando aire en el motor");
        }

        protected override void ConsumirCombustible()
        {
            Console.WriteLine("COMENZANDO FASE DE COMBUSTIÓN");

            Console.WriteLine("Inyectando combustible pulverizado en el motor");
            Console.WriteLine("La presión provoca el movimiento del pistón");

            anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
            anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);

            Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
            Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas + "\n");            
        }
    }

El motor gasolina no será modificado, de modo que ejecutará el método por defecto (no se «enganchará» al método ComprimirTurbo). Si ahora modificamos nuestro programa para instanciar un motor diesel turbo de la siguiente manera:

            Motor mGasolina = new MotorGasolina();
            Motor mDiesel = new MotorDiesel(true);

            mGasolina.RealizarFaseMotor();
            Console.WriteLine("----------------------------------------------------");
            mDiesel.RealizarFaseMotor();

Veremos que el motor diesel hace uso del gancho para añadir su propia funcionalidad, mientras que el motor gasolina ha delegado en la clase base el funcionamiento por defecto:

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

Este patrón es aconsejable en los siguientes supuestos:

  • Cuando se cuenta con un algoritmo aplicable a varias situaciones, cuya implementación difiere únicamente en algunos pasos.
  • Arquitecturas donde los pasos de un proceso estén definidos (el qué), pero sea necesario establecer los detalles sobre cómo realizarlos (el cómo).
  • Módulos en los que exista una gran cantidad de código duplicado que pueda ser factorizado en pasos comunes.

Uno de los ejemplos más claros del uso de este patrón se da en los Servlets de Java. En ellos se produce una invocación secuencial de los métodos init() (al instanciar el Servlet) y service() (al realizar una petición) que, a partir del tipo de petición, ejecutarán el método correspondiente a su tipo, como doGet(), doPost()… Estos métodos son los que el programador deberá implementar para darles una funcionalidad concreta.

Otro ejemplo podría ser la ejecución de una transacción en base de datos: los pasos estarán bien definidos (apertura de la conexión, ejecución de la consulta, procesamiento de los datos, compromiso de la operación y cierre de la conexión), pero la implementación de éstos podría variar (por ejemplo, dependiendo de la base de datos a la cual se pretenda conectar).

Fuentes:

6 comentarios

  1. Hola Daniel,

    Me ha venido sensacional este patrón porque precísamente he visto que era idóneo para implementarlo en una tarea que tengo asignada en la actualidad.

    Esperando la siguiente entrega. 🙂

    Muchas gracias.

  2. Saludos desde (Lleida) España.
    Muchas gracias por tu blog. Grande!!!!
    Da gusto encontrar a gente que comparte sus conocimientos.
    Nuevamente muchas gracias.

Deja un comentario