Strategy pattern: un patrón de diseño de software para estrategias variables de comportamiento

En la programación orientada a objetos, los design patterns, o patrones de diseño, asisten los desarrolladores con estrategias y plantillas de soluciones cuya eficacia ha sido probada. Una vez encontrado el esquema de resolución apropiado, todo lo que queda es hacer ajustes individuales. Actualmente, hay un total de 70 patrones de diseño adaptados a áreas específicas de aplicación. Los patrones strategy se centran en el comportamiento del software.

¿En qué consiste el strategy pattern?

El strategy pattern pertenece a los llamados behavioural patterns o patrones de comportamiento, que equipan un software con diferentes métodos de resolución. Estas estrategias consisten en una familia de algoritmos que están separados del programa real y son autónomos, es decir intercambiables. Un strategy pattern también incluye algunas pautas y ayudas para los desarrolladores. Por ejemplo, los strategy patterns describen cómo construir u organizar un grupo de clases y crear objetos. Una característica especial del patrón strategy es que un comportamiento variable de programas y objetos también se puede realizar en el tiempo de ejecución de un software.

¿Cómo es la representación UML de un strategy pattern?

Los strategy patterns se diseñan generalmente con el lenguaje de modelado gráfico UML (Unified Modeling Language). Sirven para visualizar los patrones de diseño con una notación estandarizada y utilizan caracteres y símbolos especiales. El UML establece distintos tipos de diagramas para la programación orientada a objetos. Para representar un strategy pattern, se suelen utilizar los llamados diagramas de clase con al menos tres componentes básicos:

  • Context (contexto o clase de contexto)
  • Strategy (estrategia o clase de estrategia)
  • ConcreteStrategy (estrategia concreta)

Con el strategy pattern, los componentes básicos asumen funciones especiales. Los patrones de comportamiento de la clase Context se almacenan en diferentes clases Strategy. Estas clases separadas contienen los algoritmos llamados ConcreteStrategies. Utilizando una referencia interna, el contexto puede acceder a las variables de computación externalizadas (ConcreteStrategyA, ConcreteStrategyB, etc.) si lo necesita. No interactúa directamente con los algoritmos, sino con una interfaz.

La interfaz Strategy encapsula las variantes de cálculo y puede ser implementada simultáneamente por todos los algoritmos. Para la interacción con Context, la interfaz genérica proporciona solo un método para activar los algoritmos de ConcreteStrategy. La interacción con Context incluye, además del acceso a Strategy, intercambio de datos. La interfaz Strategy también participa en los cambios de estrategia, que pueden tener lugar, además, en el tiempo de ejecución del programa.

Hecho

La encapsulación impide el acceso directo a los algoritmos y a estructuras de datos internos. Una instancia externa (Client, Context) solo puede acceder a los cálculos y funciones a través de interfaces definidas y, además, solo puede acceder a los métodos y elementos de datos de un objeto que sean relevantes para ella.

A continuación, explicaremos cómo se aplica un patrón de diseño en un proyecto práctico utilizando un ejemplo de strategy pattern.

Ejemplo de un strategy pattern

En nuestro ejemplo nos orientamos según el proyecto de estudio sobre strategy patterns de Philipp Hauer. En el mismo vamos a crear una aplicación de navegación que hace uso de un strategy pattern. La aplicación debe calcular una ruta basada en los medios de transporte habituales. El usuario puede elegir entre tres opciones:

  • Peatón (ConcreteStrategyA)
  • Coche (ConcreteStrategyB)
  • Transporte público (ConcreteStrategyC)

Si se transfieren estas especificaciones a un gráfico UML, se puede observar la estructura y la función del strategy pattern:

En nuestro ejemplo, el cliente o Client es la Interfaz Gráfica de Usuario (GUI) de una aplicación de navegación con botones para el cálculo de la ruta. Si el usuario hace una selección y pulsa un botón, se calcula una ruta concreta. El Context (clase Navigator) tiene la tarea de calcular y mostrar una serie de puntos de control en el mapa. La clase Navigator tiene un método para cambiar la estrategia de enrutamiento activa. Los botones permiten cambiar fácilmente entre los medios de transporte.

Si, por ejemplo, se activa un comando correspondiente con el botón peatonal del Client, se solicita el servicio “Calcular la ruta peatonal” (ConcreteStrategyA). El método executeAlgorithm() (en nuestro ejemplo, el método: calculaRuta(A, B)) acepta un origen y un destino y devuelve un conjunto de los puntos de control de la ruta. Context acepta el comando del Client y decide la estrategia apropiada basándose en las directivas previamente definidas (policy) (setStrategy: peatonal). Mediante Call, delega la solicitud al objeto Strategy y a su interfaz.

