26. Patrones de Diseño#
Los patrones de diseño son soluciones reutilizables para problemas comunes que surgen en el desarrollo de software. Estos patrones encapsulan buenas prácticas y ofrecen un enfoque probado para resolver problemas recurrentes, facilitando el diseño de sistemas más robustos y mantenibles.
Los patrones no son exclusivos de las ciencias informáticas, sino que también se encuentran en otras disciplinas como la arquitectura, el diseño industrial y la ingeniería. En cada caso, los patrones representan soluciones probadas que se pueden adaptar a diferentes contextos, manteniendo su esencia y efectividad.
Por ejemplo, en arquitectura, un patrón podría ser el diseño de una plaza central en una ciudad, que fomenta la interacción social y el flujo de personas. De manera similar, en el desarrollo de software, los patrones de diseño buscan resolver problemas recurrentes de manera eficiente, promoviendo la reutilización y la estandarización.
Es importante destacar que los patrones no son recetas estrictas, sino guías flexibles que deben ser adaptadas según las necesidades específicas del proyecto. Comprender el contexto y los requisitos es fundamental para aplicar un patrón de manera efectiva y evitar un uso inadecuado que pueda complicar el diseño en lugar de simplificarlo.
“Cada patrón describe un problema que ocurre una y otra vez en nuestro entorno, y luego describe la esencia de la solución de ese problema, de tal manera en que se puede utilizar esta solución más de un millón de veces sin hacerlo igual siquiera dos veces”
26.1. Características principales de los patrones de diseño#
- Reutilizabilidad
Los patrones permiten aplicar soluciones existentes a nuevos problemas, ahorrando tiempo y esfuerzo.
- Flexibilidad
Se pueden personalizar para adaptarse a las necesidades específicas de un proyecto o contexto.
- Comunicación
Proveen un lenguaje común entre desarrolladores, facilitando la colaboración y el entendimiento del diseño.
26.2. Clasificación de los patrones de diseño#
Los patrones de diseño se dividen en tres categorías principales:
- Patrones creacionales
Se centran en la creación de objetos, asegurando que el sistema sea independiente de cómo se crean, componen y representan los objetos. Ejemplos: :
Singleton
Factory Method
Abstract Factory
- Patrones estructurales
Se ocupan de la composición de clases y objetos para formar estructuras más grandes. Ejemplos: :
Adapter
Composite
Decorator
- Patrones de comportamiento
Se enfocan en la interacción y responsabilidad entre objetos. Ejemplos: :
Observer
Strategy
Command
Veremos en más profundidad algunos de estos patrones.
26.3. Patrón Adapter#
Figura 26.1 Patrón Adapter#
El patrón adapter permite reutilizar código heredado o legacy cuya interfaz no coincide con la esperada por el sistema en el que estamos trabajando. Este patrón actúa como un puente entre la interfaz existente y la requerida, permitiendo que componentes incompatibles trabajen juntos sin modificar su código original heredado.
En la figura Diagrama de Clases del Patrón Adapter se observan los siguientes componentes.
Figura 26.2 Diagrama de Clases del Patrón Adapter#
- Cliente
Representa el sistema nuevo que espera una interfaz específica. En este ejemplo se observa que el cliente espera una interfaz que tiene el método
request()
.- Interfaz
Define la interfaz esperada por el sistema nuevo.
- Adaptado
Representa la clase existente con la interfaz incompatible. En este ejemplo se observa que cuenta con el método
specifirequest()
.- Adaptador
Convierte la interfaz del Adaptado. Dentro del método
request()
en el Adapatador se invoca el método específico del Adaptadospecificrequest()
. Eventualmente puede realizar alguna transformación de datos o invocar otros métodos del Adaptado para conseguir querequest()
cumpla con la interfaz esperada.
26.3.1. Cómo Proceder#
Identificar los actores en juego: el Cliente y el Adaptado (componente legacy).
Identificar la Interfaz que requiere el Cliente.
Verificar que el Adaptado que se quiere utilizar puede cumplir con la Interfaz solicitada.
Diseñar un envoltorio (Adaptador) que va a contener al Adaptado.
Implementar el Adaptador para que cumpla con la Interfaz esperada por el Cliente.
El Cliente interactúa con el Adaptador como si fuera el Adaptado.
26.3.2. Ejemplo#
Supongamos que tenemos un robot que realiza mediciones, cuyo sistema de control proporciona los métodos Medir()
que devuelve un par de enteros, donde el primer número representa la distancia en metros y el segundo número la distancia en centímetros. Por ejemplo si la última medición fue de 10,5 m, entonces Medir()
devolverá el par (10, 50)
.
Nuestra empresa ha concretado la venta del robot a un cliente que necesita incorporar el robot a su sistema de producción, pero el sistema de control del cliente espera que el método Medir
devuelva un solo número que represente la distancia en pulgadas.
Identificar los actores en juego: el Cliente y el Adaptado (componente legacy).
Cliente: Sistema de control del cliente.
Adaptado: Robot que realiza mediciones.
Identificar la Interfaz que requiere el Cliente.
Interfaz: Método
Medir()
que devuelve la distancia en pulgadas.
Verificar que el Adaptado que se quiere utilizar puede cumplir con la Interfaz solicitada.
Adaptado: Robot que realiza mediciones con el método
Medir()
que devuelve la distancia en metros y centímetros y se puede convertir a pulgadas.
Diseñar un envoltorio (Adaptador) que va a contener al Adaptado.
1// Adaptador que convierte la interfaz del Adaptado a la interfaz esperada 2type RobotAdaptado struct { 3 adaptado *Robot 4}
Implementar el Adaptador para que cumpla con la Interfaz esperada por el Cliente.
1// Implementación del método requerido por la interfaz esperada 2func (r *RobotAdaptado) Medir() float64 { 3 metros, centimetros := r.adaptado.Medir() 4 totalCentimetros := (metros * 100) + centimetros 5 pulgadas := float64(totalCentimetros) / 2.54 6 return pulgadas 7}
El Cliente interactúa con el Adaptador como si fuera el Adaptado.
1// Cliente 2robot := &Robot{} 3adaptado := &RobotAdaptado{adaptado: robot} 4distancia := adaptado.Medir() 5fmt.Println(distancia) // distancia en pulgadas
En este ejemplo, el Adaptador
RobotAdaptado
convierte la interfaz delRobot
en la interfaz requerida por el Cliente, permitiendo que el sistema de control del cliente pueda utilizar el robot para realizar mediciones sin modificar el código original del robot.
26.4. Patrón Composite#
Figura 26.3 Patrón Composite#
El patrón composite permite tratar tanto a objetos individuales como a composiciones de objetos de manera uniforme. Esto significa que se pueden tratar tanto a un objeto simple como a un grupo de objetos de la misma manera, sin tener que distinguir entre ellos. Lo que simplifica el diseño y la implementación de estructuras jerárquicas de objetos.
Figura 26.4 Diagrama de Clase del Patrón Composite#
- Componente
Define la interfaz común para todos los elementos de la estructura.
- Simple
Representa los elementos individuales de la estructura.
- Compuesto
Representa los elementos que contienen otros elementos. Puede contener tanto objetos Simples como Compuestos. Se debe prever un método para agregar elementos a la colección, ya sea elementos Simple o Compuesto.
26.4.1. Cómo Proceder#
Definir una interfaz común para todos los elementos de la estructura (Componente).
Implementar los tipos de datos que representen los elementos individuales (Simple), asegurándose de que cumplan con la interfaz común (Componente).
Implementar los tipos de datos que representen los elementos compuestos (Compuesto), que contienen una colección de elementos (Componente), asegurándose de que cumplan con la interfaz común (Componente) y contemplen la posibilidad de agregar elementos a la colección.
Tratar tanto a los elementos simples como a los compuestos de manera uniforme, sin tener que distinguir entre ellos.
26.4.2. Ejemplo#
Supongamos que queremos modelar una estructura jerárquica de figuras, donde los elementos pueden ser rectángulos, círculos o triángulos o grupos compuestos. Queremos poder calcular el área total de la estructura, independientemente de si se trata de un rectángulo individual o de un grupo de elementos.
Definir una interfaz común para todos los elementos de la estructura (Componente).
1// Interfaz común para todos los elementos de la estructura 2type Figura interface { 3 Area() float64 4}
Implementar los tipos de datos que representen los elementos individuales (Simple), asegurándose de que cumplan con la interfaz común (Componente).
1// Implementación de los tipos de datos que representan los elementos individuales 2import "math" 3 4type Rectangulo struct { 5 Base float64 6 Altura float64 7} 8 9func (r *Rectangulo) Area() float64 { 10 return r.Base * r.Altura 11} 12 13type Circulo struct { 14 Radio float64 15} 16 17func (c *Circulo) Area() float64 { 18 return math.Pi * c.Radio * c.Radio 19} 20 21type Triangulo struct { 22 Base float64 23 Altura float64 24} 25 26func (t *Triangulo) Area() float64 { 27 return (t.Base * t.Altura) / 2 28}
Implementar los tipos de datos que representen los elementos compuestos (Compuesto), que contienen una colección de elementos (Componente), asegurándose de que cumplan con la interfaz común (Componente) y contemplen la posibilidad de agregar elementos a la colección.
1// Implementación de los tipos de datos que representan los elementos compuestos 2// que contienen una colección de elementos 3type Grupo struct { 4 Figuras []Figura 5} 6 7func (g *Grupo) Area() float64 { 8 var areaTotal float64 9 for _, f := range g.Figuras { 10 areaTotal += f.Area() 11 } 12 return areaTotal 13} 14 15func (g *Grupo) Agregar(f Figura) { 16 g.Figuras = append(g.Figuras, f) 17}
Tratar tanto a los elementos simples como a los compuestos de manera uniforme, sin tener que distinguir entre ellos.
Por ejemplo queremos calcular el área de un tren compuesto por una locomotora y dos vagones, cada uno con su respectiva estructura de figuras.
Figura 26.5 Tren Compuesto de Figuras#
1locomotora := &Grupo{} 2locomotora.Agregar(&Rectangulo{Base: 7, Altura: 3}) //cuerpo de la locomotora 3locomotora.Agregar(&Circulo{Radio: 1}) //rueda de la locomotora 4locomotora.Agregar(&Circulo{Radio: 1}) //rueda de la locomotora 5locomotora.Agregar(&Triangulo{Base: 2, Altura: 4}) //chimenea 6locomotora.Agregar(&Rectangulo{Base: 2, Altura: 3}) //cabina 7 8vagon1 := &Grupo{} 9vagon1.Agregar(&Rectangulo{Base: 7, Altura: 3}) //cuerpo del vagon 10vagon1.Agregar(&Circulo{Radio: 1}) //rueda del vagon 11vagon1.Agregar(&Circulo{Radio: 1}) //rueda del vagon 12 13vagon2 := &Grupo{} 14vagon2.Agregar(&Rectangulo{Base: 7, Altura: 3}) //cuerpo del vagon 15vagon2.Agregar(&Circulo{Radio: 1}) //rueda del vagon 16vagon2.Agregar(&Circulo{Radio: 1}) //rueda del vagon 17 18tren := &Grupo{} 19tren.Agregar(locomotora) 20tren.Agregar(vagon1) 21tren.Agregar(vagon2) 22 23fmt.Println("El área del tren es: ", tren.Area()) //91.84955592153875
26.5. Patrón Iterator#
Figura 26.6 Patrón Iterador#
El patrón Iterator o Iterador permite recorrer los elementos de una colección cualquiera sin exponer su estructura interna. El Iterador declara un conjunto de métodos o funciones para acceder secuencialmente a los elementos. Los métodos más comunes son:
Primero()
Se posiciona el iterador en el primer elemento de la colección
Siguiente()
Avanza el iterador al siguiente elemento
HayMas()
Devuelve
true
si todavía quedan elementos por recorrer en la colección ofalse
en caso contarioActual()
Devuelve el elemento actual donde está el iterador
26.5.1. Cómo Proceder#
Definir el comportamiento del Iterador, es decir los métodos para obtener el siguiente, etc. Es posible agregar más métodos a los mencionados, por ejemplo si necesitamos un iterador que pueda avanzar y retroceder en su recorrido habrá que agregar los métodos correspondientes.
Dentro de la colección definir un método para crear el Iterador
Implementar el Iterador vinculado siempre a una única colección
Recorrer la colección con el iterador creado
26.5.2. Ejemplo#
Supongamos que tenemos una lista enlazada simple y nos interesa crear un iterador que nos permita recorrerla e imprimir cada elemento de la lista. Por simplicidad suponemos que la lista enlazada solo contiene números enteros.
1type Nodo struct {
2 Valor int
3 Siguiente *Nodo
4}
5
6type ListaEnlazada struct {
7 Primero *Nodo
8}
9
10// Método para insertar un elemento al inicio de la lista
11func (l *ListaEnlazada) InsertarAlInicio(valor int) {
12 if l.Primero == nil {
13 l.Primero = &Nodo{Valor: valor}
14 } else {
15 nuevoNodo := &Nodo{Valor: valor, Siguiente: l.Primero}
16 l.Primero = nuevoNodo
17 }
18}
Definir el comportamiento del Iterador, es decir los métodos para obtener el siguiente, etc. Es posible agregar más métodos a los mencionados, por ejemplo si necesitamos un iterador que pueda avanzar y retroceder en su recorrido habrá que agregar los métodos correspondientes.
1// Interfaz del Iterador 2// Define los métodos que debe implementar el iterador 3type Iterador interface { 4 Primero() 5 Siguiente() 6 HayMas() bool 7 Actual() int 8}
Dentro de la colección definir un método para crear el Iterador
1type Nodo struct { 2 Valor int 3 Siguiente *Nodo 4} 5 6type ListaEnlazada struct { 7 Primero *Nodo 8} 9 10// Método para insertar un elemento al inicio de la lista 11func (l *ListaEnlazada) InsertarAlInicio(valor int) { 12 if l.Primero == nil { 13 l.Primero = &Nodo{Valor: valor} 14 } else { 15 nuevoNodo := &Nodo{Valor: valor, Siguiente: l.Primero} 16 l.Primero = nuevoNodo 17 } 18} 19 20// Método para crear un iterador de la lista 21func (l *ListaEnlazada) CrearIterador() Iterador { 22 return &IteradorLista{lista: l, actual: l.Primero} 23}
Implementar el Iterador vinculado siempre a una única colección
1type IteradorLista struct { 2 lista *ListaEnlazada 3 actual *Nodo 4} 5 6func (i IteradorLista) Primero() { 7 i.actual = i.lista.Primero 8} 9 10func (i IteradorLista) Siguiente() { 11 i.actual = i.actual.Siguiente 12} 13 14func (i IteradorLista) HayMas() bool { 15 return i.actual != nil 16} 17 18func (i IteradorLista) Actual() int { 19 return i.actual.Valor 20}
Recorrer la colección con el iterador creado
1lista := &ListaEnlazada{} 2lista.InsertarAlInicio(3) 3lista.InsertarAlInicio(2) 4lista.InsertarAlInicio(1) 5 6iterador := lista.CrearIterador() 7for iterador.Primero(); iterador.HayMas(); iterador.Siguiente() { 8 fmt.Println(iterador.Actual(), " ") 9}
26.6. Ejercicios#
Dada la siguiente definición de una matriz de números enteros:
1type Matriz struct { 2 Filas int 3 Columnas int 4 Datos [][]int 5}
Implementar un iterador que permita recorrer la matriz fila por fila.
Implementar un iterador que permita recorrer la matriz columna por columna.
Los iteradores deben implementar la interfaz ‘Iterador’ definida anteriormente.
Implementar un iterador para recorrer una lista enlazada doblemente, es decir que permita avanzar y retroceder en el recorrido de la lista.
1type IteradorDoble interface { 2 Primero() 3 Siguiente() 4 Anterior() 5 HayMas() bool 6 Actual() int 7}