Skip to content

Clase 10 — Tema 7: Patrones de Diseño de Software (Gang of Four)

Resumen Ejecutivo

Inicio del Tema 7: patrones de diseño detallado de software. El profesor aclara la diferencia entre patrones de arquitectura (Temas 5 y 6, nivel sistema/componentes) y patrones de diseño (Tema 7, nivel clase/código). Se trabaja con el catálogo Gang of Four (Gamma, Helm, Johnson, Vlissides), clasificado en tres categorías: creacionales, estructurales y de comportamiento. Se repasan en detalle: Fábrica Abstracta, Builder, Prototype y Singleton (creacionales); Adaptador, Composite, Fachada y Proxy (estructurales); y Observador (comportamiento). Punto clave para el trabajo de la asignatura: solo se pueden utilizar patrones vistos en clase y deben identificarse por nombre y catálogo. ⚠️ EXAMEN: distinguir arquitectura de diseño detallado, conocer los tres grupos del catálogo GoF y saber qué problema resuelve cada patrón.

Conceptos Clave

  • Patrones de diseño vs patrones de arquitectura: Los de arquitectura organizan el sistema a nivel de componentes/distribución. Los de diseño trabajan a nivel de clases y código — microarquitecturas. ⚠️ EXAMEN
  • Gang of Four (GoF): Gamma, Helm, Johnson y Vlissides. Catálogo canónico con tres categorías: creacionales, estructurales y de comportamiento. ⚠️ EXAMEN
  • Creacionales: orientados a la creación de objetos (Abstract Factory, Builder, Prototype, Singleton).
  • Estructurales: orientados a cómo se combinan y relacionan las clases (Adapter, Composite, Facade, Proxy).
  • Comportamiento: orientados a cómo interactúan y se comunican los objetos en tiempo de ejecución (Observer, Chain of Responsibility...).
  • Interfaz / clase abstracta: elemento central en la mayoría de patrones GoF — define el contrato que deben cumplir las clases concretas.
  • Acoplamiento y cohesión: los patrones de diseño mejoran la cohesión (clases más especializadas) y reducen el acoplamiento (menos dependencias directas entre clases). ⚠️ EXAMEN
  • Diagrama de clases UML: herramienta principal para visualizar y describir patrones. Línea discontinua + triángulo = implementación de interfaz. Línea continua + triángulo = herencia. Menos delante de un miembro = privado. Más = público.

Desarrollo del Temario

1. Patrones de diseño: contexto y utilidad

Los patrones de diseño son soluciones probadas a problemas habituales en la construcción de software orientado a objetos. No se aplican por moda, sino porque identificamos primero un problema concreto y luego buscamos el patrón que lo resuelve.

Lo primero es el problema; el patrón es la solución. ⚠️ EXAMEN

Características que debe cumplir un patrón para serlo: - Repetible y demostrado en más de un proyecto. - Descripción genérica del problema y de la solución. - Acompañado de ejemplos de código y consecuencias (positivas y negativas).

Posible inconveniente: pueden requerir introducir clases, interfaces o artefactos adicionales. Hay que valorar si el beneficio supera ese coste.

El catálogo de referencia de la asignatura es el Gang of Four. Recurso complementario muy recomendado: Refactoring Guru, con diagramas, dibujos didácticos y ejemplos de código en múltiples lenguajes (TypeScript, Java, Python...).


2. Patrones Creacionales

2.1 Abstract Factory — Fábrica Abstracta

Problema: El cliente debe crear objetos de múltiples familias relacionadas (p. ej., widgets con look & feel de Windows vs. macOS). Cada familia tiene su propia implementación de los mismos tipos de producto (botón, checkbox, radio button). El cliente acaba acoplado a todas las clases concretas de cada familia.

Solución: Definir una interfaz FábricaAbstracta con un método de creación por cada tipo de producto. Cada familia tiene su FábricaConcreta que implementa dicha interfaz. Los productos concretos también implementan interfaces por tipo de producto.

classDiagram
    class FábricaAbstracta {
        <<interface>>
        +crearBotón() Botón
        +crearCheckbox() Checkbox
    }
    class FábricaWindows {
        +crearBotón() Botón
        +crearCheckbox() Checkbox
    }
    class FábricaMac {
        +crearBotón() Botón
        +crearCheckbox() Checkbox
    }
    class Botón {
        <<interface>>
        +onClick()
    }
    class Checkbox {
        <<interface>>
        +marcar()
    }
    FábricaAbstracta <|.. FábricaWindows
    FábricaAbstracta <|.. FábricaMac
    Botón <|.. BotónWindows
    Botón <|.. BotónMac
    Checkbox <|.. CheckboxWindows
    Checkbox <|.. CheckboxMac
    FábricaWindows ..> BotónWindows
    FábricaWindows ..> CheckboxWindows
    FábricaMac ..> BotónMac
    FábricaMac ..> CheckboxMac