Con getStrategy(), la estrategia actualmente seleccionada se almacena en Context (clase Navigator). Los resultados de los cálculos de ConcreteStrategy se utilizan para el procesamiento posterior y en la visualización gráfica de la ruta en la aplicación de navegación. Si el usuario elige una ruta diferente, por ejemplo, haciendo clic en el botón “Coche”, Context cambia a la estrategia solicitada (ConcreteStrategyB) e inicia un nuevo cálculo a través de otra llamada. Al final del procedimiento, se devuelve una descripción de ruta modificada para el coche.

En nuestro ejemplo, la mecánica de patrones puede implementarse con un código relativamente claro:

Context:

public class Context {
    // Valor por defecto (comportamiento por defecto): ConcreteStrategyA
    private Strategy strategy = new ConcreteStrategyA(); 
    public void execute() { 
        //delega el comportamiento a un objeto de estrategia
        strategy.executeAlgorithm(); 
    }
    public void setStrategy(Strategy strategy) {
        strategy = strategy;
    }
    public Strategy getStrategy() { 
        return strategy; 
    } 
} 

Strategy, ConcreteStrategyA, ConcreteStrategyB:

interface Strategy { 
    public void executeAlgorithm(); 
} 
class ConcreteStrategyA implements Strategy { 
    public void executeAlgorithm() { 
        System.out.println("Concrete Strategy A"); 
    } 
} 
class ConcreteStrategyB implements Strategy { 
    public void executeAlgorithm() { 
        System.out.println("Concrete Strategy B"); 
    } 
}  

Client:

public class Client { 
    public static void main(String[] args) { 
        //Comportamiento por defecto 
        Context context = new Context(); 
        context.execute(); 
        //Cambiar el comportamiento 
        context.setStrategy(new ConcreteStrategyB()); 
        context.execute(); 
    } 
}

¿Cuáles son las ventajas y e inconvenientes del strategy pattern?

Las ventajas de usar un strategy pattern son más evidentes desde la perspectiva de un programador y administrador de sistemas. El desglose en módulos y clases autónomas ayuda a estructurar mejor el código del programa. Puesto que los módulos estén delimitados en nuestra aplicación de ejemplo, la tarea del programador será más sencilla. Así pues, se puede limitar el alcance de la clase Navigator mediante la externalización de las estrategias y se puede prescindir de la creación de subclases en Context.

Las dependencias internas de los segmentos se mantienen dentro de los límites de un código más reducido y claramente definido. Por ello, los cambios tienen menos efecto, es decir, no suelen conllevar más cambios (que requieren mucho tiempo) en la programación. En algunos casos, los cambios derivados incluso pueden descartarse por completo. Los segmentos de código más claros también pueden mantenerse mejor a largo plazo y el diagnóstico y la resolución de problemas se facilitan.

Asimismo, el manejo se hace más sencillo, ya que la aplicación de ejemplo se puede equipar con una interfaz fácil de usar. Los usuarios pueden usar los botones para controlar el comportamiento del programa (cálculo de la ruta) de forma variable y elegir entre las opciones de forma sencilla.

Puesto que el Context de la aplicación de navegación solo interactúa con una interfaz que encapsula los algoritmos, es independiente de la aplicación concreta de los algoritmos individuales. Por lo tanto, si se modifican los algoritmos o se introducen nuevas estrategias posteriormente, no es necesario cambiar el código de Context. Esto permite ampliar las funciones de cálculo de ruta con estrategias concretas adicionales para rutas en avión, transporte marítimo y tráfico de larga distancias. Las nuevas estrategias solo tienen que implementar la interfaz de Strategy correctamente.

Los strategy patterns simplifican la difícil programación del software orientado a objetos gracias a otra de sus virtudes: permiten el diseño de software reutilizable (módulos) que se puede implementar repetidamente y cuyo desarrollo se considera particularmente difícil. Esto significa que las clases Context relacionadas también podrían utilizar las estrategias externalizadas para calcular las rutas a través de la interfaz y ya no tendrían que aplicarlas por sí mismas.

A pesar de sus muchas ventajas, el strategy pattern también tiene algunos inconvenientes. Debido a su estructura más compleja, el diseño del software puede crear redundancias e ineficiencias en la comunicación interna. Por ejemplo, la interfaz strategy genérica, que todos los algoritmos deben aplicar por igual, a veces puede acabar sobredimensionada.

Un ejemplo: una vez que Context ha creado e inicializado ciertos parámetros, los pasa a la interfaz genérica y al método definido en esta. Sin embargo, la estrategia que se aplique en última instancia no necesita necesariamente todos los parámetros comunicados de Context y, por lo tanto, no los procesa. Así, la interfaz proporcionada no siempre se utiliza de manera óptima en el strategy pattern y a veces se producen transferencias de datos innecesarias, con el consiguiente esfuerzo de comunicación.

