El patrýn estrategia
El patrón Estrategia
Fecha de creación: 04.9.2004
Revisión 1.0 (04.09.2004)
Miguel Ángel Abián
mabian AT aidima DOT es
|
1. Introducción
En este artículo se expone el patrón Estrategia desde dos puntos de vista: el teórico y el práctico. Por un lado, se explican los
argumentos teóricos para usar este patrón; por otro, se dan algunos consejos para saber cuándo conviene usarlo y se exponen tres ejemplos
completos de su uso en Java. Para seguir el artículo, no se necesita ningún conocimiento previo que no sea
comprender, aun someramente, el lenguaje Java.
Según la última versión del diccionario de la Real Academia Española, un patrón es "el
modelo que sirve de muestra para sacar otra cosa igual". Aunque la definición no usa ningún término informático,
se adecua bastante bien al sentido con que se emplea patrón en la ingeniería del software. En esta
disciplina, los patrones no son muestras físicas o ejemplares a partir de los cuales se puedan construir cosas iguales; sino
representaciones abstractas que sirven para especificar y documentar soluciones recurrentes a ciertos problemas habituales.
Pese a lo mucho que se usan los patrones de software, no abundan las definiciones del término "patrón de software". Más aún:
incluso el creador del término patrón en el sentido de buena práctica (aplicado a la arquitectura, no a la construcción de software) parecía
sentirse más cómodo escribiendo un libro voluminoso, abstracto y, a ratos, confuso que definiendo de forma clara y precisa el concepto de patrón.
Una definición que no hará mal a nadie es la que aparece en [1]:
[Un patrón] es una descripción en un formato fijo de cómo resolver un cierto tipo de problemas.
Según Brad Appleton (la definición la leí hace cuatro o cinco años en http://www.enteract.com/~bradapp/docs/pattern-intro.html; la dirección ya
no funciona), un patrón es
[...] una unidad de información instructiva con nombre, la cual captura la estructura esencial y la compresión de una familia de soluciones exitosas
probadas para un problema recurrente que ocurre dentro de un cierto contexto y de un sistema de fuerzas.
La definición más completa que conozco para el término aparece en [2] y es obra de James Coplien:
Un patrón es una regla constituida por tres partes; la cual expresa una relación
entre un cierto contexto, un cierto sistema de fuerzas que ocurren repetidamente en ese contexto y
una cierta configuración de software que permite a estas fuerzas resolverse a sí mismas.
Según esta definición, tres son las partes fundamentales de un patrón: un sistema de fuerzas, un
contexto y una configuración de software.
Sistema de fuerzas es el término que
usa Coplien como generalización del tipo de criterios que usan los ingenieros de software y los programadores para argumentar
sus diseños e implementaciones. Cada vez que se diseña o implementa un componente de software, las fuerzas son
los objetivos y restricciones que deben considerar ingenieros y programadores para el componente. Hallar una solución
de software para un problema es solucionar el sistema de fuerzas asociado al problema. Una fuerza puede ser la velocidad
de ejecución, el consumo de memoria RAM, la interoperabilidad con otros programas, la existencia de errores, etc.
La expresión configuración de software es usada por Coplien para denotar cualquier
conjunto de reglas de diseño susceptible de ser utilizado para solucionar un sistema de fuerzas. Solucionar un sistema de
fuerzas es encontrar un equilibrio óptimo entre las fuerzas que afectan al sistema. Dependiendo de la naturaleza de las fuerzas,
puede ser imposible demostrar formalmente que una solución equilibra de forma óptima (esto es, resuelve) un sistema de fuerzas. Por ejemplo, si
una fuerza es el impacto social o político, no puede probarse analíticamente que una solución es óptima (al menos, no con las técnicas
matemáticas actuales).
Por último, contexto es el entorno en que se enmarca un sistema de fuerzas. Un
contexto dado puede hacer inservibles ciertas configuraciones de software o hacer que unas sean preferible
a otras.
En definitiva, la definición de Coplien plantea que un patrón es una descripción genérica de una
solución recurrente para un problema recurrente condicionado por la consecución de ciertos objetivos
sujetos a ciertas restricciones.
El concepto de patrón en el sentido que usamos aquí procede del campo de la arquitectura: fue el arquitecto Christopher Alexander quien primero manejó el término, basándose
en los temas recurrentes en el diseño de edificios y calles; más tarde, la idea fue transplantada al dominio del diseño de software. Cuando analizó la historia de la Arquitectura, Alexander encontró
diseños y construcciones que se repetían en zonas geográficas muy alejadas y de culturas muy distintas. Por ejemplo, los tejados de las casas de los pueblos pequeños eran y son muy similares (están
hechos de tejas, tienen una gran pendiente), aunque las necesidades de los habitantes han debido ser muy distintas (éstas dependen de factores geográficos, climáticos y personales). La
persistencia de estos tejados es un indicio de que existe una solución óptima (un patrón) para la construcción de tejados que se ha venido copiando, con pequeñas variaciones y quizás de forma
inconsciente, en muchos pueblos del mundo. Según Alexander,
Cada patrón describe un problema que ocurre una y otra vez en nuestro ambiente y después describe la parte más importante de la solución a ese problema de tal
manera que se pueda utilizar esta solución un millón de veces, sin hacerlo dos veces de la misma manera.
En uno de sus libros [3], aparece como ejemplo de patrón de diseño arquitectónico el de las "Ventanas de la Calle", que reproduzco aquí.
Figura 1. Un patrón de diseño arquitectónico: las ventanas de la calle. Una casa sin ventanas es fría y poco acogedora. Asimismo, es desagradable estar en un casa que dé a una calle sin ninguna ventana |
Los patrones de software se pueden dividir en distintas categorías.
En el libro [4], los patrones se clasifican en:
Patrones de diseño. Ofrecen esquemas para refinar subsistemas y componentes de
un sistema de software, o las relaciones entre ellos. Por lo general, describen una
estructura de comunicación recurrente entre componentes, que sirve para resolver
un problema general de diseño en un contexto determinado.Patrones arquitectónicos. Expresan una organización estructural básica
para un sistema de software, organización referida a un conjunto de subsistemas predefinidos.
Asimismo, especifican las responsabilidades de estos subsistemas y proporcionan directrices y
reglas para organizar las relaciones entre ellos.Patrones de implementación (idioms). Estos patrones son
de bajo nivel de abstracción, pues están asociados a lenguajes de programación concretos; describen
cómo implementar ciertas características de los componentes de software o de sus relaciones haciendo
uso de las estructuras y propiedades de un lenguaje de programación. Por ejemplo, en C++, el código
while (*destino++ = *src++); es un patrón de implementación para copiar cadenas de caracteres.
Según la bibliografía técnica, los patrones de diseño ofrecen las siguientes ventajas:
Facilitan la reutilización del diseño. Usando patrones de diseño, se pueden reusar diseños de software en tantas aplicaciones como se quiera, aun cuando éstas se escriban en distintos lenguajes de programación.
Simplifican el proceso de diseño. Si en la fase de análisis uno encuentra un problema para el cual existe un patrón de diseño, ya tiene hecha más de la mitad del trabajo: no necesita comenzar el diseño desde cero.
Facilitan la transmisión de los conocimientos adquiridos por otros programadores. Usando patrones de diseño, los programadores neófitos pueden aprender rápidamente cómo piensan los diseñadores expertos.
Proporcionan un vocabulario común y consistente para el diseño de software. Esta característica facilita la comunicación entre diseñadores. Hablar de patrones es más abstracto que hablar de clases o interfaces.
Acercan el desarrollo de software a lo que se espera de una ingeniería. El desarrollo de un vocabulario común y de una lista de buenas prácticas es común a cualquier ingeniería.
Muestran buenos principios de diseño. Principios como el encapsulado, la abstracción y la cohesión entran desde el principio en los patrones de diseño.
Ayudan a comprender las bibliotecas de clases que los usan. Java, por ejemplo, tiene muchos paquetes que se han construido basándose en patrones de diseño.
El libro más influyente en cuanto a patrones de diseño en software es [5], que define
patrón de diseño como "una descripción de clases y objetos que se comunican entre sí, adaptada para resolver un problema de diseño general
en un contexto particular". Es un libro nuevo, pero no original: clasifica y organiza unas ideas que ya llevaban cierto tiempo en el aire. Debido a su
popularidad y predicamento, este libro es de lectura obligada para quien quiera asomarse al mundo de los patrones.
En él se distinguen tres tipos de patrones de diseño para software: patrones de creación, de comportamiento y estructurales:
Patrones de creación. Son patrones que abstraen el proceso de creación de los objetos. Se dividen en patrones de creación de clases (el patrón Método de fábricas -Factory Method-, por ejemplo) y de creación de objetos
(el patrón Constructor -Builder-, p. ej.).Patrones de comportamiento. Estos patrones asignan responsabilidades y algoritmos a los objetos. Se dividen en patrones de comportamiento de objetos (el patrón Observador -Observer-, por ejemplo) y de comportamiento de
clases (el patrón Método de plantillas -Template Method-, p. ej.).Patrones estructurales. Son patrones que definen cómo deben combinarse las clases y los objetos para solucionar eficazmente ciertos problemas. Se dividen en patrones estructurales de objetos (el patrón Adaptador -Adapter-, por
ejemplo) y en patrones estructurales de clases (el patrón en patrones estructurales de clases (el patrón Compuesto o Entidad compuesta -Composite-, p. ej.).
En javaHispano existe una serie de artículos sobre patrones [6], obra de Alberto Molpeceres; así como un tutorial muy completo[7], obra de Martín Pérez.
Si el lector desea conocer más patrones, le recomiendo leer estos trabajos. El presente artículo
es una acotación marginal a ellos.
2. El patrón Estrategia
Según [5], el patrón Estrategia "define una familia de algoritmos, encapsula cada uno y los hace intercambiables. La Estrategia deja que el algoritmo varíe
independientemente de los clientes que lo usen" (página 315).
Me he decidido a escribir sobre este patrón por varios motivos:
Es un patrón muy útil para seguir el Principio Abierto/Cerrado ("Una entidad de software -clase, módulo, etcétera- debe estar abierta para su extensión, pero cerrada para su modificación").
Muestra el poder del polimorfismo en los lenguajes orientados a objetos.
Al basarse en el encapsulado y en la programación de interfaces, constituye un buen ejemplo de la aplicación de los principios del diseño orientado a objetos.
En el principio Abierto/Cerrado, "debe estar abierta para su extensión" significa que no se ha de cambiar una clase existente cuando se extiendan sus funciones; "cerrada para su modificación"
significa que deben existir subclases o clases abstractas o interfaces en las que delegar las nuevas funciones que se desee añadir. Para que el principio Abierto/Cerrado
no quede en mera palabrería o en una montaña de buenas intenciones, se necesitan varios ingredientes: abstracción, encapsulado, polimorfismo, herencia e interfaces. No por casualidad,
estas herramientas son el núcleo de la verdadera programación orientada a objetos. Sin ellas, un programa escrito en Java, Smalltalk o C++ sería tan orientado a objetos
como melódico es el chirriar de una bisagra oxidada.
Para entender las implicaciones del principio Abierto/Cerrado, se puede considerar el siguiente código, correspondiente
a un método que calcula el importe total de la compra en un supermercado:
public double calcularImporteVenta(Producto productos[]) {
double importeTotal = 0.00;
for (i=0; i < productos.length; i++) {
importeTotal += producto[i] * getPrecio();
}
return importeTotal;
}
Tal y como está escrito el método CalcularImporteVenta(), cumple perfectamente
el principio Abierto/Cerrado. Consideremos ahora que la inflación se está disparando en el país donde se encuentra el supermercado y que sus dueños, comprometidos con el bienestar del
país y con los estrictos criterios macroeconómicos del Fondo Monetario Internacional, deciden rebajar el precio de los alimentos que más influyen sobre la inflación: el pan y la
carne de pollo. Sí, algo de ironía hay en el ejemplo; pero ¿no es irónico que una institución de países industrializados proponga políticas y prácticas antiproteccionistas que
ningún país industrializado ha seguido nunca? En fin, el código del método anterior quedaría así:
public double calcularImporteVenta(Producto productos[]) {
double importeTotal = 0.00;
for (i=0; i < productos.length; i++) {
if (productos[i] instanceof BarraPan)
importeTotal += producto[i] * 0.85 * getPrecio();
else if (productos[i] instanceof Pollo)
importeTotal += producto[i] * 0.90 * getPrecio();
else
importeTotal += producto[i] * getPrecio();
}
return importeTotal;
}
Este código ya no cumple el principio Abierto/Cerrado. Si la lista de productos alimentarios que influyen en la inflación cambiara, habría que
añadir nuevos productos y eliminar otros. Se incumpliría, pues, la condición de cerrado para modificaciones. Tal como se verá en la sección 4, el patrón Estrategia permite
cumplir el principio.
La representación UML del patrón Estrategia se muestra en la figura 2.
Figura 2. Estructura del patrón Estrategia |
Las clases que aparecen en la figura se explican aquí:
Contexto.
Un objeto Contexto es un cliente que usa una operación de la interfaz ofrecida por EstrategiaAbstracta. Por tanto, mantiene una
referencia a un objeto que es instancia de alguna clase de las derivadas por herencia de la clase EstrategiaAbstracta.
Contexto desconoce qué clases implementarán la operación o cómo lo harán.EstrategiaAbstracta.
Esta clase define el comportamiento fundamental de las clases Estrategia. Al ser una clase abstracta o una interfaz (en Java o C#), no pueden
crearse instancias de ella. Para realizar operaciones, los clientes deben usar instancias de sus subclases concretas.EstrategiaConcreta1, EstragegiaConcreta2, etc.
Las instancias de estas clases especializan el comportamiento básico especificado por EstrategiaAbstracta. La especialización se consigue mediante
la implementación de las operaciones de EstrategiaAbstracta o la definición de otras nuevas.
Los objetos cliente tienen referencias a instancias de estas clases.
En Java, las subclases de EstrategiaAbstracta se derivan de ésta mediante extends (si ésta es una clase abstracta) o implements (si es una interfaz).
Existen situaciones sencillas en las que la clase EstrategiaAbstracta puede ser sustituida por una clase que implementa los comportamientos deseados; en este caso, no
se necesitan las clases EstrategiaConcreta1, EstragegiaConcreta2, etc. Al final de la siguiente sección se pondrá un ejemplo de esta situación, la cual correspondería a lo
que puede denominarse "patrón Estrategia degenerado".
Cuando se aplica a un problema, el patrón Estrategia rompe el diseño en dos partes claramente diferenciadas: la del Contexto y la de la Estrategia.
La primera parte es la parte invariante del diseño, pues su comportamiento no cambia de vez en vez. La segunda (formada por la clase EstrategiaAbstracta y sus subclases) aprehende la naturaleza cambiante y modificable del
diseño: cada subclase de EstrategiaConcreta define un comportamiento específico que puede necesitarse cuando el programa esté en ejecución. En cierto modo, el patrón Estrategia aprovecha el mecanismo de la herencia para sacar factor
común de los distintos algoritmos.
Existen varios modos de colaboración entre la Estrategia y el Contexto que permiten elegir el comportamiento deseado en cada ocasión. Los dos más habituales se detallan aquí:
Un objeto Contexto puede pasar a un objeto Estrategia, cuando sea necesario, todos los datos necesarios para el método que implementa el comportamiento deseado. La gran ventaja
de trabajar así radica en que se consigue desacoplar el Contexto de la Estrategia.Un Contexto puede pasar una referencia a sí mismo como argumento de los métodos que implementan las operaciones de la interfaz de Estrategia. De este modo, la Estrategia puede
llamar al Contexto cada vez que lo necesite. La desventaja de obrar así es que se aumenta el acoplamiento entre Contexto y Estrategia.
Normalmente, los clientes que necesitan las operaciones de la Estrategia no acceden directamente a los métodos que las implementan, sino que interaccionan solamente con el Contexto (que, a su vez, es
un cliente de la Estrategia). Los clientes crean y pasan objetos del tipo EstrategiaConcreta a objetos Contexto, que se encargan de pasar las peticiones a la Estrategia.
3. Usos del patrón Estrategia
De modo general, resulta conveniente emplear el patrón Estrategia cuando los clientes implementan algoritmos genéricos que son difíciles de reusar, de intercambiar y cuya elección puede variar en tiempo de ejecución.
Un buen indicio de lo anterior lo da la proliferación cancerosa de expresiones condicionales en el código (if, switch/case).
Por ejemplo, consideremos que se quiere escribir una aplicación encargada de cifrar texto mediante claves.
Dada una clave y un texto, la aplicación debe generar un texto indescifrable para cualquiera que no tenga la clave con la que se codificó el texto. Consideremos también que existen varias
implementaciones para el algoritmo de cifrado. Los motivos para esta multiplicidad no son teóricos: dependiendo de la longitud del texto, puede haber
implementaciones más eficaces que otras, pueden existir implementaciones más rápidas pero que consuman más recursos (memoria, espacio en el disco duro, etc.)...
Una solución a nuestro problema sería colocar toda la lógica del negocio en la clase cliente (la que usa los algoritmos). Mediante sentencias condicionales, el código del cliente elegiría el
algoritmo de cifrado que necesitara. Esta solución dista de ser idónea: según se fueran incorporando nuevas implementaciones del algoritmo, aumentaría el número de sentencias condicionales
y se necesitaría modificar el código del cliente, así como recompilarlo. Aparte, sería imposible cifrar dinámicamente un texto mediante diferentes algoritmos.
Otra solución consistiría en escribir una clase general cuyas subclases encapsularan cada una de las implementaciones del algoritmo de cifrado. Los problemas de esta otra solución no
serían despreciables: seguiría siendo necesario mantener las sentencias condicionales, se dispararía el número de subclases y el comportamiento seguiría fijado en tiempo de compilación.
Un objeto, pongamos por caso, Implementacion1 no podría comportarse como un objeto Implementacion2 durante la ejecución del programa: una vez creado, su comportamiento sería inmodificable.
Para este problema, el patrón Estrategia proporciona una excelente solución. Basta seguir estos pasos:
Identifíquese el comportamiento deseado (cifrado del texto mediante una clave).
Especifíquese la interfaz del comportamiento en una clase abstracta o en una interfaz (EstrategiaAbstracta).
Desplácese todo el código basado en sentencias condicionales a cada una de las subclases concretas de EstrategiaAbstracta.
Cada vez que se necesite una implementación concreta del algoritmo de cifrado, deléguese la petición a un objeto (Contexto) que contenga referencias a objetos del tipo EstrategiaAbstracta.
Con el patrón Estrategia, cada vez que se necesita añadir nuevas implementaciones del algoritmo de cifrado, no se precisará modificar el cliente ni el Contexto: sólo habrá que añadir una nueva clase EstrategiaConcreta.
Como los clientes usan la interfaz de métodos definida por EstrategiaAbstracta (es decir, su tipo, no la clase), un mismo objeto Texto podrá ser cifrado con todas las implementaciones.
¿Cuándo conviene usar el patrón Estrategia?
-
Cuando hay muchas clases interrelacionadas que difieren solamente en comportamiento.
Este patrón proporciona un modo de configurar el comportamiento deseado sin modificar
el tipo de cada objeto. -
Cuando se necesitan distintas variantes de un mismo algoritmo.
-
Cuando un algoritmo usa datos que no queremos que sean visibles para los clientes. El
patrón Estrategia permite evitar que los clientes conozcan las estructuras de datos que
se usen internamente.
He aquí algunas situaciones en las que se cumple una o varias de las condiciones expuestas arriba:
-
Representación gráfica de unos mismos datos en distintas formas (gráfico de barras, de porciones, etc.).
-
Compresión de ficheros en distintos formatos (Zip, GZip, Rar, etc.).
-
Grabación de ficheros en distintos formatos (HTML, PDF, Word, etc.).
-
Utilización de distintas políticas de seguridad en el acceso a los recursos de una red.
-
Encaminamiento de paquetes IP mediante diversos algoritmos (la elección dependerá del estado de las redes,
del tráficos de datos, de la distancia entre redes, etc.). -
Ordenación o búsqueda de elementos en una colección por medio de distintos algoritmos (método de la burbuja, búsqueda secuencial, etc.)
4. Algunos ejemplos del patrón Estrategia
No necesitamos recurrir al software para ver ejemplos del patrón Estrategia. La propia realidad nos ofrece
unos cuantos. Consideremos, por ejemplo, una agencia de viajes; en la figura 3 se muestra su representación UML.
Figura 3. Una agencia de viajes |
Como es bien sabido, una agencia de viajes ofrece el servicio de proporcionar viajes a sus clientes. Mediante ella, una persona
puede seleccionar el viaje deseado (medio de transporte, precio, destino) entre los que la agencia
pone a disposición de los clientes. Según el patrón Estrategia, el cliente interacciona con la agencia de viajes para
seleccionar el comportamiento deseado para su viaje.
El patrón Estrategia también puede usarse para plantear asuntos más existenciales. Vivimos en una época en que la adolescencia se prolonga hasta los treinta, en que la juventud es el valor supremo y en que algunas empresas se jactan de que la media de edad
de sus plantillas no supera los treinta y cinco años (empresas, por cierto, que omiten dar la edad media de sus directivos: si tanto
importa la juventud del personal, ¿por qué no tienen directores y presidentes con menos de cincuenta años?). El patrón Estrategia
también sirve para recordarnos el orden natural de las cosas:
Figura 4. La vida segun el patrón Estrategia |
Dependiendo de la edad de una persona, la Vida elige una especialización de Persona (Niño, Adulto o
Anciano). El patrón Estrategia permite que estas especializaciones añadan sus propios comportamientos especializados
al comportamiento base definido en Persona: trabajar(), balbucear(), cobrarPension().
Si nos restringimos a la aplicación del patrón Estrategia al software, podemos considerar el ejemplo que aparece en [5] (página 315),
consistente en un programa para dividir un texto en líneas (por ejemplo, con espaciado sencillo o doble). Gamma et al. razonan que
resulta desaconsejable escribir el código de los algoritmos de inserción de saltos de línea dentro de las clases que necesiten dichos algoritmos.
Tres son los motivos aducidos:
Los clientes se volverían más complejos si incluyeran el código de todos los algoritmos para colocar los saltos de línea.
Sería difícil modificar los algoritmos existentes y añadir nuevos.
Algunos clientes tendrían algoritmos para la inserción de saltos de línea que raras veces, o nunca, usarían.
La solución que proponen Gamma et al. consiste en aplicar el patrón Estrategia del modo representado en la figura 5.
Figura 5. Ejemplo extraído del libro Design Patterns: Elements of Reusable Object-Oriented Software |
En este ejemplo, la clase Composition mantiene una colección de instancias Component, que representan texto y elementos gráficos en un documento.
Un objeto Composition organiza los objetos componentes dependiendo de la manera de insertar saltos de línea que se adopte. La clase Composition
se encarga de mantener y actualizar los saltos de línea del texto que se muestra en un visor de texto. Cada estrategia para aplicar los saltos
de línea se implementa en subclases de la clase abstracta Compositor:
La clase SimpleCompositor implementa una estrategia simple que determina los saltos de línea de uno en uno.
La clase TeXCompositor implementa el algoritmo TeX para encontrar saltos de línea (se buscan y actualizan saltos de línea párrafo a párrafo).
La clase ArrayCompositor implementa una estrategia que selecciona los saltos de línea de modo que cada columna tenga un número fijo de elementos.
Un objeto Composition (que representa un bloque de texto) mantiene una referencia a un objeto Compositor.
Cuando un objeto Composition vuelve a componer su texto, delega esta responsabilidad en el objeto
Compositor asociado. Los clientes de Composition (un procesador de textos, por ejemplo), especifican
qué Compositor se usará eligiendo el objeto Compositor al cual se hará referencia dentro del Composition.
Otro ejemplo de uso del patrón Estrategia nos lo proporciona el paquete java.awt. Las instancias de la clase Component
del AWT declaran las instancias de la clase Contexto que permiten seleccionar y establecer distintos algoritmos de
distribución de los componentes gráficos mediante el método SetLayout() de la interfaz LayoutManager
(la clase EstrategiaAbstracta para este ejemplo), encargada de distribuir los componentes gráficos en el componente AWT. Las subclases
de LayoutManager (BorderLayout, BoxLayout, FlowLayout, ScrollPaneLayout, etc.)
encapsulan su propia implementación de setLayout().
Figura 6. El patrón Estrategia en el AWT |
Si consideramos la aplicación de la sección anterior que se encargaba de cifrar texto mediante claves, el patrón Estrategia nos
daría una solución como la que se representa en la figura:
Figura 7. El patrón Estrategia en el ejemplo de cifrado |
Sin el patrón Estrategia, la implementación del programa de codificación de textos tendría un método similar a éste:
public void cifrar(String clave, String bloqueTexto, int algoritmo) {
// Método de la clase Texto, que representa textos que van a codificarse.
if (algoritmo == ALGORITMO_X)
CifrarConAlgoritmoX(clave, bloqueTexto);
if (algoritmo == ALGORITMO_y)
CifrarConAlgoritmoY(clave, bloqueTexto);
...
}
Con el patrón Estrategia, el método quedaría así:
public setAlgoritmoCifrado(Cifrado cifrado) {
// Método de la clase Texto, que representa textos que van a cifrarse.
// Se escoge el algoritmo de cifrado en tiempo de ejecución.
this.cifrado = cifrado;
}
public void cifrar(String clave, String bloqueTexto) {
// Método de la clase Texto, que representa textos que van a cifrarse.
cifrado.codificar(clave, bloqueTexto);
}
Cualquier videojuego donde se pueda seleccionar un personaje según la etapa del juego, el entorno, las características de los enemigos, etc., es un buen ejemplo del patrón Estrategia. El cliente que elige, dependiendo
de las condiciones, un personaje dado está eligiendo una estrategia concreta, que hereda de una estrategia abstracta, con métodos básicos y comunes para todas sus subclases. Los programadores de un videojuego no programan diferentes algoritmos de
localización de los objetos y de representación gráfica de los mismos.
Dicho de otro modo: la manera de dibujar los píxeles de un ogro o de una encantadora dama no varía, ni tampoco varían los algoritmos generales como los de detección de colisiones, etc. Todos los personajes tienen los mismos. Ahora bien, dependiendo
del personaje, cambian los píxeles que lo representan, así como el comportamiento frente a los ataques del enemigo, la resistencia, la habilidad con ciertas armas, etc. Esos comportamientos específicos se definen en las estrategias concretas, mientras
que en la estrategia abstracta se definen los comportamientos generales.
En la figura 8 se muestra el patrón Estrategia para un videojuego donde el jugador puede elegir un personaje humano, leonino o batracio. Cada Estrategia concreta (Humano, Leon, Batracio) tendrá su propia implementación
de los métodos luchar(arma, energia, duracionAtaque) y huir (velocidadHuida), pues es de suponer que una rana no huirá con la misma gracia y velocidad que un
león; pero las tres subclases recurriran a los métodos de la superclase Personaje para métodos como dibujarPixel(x, y), borrarPixel(x, y), MoverPixel(x_inic, y_inic, x_dest, y_dest), etc.
Figura 8. El patrón Estrategia en un videojuego |
Por último, otro ejemplo de la aplicación del patrón Estrategia lo proporciona el ejemplo de los productos de un supermercado que se vio en la sección 2. En lugar del código que no cumple el principio Abierto/Cerrado, el
patrón permite escribir este código:
public class Producto {
private String nombre;
private double precio;
private Descuento descuento;
public Producto(String nombre) {
this.nombre = nombre;
}
public void setDescuento(Descuento descuento) {
this.descuento = descuento;
}
public void setPrecio(double precio) {
this.precio = precio;
}
public double getPrecio() {
return descuento.getPrecio(precio);
}
}
public class Descuento {
private double factor; //factor de descuento
public Descuento(double factor) {
this.factor = factor;
}
public double getPrecio(double precio) {
return precio * factor;
}
}
En este ejemplo, la clase EstrategiaAbstracta es una clase que implementa directamente sus métodos (es decir, no es abstracta ni una interfaz); en
consecuencia, son innecesarias sus subclases. Con esta solución se puede cambiar dinámicamente los descuentos de cualquier producto del supermercado, sin
modificar el código ya escrito. Por ejemplo, si se quiere fijar el descuento del pollo en un
10%, bastará escribir:
Producto pollo = new Producto("pollo");
pollo.setPrecio(30.05);
pollo.setDescuento(new Descuento(0.90));
A la vista del código de las dos últimas clases, se puede formular lo que se conoce como un corolario del principio
Abierto/Cerrado: "Si un sistema de software debe permitir un conjunto de alternativas, lo ideal es que una sola clase
en el sistema conozca el conjunto completo de alternativas".
5. Tres ejemplos de implementación en Java del patrón Estrategia
En esta sección se exponen de forma completa tres ejemplos de uso del patrón Estrategia con
Java. Los dos primeros son versiones simplificadas de situaciones ante las que me he encontrado en mi actividad profesional.
Son, pues, aplicaciones completamente prácticas del patrón. Innegable es que usar este patrón exige un poco de reflexión y
planificación previas; pero creo que merece la pena: uno ahorra mucho tiempo cuando tiene que introducir cambios o mejoras.
Y no hay que engañarse: los clientes siempre piden mejoras, modificaciones o refinamientos para sus aplicaciones (está en su
naturaleza). Anticiparse a esos cambios de manera que se modifique lo menos posible el código o los módulos ya escritos suele ser una buena práctica
(al menos, si uno no cobra por horas de trabajo).
El tercer ejemplo se aleja mucho del mundo empresarial: consiste en usar el patrón Estrategia para modelar osciladores amortiguados clásicos (no cuánticos).
Aparte de para estudiar muelles y resortes, los osciladores amortiguados son muy útiles para analizar estructuras periódicas de átomos o moléculas. De hecho, el ejemplo
se me ocurrió cuando analizaba los modos de vibración de una red de difracción tridimensional.
5.1. Un programa de cálculo de precios con descuentos
Consideremos que se nos encarga una aplicación de contabilidad para una tienda de muebles que clasifica sus productos en tres tipos: mueble clásico, mueble kit y mueble de diseño.
Dependiendo del tipo de mueble, la tienda hace un descuento por cada venta: mueble clásico, un 5%; mueble kit, un 10%; y mueble moderno, un 15%.
Supongamos también que desconocemos el patrón Estrategia y que somos novatos en la programación orientada a objetos. Entonces, nuestro primer intento sería algo como
(omito los métodos get y set que no voy a usar y casi todos los comentarios):
package mobiliario;
/**
* Intento sin el patrón Estrategia: clase Mueble.
* Representa un mueble del catálogo de la tienda.
*
*/
public class Mueble {
private double precio; // Precio del catálogo.
private String codigo; // Código en el catálogo.
private int tipo; // Tipo de mueble: clásico, kit o moderno.
public static final int CLASICO = 0;
public static final int KIT = 1;
public static final int MODERNO = 2;
public double getPrecio() {
return precio;
}
public String getCodigo() {
return codigo;
}
public int getTipo() {
return tipo;
}
public Mueble(String codigo, double precio, int tipo) throws ExcepcionTipoMueble {
if ( (tipo > Mueble.MODERNO) || (Mueble.CLASICO > tipo) )
throw new ExcepcionTipoMueble();
else {
this.codigo = codigo;
this.precio = precio;
this.tipo = tipo;
}
}
}
package mobiliario;
/**
* Intento sin el patrón Estrategia: clase VentaMueble.
* Esta clase representa la venta de un mueble del catálogo de la tienda
*/
import java.util.*;
public class VentaMueble {
private Date fecha; // Fecha de la venta.
private Mueble mueble; // Mueble que se vende.
public Date getFecha() {
return fecha;
}
public VentaMueble(Mueble mueble){
fecha = new Date();
this.mueble = mueble;
}
public double calcularPrecioVenta() throws ExcepcionTipoMueble {
// El precio de venta tiene un descuento con respecto al del catálago.
if (mueble.getTipo()==Mueble.CLASICO){
return (mueble.getPrecio())*0.95;
}
else if (mueble.getTipo()==Mueble.KIT){
return (mueble.getPrecio())*0.90;
}
else if (mueble.getTipo()==Mueble.MODERNO){
return (mueble.getPrecio())*0.85;
}
else
throw new ExcepcionTipoMueble();
}
// Ejemplo de funcionamiento del programa.
public static void main(String args[]) throws ExcepcionTipoMueble {
VentaMueble vm = new VentaMueble (new Mueble("Sofá Dalí", 1800.65, Mueble.MODERNO));
System.out.println("Precio de venta: " + vm.calcularPrecioVenta());
}
}
package mobiliario;
/**
* Intento sin el patrón Estrategia: clase ExceptionTipoMueble.
* Esta excepción se lanza cuando se introduce un tipo de mueble inválido.
*
*/
public class ExcepcionTipoMueble extends Exception {
public ExcepcionTipoMueble() {
super("Compruebe el tipo de mueble introducido.");
}
}
Los problemas de introducir toda la lógica del negocio en el código del cliente ya
se vieron, de modo general, en la sección 3. En este ejemplo, los problemas concretos son dos:
- Si se introdujera un nuevo tipo de mueble en el catálogo de la tienda, habría que modificar el
código del cliente. - Si se cambiaran los descuentos (por ejemplo, en época de rebajas), habría que modificar el código del cliente.
El cliente está fuertemente acoplado con la política de descuentos.
Nuestra primera solución es muy inestable: mínimos cambios llevan a reprogramar el cliente y a
recompilarlo. Si utilizamos el patrón Estrategia, obtendríamos una solución similar a ésta (vuelvo a
omitir los métodos get y set que no voy a usar y casi todos los comentarios):
package mobiliario;
/**
* Interfaz Descuento (Solución con el patrón Estrategia).
* Esta interfaz es la estrategia abstracta.
*/
public interface Descuento {
public double calcularPrecioVenta(double precio);
}
package mobiliario;
/**
* Interfaz DescuentoMuebleClasico (Solución con el patrón Estrategia).
* Esta clase es una de las estrategias concretas.
*/
public class DescuentoMuebleClasico implements Descuento {
// Constructor
public DescuentoMuebleClasico() {
}
// Devuelve el precio con el descuento.
public double calcularPrecioVenta(double precio) {
return (precio * 0.95);
}
}
package mobiliario;
/**
* Interfaz DescuentoMuebleKit (Solución con el patrón Estrategia).
* Esta clase es una de las estrategias concretas.
*/
public class DescuentoMuebleKit implements Descuento {
// Constructor
public DescuentoMuebleKit() {
}
// Devuelve el precio con el descuento.
public double calcularPrecioVenta(double precio) {
return (precio * 0.90);
}
}
package mobiliario;
/**
* La interfaz DescuentoMuebleModerno (Solución con el patrón Estrategia).
* Esta clase es una de las estrategias concretas.
*/
public class DescuentoMuebleModerno implements Descuento {
// Constructor
public DescuentoMuebleModerno() {
}
// Devuelve el precio con el descuento.
public double calcularPrecioVenta(double precio) {
return (precio * 0.85);
}
}
package mobiliario;
/**
* Clase Mueble (solución con el patrón Estrategia).
* Representa un mueble del catálogo de la tienda.
* Esta clase es el contexto.
*/
public class Mueble {
private String codigo; // Código en el catálogo.
private double precio; // Precio del catálogo.
private Descuento desc; // Tipo de descuento.
public double getPrecio() {
return precio;
}
public String getCodigo() {
return codigo;
}
public Descuento getDescuento() {
return desc;
}
public Mueble(String codigo, double precio, Descuento desc) throws Exception {
if ( (desc == null) ) {
// Cuando se crea un objeto Mueble, es obligatorio introducir su descuento.
throw new Exception("Debe introducir un tipo de descuento");
}
else {
this.codigo = codigo;
this.precio = precio;
this.desc = desc;
}
}
}
package mobiliario;
/**
* Clase VentaMueble (solución con el patrón Estrategia).
* Esta clase representa la venta de un mueble del catálogo de la tienda.
* Es cliente de la clase Mueble (el contexto para este caso).
*/
import java.util.*;
public class VentaMueble {
private Date fecha; // Fecha de la venta.
private Mueble mueble; // Mueble vendido.
public Date getFecha() {
return fecha;
}
public VentaMueble(Mueble mueble){
this.fecha = new Date();
this.mueble = mueble;
}
public double calcularPrecioVenta() throws Excepcion {
return (mueble.getDescuento().calcularPrecioVenta(mueble.getPrecio()));
}
// Ejemplo de funcionamiento del programa.
public static void main(String[] args) throws Exception {
Mueble m = new Mueble ("Silla Luis XVI", 1300.67, new DescuentoMuebleClasico());
VentaMueble vm = new VentaMueble (m);
System.out.println ("Código: " + m.getCodigo());
System.out.println("Precio de catálogo: " + m.getPrecio());
System.out.println("Precio de venta: " + vm.calcularPrecioVenta());
}
}
La segunda solución presenta ventajas significativas con respecto a la primera: ahora
podemos añadir nuevos tipos de descuento sin modificar la clase Mueble ni la clase VentaMueble.
Si se quiere incluir un nuevo descuento, basta añadir una nueva clase que implemente la interfaz Descuento.
En el caso de que se desee optar por una política de descuentos personalizada para cada mueble,
el patrón Estrategia permite escribir una solución como ésta:
package mobiliario;
/**
* Clase Descuento (Solución con el patrón Estrategia para el caso de políticas personalizadas
* de descuentos).
*/
public class Descuento {
private double factorDescuento; // Factor de descuento.
public Descuento (double factorDescuento) {
this.factorDescuento = factorDescuento;
}
public double calcularPrecioVenta(double precio) {
return (factorDescuento * precio);
}
}
package mobiliario;
/**
* Clase Mueble (solución con el patrón Estrategia para el caso de políticas personalizadas
* de descuentos).
*/
public class Mueble {
private String codigo; // Código en el catálogo.
private double precio; // Precio del catálogo.
private Descuento desc; // Tipo de descuento.
public double getPrecio() {
return precio;
}
public String getCodigo() {
return codigo;
}
public Descuento getDescuento() {
return desc;
}
public Mueble(String codigo, double precio, Descuento desc) throws Exception {
if ( (desc == null) ) {
// Cuando se crea un objeto Mueble, es obligatorio introducir su descuento.
throw new Exception("Debe introducir un tipo de descuento");
}
else {
this.codigo = codigo;
this.precio = precio;
this.desc = desc;
}
}
}
package mobiliario;
/**
* Clase VentaMueble (solución con el patrón Estrategia para el caso de políticas personalizadas
* de descuentos).
*/
import java.util.*;
public class VentaMueble {
private Date fecha; // Fecha de la venta.
private Mueble mueble; // Mueble vendido.
public Date getFecha() {
return fecha;
}
public VentaMueble(Mueble mueble){
this.fecha = new Date();
this.mueble = mueble;
}
public double calcularPrecioVenta() throws Exception {
return (mueble.getDescuento().calcularPrecioVenta(mueble.getPrecio()));
}
// Ejemplo de funcionamiento del programa.
public static void main(String[] args) throws Exception {
Mueble m = new Mueble ("Silla Barcelona", 4560.18, new Descuento(0.90));
VentaMueble vm = new VentaMueble (m);
System.out.println ("Código: " + m.getCodigo());
System.out.println("Precio de catálogo: " + m.getPrecio());
System.out.println("Precio de venta: " + vm.calcularPrecioVenta());
}
}
5.2. Un programa de cálculo de amortizaciones
Consideremos ahora que estamos trabajando en un módulo de software para llevar
la contabilidad de una empresa y que hemos llegado a la parte del cálculo de amortizaciones de los
bienes de la empresa (máquinas, ordenadores, mobiliario, mercancías, etc.). Según el Plan General Contable,
la amortización es "la expresión de la depreciación sistemática, anual y efectiva sufrida por el inmovilizado
inmaterial y material por su incorporación al sistema productivo".
En principio, la Hacienda española admite varias maneras
de calcular la amortización: la amortización mediante tablas, la amortización lineal, la amortización
degresiva por suma de dígitos y la amortización degresiva en progresión decreciente. Consideremos
también que la empresa para la cual trabajamos sólo emplea los tres últimos tipos de cálculo.
Lógicamente, los contables de la empresa desearán poder elegir el cálculo de amortización que
haga máxima, para cada bien, la amortización; así podrán pagar menos a la Hacienda pública.
El patrón Estrategia parece un buen candidato para echarnos una mano en el diseño
de la parte de las amortizaciones: contamos con un comportamiento abstracto (algo así como Amortizacion)
que se particulariza en comportamientos concretos (AmortizacionDegresivaDigitos, AmortizacionLineal,
AmortizacionProgresionDecreciente).
Veamos una posible forma de implementar el cálculo de amortizaciones con el patrón Estrategia (incorporo
documentación javadoc al código):
package amortizacion;
/**
* Esta interfaz es la estrategia abstracta del patron Estrategia.
*
*/
public interface Amortizacion {
/**
*
* @param precio Precio del bien.
* @param valorResidual Valor del bien al final de su vida útil.
* @param vida Años de vida útil del bien.
* @param anyo Anualidad para la cual se quiere calcular la amortización del bien.
* @return Amortización correspondiente al año anyo.
* @throws Excepcion
*/
public double calcularAmortizacion (double precio, double valorResidual, int vida, int anyo) throws Excepcion;
}
package amortizacion;
/**
* AmortizacionLineal es una estrategia concreta del patrón Estrategia.
* Implementa el cálculo de la amortización lineal (cuotas fijas).
*/
public class AmortizacionLineal implements Amortizacion {
/**
*
* @param precio Precio del bien.
* @param valorResidual Valor del bien al final de su vida útil.
* @param vida Años de vida útil del bien.
* @param anyo Anualidad para la cual se quiere calcular la amortización del bien.
* @return Amortización correspondiente al año anyo.
* @throws Excepcion
*/
public double calcularAmortizacion (double precio, double valorResidual, int vida, int anyo) throws Excepcion {
// Comprobación de los argumentos
if ( (precio > 0) && (anyo > 0) && (vida > 0) && (vida >= anyo) && (valorResidual < precio) ) {
return ( (precio - valorResidual) / vida );
} else
throw new Excepcion();
}
/**
* Constructor sin argumentos.
*/
public AmortizacionLineal() {
}
}
package amortizacion;
/**
* AmortizacionDegresivaDigitos es una estrategia concreta del patrón Estrategia.
* Implementa el cálculo de la amortización degresiva por suma de dígitos.
*/
public class AmortizacionDegresivaDigitos implements Amortizacion {
/**
*
* @param precio Precio del bien.
* @param valorResidual Valor del bien al final de su vida útil.
* @param vida Años de vida útil del bien.
* @param anyo Anualidad para la cual se quiere calcular la amortización del bien.
* @return Amortización correspondiente al año anyo.
* @throws Excepcion
*/
public double calcularAmortizacion (double precio, double valorResidual, int vida, int anyo) throws Excepcion {
// Comprobación de los argumentos
if ( (precio > 0) && (anyo > 0) && (vida > 0) && (vida >= anyo) && (valorResidual < precio) ) {
return ( (precio - valorResidual) * ( (vida - anyo + 1.0) / ( (vida * (vida + 1)) / 2.0) ) );
} else
throw new Excepcion();
}
/**
* Constructor sin argumentos.
*/
public AmortizacionDegresivaDigitos() {
}
}
package amortizacion;
/**
* AmortizacionProgresionDecreciente es una estrategia concreta del patrón Estrategia.
* Implementa el cálculo de la amortización degresiva en progresión decreciente.
*/
class AmortizacionProgresionDecreciente implements Amortizacion {
/**
*
* @param precio Precio del bien.
* @param valorResidual Valor del bien al final de su vida útil.
* @param vida Años de vida útil del bien.
* @param anyo Anualidad para la cual se quiere calcular la amortización del bien.
* @return Amortización correspondiente al año anyo.
* @throws Excepcion
*/
public double calcularAmortizacion (double precio, double valorResidual, int vida, int anyo) throws Excepcion {
// Comprobación de los argumentos
if ( (precio > 0) && (anyo > 0) && (vida > 0) && (vida >= anyo) && (valorResidual < precio) ) {
double a = valorResidual / precio;
double b = (1./vida);
double t = (1. - Math.pow(a, b));
return ( precio * t * Math.pow((1 - t), anyo - 1) );
} else
throw new Excepcion();
}
/**
* Constructor sin argumentos.
*/
public AmortizacionProgresionDecreciente() {
}
}
package amortizacion;
public class Excepcion extends Exception {
public Excepcion() {
super("Compruebe los valores introducidos en los parametros.");
}
}
package amortizacion;
/**
* Esta clase es el contexto del patrón Estrategia.
*/
public class Bien {
/**
* Precio del bien.
*/
private double precio;
/**
* Valor del bien al final de su vida útil.
*/
private double valorResidual;
/**
* Años de vida útil del bien.
*/
private int vida;
/**
* Estrategia de amortización.
*/
private Amortizacion amortizacion;
/**
* Método set para precio.
* @param precio Precio del bien.
*/
public void setPrecio(double precio) {
this.precio = precio;
}
/**
* Método get para valorResidual.
* @return Valor residual del bien.
*/
public double getValorResidual() {
return valorResidual;
}
/**
* Método set para valorResidual.
*
* @param valorResidual double
*/
public void setValorResidual(double valorResidual) {
this.valorResidual = valorResidual;
}
/**
* Método get para precio
* @return Precio del bien
*/
public double getPrecio() {
return precio;
}
/**
* Método set para vida.
*
* @param vida Años de vida útil del bien.
*/
public void setVida(int vida) {
this.vida = vida;
}
/**
* Método get para vida.
* @return Años de vida útil del bien.
*/
public int getVida() {
return vida;
}
/**
* Método set para Amortizacion.
*
* @param amort Estrategia de amortizacion
*/
public void setAmortizacion(Amortizacion amort) {
this.amortizacion = amort;
}
/**
* Constructor
*
* @param precio Precio del bien.
* @param valorResidual Valor residual del bien.
* @param vida Vida útil del bien.
* @param amort Estrategia de amortización.
*/
public Bien(double precio, double valorResidual, int vida, Amortizacion amort) {
this.precio = precio;
this.valorResidual = valorResidual;
this.vida = vida;
if (amort == null) {
this.amortizacion = new AmortizacionLineal();
} else
this.amortizacion = amort;
}
public void CambiarTipoAmortizacion (Amortizacion amort) {
if (amort != null)
setAmortizacion(amort);
}
public void calcularAmortizacion(int anyo) throws Excepcion {
System.out.println(amortizacion.calcularAmortizacion(precio, valorResidual, vida, anyo));
}
}
package amortizacion;
/**
* Esta clase es cliente de la clase Bien.
*
*/
public class Cliente {
// Ejemplo de uso del programa. Se calculan la amortizaciones al segundo año.
public static void main(String args[]) {
try {
Bien bien = new Bien(60000., 5000., 10, null);
bien.calcularAmortizacion(2);
bien.CambiarTipoAmortizacion(new AmortizacionProgresionDecreciente());
bien.calcularAmortizacion(2);
bien.CambiarTipoAmortizacion(new AmortizacionDegresivaDigitos());
bien.calcularAmortizacion(2);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
5.3. Un programa de cálculo de posiciones para osciladores amortiguados.
En el mundo físico hay muchos movimientos que son oscilatorios. El movimiento oscilatorio más sencillo
es el denominado movimiento armónico simple. Cualquier sistema que experimente una fuerza restauradora
lineal es candidato a ser considerado un oscilador armónico simple. La ecuación de movimiento para
un oscilador armónico simple que oscila en una sola dirección es (k es la constante elástica del oscilador)
O dicho de otra manera:
La solución a la ecuación diferencial de segundo grado es la siguiente:
Donde A y el ángulo phi son constantes que se determinan al especificar unas condiciones iniciales (en
Matemáticas se suelen llamar condiciones de contorno).
En cualquier sistema oscilatorio real existen fuerzas de fricción que tienden a provocar que el movimiento
se detenga al cabo de un tiempo (este tipo de movimiento se conoce como movimiento armónico amortiguado).
La ecuación general para un oscilador armónico amortiguado es (lambda es la constante de amortiguación
del oscilador)
O lo que es lo mismo:
Esta ecuación diferencial admite tres clases de soluciones:
1) Solución amortiguada (cuando gamma es menor que omega0)
La solución general es:
Si se imponen estas condiciones iniciales: t=0, x=0, v=v0, la solución queda así:
2) Solución crítica (cuando gamma es igual a omega0)
La solución general es:
Si se imponen estas condiciones iniciales: t=0, x=0, v=v0, la solución queda así:
3) Solución sobreamortiguada (cuando gamma es mayor que omega0)
La solución general es:
Si se imponen estas condiciones iniciales: t=0, x=0, v=v0, la solución queda así:
En este ejemplo, se van a implementar los distintos casos de un oscilador armónico
amortiguado usando el patrón Estrategia (en la figura se muestra la aplicación
del patrón para este ejemplo).
Figura 9. Aplicación del patrón Estrategia al análisis de los osciladores amortiguados |
package oscilador;
/**
* Esta interfaz es la estrategia abstracta del patrón Estrategia.
*
*/
public interface Fuerza {
public double calcularPosicionX(double t);
}
package oscilador;
/**
* FuerzaAmortiguadora es una estrategia concreta del patrón Estrategia.
*/
public class FuerzaAmortiguadora implements Fuerza{
/**
* Frecuencia del movimiento. (w^2 = w0^2 - gamma^2)
*/
private double omega;
/**
* Coeficiente de amortiguación del movimiento. ( gamma = lambda/(2m) )
*/
private double gamma;
/**
* Método para calcular la posición del oscilador en el instante t.
*
* @param t double
*/
public double calcularPosicionX(double t) {
return ( (1./omega) * Math.exp(-gamma * t) * Math.sin(omega * t) );
}
/**
* Constructor.
*
* @param omega0 double
* @param gamma double
*/
public FuerzaAmortiguadora(double omega0, double gamma) throws Exception {
if ( (omega0 > 0) && (gamma > 0) ) {
this.omega = Math.sqrt( (omega0 * omega0) - (gamma * gamma));
this.gamma = gamma;
}
else
throw new Exception("Revise las constantes del movimiento");
}
}
package oscilador;
/**
* FuerzaCritica es una estrategia concreta del patrón Estrategia.
*/
public class FuerzaCritica implements Fuerza{
/**
* Frecuencia del movimiento. (w^2 = w0^2 - gamma^2)
*/
private double omega;
/**
* Método para calcular la posición del oscilador en el instante t.
*
* @param t double
*/
public double calcularPosicionX(double t) {
return ( t * Math.exp(-omega * t) );
}
/**
* Constructor.
*
* @param omega0 double
*/
public FuerzaCritica(double omega0) throws Exception {
if ( (omega0 > 0) )
this.omega = omega0;
else
throw new Exception("Revise las constantes del movimiento");
}
}
package oscilador;
/**
* FuerzaSobreAmortiguadora es una estrategia concreta del patrón Estrategia.
*/
public class FuerzaSobreAmortiguadora implements Fuerza{
/**
* Coeficiente beta del movimiento (beta^2 = gamma^2 - w0^2)
*/
private double beta;
/**
* Coeficiente de amortiguación del movimiento. ( gamma = lambda/(2m) )
*/
private double gamma;
/**
* Método para calcular la posición del oscilador en el instante t.
*
* @param t double
*/
public double calcularPosicionX(double t) {
return ( (1./beta) * Math.exp(-gamma * t) * ( (Math.exp(beta * t) - (Math.exp(-beta * t)) )/2. ) );
}
/**
* Constructor.
*
* @param omega0 double
* @param gamma double
*/
public FuerzaSobreAmortiguadora(double omega0, double gamma) throws Exception {
if ( (omega0 > 0) && (gamma > 0) ) {
this.beta = Math.sqrt( (gamma * gamma) - (omega0 * omega0) );
this.gamma = gamma;
}
else
throw new Exception("Revise las constantes del movimiento");
}
}
package oscilador;
/**
* OsciladorAmortiguado es el contexto del patrón Estrategia.
*/
public class OsciladorAmortiguado {
/**
* Frecuencia natural del oscilador.(omega0^2) = k/m
*/
private double omega0;
/**
* Coeficiente de amortiguamiento del movimiento. gamma = lambda/(2m)
*/
private double gamma;
/**
* Velocidad inicial en el tiempo t=0.
*/
private double v0;
/**
* Tipo de fuerza.
*/
private Fuerza fuerza;
/**
* Método set para fuerza.
*
* @param fuerza Fuerza
*/
public void setFuerza(Fuerza fuerza) {
this.fuerza = fuerza;
}
/**
* Método para calcular la posición del oscilador en el instante t.
*
* @param t double
*/
public double calcularPosicionX(double t) {
return(v0 * fuerza.calcularPosicionX(t));
}
/**
* Constructor
*
* @param omega0 double
* @param gamma double
* @param fuerza Fuerza
*/
public OsciladorAmortiguado(double omega0, double gamma, double v0) throws Exception {
this.omega0 = omega0;
this.gamma = gamma;
this.v0 = v0;
if (gamma < omega0)
setFuerza(new FuerzaAmortiguadora(omega0, gamma));
else if (gamma == omega0)
setFuerza (new FuerzaCritica(omega0));
else if (gamma > omega0)
setFuerza (new FuerzaSobreAmortiguadora(omega0, gamma));
else
throw new Exception("Debe elegir obligatoriamente un tipo de oscilador amortiguado");
}
// Ejemplo del funcionamiento del programa
public static void main (String args[]) throws Exception {
// Oscilador sobreamortiguado
OsciladorAmortiguado oa1 = new OsciladorAmortiguado(0.1, 0.2, 1.1);
System.out.println(oa1.calcularPosicionX(5.)); // Posición a los cinco segundos
// Oscilador amortiguado
OsciladorAmortiguado oa2 = new OsciladorAmortiguado(0.2, 0.1, 1.1);
System.out.println(oa2.calcularPosicionX(2.)); // Posición a los dos segundos
// Oscilador crítico
OsciladorAmortiguado oa3 = new OsciladorAmortiguado(0.2, 0.2, 1.1);
System.out.println(oa3.calcularPosicionX(3.)); // Posición a los tres segundos
}
}
6. Ventajas y desventajas del patrón Estrategia.
El patrón Estrategia proporciona cuatro grandes ventajas:
Permite definir familias de algoritmos relacionados, lo que posibilita agrupar funciones comunes y facilitar la reutilización del código.
Es una cómoda alternativa a la subclasificación. Para compartir comportamientos suelen generarse subclases y
redefinir los métodos comunes. Este patrón permite que el comportamiento cambie dinámicamente, en tiempo de
ejecución.Elimina el uso de sentencias condicionales (if, switch/case), cuyo abuso hace difícil de leer el código.
En lugar de comprobar dinámicamente que comportamiento hay que elegir, la elección se hace cuando se crea un objeto Estrategia.Permite al cliente elegir entre diversas implementaciones de una misma operación.
Pese a que [5] señala explícitamente como ventaja del patrón Estrategia la eliminación de sentencias condicionales, creo
que esa ventaja se debe al polimorfismo, no al patrón Estrategia.
Como no podía ser de otra manera, el patrón Estrategia presenta inconvenientes. Los dos más importantes son éstos:
Incrementa el número de objetos que pueden ejecutarse en una aplicación.
Puede sobrecargar innecesariamente la aplicación si algunas implementaciones de los algoritmos no necesitan todos los argumentos
necesitados por una EstrategiaAbstracta. Un método de la estrategia abstracta puede declarar muchos argumentos innecesarios para casi todas
las estrategias concretas, que habrá que pasar cada vez que se escoja una estrategia concreta, sean usados o no por ella.
Por ejemplo, considérese un método hacerAlgo(ArrayList al, HashMap hm, HashSet hs) de una estrategia abstracta. Por estar
declarado en la estrategia abstracta, todas las estrategias concretas deberán implementarlo y deberán recibir los argumentos
al, hm y hs, aun cuando sólo usen uno de ellos. Como dentro de una colección puede haber cientos o miles de objetos, puede
generarse un tráfico de datos innecesario. Una manera de paliar el paso de información innecesaria es aumentar el acoplamiento entre
las estrategias y el contexto, de manera que el segundo delegue en las primeras.Los clientes deben conocer la existencia de las distintas estrategias. Si los clientes ignoran las estrategias o les resultan indiferentes, el uso del patrón resulta inapropiado.
Recursos
[1]
Working with Objects. The OORam Software Engineering Method,
Trygve Reenskaug
,
Prentice Hall
, ISBN
0134529308
[2] A Pattern Definition - Software Patterns,
http://hillside.net/patterns/definition.html
[3]
A Pattern Language : Towns, Buildings, Construction,
Christopher Alexander, Sara Ishikawa y Murray Silverstein
,
Oxford University Press
, ISBN
0195019199
[4]
Pattern-Oriented Software Architecture, Volume 1: A System of Patterns,
Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad, Michael Stal, Peter Sommerlad y Michael Stal
,
John Wiley & Sons
, ISBN
0471958697
[5]
Design Patterns: Elements of Reusable Object-Oriented Software,
Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides
,
Addison-Wesley Pub Co
, ISBN
0201633612
[6] Artículos sobre ingeniería del software en javaHispano,
/articles.list.action?section=5
[7] Tutoriales sobre ingeniería del software en javaHispano,
/tutorials.type.action?type=is
Acerca del autor
Miguel Ángel Abián
Es licenciado en Ciencias Físicas por la U. de Valencia y obtuvola suficiencia investigadora dentro del Dpto. Física Aplicada de la U.V con una tesina
acerca de relatividad general y electromagnetismo. Además, ha realizado diversos cursos
de postgrado sobre bases de datos, lenguajes de programación Web, sistemas Unix, comercio
electrónico, firma electrónica, UML y Java. Ha colaborado en diversos programas de investigación
TIC relacionados con el estudio de fibras ópticas y cristales fotónicos, ha obtenido becas
de investigación del IMPIVA y de la Universidad Politécnica de Valencia y ha publicado
diversos artículos en el IEEE Transactions on Microwave Theory and Techniques relacionados
con el análisis de guías de onda inhomogéneas y guías de onda elípticas.
En el ámbito laboral ha trabajado como gestor de carteras y asesor fiscal para una agencia de bolsa
y actualmente trabaja en el Laboratorio del Mueble Acabado de AIDIMA (Instituto Tecnológico
del Mueble y Afines), ubicado en Paterna (Valencia), en tareas de normalización y certificación,
traducción e interpretación y asesoramiento técnico. En dicho centro se están desarrollando
proyectos europeos de comercio electrónico B2B para la industria del mueble basados en Java y
XML (más información en www.aidima.es). Ha impartido formación en calidad, normalización y
programación para ELKEDE (Grecia), CETEBA (Brasil) y CETIBA (Túnez), entre otros.
Últimamente, aparte de asesorar técnica y financieramente a diversas empresas de
la Comunidad Valenciana, es investigador en los proyectos INTEROP y ATHENA del
Sexto Programa Marco de la Comisión Europea, que pretenden marcar las pautas
para tecnologías de la información en la próxima década y asegurar el liderazgo
de Europa en las tecnologías de la sociedad del conocimiento. Ambos proyectos
tienen como fin la interoperabilidad del software (estudian tecnologías como
J2EE, .Net y CORBA, servicios web, tecnologías orientadas a aspectos, ontologías, etc.), y
en ellos participan empresas como IBM U.K., COMPUTAS, SIEMENS, FIAT, TXT, GRAISOFT, SAP, EADS,
además de numerosas universidades europeas y centros de investigación en ingeniería del software.
Sus intereses actuales son el diseño asistido por ordenador de guías de ondas y cristales
fotónicos, la evolución de la programación orientada a objetos, Java, UEML, el intercambio
electrónico de datos, el surrealismo y París, siempre París.
Reader Comments