Patrones de Diseño - Reutilización de ideas
Sumario
Cualquier arquitectura orientada a objetos medianamente compleja está repleta de patrones que se repiten con ligeras variaciones una y otra vez a lo largo de distintos proyectos. La importancia de estudiar y refinar estas estructuras ha sido ampliamente reconocida en otras disciplinas. En particular, Christopher Alexander ha sido uno de los primeros en proponer un lenguaje de patrones en la arquitectura de edificios y ciudades. Estas ideas influyeron en los últimos años masivamente en el diseño de software orientado a objetos creando toda una rama con entidad propia de cuyos resultados se presentan algunos en este artículo.

Por Alberto López
Cualquier desarrollo de software orientado a objetos que pretenda un buen nivel en su diseño resulta una tarea tremendamente exigente. El problema principal radica en la complejidad inherente de los problemas que se pretenden solucionar y la necesidad de analizar y organizar esta complejidad de cara a poder llegar a un diseño que los solucione.

El análisis y diseño orientado a objetos (ADOO) ha sido una importante contribución que efectivamente presta una ayuda sustancial en el alcance de este objetivo, definiendo estrategias, lenguajes y notaciones que sirven de apoyo en el proceso de desenmarañar con un alto nivel de abstracción la complejidad del problema evolucionando hacia su solución.

¿Qué son exactamente los patrones de diseño?

Los patrones de diseño no son una forma más de ADOO, sino una recopilación de los esfuerzos hechos en él que sirve cómo perfecto complemento. Surgen de diseños cuya utilidad o necesidad se ha manifestado repetidamente en unos y otros proyectos y que han ido evolucionando hasta conseguir una cierta estabilidad y generalidad que los convierten en soluciones generalizadas y con ello altamente reutilizables.

Dicho de otra forma, si el ADOO exige una alta creatividad y capacidad de abstracción, los patrones de diseño son todo un saco de soluciones a problemas específicos que ayudan al desarrollador a afrontar este proceso con el mayor éxito posible recopilando en una buena medida la experiencia obtenida por otras personas.

Pero no sólo ayuda en el proceso de diseño de la solución, sino también a la hora de analizar y entender el problema. Un desarrollador con un buen conjunto de patrones en su cabeza será capaz de reconocerlos cuando estudia el problema al que se enfrenta, razón suficiente para estudiar algunos de los más interesantes.

El lector puede pensar que por lo expuesto los patrones son inherentemente geniales o, al menos, complejos. Pero muchas veces ocurre lo contrario: la mayoría son de una simplicidad sorprendente que a veces invita incluso a no hacerles el caso que se merecen.

Patron Observer

En este artículo vamos examinar tres patrones que atacan áreas de problemas muy distintas. El primero es un patrón tan útil como frecuente, lo encontramos, por ejemplo, el modelo de eventos de JDK 1.1.X o en cualquier hoja de cálculo que presenta distintas vistas sobre sus datos.

Objetivo del Observer

Este patrón establece una dependencia de uno a muchos entre un objeto de datos y n objetos (observadores) que representan estos datos, de manera que si desde uno de los observadores se cambian los datos, el objeto de datos notifica este cambio a todos los observadores.

Este patrón también se conoce como el patrón de publicación-suscripción o modelo-vista. Estos nombres sugieren las ideas básicas del patrón, que son bien sencillas: el objeto de datos, llamémoslo "Sujeto" a partir de ahora, contiene métodos mediante los cuales cualquier objeto observador o vista se puede suscribir a él pasándole una referencia a si mismo. El Sujeto mantiene así una lista de las referencias a sus observadores.

Los observadores a su vez están obligados a implementar unos métodos determinados mediante los cuales el Sujeto es capaz de notificar a sus observadores "suscritos" los cambios que sufre para que todos ellos tengan la oportunidad de refrescar el contenido representado. De manera que cuando se produce un cambio en el Sujeto, ejecutado, por ejemplo, por alguno de los observadores, el objeto de datos puede recorrer la lista de observadores avisando a cada uno.