En la aplicación, también existe una estrecha dependencia interna entre el cliente y las estrategias. Client hace la selección y solicita la estrategia concreta mediante el comando de activación (en nuestro ejemplo, el cálculo de la ruta a pie), por lo que debe conocer las ConcreteStrategies. Por lo tanto, el patrón de diseño strategy solo debe utilizarse si los cambios de estrategia y comportamiento son importantes para el uso y la funcionalidad de un software.

Naturalmente, los inconvenientes mencionados se pueden compensar parcialmente o minimizar. Por ejemplo, el número de instancias de objetos que pueden producirse en grandes cantidades en el strategy pattern puede reducirse si se aplican a un flyweight pattern. La medida también tiene un efecto positivo en los requisitos de eficiencia y en la memoria de la aplicación.

¿Dónde se utiliza el patrón strategy?

Como patrón de diseño básico en el desarrollo de software, el strategy pattern no está limitado a un solo ámbito de aplicación. Lo más importante a la hora de escoger este patrón de diseño es la naturaleza del problema que se quiera resolver. El patrón strategy es ideal para software que ha de resolver tareas y problemas variables, opciones de comportamiento y cambios.

Por ejemplo, los programas que ofrecen diferentes formatos de almacenamiento de archivos o varias funciones de clasificación y búsqueda utilizan el strategy pattern. En el ámbito de la compresión de datos también se utilizan programas que emplean diversos algoritmos de compresión basados en este patrón de diseño. Esto permite, por ejemplo, convertir de forma versátil los vídeos a un formato de archivo que ahorre espacio o restaurar archivos comprimidos (por ejemplo, archivos ZIP o RAR) a su estado original mediante estrategias especiales de descompresión. Otro ejemplo es el almacenamiento de un documento o archivo gráfico en diferentes formatos.

El patrón de diseño strategy también se utiliza en el desarrollo e implementación de software de juegos, que, por ejemplo, tienen que reaccionar con flexibilidad a las situaciones cambiantes del juego durante el tiempo de ejecución. Los diferentes personajes, los equipos especiales, los patrones de comportamiento de los personajes o sus movimientos especiales pueden almacenarse en forma de ConcreteStrategies.

Otra área de aplicación de los strategy patterns es el software de control. Mediante el intercambio de ConcreteStrategies, los segmentos de cálculo pueden adaptarse fácilmente a grupos ocupacionales, países y regiones. También los programas que traducen los datos a diferentes formatos gráficos (por ejemplo, como gráficos de líneas, circulares o de barras) utilizan strategy patterns.

Se pueden encontrar aplicaciones más específicas de los strategy patterns en la biblioteca estándar de Java (Java API) y en los conjuntos de herramientas de la interfaz gráfica de Java (por ejemplo, AWT, Swing y SWT), que utilizan un gestor de diseño en el desarrollo y la generación de interfaces gráficas de usuario. Este gestor puede aplicar diferentes estrategias para la disposición de los componentes durante el desarrollo de la interfaz. El strategy pattern se utiliza también en sistemas de base de datos, controladores de dispositivos y programas de servidores.

Resumen de las funciones más importantes del strategy pattern

Entre la amplia variedad de los patrones de diseño, el strategy pattern se distingue por las siguientes propiedades:

  • Está orientado al comportamiento (los comportamientos y los cambios son más fáciles de programar e implementar, los cambios también son posibles durante el tiempo de ejecución de un programa).
  • Está orientado a la eficiencia (la externalización simplifica y optimiza el código y su mantenimiento).
  • Está orientado al futuro (los cambios y optimizaciones también son fáciles de realizar a medio y largo plazo).
  • Tiene como objetivo la capacidad de expansión (favorecida por el diseño modular y la independencia de los objetos y las clases).
  • Tiene como objetivo la reutilización (por ejemplo, el uso recurrente de estrategias).
  • Tiene como objetivo optimizar la usabilidad, controlabilidad y configurabilidad del software.
  • Requiere consideraciones conceptuales minuciosas (¿qué se puede externalizar a las clases Strategy, dónde y cómo?).
En resumen

Los strategy patterns permiten el desarrollo eficiente y económico de software en la programación orientada a objetos con soluciones de problemas a medida. Ya en la fase de diseño, se puede preparar el proyecto para futuros cambios y mejoras. El sistema, que está orientado a la variabilidad y al dinamismo, puede así, en general, controlarse y vigilarse mejor. Los errores e inconsistencias son más fáciles de eliminar. Los componentes reutilizables e intercambiables ahorran costes de desarrollo, especialmente en proyectos complejos con una perspectiva a largo plazo. Sin embargo, es importante encontrar el equilibrio adecuado: no es raro que los patrones de diseño se usen demasiado o demasiado poco.