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.
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.
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.
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?
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.
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.
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.
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.
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.
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.
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.
Figura F - Patrón del visitante generalizado.
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.
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.
Alberto López Tallón, e-mail: alopez@aqs.es