Fig-A.gif (9871 bytes)

Figura A - El patrón de Observer permite muy fácilmente gestionar varias vistas (Observers) sobre un mismo conjunto de datos (Subject) y sincronizar unos con otros.



El ejemplo de uso típico de este patrón es el de un objeto de datos con información a modo de hoja de cálculo que es representada por distintas vistas en forma de tablas de valores, diagrama de barras y diagrama de tarta. Si modificamos, por ejemplo, uno de los valores de la tabla, los diagramas deben reflejar este cambio.

Un ejemplo muy distinto en su aplicación, pero muy similar en su estructura lo encontramos en el modelo de eventos nuevo del JDK 1.1.X. Aquí los sujetos se corresponden con los objetos capaces de emitir eventos y los observadores con los objetos interesados en recibir estos eventos que se suscriben a los primeros.

Así por ejemplo, si queremos que un campo de texto se borre al pulsar un determinado botón sería muy natural "suscribir" este campo de texto al botón de manera que cuando reciba una notificación de cambio de estado del botón, es decir, que éste se haya pulsado reaccione borrando su contenido. Podríamos suscribir tantos elementos de interface al botón como quiesiéramos de manera que todos ellos realicen las acciones que crean oportunas o ignoren el evento.

Podemos extender el esquema del Observer con todo un conjunto de métodos de notificación para discriminar tipos de cambio de estado, con el paso de determinados parámetros, etc. Pero lo más importante es que podemos observar, valga la redundancia, con estos dos ejemplos cómo problemas aparentemente totalmente distintos se pueden modelar con el mismo patrón de comportamiento, siendo éste además realmente sencillo. ¿Quién pensaría a priori que la representación de datos en una hoja de cálculo y la gestión de eventos en un interface gráfico se pueden solucionar de manera básicamente similar?

Aplicabilidad de Observer

El patrón Observer se puede aplicar en las siguientes situaciones:

Patrón Composite

El siguiente patrón es un patrón que se aplica en estructuras de contenedor-contenido, por ejemplo, componentes de interfaces gráficos.

Objetivo de Composite

Esta problemática la encontramos tanto a "bajo nivel" en interfaces gráficos, es decir, a nivel componentes básicos como botones, cajas de texto, ventanas, etc., como a "alto nivel" en las aplicaciones que se realizan con estos componentes, por ejemplo, en un editor de diagramas dónde se trata de programar la lógica de los elementos y bloques de elementos que componen el diagrama.

En ambos casos surge un problema: si se sigue la idea intuitiva de crear tipos de clases particularizadas para las funcionalidades de contenedor y componente, el código que las maneja ha de tratarlas también de manera diferente, con las complejidades que eso entraña.

Fig-B.gif (5158 bytes)

Figura B - El patrón de Composite, compuesto en castellano, ataca la creación de estructuras compuestas por componentes que a su vez sean capaces de actuar como contenedores manteniendo un esquema de uso uniforme para los objetos individuales y compuestos.



El patrón de Composite propone una solución basada en composición recursiva que evita esta distinción simplificando con ello mucho el tratamiento de las clases.

La idea fundamental en este patrón es una clase base abstracta común que representa las características de contenedores y componentes, en un entorno gráfico, por ejemplo, una de estas características sería que ambos tipos de objetos han de saber dibujarse a si mismos. Es decir, han de soportar una función al estilo de dibujar().

Si asumimos que componentes pueden actuar a su vez como contenedores, es decir, si un componente tipo botón para un interface gráfico puede actuar como contenedor de cara a albergar un icono o imagen queda claro que la clase abstracta debería incluir asimismo una función de añadir(Componente). El resultado final de estas ideas se puede apreciar en la Figura B.

