Head First Design Patterns by Eric & Elisabeth Freeman, es un interesante libro donde es posible ampliar de una manera muy ilustrativa, visual, didáctica y alternativa, el horizonte como desarrolladores de software.
Con un estilo muy personal y divertido, el libro resalta un objetivo principal como es el conocer algunos de los patrones de diseño de software más importantes, saber que existen, y tratar de aplicarlos ordenadamente sin forzar nunca su uso.
Comenta que los patrones de diseño no tienen que estar nunca dirigidos directamente al código, inicialmente deben ser absorbidos por la mente del desarrollador, y una vez que está cargada de buenos patrones de conocimiento que funcionan muy bien, entonces se está en buena disposición para aplicarlos a los nuevos diseños, animando a rehacer código antiguo cuando se observa que se están convirtiendo en un producto poco mantenible y poco flexible.
Conocer los patrones permite además compartir un vocabulario común con el resto del equipo de desarroladores, consiguiendo pensar a nivel de patrones en lugar de a nivel de objetos, a la hora de diseñar arquitecturas de aplicaciones.
Además de los conocidos principios básicos de programación orientada a objetos (abstracción, encapsulación, polimorfismo y herencia, los cuales el libro mantiene que no es suficiente conocerlos para realizar buenos diseños), resalta una serie de buenas prácticas en programación que permiten conseguir diseños más profesionales y que acepten mejor el cambio, algo que no es obvio y que solamente se consigue con la experiencia:
- Separar y encapsular lo que cambia de la parte que se mantiene estable.
- Programar hacia interfaces, no hacia implementaciones. En otras palabras, significa programar hacia un supertipo (clase abstracta o interface).
- Preferir la composición frente a la herencia. La herencia facilita la reutilización de código, pero la composición permite construir diseños más flexibles que favorecen el mantenimiento posterior. Mientras que la herencia es estática en tiempo de diseño, la composición puede ser dinámica en tiempo de ejecución.
- Buscar fervientemente diseños no acoplados, tratando de conseguir que un objeto conozcan la mínima información posible de otros objetos con los que interactúa.
- Una clase debería estar cerrada a modificaciones y abierta a extensiones de funcionalidades.
- Depender de abstracciones, no de clases concretas.
- Reducir las interacciones con demasiados objetos. Es decir, prevenir el uso de multitud de clases acopladas mediante dependencias, ya que será un diseño frágil y costoso de mantener.
- Una clase solamente debería tener una razón para cambiar, o en otras palabras, una clase solo debe de tener una única responsabilidad. A esto se le conoce como cohesión y favorece el mantenimiento del código a futuro.
Patrón STRATEGY
Dicha familia de algoritmos son encapsulados individualmente, y a la vez, se corresponden de manera común con un interfaz, el cual permitirá gestionarlos de manera unificada.
Este patrón permite a los algoritmos que puedan ser modificados de manera independiente a los clientes que los utilizan, consiguiendo con ello el alcanzar un diseño más fácil de mantener en el futuro.
En el siguiente ejemplo, se muestra una jerarquía de clases Duck, a las cuales se les ha extraído y encapsulado de manera externa la parte del código que varía, esto es por ejemplo, la manera que cada pato vuela (existiendo incluso tipos de pato de goma o de adorno, los cuales no pueden volar).
Se puede observar que el comportamiento de volar se ha encapsulado en una jerarquía de clases dirigidas por un interfaz común, el cual es utilizado por el cliente (Duck) para definir la manera en la que cada subclase de Duck vuela:
El patrón Observer define una relación de dependencia uno-a-varios entre objetos, de manera que cuando uno cambia de estado, todos los objetos dependientes son notificados y actualizados automáticamente.
Se entiende fácilmente cuando pensamos en una suscripción a un periódico digital de noticias, en la que cuando se produce una nueva noticia importante, todos los subcriptores al periódico son notificados en tiempo real del nuevo evento producido.
Un detalle interesante en la utilización de este patrón es que existe dos modos de uso en lo que se refiere al modo de enviar la información de una nueva notificación a los suscriptores, el modelo Push o Pull.
Utilizar el modelo Push significa que el objeto que emite la notificación envía a los suscriptores toda la información de su estado (push), y son los suscriptores los que deciden que parte de dicha información les interesa descartando el resto. Además el objeto observado (publisher) necesita conocer detalles de los objetos observadores para enviarles la información.
Un modo distinto de hacer las cosas es utilizar el modelo Pull, de manera que el objeto obervado, cuando emite una nueva notificación, solamente se lo hace saber a los observadores los cuales a continuación, tienen la posibilidad de extraer (pull) la información que realmente les interesa del objeto observado mediante un conjunto de getter methods controlados. De esta manera, los objetos observadores dejan de recibir información que quizás no les sea de utilidad, y además, el objeto observado ya no necesita saber detalles concretos de como enviar la información a sus subcriptores permitiendo un desacoplo mayor.
Se entiende fácilmente cuando pensamos en una suscripción a un periódico digital de noticias, en la que cuando se produce una nueva noticia importante, todos los subcriptores al periódico son notificados en tiempo real del nuevo evento producido.
![]() |
Observer Pattern |
Un detalle interesante en la utilización de este patrón es que existe dos modos de uso en lo que se refiere al modo de enviar la información de una nueva notificación a los suscriptores, el modelo Push o Pull.
Utilizar el modelo Push significa que el objeto que emite la notificación envía a los suscriptores toda la información de su estado (push), y son los suscriptores los que deciden que parte de dicha información les interesa descartando el resto. Además el objeto observado (publisher) necesita conocer detalles de los objetos observadores para enviarles la información.
Un modo distinto de hacer las cosas es utilizar el modelo Pull, de manera que el objeto obervado, cuando emite una nueva notificación, solamente se lo hace saber a los observadores los cuales a continuación, tienen la posibilidad de extraer (pull) la información que realmente les interesa del objeto observado mediante un conjunto de getter methods controlados. De esta manera, los objetos observadores dejan de recibir información que quizás no les sea de utilidad, y además, el objeto observado ya no necesita saber detalles concretos de como enviar la información a sus subcriptores permitiendo un desacoplo mayor.
Patrón DECORATOR
El patrón Decorator permite adjuntar responsabilidades adicionales a un objeto en tiempo de ejecución. Los decoradores son una alternativa flexible y dinámica a la herencia para extender funcionalidades.
Truco: Es posible pensar en los objetos decoradores como envoltorios de un objeto original al que se le van añadiendo nuevas funcionalidades:
El patrón de diseño se construye en la premisa de que los objetos Decoradores son del mismo tipo que los objetos que decoran, utilizando para ello la herencia.
Sin embargo, la funcionalidad se consigue mediante la composición, utilizando en los objetos decoradores una variable que hace referencia al objeto que es decorado. En la imagen siguiente se puede comprobar como las clases concretas de CondimentDecorator tienen una referencia a Beverage sobre la que aplican el nuevo comportamiento:
Premisas a tener en cuenta:
Truco: Es posible pensar en los objetos decoradores como envoltorios de un objeto original al que se le van añadiendo nuevas funcionalidades:
![]() |
Cálculo del coste final de un café "DarkRoast" más suplementos |
El patrón de diseño se construye en la premisa de que los objetos Decoradores son del mismo tipo que los objetos que decoran, utilizando para ello la herencia.
Sin embargo, la funcionalidad se consigue mediante la composición, utilizando en los objetos decoradores una variable que hace referencia al objeto que es decorado. En la imagen siguiente se puede comprobar como las clases concretas de CondimentDecorator tienen una referencia a Beverage sobre la que aplican el nuevo comportamiento:
Premisas a tener en cuenta:
- Es posible utilizar uno o varios decoradores para envolver un objeto.
- Dado que un decorador es del mismo tipo que el objeto que decora, es posible trabajar polimórficamente con objetos decorados y objetos originales.
- El decorador añade nuevo comportamiento antes y/o después de que el objeto que es decorado realice su trabajo.
- Los objetos pueden ser decorados en cualquier momento, con lo que se permite trabajar dinámicamente en tiempo de ejecución, con todos los decoradores que sean necesarios.
Patrón FACTORY
El patrón Factory es usualmente desglosado en dos tipos de patrones denominados Factory Method y Abstract Factory. Ambos patrones permiten la encapsulación de la instanciacion de objetos y conseguir diseños más flesibles y desacoplados.
No obstante existe una aproximación más asequible denominada Simple Factory, la cual no se considera que sea un patrón de diseño, pero es ampliamente usado. Permite encapsular nuevamente la parte del código que varía, y así desacoplar el cliente que usa el código de la implementación de las clases concretas (new) que se utilizarán finalmente.
Así, el consumidor de los productos de la factoría (PizzStore en la ilustración siguiente), se compone de un objeto Factoría Simple, el cual contiene un método que permite la instanciación del tipo de producto (Pizza) que se desee. Este "método creación", por comodidad, se suele declarar estático para evitar crear instancias del objeto Factoría, es por esto que en ocasiones esta técnica recibe el nombre de Factoría Estática.
Lo que se consigue con esta estrategia es ubicar en un mismo lugar la zona de instanciación de objetos que es susceptible de variaciones y modificaciones, favoreciendo el mantenimiento, además de permitir que varios clientes de la factoría puedan beneficiarse de ella sin realizar duplicaciones de código.
![]() |
Factoría Simple |
Una primera acepción del patrón Factory lo representa el denominado Factory Method o patrón de método creación. Este patrón permite manejar la creación de un único tipo de producto, dejando a las subclases la responsabilidad de decidir que tipo de objeto crear finalmente.
Dicho de otra manera, se define un interfaz para la creación de objetos, dejando a las subclases de este interfaz decidir que clase concreta crear.
Para ello, el objeto Factory, es representado mediante una clase abstracta la cual declara también el método de creación abstracto (Factory Method), de manera que la subclases tienen la responsabilidad de implementar este método de manera concreta para crear el tipo de productos adecuado.
Frecuentemente, el objeto Factory, contiene código que trabaja de manera genérica con una clase abstracta Producto, de manera que realmente no conoce que tipo de producto concreto una subclase concreta ha instanciado (poliformismo). En el ejemplo siguiente, la clase abstracta Factory es "PizzaStore", la cual, además de definir el método de creación (createPizza) que la subclases deben concretar, puede trabajar de manera polimorfica con objetos Pizza sin conocer que tipo de pizza concreta es dentro de la jerarquía de clases Producto.
![]() |
Patrón Factoy Method |
Principio de Inversión de Dependencia.
Se trata de uno de los principios SOLID de Robert C. Martin, que nos recuerda que las clases deben depender de abstracciones y no de implementaciones o clases concretas. De esta manera se consigue que una clase interactue con otras clases sin conocerse directamente y así favorecer el mantenimiento del código. Para ello:
- Tratar de evitar que una variable tenga una referencia a una clase concreta (new). Utilizar una factoría para evitar esto.
- Tratar de evitar que una clase derive de una clase concreta. La alternativa es derivar de abstracciones como un interface o una clase abstracta.
- A ser posible, no sobreescribir un método de una clase base. Esto significa que dicha clase base no es exactamente una abstracción.
Una segunda acepción de patrón Factory es el denominado Abstract Factory. Este patrón provee una interfaz que permite crear familias de productos sin especificar clases concretas. (A diferencia del Factory Method que provee un interfaz para crear un único producto).
Desde la factoría abstracta, podemos derivar una o varias factorías concretas, las cuales son capaces de crear los mismos productos pero con diferentes implementaciones. De esta forma, el cliente que utiliza una factoría abstracta, puede recibir distintas instancias concretas de la factoría, consiguiendo generar los mismos productos pero en diferentes versiones según interese.
La ventaja de todo esto es que el código del cliente no varía y se mantiene siempre igual, estando desacoplado de las familias de productos concretos.
![]() |
Patrón Abstract Factory |
Resumen:
- Factory Method: Útil para desacoplar código de clases concretas. Basta con hacer una subclase de la clase abstracta e implementar el Factory Method que contiene.
- Abstract Factory: Útil cuando se disponga de familias de productos relacionados, y se necesiten distintas versiones de los mismos.
Patrón SINGLETON
El patrón Singleton permite crear una única instancia de un objeto y provee un punto de acceso global a la misma. Esto es muy útil en ciertas situaciones en las que necesitamos solamente un objeto porque de lo contrario, podrían aparecer problemas relacionados con comportamientos entraños, uso conflictivo de recursos o resultados inconsistentes.
Es muy probable que interese tener solamente un único objeto que se encargue de gestionar por ejemplo una thread pool común, una única conexión de comunicación, la información de preferencias en el registro, log de eventos, objetos que gestionan impresoras, tarjetas gráficas, etc..
Un detalle importante a tener en cuenta, dado que se pretende crear una instancia única del objeto, es la gestión del mismo mediante multitarea. Existe el riesgo de que se creen varias instancias cuando hay varios hilos de ejecución simultáneos que necesitan acceder a ella. Para ello es necesario establecer una sincronización, de manera que no es posible acceder simultáneamente desde dos hilos a la misma instancia:
public class Singleton {
private static Singleton intanciaUnica;
//Más variables de instancia útiles aquí
//Constructor privado
private Singleton() {}
public static synchronized Singleton getInstancia() {
if(intanciaUnica == null) {
instanciaUnica = new Singleton();
}
return instanciaUnica;
}
//Otros métodos útiles aquí
}
Dado que la sincronización de un método es cara en términos de rendimiento, si el funcionamiento de la aplicación se ve afectada significativamente, es posible insertar algunas modificaciones para conseguir reducir su uso, de manera que solamente se realiza la sincronización la primera vez que se instancia la variable:
public class Singleton {
private volatile static Singleton intanciaUnica;
//Más variables de instancia útiles aquí
//Constructor privado
private Singleton() {}
public static Singleton getInstancia() {
if(intanciaUnica == null) {
synchronized (Singleton.class) {
if(intanciaUnica == null) {
instanciaUnica = new Singleton();
}
}
}
return instanciaUnica;
}
//Otros métodos útiles aquí
}
Patrón COMMAND
El patrón Command permite desacoplar un objeto que solicita realizar una acción, del objeto que llevará a cabo dicha acción. Es decir, el objeto requester, no conoce los detalles de como se ejecutará la acción, solamente solicita que se realice.
Por ejemplo, podemos pensar en un control remoto cuyas teclas son configurables para realizar acciones muy diferentes. Pulsando un determinado botón puede realizarse una determinada tarea, pero si interesa, al cabo del tiempo ese mismo botón puede configurarse para realizar otro tipo de tarea. El patrón permite el desvincular el hecho de pulsar un botón en el invocador, con la acción final que lleva asociada y que es ejecutada por el receptor.
![]() |
Patrón Command |
En la imagen, el cliente tiene la responsabilidad de crear los comandos concretos (ConcreteCommand basados en el interface Command), y de crear también el receptor de las acciones a realizar. El invocador de la petición (que desconoce los detalles de como realizarla) se encarga de almacenar el listado de comandos que pueden invocarse (setCommand). Además, cuando se le ordene, llamara al método execute() de cualquiera de los comandos de que dispone.
El receptor es quien conoce todos los detalles para ejecutar la solicitud recibida. El comando concreto, que implementa el interface común (a través del cual el invocador los maneja a todos por igual), establece el enlace entre una petición y el receptor que debe ejecutarla.
Es posible introducir la idea de poder ejecutar macros de comandos, es decir, varios comandos agrupados baja la misma petición. Para ello, se construiría una MacroCommand que implementase el interfaz Command, y que permitiese llamar al método execute() de un array de comandos.
Los comandos ofrecen una manera de encapsular la invocación de métodos (receptor + conjunto de acciones sobre el mismo), lo que podría llamarse trabajos (jobs). Una aplicación práctica de este patrón podría implementarse en una cola de trabajos (jobs queue). Por un extremo de la cola, los trabajos (commands) son añadidos, mientras que al otro extremo de la cola se sitúan un conjunto de hilos (threads), los cuales extraen un comando de la cola, llaman a sus respectiva método execute(), esperan a que el comando finalice, descartan el comando y extraen de la cola el siguiente.
De esta manera el hilo que ejecuta el trabajo está totalmente desacoplado de las acciones que llevan implícitas cada uno de estos trabajos. Así, puede estar ejecutando un cálculo matemático, una descarga de un recurso de la red o cualquier otro tipo de tarea.
Patrón ADAPTER / FACADE
El patrón adapter convierte un interfaz de una clase en otra que la clase cliente espera. Un adaptador permite que dos clases puedan trabajar juntas y que de otra manera no podrían por tener interfaces distintos.
Es decir, este patrón permite que un cliente utilice una clase con un interfaz incompatible, creando un adaptador que realiza la conversión.
En cambio el patrón facade provee un interfaz común que gestiona los distintos interfaces que forman un determinado subsistema. Facade define un interfaz de alto nivel que hace que sea más fácil de utilizar el subsistema subyacente.
Una fachada no solamente consiste en un interface simplificado, sino que desacopla un cliente de los componentes del subsistema.
Aunque adaptadores y fachadas sirven de envoltorio a multiples clases, la diferencia fundamental es que la intención del patrón facade es la de simplificar, mientras que la finalidad del adaptador es convertir un interfaz en otro diferente.
Patrón TEMPLATE METHOD
El patrón método plantilla (Template Method), permite definir un método, que contiene el esqueleto de un algoritmo formado por una serie de pasos. La clave es que algunos de estos pasos es delegada su implementación a las subclases, de manera que cada una de estas subclases introduce ligeras modificaciones al algoritmo definido en el método plantilla, y cuya estructura es común a todas.
La clave es que alguno de estos pasos que conforman el algoritmo es definido en la clase que alberga el método plantilla como abstracto, con lo que la responsabilidad de definirlo de manera apropiada queda delegado a las subclases.
![]() |
Ejemplo de TemplateMethod compuesto de dos pasos, primitiveOperation1 y 2. |
Un concepto interesante es el de los "hooks". Un gancho o hook es un método concreto (no abstracto) definido en la clase abstracta donde se sitúa el TemplateMethod, y el cual forma parte del conjunto de pasos que definen el algoritmo del TemplateMethod. Lo interesante es que este método puede ser sobreescrito por las subclases, de manera que pueden modificar el comportamiento según interese. Es más, en muchas ocasiones, los métodos hook son definidos en la clases abstracta vacíos, de manera que una subclase puede decidir si sobreescribirlo o no, alterando o no la ejecución del algoritmo en el método template:
abstract class AbstractClass {
//Método plantilla con los diferentes pasos del algoritmo. final void TemplateMethod() {
Operacion_1();
Operacion_2();
Operacion_Concreta();
Hook();
}
//Métodos cuya implementación se delega obligatoriamente a las subclases. abstract void Operacion_1();
abstract void Operacion_2();
//Método implementado en la clase abstracta. final void Operacion_Concreta() {
//Implementación aquí
}
//Método gancho. Implementación concreta que no hace nada.
//La implementación por parte de las subclases es opcional.
void Hook() { }
}
En definitiva, el patrón Template Method nos ofrece una técnica muy interesante para reutilizar código. Encapsular algoritmos es posible también hacerlo mediante el patrón Strategy. Mientras que Template Method utiliza la herencia para ello, el patrón Strategy utiliza la composición.
Patrón Modelo-Vista-Controlador (MVC)
Modelo-Vista-Controlador es un patrón compuesto a su vez del patrón Observer, Strategy y Composite.
El Modelo hace uso del patrón Observer para mantener a sus observadores actualizados y desacoplados al mismo tiempo.
El Controlador es la estrategia (Strategy) utilizada por la Vista. La Vista, puede por tanto utilizar diferentes implementaciones del Controlador para obtener diferentes comportamientos.
La Vista utiliza el patrón Composite para implementar el interfaz de usuario, el cual normalmente consiste en diferentes componentes anidados como los paneles, botones o cajas de texto.
Estos patrones trabajan juntos para mantener desacopladas estas tres capas en el modelo MVC, el cual mantiene el diseño claro y simple.
El patrón Adapter puede ser utilizado para adaptar un nuevo Modelo a una Vista y Controlador ya existente.
RESUMEN
El catálogo de patrones expuestos los podemos agrupar en varias categorías.
Estas son De Creación, donde se realiza la instanciación de objetos y se provee una manera de desacoplar el cliente de los objetos que necesita crear. De Comportamiento, donde se agrupan los patrones que indican como las clases y objetos se distribuyen las responsabilidades, y Estructurales, donde se indica como componer clases u objetos en estructuras más grandes.
Así tenemos:
Patrones de Creación
- Singleton: Se asegura que solamente se crea un único objeto.
- Factory Method: Las subclases deciden que clase concreta crear.
- Abstract Factory: Permite a un cliente crear familias de objetos sin especificar sus clases concretas.
Patrones de Comportamiento
- Template Method: Las subclases deciden cómo implementar partes concretas de un determinado algoritmo.
- Iterator: Provee una manera de recorrer una colección de objetos sin exponer su implementación.
- Command: Encapsula una orden como un objeto.
- Observer: Permite a los objetos ser notificados cuando un cambio de estado ha ocurrido.
- State: Encapsula comportamientos basados en un estado concreto y usa la delegación para saltar entre comportamientos.
- Strategy: Encapsula comportamientos intercambiables y usa la delegación para decidir cual utilizar finalmente.
Patrones Estructurales
- Decorator: Envuelve un objeto y le provee de nuevo comportamiento.
- Proxy: Envuelve un objeto para controlar el acceso al mismo.
- Composite: Permite que un cliente pueda tratar colecciones de objetos y objetos individuales de manera uniforme.
- Facade: Simplifica la intefaz de un conjunto de clases.
- Adapter: Envuelve un objeto y provee un interfaz diferente para él.
Otra agrupación de patrones se realiza según el criterio si trabajan con clases u objetos:
- Clases (Template Method, Factory Method, Adapter): Describen las relaciones entre clases definidas por herencia. Estas relaciones se establecen en tiempo de compilación.
- Objetos (Composite, Decorator, Observer, Strategy..): Describen las relaciones entre objetos y son definidas por composición. Estas relaciones se crean en tiempo de ejecución y son más dinámicas y flexibles.
- Mantener las cosas simples (KISS). El objetivo es simplicidad, no aplicar patrones forzadamente.
- La experiencia y el conocimiento permiten utilizar patrones de diseño a un diseño, estudiando siempre las consecuencias que implica.
- En refactorizaciones puede ser un buen momento para valorar la utilización de patrones, ya que cuando se realizan cambios en el código para reorganizarlo, mejorando su estructura y no su comportamiento.
- La posibilidad de eliminar patrones para simplicar tambien es una opción.
- Si no es necesario hoy, resistir la tentación de hacerlo ahora, es un pensamiento hipotético que puede que nunca se llegue a utilizar.
- En definitiva, centrar el pensamiento en el diseño, no en patrones. Hay que utilizarlos solamente cuando hay una necesidad natural.
![]() |
Ralph Johnson: "Go for simplicity and don't become over-excited" |
Referencias:
- Libro "Head First Design Patterns". Eric Freeman, Elisabeth Freeman.
No hay comentarios:
Publicar un comentario
Por favor, si consideras necesario realizar algún aporte, feel free to do it!!