Resultado: El cliente solo conoce la interfaz de la fábrica y las interfaces de los productos. El acoplamiento con las clases concretas queda encapsulado en las fábricas concretas. ⚠️ EXAMEN

Otros ejemplos: videojuego con familias de personajes; librería de componentes gráficos.


2.2 Builder — Constructor

Problema: Construir objetos complejos cuyo constructor requeriría decenas de parámetros (p. ej., un codificador de vídeo con formato, códec, resolución, subtítulos...). El cliente acaba conociendo y ensamblando todos los subcomponentes: alto acoplamiento y poca especialización.

Solución: Encapsular el proceso de construcción en un objeto Builder con métodos descriptivos para añadir cada parte, y un método build() final que devuelve el objeto construido. Un Director (o el propio cliente) orquesta los pasos.

Regla de oro: en cualquier implementación del Builder debe existir un método build() (o construir()). Si no aparece, el patrón no está bien aplicado. ⚠️ EXAMEN

Ejemplos conocidos: UriBuilder (Java/Android), NotificationBuilder (Android), StringBuilder.

Resultado: el cliente llama a métodos semánticamente expresivos; el Builder valida parámetros y encapsula la complejidad de ensamblado. Código del cliente más limpio, clases más pequeñas y especializadas.


2.3 Prototype — Prototipo

Problema: Necesitamos crear muchas copias de objetos cuya creación es costosa (p. ej., objetos construidos con un Builder de 6 pasos, o 500 hormigas de un videojuego).

Solución: Cualquier objeto que sea clonable implementa una interfaz con el método clonar() (en inglés, clone()). El objeto sabe cómo crear una copia de sí mismo, opcionalmente con pequeñas variaciones.

classDiagram
    class Forma {
        <<abstract>>
        #centro: Punto
        #color: String
        +clonar() Forma*
    }
    class Círculo {
        -radio: double
        +clonar() Forma
    }
    class Rectángulo {
        -ancho: double
        -alto: double
        +clonar() Forma
    }
    Forma <|-- Círculo
    Forma <|-- Rectángulo

Regla de oro: si aparece un Prototype en el trabajo y no hay un método clonar() o clone(), el patrón no está bien aplicado. ⚠️ EXAMEN

Ejemplos cotidianos: copiar y pegar una autoforma en Word (se clona el objeto con un ligero desplazamiento de posición).


2.4 Singleton

Problema: Queremos garantizar que en toda la ejecución del programa solo exista una única instancia de una determinada clase (p. ej., el objeto que gestiona la conexión a la base de datos, un objeto mediador).

Solución: Tres elementos obligatorios: 1. Atributo privado estático que guarda la única instancia. 2. Constructor privado (no invocable desde fuera). 3. Método estático público obtenerInstancia() que crea la instancia si no existe y la devuelve.

classDiagram
    class Singleton {
        -instancia: Singleton$
        -Singleton()
        +obtenerInstancia() Singleton$
        +operación()
    }

Los patrones son combinables: un objeto Mediador tiene todo el sentido que sea también Singleton. ⚠️ EXAMEN


3. Patrones Estructurales

3.1 Adapter — Adaptador

Problema: Queremos reutilizar clases de distintas librerías que ofrecen la misma funcionalidad pero con interfaces incompatibles (p. ej., una librería usa getCenter() y otra getPos() para obtener la posición de una figura).

Solución: Crear una clase adaptadora que implementa la interfaz deseada por el cliente y delega internamente en el objeto de la librería externa. Hay dos variantes: por composición (el adaptador contiene una referencia al objeto adaptado) o por herencia.

Resultado: el cliente solo trabaja con la interfaz común. Si se cambia la librería, solo hay que modificar el adaptador, no el código cliente.


3.2 Composite

Problema: Tenemos una jerarquía de objetos (árbol: hojas y contenedores) y queremos aplicar operaciones sobre cualquier nodo sin distinguir si es hoja o contenedor (p. ej., mover una agrupación de formas en Word, cambiar el color de un robot articulado).

Solución: Definir una interfaz o clase abstracta Componente con la operación que se quiere propagar (p. ej., mover(), setColor()). Tanto las hojas (Leaf) como los contenedores (Composite) la implementan. El contenedor propaga el mensaje a sus hijos recursivamente.

classDiagram
    class Componente {
        <<abstract>>
        +operación()*
    }
    class Hoja {
        +operación()
    }
    class Contenedor {
        -hijos: List~Componente~
        +añadir(c: Componente)
        +operación()
    }
    Componente <|-- Hoja
    Componente <|-- Contenedor
    Contenedor o-- Componente

Resultado: el cliente solo necesita conocer el nodo raíz; la propagación es automática.


3.3 Facade — Fachada

Problema: El cliente tiene alto acoplamiento con muchas clases de subsistemas distintos, conoce demasiados métodos de demasiadas clases.

Solución: Un objeto Fachada centraliza y simplifica el acceso a ese conjunto de clases, ofreciendo una interfaz unificada. El cliente solo conoce la fachada.