Aquí la clase componente asume esa dualidad entre contenedor de componentes y componente en sí. La nota acerca del método operación() es especialmente interesante, refleja muy bien la ventaja del esquema recursivo en este caso: si sobre un contenedor se actúa de una forma que afecta de manera análoga a sus componentes, éste puede delegar el trabajo recursivamente a estos.

Fig-C.gif (8970 bytes)

Figura C - Ejemplo concreto que utiliza Composite, se trata concretamente de algunas clases de la librería AWT para interfaces gráficos en el JDK 1.1.X. Obsérvense las ligeras variaciones con respecto al patrón de la Figura B, así por ejemplo, el componente no dispone de funciones de añadir e eliminar componentes.



El caso se da, por ejemplo, para movimientos. Si operacion() fuera mover(dx,dy), dónde dx y dy denotan los pixels horizontales y verticales de desplazamiento, una ventana sobre la que se invoca esta operación podría implementar el movimiento repintándose en la nueva posición e invocando sucesivamente los métodos mover(dx,dy) de sus componentes que se repintarían a si mismo en la posición correcta e invocarían a su vez a mover(dx,dy) de sus componentes y así sucesivamente.

Aplicabilidad de Composite

El patrón de Composite su puede utilizar cuando: Como ya expone el párrafo sobre la aplicabilidad de este patrón, facilidad la creación de objetos más complejos a partir de un conjunto objetos individuales manteniendo unas pautas uniformes en su tratamiento que generalizan su uso facilitando así enormemente su manejo por los clientes.

Aunque nos vinculamos mucho al ámbito gráfico en el ejemplo, hay que resaltar que existen otros problemas totalmente distintos dónde resulta igual de útil. Imaginemos, por ejemplo, la construcción de componentes electrónicos y la simulación de sus características como potencia total consumida, etc.

Patrón Visitor

Los patrones anteriores son tan sencillos de entender como frecuentes de aplicar, el último patrón, el Visitor (visitante), no es de un uso tan amplio, pero de un carácter especialmente curioso y sorprendente en su diseño.

Objetivo de Visitor

Lo que intenta el visitante es representar una operación a realizar sobre los elementos de una estructura de objetos. Consigue el objetivo sorprendente de permitir la definición de una nueva operación, ¡sin cambiar las clases de los elementos sobre los que opera!

Un ejemplo concreto de la utilidad de esto puede ser un compilador que representa programas como árboles sintácticos abstractos.

Tendrá que definir operaciones para la comprobación de tipos, comprobación de variables no inicializadas, generar código, etc.

Fig-D.gif (5233 bytes)

Figura D - Diseño a priori de los nodos sintácticos que utilizaría el hipotético compilador utilizado descrito en el artículo. Solamente se recogen dos tipos de nodos, un diseño real incluiría lógicamente a un número de nodos mucho mayor.



Estas funcionalidades básicas se podrían ver ampliadas sucesivamente para mejorar las prestaciones del compilador, por ejemplo, resaltando la sintaxis, calculando determinadas estadísticas, etc.

La mayoría de estas operaciones tendrán que tratar a los distintos tipos de nodos de manera diferente, es decir, estas operaciones se realizarán de manera distinta en nodos de asignación que en nodos de llamada a función, Figura D.

El problema con la estructura de la Figura D es que distribuir las operaciones nuevas entre todas las clases es una tarea ardua que lleva además a un diseño difícil de entender, mantener y cambiar. Además, añadir una nueva operación requerirá la recompilación de todas las clases existentes.

Ante este escenario sería entonces deseable que se pudieran añadir las nuevas operaciones de una forma independiente, es decir, sin provocar cambios en la estructura de objetos existente.

¿Cómo resolver esta aparente paradoja? La solución consiste en encapsular las operaciones relacionadas en un visitante y pasar este visitante como argumento a los distintos elementos de la estructura de objetos, en el ejemplo los nodos.

Fig-E.gif (11350 bytes)

Figura E - Diseño de los nodos sintácticos conforme al patrón del vistante.



Cuando un nodo acepta al visitante, llama a un método que guarda una estrecha relación con su nombre a través de la referencia a ese visitante pasada como argumento en el método de aceptación. De manera que el visitante ejecuta un método específico para la clase del elemento.

Con el objetivo de hacer funcionar los visitantes para más de una operación necesitamos una clase padre abstracta para todos los visitantes de los distintos elementos con operaciones cuyo nombre refleje las clase visitadas. Una aplicación que quiera añadir una operación nueva definirá una nueva subclase de las clase visitante abstracta evitando con ello añadir código específico a la aplicación en sí.

Con el patrón del visitante, el usuario define dos tipos de jerarquías: una para los elementos sobre los que se opera y otra para los visitantes que definen las operaciones sobre los elementos. Se crean nuevas operaciones añadiendo subclases nuevas a la jerarquía de visitantes, Figura E.

Fig-F.gif (10606 bytes)

Figura F - Patrón del visitante generalizado.


Aplicabilidad de Visitante

El patrón del visitante se debe utilizar cuando: El patrón del visitante, sin embargo, no es apto para los casos en los que la estructura de objeto en sí cambie con cierta frecuencia puesto que el coste asociado sería la redefinición del interface en todos los visitantes. En este caso sería probablemente mejor definir las operaciones dentro de las clases de la aplicación.

Conclusiones

Los patrones de diseño proporcionan al desarrollador un bagaje importante para enfrentarse a muchas problemáticas que aparecen con frecuencia en su trabajo a la vez que le facilitan la labor de organizar sus ideas proporcionándolo piezas genéricas de soluciones particulares que allanan el camino hacia la solución total del problema.

No en vano, el desarrollador dotado de un buen número de patrones los reconocerá continuamente en las distintas librerías de calidad que pueda utilizar. Las JFCs de Sun son un buen ejemplo de ello, están repletas.

Aquí en este artículo solo hemos podido rascar la superficie de este campo, pero espero que el lector haya podido apreciar su utilidad y esté motivando para seguir coleccionando nuevos patrones por cuenta propia.

Bibliografía

[1] Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides. Design Patterns – Elements of Reusable Object-Oriented Software. Addison Wesley 1995.

El "clásico" por excelencia, o sea, la típica referencia que todo el mundo que algo conoce de este campo suele dar. La presentación de los 23 patrones es clara, inteligente y consigue un equilibrio acertado entre la abstracción de la exposición de cada uno y la concreción en el ejemplo que no deja al lector "pegado" éste, sino que le permite ver estructura del problema de fondo que ataca cada patrón. Sin duda es la fuente principal de este artículo.

Mi primera impresión al leer el libro hace unos dos años fue que había unos pocos patrones interesantes, como el Observer, pero que el resto no merecía demasiada atención, opinión que cambio sustancialmente conforme he seguido trabajando posteriormente en mis proyectos.

Quizás el punto más débil del libro sea su fecha de publicación y su marcada orientación a C++, desde 1995 han surgido muchos nuevos patrones interesantes y no está claro por tanto que el libro refleje los más interesantes en la actualidad.

[2] Martin Fowler. Analysis Patterns : Reusable Object Models. Adison Wesley 1996.

Este libro es el complemento al anterior, concentrándose como indica su título en la fase de análisis de distintos dominios de problemas como el área financiera o de salud.

[3] Design Patterns Home Page. http://hillside.net/patterns/patterns.html

Una de las páginas dedicadas más importantes.

[4] Cetus Links. http://www.objenv.com/cetus/software.html

Esta página contiene una colección enorme de excelente calidad y bien organizada de todo tipo de temas que giran alrededor de la orientación a objetos, entre ellos, patrones de diseño.

Autor

Este artículo ha sido publicado originalmente en la revista RPP - Revista Profesional de Programadores en la edición de Septiembre de 1998.

Alberto López Tallón, e-mail: alopez@aqs.es