Resultado: reduce el acoplamiento del cliente con el subsistema; facilita sustitución del subsistema.


3.4 Proxy

Problema: Queremos añadir capacidades a un objeto (caché, lazy load, control de acceso, logging) sin modificarlo.

Solución: El proxy implementa la misma interfaz que el objeto real (Sujeto). El cliente trabaja con el proxy, que delega en el objeto real cuando procede, añadiendo la funcionalidad extra antes o después.

classDiagram
    class Sujeto {
        <<interface>>
        +operación()
    }
    class SujetoReal {
        +operación()
    }
    class Proxy {
        -sujetoReal: SujetoReal
        +operación()
    }
    Sujeto <|.. SujetoReal
    Sujeto <|.. Proxy
    Proxy --> SujetoReal

Ejemplos: proxy que cachea respuestas HTTP, lazy load de imágenes en una web. En el foro de dinamización hay un ejemplo completo en Python con vídeo. ⚠️ EXAMEN (ventajas del proxy cacheador)


4. Patrones de Comportamiento

4.1 Observer — Observador

Problema: Un objeto (Sujeto) cambia de estado y otros objetos (Observadores) necesitan ser notificados automáticamente, sin que el sujeto esté acoplado a ellos directamente.

Solución: El sujeto mantiene una lista de observadores. Los observadores se registran/desregistran con attach()/detach(). Cuando cambia el estado, el sujeto llama a notify(), que itera la lista e invoca update() en cada observador.

classDiagram
    class Sujeto {
        -observadores: List~Observador~
        -estado
        +attach(o: Observador)
        +detach(o: Observador)
        +notify()
        +setState()
    }
    class Observador {
        <<interface>>
        +update()
    }
    class ObservadorConcreto1 {
        +update()
    }
    class ObservadorConcreto2 {
        +update()
    }
    Sujeto o-- Observador
    Observador <|.. ObservadorConcreto1
    Observador <|.. ObservadorConcreto2

Regla de oro para el trabajo: deben aparecer los métodos attach, detach y notify (o equivalentes). ⚠️ EXAMEN

Ejemplos: indicador de fortaleza de contraseña (notificado de cada cambio en el input); sensores domóticos que avisan al cambiar de valor.

Aclaración: los addEventListener del DOM se parecen más al patrón Cadena de Responsabilidad (un evento atraviesa nodos en jerarquía) que al Observador (un objeto notifica cambios de estado a una lista de suscriptores).


5. Patrones de Interacción (UI Patterns)

Menos relevantes para el examen. No tienen catálogos canónicos como GoF; hay múltiples webs con recopilaciones pero sin estandarización. Para el trabajo: indicar siempre el catálogo del que se ha extraído el patrón y por qué recibe ese nombre, ya que la terminología no está unificada (un mismo patrón puede llamarse de forma diferente según el catálogo).


6. Consideraciones para el trabajo de la asignatura

  • Solo se califican patrones vistos en clase. No usar Strategy, Memento, Visitor ni otros no cubiertos.
  • El apartado 3 del trabajo pide diseño a nivel de clases con al menos tres patrones GoF identificados por nombre y catálogo.
  • No copiar el diagrama genérico de la presentación: adaptar el diagrama al problema concreto del trabajo.
  • El apartado 4 (patrones de interacción UI) vale dos puntos; no requiere formalidad extrema pero debe indicarse el patrón y el catálogo de origen.
  • Herramientas válidas para los diagramas: papel, Sigma, autoformas — lo importante es la corrección del diagrama, no la herramienta.

Preguntas de Autoevaluación

  1. ¿Qué diferencia hay entre un patrón de arquitectura y un patrón de diseño detallado? Da un ejemplo de cada tipo visto en la asignatura. ⚠️ EXAMEN
  2. ¿Cuáles son las tres categorías del catálogo Gang of Four? ¿Qué tipo de problema aborda cada una? ⚠️ EXAMEN
  3. ¿Cuándo usarías Abstract Factory en lugar de simplemente instanciar directamente los objetos que necesitas?
  4. ¿Qué elemento es obligatorio en toda implementación del patrón Builder? ¿Y en el Prototype?
  5. Explica cómo el Singleton impide la creación de más de una instancia. ¿Qué tres elementos del código lo hacen posible? ⚠️ EXAMEN
  6. ¿Qué problema resuelve el patrón Adapter? ¿En qué dos variantes se puede implementar?
  7. Describe el flujo de mensajes en el patrón Composite cuando se invoca mover() sobre el nodo raíz de una jerarquía.
  8. ¿En qué se diferencia un Proxy de un Adapter? ¿Qué tipo de funcionalidad añade típicamente un Proxy?
  9. Dibuja el diagrama de clases del patrón Observer indicando qué métodos son obligatorios en el Sujeto y en el Observador.
  10. ¿Por qué los addEventListener del DOM se parecen más al patrón Cadena de Responsabilidad que al Observador?