Patrones de Arquitectura de Software en Java:
Una Guía Práctica
Alejandro G Vera
1
Libro: Patrones de Arquitectura de Software en
Java: Una Guía Práctica
Autor: Alejandro G Vera
Tabla de Contenidos
Parte 1: Introducción a los Patrones de Diseño y Arquitectura
Capítulo 1: ¿Qué son los Patrones de Software? (pág. 5)
o Definición de patrón de diseño y patrón de arquitectura.
o La importancia de un vocabulario común.
o Historia y origen: El "Gang of Four" (GoF).
o Beneficios del uso de patrones: Reusabilidad, mantenibilidad,
escalabilidad.
o Antipatrones: Qué son y cómo evitarlos.
Capítulo 2: Principios de Diseño de Software (SOLID) (pág. 13)
o Principio de Responsabilidad Única (SRP).
o Principio de Abierto/Cerrado (OCP).
o Principio de Sustitución de Liskov (LSP).
o Principio de Segregación de Interfaces (ISP).
o Principio de Inversión de Dependencias (DIP).
o Ejemplos prácticos en Java para cada principio.
Parte 2: Patrones Creacionales
Capítulo 3: Singleton (pág. 35)
o Propósito: Garantizar una única instancia de una clase.
o Implementaciones: Eager, Lazy, Thread-Safe.
o Ejemplo: Gestión de una conexión a base de datos o un gestor de
configuración.
o Ventajas y desventajas.
Capítulo 4: Factory Method (pág. 45)
o Propósito: Delegar la creación de objetos a subclases.
o Estructura UML y participantes.
o Ejemplo: Un sistema de notificaciones que crea diferentes tipos de
mensajes (Email, SMS, Push).
o Cuándo utilizarlo.
Capítulo 5: Abstract Factory (pág. 54)
o Propósito: Crear familias de objetos relacionados sin especificar sus
clases concretas.
o Diferencias con Factory Method.
o Ejemplo: Creación de interfaces de usuario para diferentes sistemas
operativos (Windows, macOS).
Capítulo 6: Builder (pág. 63)
o Propósito: Construir objetos complejos paso a paso.
o Ventajas sobre constructores telescópicos y JavaBeans.
2
o Ejemplo: Construcción de un objeto HttpRequest con múltiples
parámetros opcionales.
Capítulo 7: Prototype (pág. 72)
o Propósito: Crear nuevos objetos a partir de una instancia existente
(clonación).
o Interfaces Cloneable y sus particularidades.
o Ejemplo: Creación de múltiples objetos de configuración a partir de una
plantilla base.
Parte 3: Patrones Estructurales
Capítulo 8: Adapter (pág. 85)
o Propósito: Permitir la colaboración entre interfaces incompatibles.
o Tipos: Adaptador de objetos y de clases.
o Ejemplo: Adaptar una API antigua a una nueva interfaz estándar en el
sistema.
Capítulo 9: Decorator (pág. 105)
o Propósito: Añadir funcionalidades a objetos de forma dinámica.
o Alternativa a la herencia.
o Ejemplo: Envolver un InputStream de Java con BufferedInputStream y
GZIPInputStream.
Capítulo 10: Facade (pág. 118)
o Propósito: Proporcionar una interfaz simplificada a un subsistema
complejo.
o Ejemplo: Una fachada para un sistema de procesamiento de video que
oculta la complejidad de la manipulación de códecs, audio y
renderización.
Capítulo 11: Composite (pág. 134)
o Propósito: Componer objetos en estructuras de árbol para representar
jerarquías de parte-todo.
o Ejemplo: Un sistema de archivos con directorios y archivos, o una GUI
con contenedores y componentes.
Capítulo 12: Proxy (pág. 140)
o Propósito: Proporcionar un sustituto o marcador de posición para otro
objeto para controlar el acceso a él.
o Tipos: Proxy virtual, de protección, remoto.
o Ejemplo: Un proxy para el acceso a imágenes de alta resolución (carga
perezosa o lazy loading).
Parte 4: Patrones de Comportamiento
Capítulo 13: Observer (pág. 146)
o Propósito: Definir una dependencia uno a muchos entre objetos, de modo
que cuando un objeto cambia de estado, todos sus dependientes son
notificados.
o Ejemplo: Implementación de un sistema de subastas donde los postores
son notificados de nuevas ofertas.
Capítulo 14: Strategy (pág. 154)
o Propósito: Definir una familia de algoritmos, encapsular cada uno de
ellos y hacerlos intercambiables.
3
o Ejemplo: Un sistema de comercio electrónico que permite seleccionar
diferentes estrategias de cálculo de envío (por peso, por distancia, tarifa
fija).
Capítulo 15: Template Method (pág. 161)
o Propósito: Definir el esqueleto de un algoritmo en una operación,
difiriendo algunos pasos a las subclases.
o Ejemplo: Un framework para el procesamiento de datos que define los
pasos (leer, procesar, escribir) y permite que las subclases implementen
los detalles.
Capítulo 16: Command (pág. 167)
o Propósito: Encapsular una solicitud como un objeto, permitiendo
parametrizar clientes con diferentes solicitudes, encolar o registrar
solicitudes y soportar operaciones que se pueden deshacer.
o Ejemplo: Implementación de las funciones "Deshacer" y "Rehacer" en un
editor de texto.
Capítulo 17: State (pág. 174)
o Propósito: Permitir que un objeto altere su comportamiento cuando su
estado interno cambia.
o Ejemplo: El comportamiento de un objeto Documento que cambia según
su estado (Borrador, En Revisión, Publicado).
Parte 5: Patrones de Arquitectura
Capítulo 18: Model-View-Controller (MVC) (pág. 182)
o Conceptos fundamentales: Modelo, Vista, Controlador.
o Flujo de la solicitud y responsabilidades de cada componente.
o Ejemplo: Implementación de una aplicación web simple utilizando
Spring MVC.
Capítulo 19: Model-View-Presenter (MVP) y Model-View-ViewModel
(MVVM) (pág. 190)
o Evolución de MVC.
o Diferencias clave en la interacción y el acoplamiento.
o Ejemplo conceptual de cómo se transformarían los componentes de
MVC a MVP o MVVM.
Capítulo 20: Arquitectura de Microservicios (pág. 198)
o Principios: Descentralización, independencia, automatización.
o Ventajas y desafíos.
o Patrones asociados: API Gateway, Service Discovery, Circuit Breaker.
o Ejemplo conceptual de cómo se descompondría una aplicación
monolítica en microservicios.
4
Parte 1: Introducción a los Patrones de Diseño y
Arquitectura
Capítulo 1: El Lenguaje del Diseño de Software: Una
Introducción a los Patrones
En el vasto y dinámico mundo de la ingeniería de software, la creación de
sistemas robustos, flexibles y duraderos no es producto del azar, sino de un
diseño deliberado y disciplinado. Los desarrolladores experimentados, a lo
largo de décadas de práctica, han observado que ciertos problemas de diseño
no son únicos; reaparecen constantemente en diferentes contextos y
proyectos. En lugar de reinventar la solución cada vez, han destilado la
sabiduría colectiva en un conjunto de soluciones probadas y reutilizables. Estas
soluciones formalizadas son lo que conocemos como patrones.
Este capítulo introduce el concepto fundamental de los patrones de software,
no como recetas rígidas, sino como un lenguaje sofisticado que permite a los
profesionales del software comunicar ideas complejas, construir sobre el
conocimiento acumulado y tomar decisiones de diseño más inteligentes y
eficientes. Exploraremos qué son, de dónde vienen y por qué son una
herramienta indispensable en el arsenal de cualquier desarrollador moderno.
1.1 Definiendo el Léxico: Patrones de Diseño vs. Patrones de Arquitectura
Para comenzar, es crucial establecer una distinción fundamental que a menudo
genera confusión: la diferencia entre patrones de arquitectura y patrones de
diseño. Aunque ambos abordan la resolución de problemas recurrentes, operan
en niveles de abstracción y alcance muy diferentes. Una analogía útil proviene
de la construcción civil: un patrón de arquitectura es como decidir el tipo de
edificación que se va a construir —un rascacielos, una casa suburbana o un
puente—, definiendo su estructura macro y sus características fundamentales.
Por otro lado, un patrón de diseño es análogo a decidir el tipo de cimientos, el
trazado de la plomería o la estructura de una viga de soporte; son soluciones a
problemas específicos dentro de la estructura mayor.
Los patrones de arquitectura son soluciones generales que abordan
problemas de diseño de alto nivel y afectan a todo el sistema. Definen las
1
características estructurales básicas de una aplicación, cómo se organizan sus
principales componentes y cómo interactúan entre sí. Estas decisiones tienen
2
un impacto profundo en la calidad, el rendimiento, la escalabilidad y la
mantenibilidad de todo el producto. Ejemplos clásicos de patrones de
arquitectura incluyen:
Modelo-Vista-Controlador (MVC): Separa la representación de la
información de la interacción del usuario con ella.
5
Capas (Layered): Organiza el sistema en capas horizontales, donde cada
capa tiene una responsabilidad específica (p. ej., presentación, lógica de
negocio, acceso a datos).
Microservicios: Estructura una aplicación como una colección de
servicios pequeños, autónomos y débilmente acoplados.
Publicador-Suscriptor (Publish-Subscribe): Permite la comunicación
asíncrona entre componentes a través de un bus de mensajes,
desacoplando a los emisores de los receptores. 2
En contraste, los patrones de diseño se centran en problemas más
específicos y localizados a nivel de componentes individuales, clases y
objetos. Están más orientados a la implementación y proporcionan plantillas
1
para refinar los subsistemas o componentes de una arquitectura más grande.
Resuelven problemas recurrentes relacionados con cómo las clases y los
objetos interactúan y colaboran para cumplir con sus responsabilidades. 3
Algunos ejemplos conocidos son:
Singleton: Asegura que una clase solo tenga una única instancia y
proporciona un punto de acceso global a ella.
Factory Method: Define una interfaz para crear un objeto, pero permite
que las subclases alteren el tipo de objetos que se crearán.
Observer: Define una dependencia de uno a muchos entre objetos, de
modo que cuando un objeto cambia de estado, todos sus dependientes
son notificados y actualizados automáticamente.
La siguiente tabla resume las diferencias clave para proporcionar una
referencia clara.
Tabla 1: Comparativa de Patrones de Arquitectura y Diseño
Característica Patrón de Arquitectura Patrón de Diseño
Alcance Sistema completo Módulo, componente o conjunto
de clases específico
Nivel de Alto (conceptual, esquelético) Bajo (concreto, orientado a la
Abstracción implementación)
Enfoque Estructura y comunicación entre Creación, estructura e interacción
subsistemas de clases/objetos
Ejemplos MVC, Microservicios, Capas, Singleton, Strategy, Observer,
Cliente-Servidor Decorator
Impacto Define la escalabilidad, Define la reusabilidad,
resiliencia y despliegue del mantenibilidad y cohesión del
sistema código
Es fundamental comprender que la relación entre estos dos tipos de patrones
no es de exclusión, sino de anidamiento y jerarquía. Una decisión
arquitectónica establece el marco y el contexto en el que se aplicarán
posteriormente múltiples patrones de diseño. Por ejemplo, al elegir una
6
arquitectura de Microservicios , se crea la necesidad de utilizar patrones de
4
diseño específicos para gestionar la comunicación entre servicios (como
Proxy o Facade) o para garantizar la resiliencia (como Circuit Breaker). De
manera similar, en una arquitectura MVC, el componente Modelo podría
implementarse utilizando un patrón Singleton para garantizar un único estado,
mientras que la Vista podría usar un patrón Observer para actualizarse
automáticamente cuando los datos del Modelo cambian. La elección de un
patrón de arquitectura, por lo tanto, no es una decisión aislada; guía y, a
menudo, exige el uso de ciertos patrones de diseño para que la arquitectura
sea viable y efectiva.
1.2 La Importancia Crítica de un Vocabulario Común
Si bien los patrones ofrecen soluciones técnicas probadas, su beneficio más
profundo y a menudo subestimado es la creación de un vocabulario común. 5
Los patrones son para los ingenieros de software lo que la notación musical es
para los músicos o los planos para los arquitectos: un lenguaje estandarizado
para comunicar ideas complejas de manera precisa, eficiente y sin
ambigüedades.
Cuando un equipo de desarrollo comparte un conocimiento de los patrones, la
comunicación se transforma. En lugar de describir laboriosamente un complejo
mecanismo de interacción de objetos desde cero, un desarrollador puede
simplemente decir: "Implementemos un patrón Observer para notificar a los
componentes de la interfaz de usuario cuando los datos del perfil cambien".
Esta simple frase transmite instantáneamente una gran cantidad de información
sobre la estructura, los participantes (Sujeto, Observador) y el comportamiento
del diseño propuesto. Este lenguaje compartido aporta beneficios críticos:
7
Eficiencia y Precisión: Proporciona un conjunto estándar de términos que
acelera las discusiones de diseño y reduce drásticamente el riesgo de
malentendidos. La comunicación se eleva a un nivel de abstracción más
6
alto, permitiendo a los equipos centrarse en la lógica de negocio en lugar
de en los detalles de la plomería del código.
2
Colaboración Mejorada: Un vocabulario común alinea a todos los
miembros del equipo, desde arquitectos hasta desarrolladores junior, bajo
un conjunto de normas y conceptos compartidos. Esto garantiza la
coherencia en el diseño y la implementación a lo largo de todo el proyecto. 3
Documentación Intrínseca: El propio nombre de un patrón sirve como
una forma concisa y potente de documentación. Cuando se revisa el
código y se encuentra una clase llamada SingletonConnectionManager o
CreditCardPaymentStrategy, su propósito e intención son inmediatamente
claros para cualquiera que conozca los patrones, reduciendo la necesidad
de comentarios extensos y documentación externa. 6
Este vocabulario no solo es una herramienta para la comunicación sincrónica
dentro de un equipo, sino que también es un puente que trasciende el tiempo y
los miembros del equipo. Consideremos la incorporación de un nuevo
desarrollador a un proyecto existente. Si el código está estructurado en torno a
7
patrones reconocibles, como una capa de persistencia que utiliza el patrón
Repository o una interfaz de sistema complejo simplificada con un
11
Facade , el nuevo miembro puede inferir rápidamente la intención y la
12
estructura de grandes porciones de la base de código. Esto reduce
drásticamente la curva de aprendizaje y acelera su capacidad para contribuir
de manera efectiva. Del mismo modo, al mantener o refactorizar código
heredado escrito años atrás, reconocer un patrón
Decorator o un Strategy revela instantáneamente la intención del arquitecto
original, permitiendo realizar cambios con mayor confianza y menor riesgo.
1.3 Los Orígenes: Del Arquitecto Christopher Alexander al "Gang of Four"
(GoF)
La historia de los patrones de diseño de software tiene un origen sorprendente
fuera del ámbito de la informática. El concepto fue introducido por primera vez
por el arquitecto Christopher Alexander en la década de 1970. En sus obras,
como A Pattern Language: Towns, Buildings, Construction, Alexander propuso
que los problemas de diseño arquitectónico (desde el trazado de una calle
hasta la ubicación de una ventana) no eran únicos y que existían soluciones
probadas y atemporales que podían ser documentadas y compartidas en un
formato estructurado que él denominó "patrones". 11
Esta idea trascendental de un "lenguaje de patrones" resonó profundamente en
la comunidad de la incipiente ingeniería de software, que se enfrentaba a
desafíos similares de complejidad y repetición. Sin embargo, el punto de
inflexión que catapultó el concepto a la corriente principal del desarrollo de
software fue la publicación en 1994 del libro Design Patterns: Elements of
Reusable Object-Oriented Software. 5
Escrito por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides,
quienes desde entonces son conocidos afectuosamente como el "Gang of
Four" (GoF), este libro se convirtió en un texto fundamental en la disciplina. 11
En él, los autores catalogaron metódicamente
23 patrones de diseño fundamentales orientados a objetos, extraídos de su
vasta experiencia en la construcción de sistemas de software reales. 5
Estos 23 patrones fueron organizados en tres categorías principales, que
siguen siendo el estándar de clasificación en la actualidad :3
1. Patrones Creacionales: Se ocupan de los mecanismos de creación de
objetos, tratando de crear objetos de una manera adecuada a la situación.
El objetivo es aumentar la flexibilidad y la reutilización del código existente.
Ejemplos: Factory Method, Abstract Factory, Singleton, Builder, Prototype.
2. Patrones Estructurales: Explican cómo ensamblar objetos y clases en
estructuras más grandes, manteniendo al mismo tiempo la flexibilidad y
eficiencia de la estructura. Ejemplos: Adapter, Decorator, Proxy, Facade,
Bridge, Composite, Flyweight.
8
3. Patrones de Comportamiento: Se centran en los algoritmos y la
asignación de responsabilidades entre objetos, describiendo no solo la
estructura sino también los patrones de comunicación entre ellos.
Ejemplos: Strategy, Observer, Command, Iterator, State, Template
Method, Chain of Responsibility.
El verdadero genio del libro del GoF no fue la invención de estos patrones.
Muchos de ellos ya existían de forma ad-hoc en sistemas de software
complejos; eran parte del conocimiento tácito y tribal de los desarrolladores
expertos. La contribución monumental del GoF fue su
5
codificación y formalización. Al dar a cada patrón un nombre, una
descripción del problema que resuelve, una plantilla para la solución y un
análisis de sus consecuencias, transformaron el conocimiento implícito en un
cuerpo de conocimiento explícito, transferible y enseñable. Crearon el
14
vocabulario que hoy permite a los desarrolladores de todo el mundo
comunicarse y construir sobre una base de sabiduría compartida.
1.4 Los Pilares de un Software Robusto: Beneficios Clave de los Patrones
La adopción de patrones de diseño no es un ejercicio académico; es una
práctica pragmática que produce beneficios tangibles en la calidad del
software. Estos beneficios se pueden agrupar en tres pilares fundamentales:
reusabilidad, mantenibilidad y escalabilidad. Lejos de ser independientes, estos
pilares se refuerzan mutuamente, creando un ciclo virtuoso que conduce a
sistemas más robustos y duraderos.
Reusabilidad
La reusabilidad en el contexto de los patrones va más allá de simplemente
copiar y pegar fragmentos de código. Se trata de la reutilización de soluciones
de diseño probadas y validadas. Cuando se enfrenta un problema de diseño
4
común, en lugar de empezar desde cero, un desarrollador puede aplicar un
patrón conocido, ahorrando tiempo y esfuerzo y aprovechando una solución
que ya ha demostrado su eficacia en innumerables proyectos anteriores. Los 10
patrones promueven la creación de componentes modulares que encapsulan
una funcionalidad específica, permitiendo que estas soluciones de diseño se
apliquen en diferentes contextos o incluso en proyectos completamente
distintos.
3
Por ejemplo, el patrón Strategy permite definir una familia de algoritmos,
encapsular cada uno en una clase separada y hacerlos intercambiables. Un 6
cliente que necesita realizar un cálculo puede utilizar diferentes estrategias (p.
ej.,
Suma, Resta, Multiplicacion) sin cambiar su propio código. La lógica de cada
algoritmo se vuelve reutilizable en cualquier parte del sistema que necesite esa
operación específica. 16
9
Mantenibilidad
Un sistema es mantenible si es fácil de entender, modificar, corregir y
evolucionar. Los patrones de diseño son una de las herramientas más eficaces
para mejorar la mantenibilidad del software. Al proporcionar una estructura
10
clara y soluciones estandarizadas, el código resultante es más organizado,
limpio y predecible. 6
Fundamentalmente, los patrones promueven dos principios clave del buen
diseño: alta cohesión (los elementos de un módulo están relacionados
funcionalmente) y bajo acoplamiento (los módulos son independientes entre
sí). Un bajo acoplamiento significa que un cambio en una parte del sistema
tiene un impacto mínimo o nulo en otras partes, lo que reduce el riesgo de
introducir errores y facilita la evolución del software.
6
El patrón Observer es un excelente ejemplo. Permite que un objeto (el
"sujeto") notifique a una lista de objetos dependientes (los "observadores")
sobre cualquier cambio de estado, sin que el sujeto necesite saber nada sobre
quiénes son los observadores. Esto desacopla completamente al sujeto de sus
observadores. Se pueden agregar o eliminar nuevos observadores en cualquier
momento sin modificar una sola línea de código en el sujeto, lo que demuestra
una mantenibilidad excepcional.4
Escalabilidad
La escalabilidad es la capacidad de un sistema para manejar una carga
creciente —ya sea en términos de usuarios, transacciones o datos— de
manera eficiente. Si bien la escalabilidad a menudo se asocia con decisiones
19
de infraestructura, el diseño del software es igualmente crucial. Un diseño
deficiente puede crear cuellos de botella que impiden que un sistema escale,
sin importar cuántos recursos de hardware se le asignen.
Los patrones de diseño ayudan a construir sistemas escalables al promover el
desacoplamiento, la gestión eficiente de recursos y la flexibilidad. Patrones
4
como el
Singleton pueden optimizar el uso de recursos al garantizar que solo exista
una instancia de un objeto costoso (como una conexión a una base de datos). 16
En arquitecturas distribuidas como los microservicios, patrones como
Factory Method o Abstract Factory son vitales para crear configuraciones de
objetos que puedan adaptarse dinámicamente a diferentes entornos, como la
nube, permitiendo que los componentes se escalen de forma independiente. 4
Estos tres beneficios forman un sistema interconectado. Un código altamente
mantenible, con componentes bien definidos y desacoplados, es
inherentemente más fácil de extraer para su reutilización. Un sistema
construido a partir de estos componentes modulares y reutilizables es, a su
10
vez, mucho más fácil de escalar, ya que las partes individuales pueden
optimizarse o distribuirse sin afectar al todo. Finalmente, el proceso de escalar
un sistema a menudo revela áreas que necesitan ser refactorizadas, una tarea
que solo es factible de manera segura y eficiente en una base de código
mantenible.
1.5 El Lado Oscuro del Diseño: Reconociendo y Evitando Antipatrones
Así como existen buenas prácticas probadas, también existen "malas prácticas"
que, lamentablemente, son igual de comunes. Estas soluciones recurrentes
pero contraproducentes a problemas de diseño se conocen como
antipatrones. Son el "gemelo malvado" de los patrones de diseño: soluciones
que parecen seductoramente simples o rápidas a primera vista, pero que a
largo plazo introducen complejidad, fragilidad y deuda técnica, causando más
problemas de los que resuelven. 21
Estudiar los antipatrones es tan crucial como aprender los patrones. Nos
enseña a reconocer las "banderas rojas" en el diseño de software, a
diagnosticar problemas en el código existente y a evitar caer en las mismas
trampas que otros han encontrado antes. Un antipatrón no es simplemente un
21
mal código; es un patrón de mal código que se repite en la industria. A
continuación se presentan algunos de los más notorios.
Tabla 2: Antipatrones Comunes y sus Consecuencias
Antipatrón Descripción Breve Consecuencias Negativas
God Object / Una única clase o módulo Viola el Principio de
God Class asume demasiadas Responsabilidad Única. Genera
responsabilidades no un altísimo acoplamiento, baja
relacionadas, convirtiéndose en cohesión, y hace que el código
el centro neurálgico del sea extremadamente frágil, difícil
sistema.25 de probar y casi imposible de
mantener.26
Spaghetti Código sin una estructura clara, Es prácticamente imposible de
Code con un flujo de control mantener o depurar. Cualquier
enrevesado, saltos pequeño cambio puede tener
impredecibles y dependencias efectos catastróficos e
enmarañadas, como un plato de impredecibles en todo el sistema.
espaguetis.24 Dificulta enormemente la
colaboración.26
Golden La tendencia a aferrarse a una Conduce a soluciones
Hammer herramienta, tecnología o patrón subóptimas, complejidad
familiar y aplicarlo a todos los innecesaria y una pérdida
problemas, incluso cuando no significativa de eficiencia. Impide
es la solución más adecuada.23 el aprendizaje y la adopción de
herramientas más apropiadas.28
Lava Flow Fragmentos de código viejo, de Infla la base de código, genera
funcionalidad incierta o mal confusión y ralentiza el desarrollo.
documentado que se dejan en la Existe el riesgo constante de que
11
base de código por miedo a que se active accidentalmente,
su eliminación pueda romper introduciendo errores difíciles de
algo.23 rastrear.
Copy-Paste Reutilización de código Crea una pesadilla de
Programming mediante el copiado y pegado mantenimiento. Si se encuentra
de fragmentos en lugar de crear un error en el código original,
abstracciones o funciones debe ser corregido manualmente
genéricas.23 en cada una de las copias, un
proceso propenso a errores y
omisiones.
Es crucial entender que la línea entre un patrón y un antipatrón puede ser
delgada y, a menudo, depende del contexto. De hecho, muchos antipatrones
surgen de la aplicación incorrecta o el abuso de un patrón de diseño
perfectamente válido. Por ejemplo, el uso excesivo e indiscriminado del patrón
Singleton para proporcionar acceso global a objetos que no lo necesitan
conduce a un antipatrón conocido como "Singletonitis", que crea un
acoplamiento oculto y dificulta enormemente las pruebas unitarias. De manera
22
similar, un patrón
Facade está diseñado para simplificar la interfaz de un subsistema complejo;
sin embargo, si la fachada comienza a acumular lógica de negocio propia en
lugar de simplemente delegar llamadas, puede degenerar rápidamente en un
God Object. 25
Esto nos lleva a una conclusión vital: no basta con memorizar los patrones. Es
imperativo entender profundamente el problema que cada patrón resuelve y el
contexto en el que es apropiado aplicarlo. La disciplina y el juicio son tan
importantes como el conocimiento técnico para distinguir entre una solución
elegante y una trampa de diseño.
12
Capítulo 2: Los Cimientos del Diseño Moderno: Principios
SOLID
Si los patrones de diseño son el vocabulario para construir software robusto,
los principios SOLID son la gramática que gobierna ese lenguaje. Son los
cimientos sobre los que se erigen las buenas prácticas de la programación
orientada a objetos. Antes de sumergirnos en el catálogo de patrones
específicos, es esencial comprender estos cinco principios fundamentales.
Ignorarlos es construir sobre arena; dominarlos es asegurar que nuestras
creaciones de software sean sólidas, mantenibles y capaces de resistir el paso
del tiempo y la inevitable marea del cambio.
2.1 Introducción a SOLID: Reglas para un Código Cohesivo y de Bajo
Acoplamiento
SOLID es un acrónimo mnemotécnico que representa cinco principios de
diseño de clases en la programación orientada a objetos. Fueron introducidos y
popularizados por Robert C. Martin (conocido como "Uncle Bob"), uno de los
pensadores más influyentes en el campo del desarrollo de software ágil y la
artesanía del software. 31
Estos principios no son leyes inflexibles, sino directrices heurísticas diseñadas
para combatir los síntomas más comunes del mal diseño de software:
Rigidez: Un sistema es rígido cuando un pequeño cambio requiere una
cascada de modificaciones en múltiples partes del código.
Fragilidad: Un sistema es frágil cuando un cambio en una parte rompe
inesperadamente otras partes que no parecían estar relacionadas.
Inmovilidad: Un sistema es inmóvil cuando sus componentes están tan
entrelazados que es imposible reutilizarlos en otros contextos.
Al aplicar los principios SOLID, los desarrolladores pueden crear sistemas con
un bajo acoplamiento (los componentes son más independientes) y una alta
cohesión (los componentes tienen responsabilidades claras y enfocadas). El
resultado es un código más fácil de entender, mantener, extender y probar. 31
Existe una profunda sinergia entre los principios SOLID y los patrones de
diseño. Muchos patrones de diseño son, en esencia, implementaciones
concretas que encapsulan uno o más de estos principios. Por ejemplo, el
5
patrón Strategy es una manifestación del Principio de Abierto/Cerrado.
Entender SOLID, por lo tanto, no solo mejora el diseño de nuestras propias
clases, sino que también nos proporciona una comprensión más profunda de
por qué los patrones de diseño funcionan y cuándo deben ser utilizados.
Tabla 3: Resumen de los Principios SOLID
Acrónimo Principio Objetivo Principal
13
S Single Responsibility Una clase debe tener una y solo una razón
Principle (Principio de para cambiar.
Responsabilidad Única)
O Open/Closed Principle Las entidades de software deben estar
(Principio de abiertas a la extensión, pero cerradas a la
Abierto/Cerrado) modificación.
L Liskov Substitution Principle Los subtipos deben ser sustituibles por sus
(Principio de Sustitución de tipos base sin alterar la corrección del
Liskov) programa.
I Interface Segregation Ningún cliente debe ser forzado a depender
Principle (Principio de de métodos que no utiliza. Es mejor tener
Segregación de Interfaces) muchas interfaces específicas que una sola
interfaz general.
D Dependency Inversion Los módulos de alto nivel no deben
Principle (Principio de depender de los de bajo nivel. Ambos
Inversión de Dependencias) deben depender de abstracciones.
En las siguientes secciones, exploraremos cada uno de estos principios en
detalle, utilizando ejemplos prácticos en Java para ilustrar tanto su violación
como su correcta aplicación.
2.2 Principio de Responsabilidad Única (SRP)
El Principio de Responsabilidad Única (SRP, por sus siglas en inglés) es quizás
el más fácil de entender conceptualmente, pero uno de los más difíciles de
aplicar correctamente en la práctica. Establece que una clase debe tener una
y solo una razón para cambiar. 32
Esta definición es más sutil de lo que parece. No significa que una clase deba
hacer una sola cosa. Más bien, se refiere a que una clase debe tener una única
responsabilidad sobre una parte de la funcionalidad del sistema. Una forma
más precisa de pensar en esto es que una clase debe ser responsable ante un
único "actor" o una única fuente de cambio en los requisitos del negocio. Si una
clase tiene métodos que sirven a diferentes actores (por ejemplo, al
departamento de finanzas y al de recursos humanos), entonces tiene más de
una razón para cambiar y está violando el SRP.
Los síntomas de una violación del SRP son fáciles de detectar: clases que se
vuelven excesivamente largas, la necesidad de modificar múltiples métodos no
relacionados para implementar una nueva funcionalidad, o la mezcla de
responsabilidades de diferentes capas de la arquitectura, como la lógica de
negocio con la lógica de presentación o el acceso a datos. 31
14
Ejemplo Práctico en Java
Imaginemos una aplicación que necesita calcular el área de varias figuras
geométricas y mostrar el resultado en la consola. Un primer enfoque podría
violar el SRP.
Situación Inicial (Violación del SRP)
Inicialmente, podríamos tener una clase CalculationService que no solo realiza
los cálculos, sino que también imprime el resultado.
Java
// Clases base para las figuras
abstract class Polygon {
public abstract double getArea();
}
class Circle extends Polygon {
private int radius;
public Circle(int radius) { this.radius = radius; }
public double getArea() { return Math.PI * Math.pow(radius, 2); }
}
class Square extends Polygon {
private int side;
public Square(int side) { this.side = side; }
public double getArea() { return Math.pow(side, 2); }
}
// Servicio que viola el SRP
public class CalculationService {
// Este método mezcla el cálculo con la presentación
public void sumAreas(Polygon polygon1, Polygon polygon2) {
double totalArea = polygon1.getArea() + polygon2.getArea();
System.out.println("El resultado de la suma es: " + totalArea);
}
}
En este diseño, la clase CalculationService tiene dos responsabilidades:
1. Lógica de negocio: Calcular la suma de las áreas.
2. Lógica de presentación: Imprimir el resultado en la consola.
Si en el futuro necesitamos cambiar el formato de salida (por ejemplo, a JSON,
HTML o una interfaz gráfica), tendríamos que modificar la clase
CalculationService. Del mismo modo, si se añade una nueva operación de
15
cálculo, también se modificaría esta clase. Tiene dos razones para cambiar,
violando así el SRP. 31
Refactorización (Cumplimiento del SRP)
Para cumplir con el SRP, debemos separar estas responsabilidades en clases
distintas.
Java
// Las clases de figuras permanecen igual
// Servicio de cálculo con una única responsabilidad
public class CalculationService {
public double sumAreas(Polygon polygon1, Polygon polygon2) {
return polygon1.getArea() + polygon2.getArea();
}
public double diffAreas(Polygon polygon1, Polygon polygon2) {
return polygon1.getArea() - polygon2.getArea();
}
}
// Nuevo servicio de presentación con una única responsabilidad
public class PrintService {
public void printResult(String label, double result) {
System.out.println(label + result);
}
}
// Clase principal que utiliza los servicios
public class Main {
public static void main(String args) {
// Crear instancias de los servicios
CalculationService calculationService = new CalculationService();
PrintService printService = new PrintService();
// Crear figuras
Circle circle = new Circle(5);
Square square = new Square(6);
// Usar los servicios de forma desacoplada
double result = calculationService.sumAreas(circle, square);
printService.printResult("El resultado de la suma es: ", result);
}
}
16
Con esta refactorización, CalculationService ahora solo se ocupa de los
cálculos y PrintService solo de la presentación. Si necesitamos cambiar el
formato de impresión, solo modificamos PrintService. Si necesitamos añadir un
nuevo cálculo, solo modificamos CalculationService. Cada clase tiene ahora
una única razón para cambiar, lo que resulta en un código más cohesivo,
mantenible y reutilizable. 31
2.3 Principio de Abierto/Cerrado (OCP)
El Principio de Abierto/Cerrado (OCP, por sus siglas en inglés) es uno de los
pilares del diseño orientado a objetos. Fue acuñado por Bertrand Meyer y
establece que las entidades de software (clases, módulos, funciones, etc.)
deben estar abiertas para la extensión, pero cerradas para la
modificación. 33
Esto puede sonar contradictorio, pero la idea central es poderosa: deberíamos
ser capaces de añadir nueva funcionalidad a un sistema sin alterar el código
existente que ya ha sido probado y funciona correctamente. Esto minimiza el
37
riesgo de introducir errores en la funcionalidad existente y promueve un diseño
más estable y mantenible.
La clave para lograr el OCP es la abstracción. Al depender de interfaces o
clases abstractas en lugar de implementaciones concretas, podemos introducir
nuevas funcionalidades creando nuevas clases que implementen esas
interfaces, sin necesidad de tocar el código que las utiliza. El polimorfismo es el
mecanismo que permite que esto funcione sin problemas. 36
Ejemplo Práctico en Java
Consideremos un sistema de procesamiento de pagos. Inicialmente, solo
necesita manejar pagos con tarjeta de crédito, pero sabemos que en el futuro
podría necesitar soportar otros métodos como PayPal o transferencias
bancarias.
Situación Inicial (Violación del OCP)
Un enfoque ingenuo podría utilizar una estructura condicional para manejar los
diferentes tipos de pago.
Java
public class ProcesadorPagos {
public void procesarPago(String metodoPago) {
if (metodoPago.equals("tarjeta")) {
System.out.println("Procesando pago con tarjeta de crédito...");
// Lógica específica para tarjetas de crédito
17
} else if (metodoPago.equals("paypal")) {
System.out.println("Procesando pago con PayPal...");
// Lógica específica para PayPal
}
// ¿Y si añadimos transferencia bancaria? Necesitamos añadir otro 'else if'.
}
}
Este diseño viola flagrantemente el OCP. Cada vez que se añade un nuevo
método de pago, la clase ProcesadorPagos debe ser modificada. Esto la hace
frágil, propensa a errores y difícil de mantener a medida que crece. 37
Refactorización (Cumplimiento del OCP)
Para cumplir con el OCP, diseñamos el sistema para que sea extensible desde
el principio, utilizando una abstracción.
1. Crear una interfaz de abstracción:
Definimos una interfaz MetodoPago que establece el contrato para todos
los métodos de pago.
Java
public interface MetodoPago {
void procesar();
}
2. Crear implementaciones concretas:
Creamos clases específicas para cada método de pago que implementen
la interfaz.
Java
public class PagoTarjeta implements MetodoPago {
@Override
public void procesar() {
System.out.println("Procesando pago con tarjeta de crédito...");
// Lógica específica para tarjetas de crédito
}
}
public class PagoPayPal implements MetodoPago {
@Override
public void procesar() {
System.out.println("Procesando pago con PayPal...");
// Lógica específica para PayPal
}
}
3. Modificar el cliente para que dependa de la abstracción:
La clase ProcesadorPagos ahora depende de la interfaz MetodoPago, no
de las implementaciones concretas.
Java
18
public class ProcesadorPagos {
public void procesar(MetodoPago metodoPago) {
metodoPago.procesar();
}
}
Ahora, si necesitamos añadir un nuevo método de pago, como una
transferencia bancaria, simplemente creamos una nueva clase:
Java
public class PagoTransferencia implements MetodoPago {
@Override
public void procesar() {
System.out.println("Procesando pago con transferencia bancaria...");
// Lógica específica
}
}
La clase ProcesadorPagos no necesita ninguna modificación. Está cerrada
a la modificación, pero está abierta a la extensión a través de la creación de
nuevas clases que implementen la interfaz MetodoPago. Este diseño es
robusto, flexible y cumple perfectamente con el OCP. 37
2.4 Principio de Sustitución de Liskov (LSP)
El Principio de Sustitución de Liskov (LSP), formulado por Barbara Liskov, es
fundamental para garantizar la integridad de las jerarquías de herencia. El
principio establece que los objetos de una superclase deben poder ser
reemplazados por objetos de una subclase sin afectar la corrección del
programa. 38
En otras palabras, si una clase Hija es un subtipo de una clase Padre, entonces
debemos poder usar un objeto de Hija en cualquier lugar donde se espere un
objeto de Padre, y el programa debe seguir funcionando como se espera. El
LSP no se trata solo de la herencia sintáctica (una relación "es un"), sino de la
consistencia de comportamiento. Una subclase no debe alterar el
comportamiento fundamental o las invariantes que un cliente espera de la clase
base. 41
Una violación común del LSP ocurre cuando una subclase lanza una excepción
en un método que la clase base declara como funcional, o cuando deja un
método heredado vacío porque no le aplica. El famoso y contraintuitivo
40
19
ejemplo del "cuadrado es un rectángulo" ilustra este punto: aunque
matemáticamente un cuadrado es un tipo de rectángulo, si la clase
Cuadrado hereda de Rectangulo y modifica el comportamiento de los métodos
setAncho() y setAlto() para mantener los lados iguales, puede romper las
expectativas de un cliente que espera poder cambiar el ancho y el alto de un
rectángulo de forma independiente. 42
Ejemplo Práctico en Java
Usemos un ejemplo de una aplicación bancaria para ilustrar una violación clara
del LSP y cómo corregirla.
Situación Inicial (Violación del LSP)
Supongamos que tenemos una clase abstracta Account que define el
comportamiento básico de una cuenta bancaria, incluyendo depósitos y retiros.
Java
public abstract class Account {
protected java.math.BigDecimal balance;
public Account() {
this.balance = java.math.BigDecimal.ZERO;
}
public abstract void deposit(java.math.BigDecimal amount);
public abstract void withdraw(java.math.BigDecimal amount);
}
Un servicio, BankingAppWithdrawalService, opera sobre la abstracción Account
para realizar retiros.
Java
public class BankingAppWithdrawalService {
private Account account;
public BankingAppWithdrawalService(Account account) {
this.account = account;
}
20
public void withdraw(java.math.BigDecimal amount) {
System.out.println("Iniciando retiro de: " + amount);
account.withdraw(amount);
}
}
Ahora, el banco introduce un nuevo producto: una cuenta de depósito a plazo
fijo (FixedTermDepositAccount). Este tipo de cuenta no permite retiros. Un
desarrollador podría decidir que, como es un tipo de cuenta, debe heredar de
Account. Para manejar la restricción de retiro, la subclase lanza una excepción.
Java
public class FixedTermDepositAccount extends Account {
@Override
public void deposit(java.math.BigDecimal amount) {
System.out.println("Depositando " + amount + " en cuenta a plazo fijo.");
this.balance = this.balance.add(amount);
}
@Override
public void withdraw(java.math.BigDecimal amount) {
throw new UnsupportedOperationException("Los retiros no están permitidos en
cuentas de depósito a plazo fijo.");
}
}
El problema se hace evidente cuando intentamos usar esta nueva clase con
nuestro servicio existente:
Java
public class Main {
{
public static void main(String args)
Account myFixedAccount = new FixedTermDepositAccount();
myFixedAccount.deposit(new java.math.BigDecimal("1000.00"));
BankingAppWithdrawalService service = new
BankingAppWithdrawalService(myFixedAccount);
// La siguiente línea provocará un error en tiempo de ejecución
service.withdraw(new java.math.BigDecimal("100.00"));
}
}
21
El programa se compila sin problemas, pero falla en tiempo de ejecución con
una UnsupportedOperationException. La subclase FixedTermDepositAccount
no es sustituible por su clase base Account sin romper el programa. Esto es
una clara violación del LSP. 43
Refactorización (Cumplimiento del LSP)
La solución no es manejar la excepción en el cliente, sino rediseñar la jerarquía
de herencia para que sea conductualmente consistente. El error fue colocar la
capacidad de withdraw() en la clase base Account, asumiendo que todas las
cuentas pueden hacerlo.
1. Segregar las capacidades:
Movemos el comportamiento de retiro a una abstracción más específica.
Java
// La clase base solo tiene comportamientos comunes a TODAS las cuentas
public abstract class Account {
//... Lógica de depósito y balance...
public abstract void deposit(java.math.BigDecimal amount);
}
// Nueva interfaz para cuentas que permiten retiros
public interface WithdrawableAccount {
void withdraw(java.math.BigDecimal amount);
}
2. Reestructurar la jerarquía:
Las clases ahora heredan o implementan solo los comportamientos que
pueden cumplir.
Java
// Una cuenta corriente sí permite retiros
public class CurrentAccount extends Account implements WithdrawableAccount {
@Override
public void deposit(java.math.BigDecimal amount) { /*... */ }
@Override
public void withdraw(java.math.BigDecimal amount) {
System.out.println("Retirando " + amount + " de la cuenta corriente.");
// Lógica de retiro
}
}
// La cuenta a plazo fijo ya no hereda el comportamiento de retiro
public class FixedTermDepositAccount extends Account {
@Override
public void deposit(java.math.BigDecimal amount) { /*... */ }
}
22
3. Ajustar el cliente para que dependa de la abstracción correcta:
El servicio de retiro ahora depende explícitamente de
WithdrawableAccount.
Java
public class BankingAppWithdrawalService {
private WithdrawableAccount withdrawableAccount;
public BankingAppWithdrawalService(WithdrawableAccount withdrawableAccount) {
this.withdrawableAccount = withdrawableAccount;
}
public void withdraw(java.math.BigDecimal amount) {
withdrawableAccount.withdraw(amount);
}
}
Con este nuevo diseño, es imposible pasar un FixedTermDepositAccount al
BankingAppWithdrawalService a nivel de compilación, previniendo el error en
tiempo de ejecución. La jerarquía ahora es robusta y cumple con el LSP, ya
que las subclases son verdaderamente sustituibles por sus supertipos en los
contextos apropiados. 43
2.5 Principio de Segregación de Interfaces (ISP)
El Principio de Segregación de Interfaces (ISP) aborda el problema de las
"interfaces gordas" o "fat interfaces". Su postulado es simple y directo: ningún
cliente debería ser forzado a depender de métodos que no utiliza. 44
En esencia, el ISP nos insta a favorecer muchas interfaces pequeñas y
específicas del cliente en lugar de una única interfaz grande y de propósito
general. Cuando una clase implementa una interfaz que contiene métodos que
no necesita, se ve obligada a proporcionar implementaciones vacías o a lanzar
excepciones. Esta práctica no solo añade código innecesario y confuso, sino
que también crea un acoplamiento no deseado. Si la interfaz "gorda" cambia en
uno de los métodos que la clase no utiliza, la clase aún así podría necesitar ser
recompilada, lo que la hace frágil a cambios irrelevantes para ella. 47
El ISP es similar en espíritu al Principio de Responsabilidad Única, pero
aplicado a las interfaces. Promueve la creación de interfaces cohesivas, donde
cada una define un "rol" o un conjunto de comportamientos estrechamente
relacionados. 48
Ejemplo Práctico en Java
Consideremos un sistema que gestiona diferentes tipos de pagos, como pagos
bancarios y pagos de préstamos.
23
Situación Inicial (Violación del ISP)
Un enfoque inicial podría definir una única y gran interfaz Payment para todos
los tipos de pago.
Java
// Interfaz "gorda" que viola el ISP
public interface Payment {
void initiatePayments(); // Específico para pagos bancarios
void intiateLoanSettlement(); // Específico para préstamos
void initiateRePayment(); // Específico para préstamos
Object status(); // Común a ambos
java.util.List<Object> getPayments(); // Común a ambos
}
Ahora, cuando creamos las clases de implementación, nos encontramos con
un problema. La clase BankPayment no tiene nada que ver con la liquidación
de préstamos, y la clase LoanPayment no inicia pagos bancarios genéricos.
Java
public class BankPayment implements Payment {
@Override
public void initiatePayments() { /* Lógica real aquí */ }
@Override
public Object status() { /* Lógica real aquí */ }
@Override
public java.util.List<Object> getPayments() { /* Lógica real aquí */ }
// Métodos forzados que no tienen sentido para esta clase
@Override
public void intiateLoanSettlement() {
throw new UnsupportedOperationException("No aplicable para pagos
bancarios.");
}
@Override
public void initiateRePayment() {
throw new UnsupportedOperationException("No aplicable para pagos
bancarios.");
}
24
}
public class LoanPayment implements Payment {
// Métodos forzados que no tienen sentido para esta clase
@Override
public void initiatePayments() {
throw new UnsupportedOperationException("No aplicable para pagos de
préstamos.");
}
@Override
public void intiateLoanSettlement() { /* Lógica real aquí */ }
@Override
public void initiateRePayment() { /* Lógica real aquí */ }
@Override
public Object status() { /* Lógica real aquí */ }
@Override
public java.util.List<Object> getPayments() { /* Lógica real aquí */ }
}
Este diseño es problemático. Las clases están contaminadas con métodos que
no les pertenecen, lo que las hace más difíciles de entender y mantener. Esto
es una clara violación del ISP. 45
Refactorización (Cumplimiento del ISP)
La solución es segregar la interfaz Payment en interfaces más pequeñas y
cohesivas.
1. Crear una interfaz base con los métodos comunes:
Java
public interface Payment {
Object status();
java.util.List<Object> getPayments();
}
2. Crear interfaces específicas para cada "rol":
Java
// Interfaz para pagos bancarios
public interface Bank extends Payment {
void initiatePayments();
}
// Interfaz para pagos de préstamos
public interface Loan extends Payment {
void intiateLoanSettlement();
void initiateRePayment();
25
}
3. Implementar solo las interfaces necesarias:
Ahora, cada clase implementa únicamente las interfaces que definen los
comportamientos que realmente posee.
Java
public class BankPayment implements Bank {
@Override
public void initiatePayments() { /* Lógica real aquí */ }
@Override
public Object status() { /* Lógica real aquí */ }
@Override
public java.util.List<Object> getPayments() { /* Lógica real aquí */ }
}
public class LoanPayment implements Loan {
@Override
public void intiateLoanSettlement() { /* Lógica real aquí */ }
@Override
public void initiateRePayment() { /* Lógica real aquí */ }
@Override
public Object status() { /* Lógica real aquí */ }
@Override
public java.util.List<Object> getPayments() { /* Lógica real aquí */ }
}
El código resultante es mucho más limpio, cohesivo y robusto. Las clases ya no
se ven forzadas a implementar métodos que no necesitan, y el sistema se
vuelve más modular y fácil de entender y extender. Este es el poder del
Principio de Segregación de Interfaces. 45
2.6 Principio de Inversión de Dependencias (DIP)
El Principio de Inversión de Dependencias (DIP) es el último principio de SOLID
y, posiblemente, el que tiene el impacto más profundo en la arquitectura de un
sistema. Su objetivo es desacoplar los componentes de software, permitiendo
que sean más flexibles, modulares y, crucialmente, más fáciles de probar.
El principio se define en dos partes : 50
1. Los módulos de alto nivel no deben depender de los módulos de bajo
nivel. Ambos deben depender de abstracciones.
26
2. Las abstracciones no deben depender de los detalles. Los detalles
deben depender de las abstracciones.
Desglosemos esto:
Módulos de alto nivel: Contienen la lógica de negocio importante de la
aplicación (el "qué" y el "cómo" de las operaciones principales). 51
Módulos de bajo nivel: Proporcionan detalles de implementación, como el
acceso a una base de datos, la comunicación por red o la escritura en el
sistema de archivos.
51
Abstracciones: Son interfaces o clases abstractas que definen un
contrato, pero no una implementación.
Detalles: Son las clases concretas que implementan esas abstracciones.
Tradicionalmente, el flujo de dependencias va desde el alto nivel hacia el bajo
nivel. Por ejemplo, una clase de lógica de negocio (PostRepository) dependería
directamente de una clase de acceso a base de datos (MySqlDatabase). El DIP
"invierte" esta dirección de dependencia. En lugar de que el módulo de alto
nivel dependa del de bajo nivel, ambos pasan a depender de una abstracción
(una interfaz Database) que es propiedad del módulo de alto nivel.
Es importante no confundir el Principio de Inversión de Dependencias (DIP)
con el patrón de Inyección de Dependencias (DI). DIP es el principio de
diseño. DI es una técnica común para implementar el DIP, mediante la cual las
dependencias de un objeto (los detalles) se le "inyectan" desde el exterior
(normalmente a través del constructor), en lugar de que el objeto las cree
internamente. 50
Ejemplo Práctico en Java
Imaginemos un repositorio que necesita obtener una lista de artículos de un
blog.
Situación Inicial (Violación del DIP)
En un diseño que viola el DIP, el módulo de alto nivel (PostRepository) crea y
depende directamente de un módulo de bajo nivel (WebServiceConcreto).
Java
// Módulo de bajo nivel (detalle)
public class WebServiceConcreto {
public String getPostsAsJson() {
System.out.println("Obteniendo posts desde el servicio web real...");
// Lógica para hacer una llamada de red y obtener un JSON
return "[{\"title\":\"Post 1\"}, {\"title\":\"Post 2\"}]";
}
}
27
// Módulo de alto nivel (lógica de negocio)
public class PostRepository {
private WebServiceConcreto webService;
public PostRepository() {
// El módulo de alto nivel crea y depende directamente del de bajo nivel.
this.webService = new WebServiceConcreto();
}
public java.util.List<String> getAllPosts() {
String json = webService.getPostsAsJson();
// Lógica para parsear el JSON y devolver una lista de títulos
System.out.println("Parseando JSON y devolviendo posts.");
return java.util.Arrays.asList("Post 1", "Post 2");
}
}
Este diseño tiene graves problemas : 51
Alto Acoplamiento: PostRepository está fuertemente acoplado a
WebServiceConcreto. Es imposible cambiar la fuente de datos (por
ejemplo, a otro servicio web o a una base de datos local) sin modificar
PostRepository.
Intestabilidad: PostRepository no se puede probar de forma unitaria y
aislada. Probar getAllPosts() requiere una llamada de red real, lo que hace
que la prueba sea lenta, frágil y dependiente de un servicio externo.
Refactorización (Cumplimiento del DIP)
Para cumplir con el DIP, introducimos una abstracción y usamos la inyección
de dependencias.
1. Crear una interfaz de abstracción:
Definimos una interfaz que pertenece al módulo de alto nivel y que dicta el
contrato que los módulos de bajo nivel deben cumplir.
Java
public interface IWebService {
String getPostsAsJson();
}
2. Hacer que el detalle dependa de la abstracción:
La clase concreta ahora implementa la interfaz.
Java
public class WebServiceConcreto implements IWebService {
@Override
public String getPostsAsJson() {
System.out.println("Obteniendo posts desde el servicio web real...");
return "[{\"title\":\"Post 1\"}, {\"title\":\"Post 2\"}]";
}
28
}
3. Hacer que el módulo de alto nivel dependa de la abstracción:
El PostRepository ahora depende de la interfaz IWebService, y la
implementación concreta se le "inyecta" a través del constructor.
Java
public class PostRepository {
private IWebService webService;
// La dependencia se inyecta desde el exterior
public PostRepository(IWebService webService) {
this.webService = webService;
}
public java.util.List<String> getAllPosts() {
String json = webService.getPostsAsJson();
System.out.println("Parseando JSON y devolviendo posts.");
return java.util.Arrays.asList("Post 1", "Post 2");
}
}
Ahora, el sistema es flexible y comprobable. En producción, podemos inyectar
una instancia de WebServiceConcreto:
Java
IWebService realWebService = new WebServiceConcreto();
PostRepository repository = new PostRepository(realWebService);
repository.getAllPosts();
Y para las pruebas unitarias, podemos crear un "mock" o un "stub" que
implemente la misma interfaz y devuelva datos predecibles sin hacer una
llamada de red:
Java
// Mock para pruebas
class MockWebService implements IWebService {
@Override
public String getPostsAsJson() {
System.out.println("Obteniendo posts desde el mock...");
return "";
29
}
}
// En un test
IWebService mockWebService = new MockWebService();
PostRepository testRepository = new PostRepository(mockWebService);
// Ahora podemos probar testRepository de forma aislada
El PostRepository ya no sabe ni le importa de dónde vienen los datos. Solo
sabe que interactúa con algo que cumple el contrato de IWebService. La
dependencia se ha invertido: tanto el módulo de alto nivel como el de bajo nivel
ahora dependen de la abstracción, logrando un diseño desacoplado, flexible y
robusto. 51
Obras citadas
1. yapiko.com, fecha de acceso: junio 28, 2025,
https://yapiko.com/es/blog/patrones-arquitectura-
software/#:~:text=En%20resumen%2C%20los%20patrones%20de,m%C3
%A1s%20orientados%20a%20la%20implementaci%C3%B3n.
2. Patrones de arquitectura de software | Yapiko, fecha de acceso: junio 28,
2025, https://yapiko.com/es/blog/patrones-arquitectura-software/
3. Software Design Patterns Tutorial - GeeksforGeeks, fecha de acceso:
junio 28, 2025, https://www.geeksforgeeks.org/system-design/software-
design-patterns/
4. Patrones de diseño: ¿qué son?, usos, tipos y ventajas, fecha de acceso:
junio 28, 2025, https://www.itmastersmag.com/transformacion-
digital/patrones-de-diseno-descripciones-estandarizadas-para-problemas-
repetitivos/
5. Patrones de Diseño: Fundamentos para una Arquitectura Eficaz, fecha de
acceso: junio 28, 2025, https://notasweb.me/entrada/patrones-de-diseno/
6. ¿Qué son los patrones de diseño en programación y porque debes
aprenderlos?, fecha de acceso: junio 28, 2025,
https://impulso06.com/que-son-los-patrones-de-diseno-en-programacion-
y-porque-debes-aprenderlos/
7. What are design patterns? : r/learnprogramming - Reddit, fecha de
acceso: junio 28, 2025,
https://www.reddit.com/r/learnprogramming/comments/1i2olwx/what_are_
design_patterns/
8. Vocabularios de datos: ¿por qué son importantes? | datos.gob.es, fecha
de acceso: junio 28, 2025, https://datos.gob.es/es/blog/vocabularios-de-
datos-por-que-son-importantes
9. Sistemas de diseño: sus principales beneficios y ventajas - TTANDEM -
Digital Studio, fecha de acceso: junio 28, 2025,
https://www.ttandem.com/blog/desarrollo-que-son-los-sistemas-de-
diseno/7-ventajas-de-los-sistemas-de-diseno/
10. Enhancing Software Development Efficiency: The ... - AI Publications,
fecha de acceso: junio 28, 2025,
https://aipublications.com/uploads/issue_files/7IJEBM-MAR20253-
Enhancing.pdf
30
11. Patrones de diseño de software y su aplicación - Brave Developer, fecha
de acceso: junio 28, 2025,
https://bravedeveloper.com/2021/04/27/patrones-de-diseno-de-software-
y-su-aplicacion/
12. Design Patterns Elements of Reusable Object-Oriented Software -
Javier8a.com, fecha de acceso: junio 28, 2025,
https://www.javier8a.com/itc/bd1/articulo.pdf
13. Software design pattern - Wikipedia, fecha de acceso: junio 28, 2025,
https://en.wikipedia.org/wiki/Software_design_pattern
14. Design Patterns: Elements of Reusable Object-Oriented Software -
Goodreads, fecha de acceso: junio 28, 2025,
https://www.goodreads.com/book/show/85009.Design_Patterns
15. 8 patrones de diseño que todo desarrollador debe conocer, fecha de
acceso: junio 28, 2025, https://programacionymas.com/blog/aprende-
patrones-diseno
16. Benefits of using design patterns in software development - MoldStud,
fecha de acceso: junio 28, 2025, https://moldstud.com/articles/p-benefits-
of-using-design-patterns-in-software-development
17. How Design Patterns Can Improve Software Development, fecha de
acceso: junio 28, 2025, https://onwavegroup.com/blog/how-design-
patterns-can-improve-software-development
18. How do Design Patterns Impact System Performance? - GeeksforGeeks,
fecha de acceso: junio 28, 2025, https://www.geeksforgeeks.org/system-
design/how-do-design-patterns-impact-system-performance/
19. Design Patterns for Scalable Software Systems - Number Analytics, fecha
de acceso: junio 28, 2025, https://www.numberanalytics.com/blog/design-
patterns-scalable-software-systems
20. Understanding Design Patterns in Software Development - Teamhub.com,
fecha de acceso: junio 28, 2025, https://teamhub.com/blog/understanding-
design-patterns-in-software-development/
21. codeyourapps.com, fecha de acceso: junio 28, 2025,
https://codeyourapps.com/que-son-los-antipatrones-de-diseno-swift-y-
cuales-son-los-mas-
comunes/#:~:text=Los%20antipatrones%20o%20trampas%2C%20son,sis
temas%20disfuncionales%20durante%20una%20auditor%C3%ADa.
22. ¿Qué son los antipatrones de diseño Swift y cuáles son los más
comunes?, fecha de acceso: junio 28, 2025,
https://codeyourapps.com/que-son-los-antipatrones-de-diseno-swift-y-
cuales-son-los-mas-comunes/
23. The Dark Side Of Software: Anti-Patterns (and How To Fix Them), fecha
de acceso: junio 28, 2025, https://www.paulsblog.dev/the-dark-side-of-
software-anti-patterns-and-how-to-fix-them/
24. Qué son los antipatrones de diseño | OpenWebinars, fecha de acceso:
junio 28, 2025, https://openwebinars.net/blog/que-son-los-antipatrones-
de-diseno/
25. Anti-patrones: la mejor forma de hacer un pésimo sistema de software. |
SG Buzz, fecha de acceso: junio 28, 2025,
https://sg.com.mx/revista/11/anti-patrones-la-mejor-forma-hacer-un-
pesimo-sistema-software
31
26. What is Anti-Patterns in Software Development? - Teamhub.com, fecha
de acceso: junio 28, 2025, https://teamhub.com/blog/understanding-anti-
patterns-in-software-development/
27. 6 Types of Anti Patterns to Avoid in Software Development |
GeeksforGeeks, fecha de acceso: junio 28, 2025,
https://www.geeksforgeeks.org/6-types-of-anti-patterns-to-avoid-in-
software-development/
28. What are Software Anti-Patterns? | Lucidchart Blog, fecha de acceso:
junio 28, 2025, https://www.lucidchart.com/blog/what-are-software-anti-
patterns
29. How to Detect and Prevent Anti-Patterns in Software Development ...,
fecha de acceso: junio 28, 2025, https://digma.ai/how-to-detect-and-
prevent-anti-patterns/
30. Understanding Anti Patterns of Software Engineering | by Sandesh
Gaonkar - Medium, fecha de acceso: junio 28, 2025,
https://medium.com/@sandy619g/understanding-anti-patterns-of-
software-engineering-7cc1353abd1d
31. ¿Cuáles son los principios S.O.L.I.D. - "Single Responsability", fecha de
acceso: junio 28, 2025, https://trbl-services.eu/blog-solid-single-
responsability/
32. Principio de responsabilidad única en programación - KeepCoding, fecha
de acceso: junio 28, 2025, https://keepcoding.io/blog/principio-de-
responsabilidad-unica/
33. Principios SOLID con ejemplos - En Mi Local Funciona, fecha de acceso:
junio 28, 2025, https://www.enmilocalfunciona.io/principios-solid/
34. SOLID: Principio de Responsabilidad Única (SRP) - YouTube, fecha de
acceso: junio 28, 2025, https://www.youtube.com/watch?v=73IBjmyjDX0
35. Cómo implementar el Principio Abierto-Cerrado (OCP) en el desarrollo de
software: Ejemplo y UML. | Preguntas | Prime Institute, fecha de acceso:
junio 28, 2025, https://www.primeinstitute.com/preguntas/como-
implementar-el-principio-abierto-cerrado-ocp-en-el-desarrollo-de-software-
ejemplo-y-uml-29317
36. [OCP] Principio Abierto - Cerrado - Güeb de Joaquin, fecha de acceso:
junio 28, 2025,
http://joaquin.medina.name/web2008/documentos/informatica/documenta
cion/logica/OOP/Principios/Oop_Solid_OCP/2012_09_04_SOLID_Principi
oAbiertoCerrado.html
37. ¿Para qué sirve el principio OCP y cómo aplicarlo? - KeepCoding, fecha
de acceso: junio 28, 2025, https://keepcoding.io/blog/que-es-el-principio-
ocp-y-como-aplicarlo/
38. 3 ⃣ Principio de SUSTITUCIÓN de LISKOV Por fin lo entenderás [SOLID]
- YouTube, fecha de acceso: junio 28, 2025,
https://www.youtube.com/watch?v=6eInyxthvdo
39. Liskov Substitution Principle (LSP) | by Tushar Ghosh - Medium, fecha de
acceso: junio 28, 2025, https://tusharghosh09006.medium.com/liskov-
substitution-principle-lsp-744eceb29e8
40. Principio de sustitución de Liskov (SOLID 3ª parte) | DevExpert, fecha de
acceso: junio 28, 2025, https://devexpert.io/blog/principio-de-sustitucion-
de-liskov
32
41. Principio de sustitución de Liskov - Keyvan Akbary, fecha de acceso: junio
28, 2025, https://keyvanakbary.com/principio-de-sustitucion-de-liskov/
42. How to apply the Liskov substitution principle in Java | TheServerSide,
fecha de acceso: junio 28, 2025,
https://www.theserverside.com/video/How-to-apply-the-Liskov-
substitution-principle-in-Java
43. Liskov Substitution Principle in Java | Baeldung, fecha de acceso: junio
28, 2025, https://www.baeldung.com/java-liskov-substitution-principle
44. Interface Segregation Principle y Spring Data - Arquitectura Java, fecha
de acceso: junio 28, 2025, https://www.arquitecturajava.com/interface-
segregation-principle-y-spring-data/
45. Interface Segregation Principle in Java | Baeldung, fecha de acceso: junio
28, 2025, https://www.baeldung.com/java-interface-segregation
46. What is the reasoning behind the Interface Segregation Principle? - Stack
Overflow, fecha de acceso: junio 28, 2025,
https://stackoverflow.com/questions/58988/what-is-the-reasoning-behind-
the-interface-segregation-principle
47. Principio de Segregación de Interfaces (SOLID 4ª parte) - DevExpert,
fecha de acceso: junio 28, 2025, https://devexpert.io/blog/principio-de-
segregacion-de-interfaces
48. Principio de Segregación de interfaces — SOLID | by Eduardo | eduesqui
- Medium, fecha de acceso: junio 28, 2025,
https://medium.com/eduesqui/principio-de-segregaci%C3%B3n-de-
interfaces-solid-255c24be0bab
49. SOLID Principles With Java Examples | by Inoka Madhuwanthi - Medium,
fecha de acceso: junio 28, 2025,
https://medium.com/@imadhuwanthi411/solid-principles-with-java-
examples-e8dac4308317
50. Inversión de Dependencias vs Inyección de Dependencias vs Inversión
de Control - AITOR RODRÍGUEZ WEBLOG, fecha de acceso: junio 28,
2025,
http://aitorrm.github.io/t%C3%A9cnicas%20y%20metodolog%C3%ADas/d
ip_di_ioc/
51. ▷ 【SOLID】 Principio de Inversión de Dependencias - [2025], fecha de
acceso: junio 28, 2025, https://alexandrefreire.com/principios-
solid/inversion-dependencias/
52. SOLID: Principio de Inversión de Dependencia (DIP) - YouTube, fecha de
acceso: junio 28, 2025, https://www.youtube.com/watch?v=OqxpDAjBr8o
33
Parte 2: Patrones Creacionales
Análisis Exhaustivo de Patrones de Diseño
Creacionales: Singleton y Factory Method
Introducción
En el ámbito del desarrollo de software, los patrones de diseño emergen como
soluciones estandarizadas, probadas y reutilizables para problemas recurrentes
que surgen en el diseño de sistemas orientados a objetos. Estas plantillas,
1
popularizadas por el libro
Design Patterns: Elements of Reusable Object-Oriented Software, no son
algoritmos específicos, sino descripciones o modelos conceptuales que pueden
ser implementados para resolver un problema de diseño en diferentes
contextos. Su adopción no solo agiliza el proceso de desarrollo, sino que
también mejora la calidad, flexibilidad, mantenibilidad y escalabilidad del
software.1
Los patrones de diseño se clasifican en tres categorías principales:
creacionales, estructurales y de comportamiento. Este informe se centra en los
2
patrones creacionales, cuyo propósito fundamental es abstraer y controlar el
proceso de instanciación de objetos. Al encapsular la lógica de creación, estos
3
patrones otorgan al sistema una mayor flexibilidad sobre qué objetos se crean,
cómo se crean y cuándo, reduciendo así las dependencias directas y el
acoplamiento entre clases.
Dentro de esta categoría, se analizarán en profundidad dos de los patrones
más fundamentales y discutidos: Singleton y Factory Method. Aunque ambos
son creacionales, abordan problemas de diseño diametralmente opuestos. El
patrón Singleton se enfoca en la unicidad y el control de acceso, garantizando
que una clase solo pueda tener una única instancia en toda la aplicación. Por
otro lado, el patrón Factory Method se centra en la flexibilidad y la delegación
de la creación, permitiendo que una superclase defina una interfaz para crear
un objeto, pero delegando en sus subclases la responsabilidad de decidir qué
clase concreta instanciar. Este análisis exhaustivo desglosará sus propósitos,
implementaciones, estructuras, ventajas, desventajas y casos de uso,
proporcionando una comprensión matizada de su rol en la arquitectura de
software moderna.
34
Capítulo 3: El Patrón Singleton - Control y Unicidad en la
Instanciación
3.1 Propósito Fundamental: Garantizar una Instancia Única
El patrón de diseño Singleton es uno de los patrones creacionales más
conocidos y, a la vez, más controvertidos en la ingeniería de software. Su
5
propósito principal es doble y se define formalmente como un mecanismo para
asegurar que una clase tenga una y solo una instancia, y proporcionar un
punto de acceso global a dicha instancia. La necesidad de este patrón
6
surge en escenarios donde es crucial controlar el número de instancias de una
clase, típicamente para gestionar recursos compartidos, limitados o costosos,
como una conexión a una base de datos, un gestor de configuración, un pool
de hilos o un servicio de logging.
1
Una analogía efectiva es la del gobierno de un país. Un país solo puede tener
7
un gobierno oficial en un momento dado. Independientemente de los individuos
que lo compongan, el título "El Gobierno de X" actúa como un punto de acceso
global que identifica de manera unívoca al grupo de personas a cargo. De
manera similar, el patrón Singleton asegura que, sin importar cuántas partes de
una aplicación necesiten un objeto, todas ellas trabajarán con la misma y única
instancia.
El Doble Problema y la Violación Inherente del Principio de
Responsabilidad Única (SRP)
Una observación crítica, y fuente de gran parte del debate en torno a este
patrón, es que el Singleton resuelve dos problemas distintos simultáneamente,
lo que inherentemente vulnera el Principio de Responsabilidad Única (SRP) del
acrónimo SOLID. Los dos problemas que aborda son:
2
1. Garantizar la Instancia Única: El patrón impone una restricción sobre la
propia clase para que no se puedan crear múltiples objetos de su tipo. Esto
es fundamental cuando un único objeto debe coordinar acciones en todo el
sistema.
2. Proveer Acceso Global: El patrón ofrece un método de acceso estático y
universal, como getInstance(), que permite a cualquier componente del
sistema obtener la referencia a esa única instancia sin necesidad de
pasarla como parámetro a través de múltiples capas de la aplicación. Este
5
comportamiento es similar al de una variable global, pero ofrece un mayor
control sobre la inicialización y el acceso.
Esta dualidad de responsabilidades es intrínseca al patrón. El SRP establece
que una clase debe tener una única razón para cambiar. Sin embargo, una
clase Singleton tiene dos: su lógica de negocio (por ejemplo, gestionar la
configuración) y la gestión de su propio ciclo de vida y unicidad (la lógica de
instanciación y acceso). Esto significa que la clase podría necesitar ser
35
modificada si cambia la forma en que se leen las configuraciones, o si se
necesita alterar la estrategia de instanciación (por ejemplo, de una inicialización
ansiosa a una perezosa para mejorar el rendimiento). Esta violación no es un
efecto secundario de una mala implementación, sino una característica
fundamental de su diseño, lo que alimenta el debate sobre si Singleton debe
ser considerado un "antipatrón". 2
3.2 Anatomía de la Implementación del Singleton
Independientemente de la estrategia de inicialización, todas las
implementaciones del patrón Singleton comparten una estructura común
basada en tres componentes clave que trabajan en conjunto para hacer cumplir
sus reglas.7
1. Constructor Privado: Este es el mecanismo central y no negociable del
patrón. Al declarar el constructor de la clase como private, se impide que el
código cliente (cualquier código fuera de la propia clase) pueda instanciar
objetos utilizando el operador new. Esta restricción otorga a la clase el
5
control total y exclusivo sobre cómo y cuándo se crean sus propias
instancias.
2. Campo Estático Privado para la Instancia: La clase Singleton contiene
un campo (una variable) estático y privado que almacenará la referencia a
la única instancia que se creará. Al ser estático, este campo pertenece a la
clase en sí, no a ninguna instancia de la misma, y por lo tanto, su valor es
compartido a través de toda la aplicación. 2
3. Método de Acceso Estático Público (getInstance): Se proporciona un
método público y estático, comúnmente llamado getInstance(), que actúa
como el único punto de entrada para obtener la instancia del Singleton. 8
Este método encapsula la lógica de creación: en la primera llamada, crea
el nuevo objeto y lo guarda en el campo estático; en todas las llamadas
posteriores, simplemente devuelve la instancia ya existente almacenada
en caché. 7
3.3 Estrategias de Inicialización y Gestión de Concurrencia
La forma en que la única instancia es creada e inicializada es un aspecto
crucial del patrón, con diferentes implementaciones que ofrecen distintos
balances entre simplicidad, rendimiento y seguridad en entornos de múltiples
hilos (concurrencia).
3.3.1 Inicialización Ansiosa (Eager Initialization)
En esta estrategia, la instancia del Singleton se crea en el mismo momento en
que la clase es cargada en la memoria por la Máquina Virtual de Java (JVM),
antes de que cualquier hilo pueda siquiera solicitarla. Esto se logra inicializando
el campo estático directamente en su declaración. 9
36
Java
public class EagerSingleton {
// La instancia se crea cuando la clase es cargada.
private static final EagerSingleton instance = new EagerSingleton();
// Constructor privado para evitar la instanciación externa.
private EagerSingleton() {}
// Método de acceso global.
public static EagerSingleton getInstance() {
return instance;
}
}
Ventajas: Es la implementación más simple y es inherentemente segura
para hilos (thread-safe). La JVM garantiza que la inicialización de campos
estáticos se completa de manera segura antes de que la clase pueda ser
accedida por múltiples hilos, eliminando cualquier riesgo de condiciones de
carrera.14
Desventajas: Su principal inconveniente es que la instancia se crea
siempre, independientemente de si será utilizada o no durante la ejecución
del programa. Si el objeto Singleton es pesado y consume muchos
recursos (memoria, conexiones, etc.), su creación puede impactar
negativamente el tiempo de arranque de la aplicación y desperdiciar
recursos si nunca llega a usarse. 13
3.3.2 Inicialización Perezosa (Lazy Initialization)
A diferencia del enfoque ansioso, la inicialización perezosa pospone la creación
de la instancia hasta el momento exacto en que es solicitada por primera vez a
través del método getInstance(). Esto se logra mediante una comprobación de
nulidad. 9
Java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
// La instancia se crea solo si aún no existe.
37
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
Ventajas: El principal beneficio es la eficiencia en el uso de recursos. La
instancia no se crea a menos que sea estrictamente necesaria, lo cual es
ideal para objetos costosos que no siempre son requeridos. 7
Desventajas: Esta implementación simple no es segura para hilos. En
un entorno concurrente, es posible que dos hilos evalúen la condición if
(instance == null) simultáneamente, la encuentren verdadera y ambos
procedan a crear una instancia. El resultado es la creación de dos objetos
distintos, lo que rompe por completo la garantía fundamental del patrón
Singleton. 9
3.3.3 Implementaciones Seguras para Hilos (Thread-Safe)
Para solucionar la vulnerabilidad de la inicialización perezosa en entornos
concurrentes, se han desarrollado varias técnicas, cada una con sus propias
compensaciones. La evolución de estas técnicas refleja una madurez creciente
en la comprensión de los desafíos de la programación concurrente.
Método getInstance() Sincronizado: La solución más directa es declarar
todo el método getInstance() con la palabra clave synchronized. Esto
impone un bloqueo (lock) que asegura que solo un hilo pueda ejecutar el
método a la vez, resolviendo así el problema de la doble creación. 9
Java
public class ThreadSafeLazySingleton {
private static ThreadSafeLazySingleton instance;
private ThreadSafeLazySingleton() {}
// El método está sincronizado para controlar el acceso concurrente.
public static synchronized ThreadSafeLazySingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeLazySingleton();
}
return instance;
}
}
Si bien esta solución es funcional y segura, introduce una penalización de
rendimiento significativa. El bloqueo se aplica en cada llamada al método,
incluso después de que la instancia ya ha sido creada y la sincronización
ya no es necesaria. En aplicaciones de alta concurrencia, este cuello de
botella puede degradar el rendimiento general. 13
38
Bloqueo de Doble Verificación (Double-Checked Locking): Este es un
intento de optimizar el enfoque anterior reduciendo el alcance de la
sincronización. El bloqueo solo se aplica cuando la instancia es nula,
evitando la sobrecarga en llamadas posteriores. Se realiza una primera
verificación de nulidad sin bloqueo, y si es verdadera, se entra en un
bloque sincronizado donde se realiza una segunda verificación antes de
crear la instancia. 13
Java
public class DoubleCheckedLockingSingleton {
// 'volatile' asegura que los cambios sean visibles para todos los hilos.
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {}
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) { // Primera verificación (sin bloqueo)
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) { // Segunda verificación (con bloqueo)
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
Es crucial declarar la variable instance como volatile. Esto garantiza dos
cosas: primero, que cualquier escritura en la variable instance sea visible
inmediatamente para todos los demás hilos; segundo, previene problemas
de reordenamiento de instrucciones por parte del compilador o la CPU, un
error sutil que podría hacer que un hilo viera una referencia al objeto
parcialmente construido. Aunque más eficiente que el método
completamente sincronizado, su implementación es compleja y propensa a
errores si no se comprende a fondo el modelo de memoria de Java. 15
Initialization-on-demand Holder Idiom (Bill Pugh Singleton):
Considerada la solución estándar y preferida en Java para lograr una
inicialización perezosa y segura para hilos. Este enfoque aprovecha las
garantías del cargador de clases de la JVM para lograr la sincronización de
manera implícita y eficiente. La instancia del Singleton se crea dentro de
una clase estática interna privada (Holder). 13
Java
public class BillPughSingleton {
private BillPughSingleton() {}
// Clase estática interna que contiene la instancia.
private static class SingletonHolder {
private static final BillPughSingleton INSTANCE = new
BillPughSingleton();
}
// No se necesita sincronización.
39
public static BillPughSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
La JVM no cargará la clase SingletonHolder en memoria hasta que el
método getInstance() sea invocado por primera vez. Dado que la carga de
clases en la JVM es un proceso inherentemente seguro para hilos, este
enfoque garantiza una inicialización perezosa, segura y de alto rendimiento
sin la sobrecarga de la sincronización explícita. Representa una solución
15
elegante que trabaja
con las garantías de la plataforma en lugar de luchar contra ellas.
Tabla 3.1: Matriz de Comparación de Implementaciones de Singleton
Implementaci Tipo de Segurida Impacto en Complejid Recomendaci
ón Inicializaci d para Rendimien ad ón de Uso
ón Hilos to
Eager Ansiosa Sí Bajo Baja Cuando la
Initialization (potencial instancia
desperdici siempre es
o de necesaria o
recursos al su creación
inicio) es barata.
Lazy Perezosa No Bajo Baja Solo en
Initialization entornos de
un solo hilo
(muy raro).
Synchronize Perezosa Sí Alto (cuello Baja Cuando la
d Method de botella simplicidad es
en cada más
llamada) importante
que el
rendimiento
en entornos
concurrentes.
Double- Perezosa Sí Medio Alta Desaconsejad
Checked (sobrecarg o; propenso a
Locking a solo en errores
la primera sutiles.
creación) Superado por
Bill Pugh.
Bill Pugh Perezosa Sí Bajo Media La mejor
Idiom práctica
recomendad
a para la
mayoría de
los casos en
Java.
40
3.4 Caso de Estudio: Gestor de Configuración de Aplicación
(ConfigurationManager)
Un caso de uso arquetípico para el patrón Singleton es la gestión de la
configuración de una aplicación. Es deseable que las propiedades de
configuración (como URLs de servicios, credenciales de base de datos o claves
de API) se carguen desde un archivo una sola vez y estén disponibles de
manera consistente para todos los componentes del sistema. Crear múltiples
gestores de configuración podría llevar a estados inconsistentes y a la recarga
innecesaria y costosa del archivo de propiedades. 1
A continuación se presenta una implementación de un ConfigurationManager
utilizando el robusto enfoque de Bill Pugh.
Escenario: La aplicación necesita leer propiedades desde un archivo
config.properties.
Java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class ConfigurationManager {
private final Properties properties;
// El constructor privado carga las propiedades desde el archivo.
private ConfigurationManager() {
properties = new Properties();
try (InputStream input = new FileInputStream("config.properties")) {
properties.load(input);
} catch (IOException ex) {
// En una aplicación real, se manejaría el error de forma más robusta.
ex.printStackTrace();
throw new RuntimeException("No se pudo cargar el archivo de configuración.",
ex);
}
}
// Clase estática interna para la inicialización segura y perezosa.
private static class ConfigurationHolder {
private static final ConfigurationManager INSTANCE = new
ConfigurationManager();
}
// Punto de acceso global a la única instancia.
41
public static ConfigurationManager getInstance() {
return ConfigurationHolder.INSTANCE;
}
// Método para obtener un valor de configuración.
public String getProperty(String key) {
return properties.getProperty(key);
}
}
Análisis del Ejemplo:
Esta implementación asegura que:
1. El archivo config.properties se lee y procesa una sola vez, durante la
creación de la única instancia de ConfigurationManager.
2. La creación es perezosa, ocurriendo solo la primera vez que getInstance()
es llamado.
3. El acceso es seguro para hilos sin penalizaciones de rendimiento gracias
al Bill Pugh Idiom.
4. Cualquier parte de la aplicación, desde la capa de datos hasta la de
presentación, puede obtener la misma configuración llamando a
ConfigurationManager.getInstance().getProperty("db.url"), garantizando la
consistencia del estado de configuración en todo el sistema.
3.5 Análisis Crítico: Ventajas, Desventajas y el Debate del "Antipatrón"
El patrón Singleton, a pesar de su aparente simplicidad, es uno de los más
debatidos. Su evaluación requiere un análisis equilibrado de sus beneficios y
sus significativos inconvenientes, que han llevado a muchos desarrolladores a
considerarlo un "antipatrón". 2
Ventajas
Control de Instancia y Acceso Global: Garantiza que solo existe un
objeto de una clase, lo cual es indispensable para ciertos recursos.
Proporciona un punto de acceso global y controlado, evitando la
contaminación del espacio de nombres con variables globales. 7
Eficiencia y Consistencia de Estado: Al evitar la creación de múltiples
instancias, puede ahorrar memoria y recursos computacionales. Más
importante aún, asegura la consistencia de los datos al centralizar el
estado y el acceso a un recurso compartido, previniendo conflictos y datos
desactualizados. 2
Inicialización Perezosa: Permite diferir la creación del objeto hasta que
sea necesario, optimizando el tiempo de arranque de la aplicación.7
Desventajas y la Controversia del Antipatrón
42
Las críticas al Singleton son profundas y se centran en cómo afecta
negativamente la calidad, mantenibilidad y testeabilidad del código. 2
Violación del Principio de Responsabilidad Única (SRP): Como se
discutió previamente, la clase asume la responsabilidad tanto de su lógica
de negocio como de la gestión de su propio ciclo de vida. 2
Acoplamiento Fuerte: El código cliente se acopla directamente a la clase
concreta del Singleton a través de la llamada estática
MiSingleton.getInstance(). Esto hace que sea muy difícil sustituir la
implementación del Singleton por otra (por ejemplo, una versión de prueba
o una implementación alternativa) sin modificar todo el código cliente que
depende de ella. 2
Estado Global Oculto: El Singleton introduce un estado global en la
aplicación de forma implícita. Las dependencias de un componente con el
Singleton no son visibles en su firma (constructor o métodos), lo que oculta
las dependencias y hace que el flujo de control y datos del programa sea
más difícil de razonar y depurar. 11
Dificultades en Pruebas Unitarias: Este es uno de los inconvenientes
más graves. La naturaleza estática y el estado global persistente del
Singleton hacen que sea extremadamente difícil de aislar para las pruebas
unitarias. No se pueden crear fácilmente objetos simulados (mocks) para
reemplazar al Singleton. Peor aún, el estado del Singleton puede persistir
entre pruebas, haciendo que el resultado de una prueba dependa del
orden en que se ejecuten y del estado dejado por la prueba anterior, lo que
conduce a pruebas frágiles y poco fiables. 2
Mitigación de Riesgos con Inyección de Dependencias (DI)
Una forma moderna y efectiva de obtener los beneficios del Singleton (una
única instancia gestionada) mientras se mitigan sus peores desventajas es
utilizarlo en conjunto con un framework de Inyección de Dependencias (DI),
como Spring o Guice. 2
El problema fundamental del acoplamiento del Singleton es la llamada estática
directa (Cliente -> MiSingleton.getInstance()), donde el cliente tiene
conocimiento explícito de la clase Singleton. La DI invierte esta relación de
dependencia. En lugar de que el cliente busque activamente su dependencia,
la dependencia se le "inyecta" desde el exterior, generalmente a través de su
constructor.
Un contenedor de DI puede ser configurado para gestionar el ciclo de vida de
un objeto y asegurarse de que solo se cree una instancia (ámbito "singleton").
El código cliente, sin embargo, ya no depende de la clase concreta, sino de una
interfaz.
Sin DI: public class MiCliente { private IServicio servicio =
ServicioSingleton.getInstance();... }
Con DI: public class MiCliente { private final IServicio servicio; public
MiCliente(IServicio servicio) { this.servicio = servicio; }... }
43
En el segundo caso, el contenedor de DI se encarga de crear la única instancia
de ServicioReal y pasarla al constructor de MiCliente. Para las pruebas, se
puede inyectar fácilmente una implementación simulada (MockServicio). De
este modo, se mantiene el beneficio de la instancia única, pero se elimina el
acoplamiento fuerte y se recupera la testeabilidad. El patrón no desaparece,
sino que su responsabilidad de gestión del ciclo de vida se eleva a un
componente de infraestructura (el contenedor de DI), limpiando el diseño de la
aplicación.
2
44
Capítulo 4: El Patrón Factory Method - Flexibilidad en la
Creación de Objetos
4.1 Propósito Fundamental: Delegar la Creación a Subclases
El Factory Method es un patrón de diseño creacional que aborda el problema
de la creación de objetos de una manera flexible y desacoplada. Su propósito
formal es proporcionar una interfaz para crear objetos en una superclase,
pero permitir que las subclases alteren el tipo de objetos que se crearán. 18
En esencia, el patrón delega la responsabilidad de la instanciación de objetos,
que normalmente recae en un constructor, a un método especializado: el
"método fábrica".4
El problema que resuelve se puede ilustrar con una aplicación de gestión
logística. Supongamos que la aplicación se diseña inicialmente para gestionar
18
entregas solo por tierra, por lo que el código está plagado de llamadas a
new Camion(). Si más tarde surge la necesidad de añadir transporte marítimo,
habría que buscar todas las instancias de new Camion() y reemplazarlas con
una lógica condicional (if/else) para decidir si crear un Camion o un Barco. Este
enfoque es rígido, viola el principio de Abierto/Cerrado y conduce a un código
difícil de mantener.
El Factory Method soluciona esto reemplazando las llamadas directas al
constructor (new Camion()) por una llamada a un método fábrica (por ejemplo,
crearTransporte()). La superclase de logística (Logistica) contendría este
método, y el código de negocio operaría con el objeto Transporte devuelto.
Luego, se crearían subclases como LogisticaTerrestre y LogisticaMaritima,
cada una sobrescribiendo el método crearTransporte() para devolver new
Camion() y new Barco(), respectivamente. De esta manera, el código cliente
puede trabajar con diferentes tipos de logística sin conocer los detalles de qué
tipo de transporte concreto se está creando.18
4.2 Estructura y Participantes (Análisis UML)
La estructura del patrón Factory Method se define por la colaboración de cuatro
participantes clave, cuyas relaciones se pueden visualizar claramente en un
diagrama de clases UML. 22
Diagrama de Clases UML y Participantes
Un diagrama UML típico para el Factory Method muestra dos jerarquías de
clases paralelas: una para los "Creadores" y otra para los "Productos".
1. Producto (Product):
45
o Rol: Define la interfaz común para todos los objetos que pueden ser
creados por el método fábrica.
o Descripción: Es típicamente una clase abstracta o una interfaz que
declara las operaciones que todos los productos concretos deben
implementar. Por ejemplo, una interfaz Transporte con un método
entregar().
18
2. Producto Concreto (ConcreteProduct):
o Rol: Implementa la interfaz del Producto.
o Descripción: Son las clases de los objetos que el método fábrica
realmente crea. Por ejemplo, las clases Camion y Barco que
implementan Transporte. 4
3. Creador (Creator):
o Rol: Declara el método fábrica, que devuelve un objeto de tipo Product.
También puede contener código de negocio que depende de los
productos, pero que opera sobre ellos a través de la interfaz Product.
o Descripción: Esta clase, a menudo abstracta, no conoce qué
ConcreteProduct se creará. Puede proporcionar una implementación
por defecto para el método fábrica. Su responsabilidad principal no es
la creación en sí, sino contener la lógica que utiliza los productos.
4
4. Creador Concreto (ConcreteCreator):
o Rol: Sobrescribe el método fábrica para devolver una instancia de un
ConcreteProduct específico.
o Descripción: Aquí es donde reside la decisión final sobre qué clase
instanciar. Cada ConcreteCreator está asociado con un
ConcreteProduct específico. Por ejemplo, LogisticaTerrestre
sobrescribe crearTransporte() para devolver un Camion. 4
Diagrama de Secuencia
El flujo de interacción en el patrón es el siguiente :
22
1. El código Cliente necesita un producto, pero en lugar de crearlo
directamente, interactúa con un objeto Creador Concreto.
2. El Cliente invoca un método en el Creador Concreto que, a su vez,
necesita un producto para completar su tarea.
3. El Creador Concreto llama a su propio método fábrica (factoryMethod())
para obtener el producto.
4. El método fábrica, que ha sido sobrescrito en el Creador Concreto, ejecuta
new ConcreteProduct() y devuelve la instancia recién creada.
5. El Creador Concreto recibe el Producto Concreto y lo utiliza para
completar la operación solicitada por el Cliente. Es importante destacar
que el código de negocio dentro del Creador trata al objeto devuelto a
través de la interfaz Producto, sin conocer su tipo concreto.
4.3 Caso de Estudio: Sistema de Notificaciones Multi-canal
Para ilustrar de manera práctica el patrón, se implementará el sistema de
notificaciones solicitado, capaz de enviar mensajes a través de diferentes
46
canales como Email, SMS y notificaciones Push. Este ejemplo demuestra la
27
flexibilidad y extensibilidad que aporta el Factory Method.
Escenario: Una aplicación necesita enviar notificaciones a los usuarios.
Inicialmente, solo se usa email, pero se prevé añadir SMS y Push en el futuro.
Implementación Detallada (Java)
1. Interfaz Notification (Producto):
Define el contrato común para todas las notificaciones.
Java
public interface Notification {
void send(String message);
}
2. Clases de Productos Concretos (EmailNotification, SmsNotification,
PushNotification):
Implementan la interfaz con la lógica específica de cada canal.
Java
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Enviando Email con mensaje: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Enviando SMS con mensaje: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Enviando Notificación Push con mensaje: " + message);
}
}
3. Clase Abstracta NotificationFactory (Creador):
Declara el método fábrica abstracto y puede contener lógica de negocio
común.
Java
public abstract class NotificationFactory {
// El método fábrica abstracto que las subclases deben implementar.
public abstract Notification createNotification();
47
// Lógica de negocio que utiliza el producto.
public void notifyUser(String message) {
Notification notification = createNotification();
notification.send(message);
}
}
4. Clases de Creadores Concretos (EmailFactory, SmsFactory, PushFactory):
Cada fábrica sabe cómo crear un tipo específico de notificación.
Java
public class EmailFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new EmailNotification();
}
}
public class SmsFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new SmsNotification();
}
}
public class PushFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new PushNotification();
}
}
5. Código Cliente:
El cliente decide qué fábrica usar, pero el resto de su lógica es
independiente del tipo de notificación.
Java
public class Application {
{
public static void main(String args)
NotificationFactory factory = getConfiguredFactory("SMS"); // La
configuración podría venir de un archivo
factory.notifyUser("¡Tienes una nueva oferta!");
factory = getConfiguredFactory("Email");
factory.notifyUser("Confirmación de tu pedido.");
}
private static NotificationFactory getConfiguredFactory(String type) {
if (type.equalsIgnoreCase("Email")) {
return new EmailFactory();
48
} else if (type.equalsIgnoreCase("SMS")) {
return new SmsFactory();
} else {
return new PushFactory();
}
}
}
Análisis del Ejemplo:
El código cliente (Application) se desacopla de las clases de notificación concretas.
Puede ser configurado en tiempo de ejecución para usar una fábrica u otra, y el
método notifyUser funcionará de la misma manera. Si en el futuro se necesita añadir
un canal de notificaciones de Slack, solo se requeriría crear dos nuevas clases:
SlackNotification y SlackFactory. No sería necesario modificar ninguna de las clases
existentes (NotificationFactory, Application, etc.), cumpliendo así con el Principio de
Abierto/Cerrado.28
4.4 Criterios de Aplicación: ¿Cuándo Utilizar Factory Method?
El patrón Factory Method es una herramienta poderosa, pero su uso debe ser
justificado. Es particularmente apropiado en las siguientes situaciones : 4
Cuando una clase no puede anticipar la clase de los objetos que debe
crear. Esto ocurre en sistemas donde los tipos de objetos dependen de la
configuración, de la entrada del usuario o de la evolución del sistema a lo
largo del tiempo.
Cuando una clase quiere que sus subclases especifiquen los objetos
que crea. Este es el caso de uso principal en el diseño de frameworks y
bibliotecas. El framework define la estructura general y los puntos de
extensión ("hooks"), y el desarrollador que utiliza el framework implementa
esos hooks creando subclases que proporcionan los objetos concretos. 30
Cuando se desea localizar la lógica de creación de una familia de
productos en un solo lugar. Centralizar la creación de objetos
relacionados mejora la mantenibilidad, ya que cualquier cambio en cómo
se construyen los objetos se realiza en un único punto.
Para desacoplar el código cliente de las clases de producto
concretas. El cliente solo necesita conocer la interfaz del producto, no su
implementación específica. Esto promueve un diseño más flexible y reduce
las dependencias.
En sistemas que requieren una alta extensibilidad. El patrón es ideal
para software al que se le añadirán regularmente nuevos tipos de
productos que deben seguir un proceso de creación similar. 23
4.5 Análisis Comparativo: Ventajas y Desventajas
Como todo patrón de diseño, Factory Method ofrece un conjunto de beneficios
a cambio de ciertas complejidades.
49
Ventajas
Desacoplamiento: La ventaja más significativa es que evita el
acoplamiento fuerte entre el código cliente (o el Creador) y las clases de
Producto Concreto. El Creador trabaja exclusivamente con la interfaz del
Producto. 18
Cumplimiento del Principio de Responsabilidad Única (SRP): El código
responsable de la creación de un producto se puede aislar en un único
lugar (el método fábrica), separándolo de la lógica de negocio que utiliza el
producto. Esto hace que el código sea más cohesivo y fácil de mantener. 18
Cumplimiento del Principio de Abierto/Cerrado (OCP): El sistema es
"abierto" a la extensión pero "cerrado" a la modificación. Se pueden
introducir nuevos tipos de productos (y sus correspondientes fábricas) sin
necesidad de alterar el código cliente existente que ya funciona
correctamente. 18
Desventajas
Aumento de la Complejidad del Código: La principal desventaja es que
su implementación puede llevar a una proliferación de clases. Por cada
ConcreteProduct que se añade, a menudo se necesita un ConcreteCreator
correspondiente. En sistemas con una jerarquía de productos simple, esto
puede introducir una sobrecarga y una complejidad innecesarias, haciendo
el código más difícil de navegar al principio.
19
El Patrón como Habilitador de Arquitecturas Extensibles
Más allá de ser una solución aislada, el Factory Method debe entenderse como
un "patrón habilitador" o un bloque de construcción fundamental en el diseño
de software. Su mecanismo de delegar la creación a una subclase crea un
"gancho" (hook) en la superclase Creadora, un punto de extensión deliberado.
Este concepto es tan poderoso que otros patrones más complejos se
construyen sobre él. Por ejemplo:
4
El patrón Template Method define el esqueleto de un algoritmo en una
operación, difiriendo algunos pasos a las subclases. Uno de esos pasos
puede ser perfectamente un Factory Method que crea un objeto necesario
para el algoritmo.
El patrón Abstract Factory, que se utiliza para crear familias de objetos
relacionados, puede ser implementado como una clase que contiene
múltiples Factory Methods, cada uno responsable de crear un tipo de
producto diferente dentro de esa familia.
Por lo tanto, dominar el Factory Method es un paso crucial para comprender
conceptos más avanzados de diseño de software, como la Inversión de Control
(IoC). No es simplemente una técnica para instanciar objetos, sino un principio
50
fundamental sobre cómo construir sistemas que sean inherentemente flexibles,
mantenibles y extensibles a largo plazo.
Conclusión y Síntesis
Este informe ha analizado en profundidad dos patrones de diseño creacionales
fundamentales: Singleton y Factory Method. Aunque ambos gestionan la
creación de objetos, sus propósitos, estructuras y consecuencias en el diseño
de software son marcadamente diferentes.
Singleton se centra en la restricción y el control. Su objetivo es asegurar
la existencia de una única instancia de una clase y proporcionar un acceso
global a ella. Es una solución para problemas de gestión de recursos
compartidos y estado global consistente. Sin embargo, su uso es
controvertido debido a que viola principios de diseño clave como el SRP,
introduce un acoplamiento fuerte y complica significativamente las pruebas
unitarias. La recomendación moderna es utilizar el patrón Singleton con
extrema precaución. En muchos casos, sus beneficios pueden lograrse de
una manera más limpia y mantenible delegando la gestión del ciclo de vida
de la instancia a un contenedor de Inyección de Dependencias, que
puede garantizar la unicidad sin los efectos secundarios negativos del
patrón clásico.
Factory Method se centra en la flexibilidad y la delegación. Su objetivo
es desacoplar al cliente de la creación de objetos concretos, permitiendo
que las subclases decidan qué instancia específica crear. Es una
herramienta de cabecera para construir sistemas extensibles,
especialmente en el contexto de frameworks y bibliotecas, donde es vital
permitir a los usuarios extender la funcionalidad sin modificar el código
base. Aunque su implementación puede aumentar el número de clases en
el sistema, el beneficio en términos de mantenibilidad, desacoplamiento y
adhesión a los principios SOLID (como el OCP) a menudo supera este
coste.
En síntesis, la elección entre estos patrones, o la decisión de no usarlos, debe
basarse en una comprensión clara del problema específico a resolver. Mientras
que el Singleton es una herramienta de filo agudo que debe manejarse con
cuidado y a menudo puede ser reemplazada por técnicas más modernas, el
Factory Method sigue siendo un pilar robusto y esencial para el diseño de
software flexible y preparado para el futuro.
Obras citadas
1. Patrones de diseño: ¿qué son?, usos, tipos y ventajas - IT Masters Mag,
fecha de acceso: junio 28, 2025,
https://www.itmastersmag.com/transformacion-digital/patrones-de-diseno-
descripciones-estandarizadas-para-problemas-repetitivos/
2. Patrón Singleton - Adictos al trabajo, fecha de acceso: junio 28, 2025,
https://adictosaltrabajo.com/2024/07/15/patron-singleton/
3. Patrón creacional - SINGLETON - Somos PNT, fecha de acceso: junio 28,
2025, https://somospnt.com/blog/166-patron-creacional-singleton
51
4. Patrones de Diseño, fecha de acceso: junio 28, 2025,
http://arantxa.ii.uam.es/~eguerra/docencia/0708/04%20Creacion.pdf
5. SINGLETON | PATRONES de DISEÑO - YouTube, fecha de acceso:
junio 28, 2025, https://www.youtube.com/watch?v=GGq6s7xhHzY
6. Singleton - Patrones de diseño - DevExpert, fecha de acceso: junio 28,
2025, https://devexpert.io/singleton-patrones-diseno/
7. Singleton - Refactoring.Guru, fecha de acceso: junio 28, 2025,
https://refactoring.guru/es/design-patterns/singleton
8. Desmitificando el Patrón Singleton: Ventajas, Desafíos y Aplicaciones
Prácticas - YouTube, fecha de acceso: junio 28, 2025,
https://www.youtube.com/watch?v=RTCptUPQnMw
9. Singleton Design Pattern with Lazy and Eager Approach in Java:
Explained with Examples | by Vinod Kumar | Medium, fecha de acceso:
junio 28, 2025, https://medium.com/@vinodkumarbheel61/singleton-
design-pattern-with-lazy-and-eager-approach-in-java-explained-with-
examples-782efa9691eb
10. Patrones de Diseño: Método Singleton : r/programacion - Reddit, fecha de
acceso: junio 28, 2025,
https://www.reddit.com/r/programacion/comments/gxxa0e/patrones_de_di
se%C3%B1o_m%C3%A9todo_singleton/
11. Singleton: El Patrón del mal. Variables globales permitidas y… | by
Maximiliano Contieri | Diseño de Software | Medium, fecha de acceso:
junio 28, 2025, https://medium.com/dise%C3%B1o-de-software/singleton-
el-patr%C3%B3n-del-mal-f3fdab0e16a2
12. Patron singleton: una clase propia - IONOS, fecha de acceso: junio 28,
2025, https://www.ionos.com/es-us/digitalguide/paginas-web/desarrollo-
web/patron-singleton/
13. Java Singleton Design Pattern Practices with Examples ..., fecha de
acceso: junio 28, 2025, https://www.geeksforgeeks.org/java-singleton-
design-pattern-practices-examples/
14. Eager and Lazy Instantiation in Singleton Design Pattern implementation -
Simply Engineer, fecha de acceso: junio 28, 2025,
https://sandeepdass003.wordpress.com/2018/02/23/eager-and-lazy-
instantiation-in-singleton-design-pattern-implementation/
15. Explore Different Ways to Implement Thread-Safe Singleton Pattern in
Java - initgrep, fecha de acceso: junio 28, 2025,
https://www.initgrep.com/posts/design-patterns/thread-safety-in-java-
singleton-pattern
16. The Singleton Design Pattern: Ensuring a Single Instance in Java - DEV
Community, fecha de acceso: junio 28, 2025,
https://dev.to/adityapratapbh1/the-singleton-design-pattern-ensuring-a-
single-instance-in-java-5c1o
17. Singleton with or without holder = lazy vs eager initialisation? - Stack
Overflow, fecha de acceso: junio 28, 2025,
https://stackoverflow.com/questions/34506466/singleton-with-or-without-
holder-lazy-vs-eager-initialisation
18. Factory Method - Refactoring.Guru, fecha de acceso: junio 28, 2025,
https://refactoring.guru/es/design-patterns/factory-method
19. Factory Method - Refactoring.Guru, fecha de acceso: junio 28, 2025,
https://refactoring.guru/design-patterns/factory-method
52
20. Factory method pattern - Wikipedia, fecha de acceso: junio 28, 2025,
https://en.wikipedia.org/wiki/Factory_method_pattern
21. Factory Method (patrón de diseño) - Wikipedia, la enciclopedia libre, fecha
de acceso: junio 28, 2025,
https://es.wikipedia.org/wiki/Factory_Method_(patr%C3%B3n_de_dise%C
3%B1o)
22. Factory Method. Patrones de Diseño PARTE 5 | by Codeicus - Medium,
fecha de acceso: junio 28, 2025, https://medium.com/somos-
codeicus/factory-method-2d3cb7589251
23. What is a factory pattern? Definition, UML diagram, and example -
IONOS, fecha de acceso: junio 28, 2025,
https://www.ionos.com/digitalguide/websites/web-development/what-is-a-
factory-method-pattern/
24. Understanding the Factory Method Design Pattern: The Key to Flexibility
in Object Creation, fecha de acceso: junio 28, 2025,
https://medium.com/@kalanamalshan98/understanding-the-factory-
method-design-pattern-the-key-to-flexibility-in-object-creation-
91903a7f8485
25. Patrón Factory Method | Flexibiliza la Creación de Objetos - Codeando
Simple, fecha de acceso: junio 28, 2025,
https://codeandosimple.com/design-patterns-factory-method.html
26. Patrón de Diseño Factory - Oscar Blancarte - Software Architecture, fecha
de acceso: junio 28, 2025,
https://www.oscarblancarteblog.com/2014/07/18/patron-de-diseno-factory/
27. Como usar el Patrón Factory en Javacript - Javascript en español, fecha
de acceso: junio 28, 2025, https://javascript.com.es/como-usar-el-patron-
factory-en-javacript
28. Factory Method explicado fácil | Patrón de diseño con ejemplos reales -
YouTube, fecha de acceso: junio 28, 2025,
https://www.youtube.com/watch?v=jWmC_aBJM_8
29. ¿Qué es el patrón Factory? Definición, diagrama UML y ejemplo ..., fecha
de acceso: junio 28, 2025, https://www.ionos.com/es-
us/digitalguide/paginas-web/desarrollo-web/patron-factory/
30. The Factory Method Design Pattern - UMLBoard, fecha de acceso: junio
28, 2025, https://www.umlboard.com/design-patterns/factory-method.html
31. Patrones de Diseño - Factory Method - Tomás Hernández, fecha de
acceso: junio 28, 2025, https://www.tomihq.com/blog/pattern-design-
factory
53
Capítulo 5: El Patrón Abstract Factory: Orquestación de
Familias de Objetos
Sección 5.1: Propósito e Intención Fundamentales
El patrón de diseño Abstract Factory es un patrón creacional cuyo propósito
fundamental es proporcionar una interfaz para crear familias de objetos
relacionados o dependientes sin necesidad de especificar sus clases
concretas. Este patrón se vuelve indispensable en escenarios donde un
1
sistema debe ser configurado para operar con múltiples familias de productos,
garantizando al mismo tiempo que los productos de una familia dada sean
siempre compatibles y coherentes entre sí. 1
Para comprender el concepto central de "familia de productos", resulta
ilustrativa la analogía de un simulador de una tienda de muebles. Considérese
1
que la aplicación debe manejar productos como sillas, sofás y mesillas. Estos
productos no existen de forma aislada, sino que se presentan en variantes o
estilos, como "Moderno", "Victoriano" o "ArtDecó". Una "familia de productos"
estaría compuesta por todos los muebles de un mismo estilo; por ejemplo, la
familia "Victoriana" incluiría la
SillaVictoriana, el SofaVictoriano y la MesillaVictoriana. El problema que el
patrón Abstract Factory busca resolver es evitar la inconsistencia que surgiría
al mezclar productos de diferentes familias, como combinar un sofá de estilo
moderno con sillas de estilo victoriano en una misma escena. Un cliente que
1
recibe muebles que no combinan estaría, con razón, insatisfecho.
El patrón aborda este desafío al encapsular la creación de todos los productos
de una familia dentro de una "fábrica" específica. Si el código cliente necesita
crear un mueble, no lo instancia directamente, sino que solicita a la fábrica de
la familia activa que lo cree. De esta manera, si la aplicación está configurada
para usar la FabricaModerna, cualquier solicitud de un sofá resultará
inevitablemente en un SofaModerno, garantizando la cohesión estilística en
todo momento. 1
Esta aproximación resuelve dos problemas críticos en el diseño de software.
Primero, logra la independencia del cliente: el código que utiliza los objetos
(el cliente) no necesita conocer las clases concretas de los productos que está
creando, lo que lo desacopla de la implementación específica. Segundo,
2
impone la
coherencia de la familia: asegura que los productos diseñados para ser
utilizados juntos se empleen de manera consistente, cumpliendo con esta
restricción a lo largo de toda la aplicación.
1
Sin embargo, una comprensión más profunda revela que la función del patrón
Abstract Factory trasciende la mera instanciación de objetos. Su rol es, en
esencia, el de un gobernador de diseño en tiempo de ejecución. Un
desarrollador podría, por ejemplo, utilizar constructores directos o incluso el
54
patrón Factory Method para crear elementos de una interfaz de usuario (UI),
como botones y casillas de verificación. No obstante, ninguno de estos
enfoques impide intrínsecamente que el desarrollador instancie un
WindowsButton y un MacCheckbox en la misma ventana de la aplicación. Tal
combinación daría lugar a una UI visualmente discordante e inconsistente.
El patrón Abstract Factory eleva el nivel de abstracción para prevenir esta
situación. En lugar de solicitar "un botón", el cliente solicita a una "fábrica de
UI" activa que cree "su versión de un botón". Si el cliente ha sido configurado
en tiempo de ejecución con una WindowsUIFactory, es estructuralmente
imposible que reciba un MacCheckbox. Por lo tanto, el patrón actúa como un
guardián que encapsula las reglas de un sistema de diseño (como "Material
Design" de Google o las "Human Interface Guidelines" de Apple) y garantiza
que el código cliente no pueda violarlas, imponiendo así una disciplina de
diseño coherente en toda la aplicación.
Sección 5.2: Análisis Estructural y Participantes Clave
La estructura del patrón Abstract Factory se compone de cinco participantes
clave que colaboran para desacoplar al cliente de las clases concretas de los
productos que crea. La comprensión de estos roles es fundamental para su
1
correcta implementación.
1. AbstractFactory (Fábrica Abstracta): Es una interfaz o clase abstracta
que declara un conjunto de métodos para crear cada uno de los productos
abstractos que componen la familia. Por ejemplo, una GUIFactory podría
declarar los métodos createButton() y createCheckbox(). Esta interfaz
1
define el contrato para todas las fábricas concretas, pero no implementa la
lógica de creación.
2. ConcreteFactory (Fábrica Concreta): Son las clases que implementan la
interfaz AbstractFactory. Cada fábrica concreta corresponde a una variante
específica de productos. Por ejemplo, WindowsFactory y MacOSFactory
serían implementaciones de GUIFactory. La WindowsFactory
implementaría createButton() para devolver una instancia de
WindowsButton, mientras que la MacOSFactory devolvería un
MacOSButton. Es aquí donde reside la lógica de instanciación de los
1
productos concretos.
3. AbstractProduct (Producto Abstracto): Son las interfaces o clases
abstractas para cada tipo de producto distinto pero relacionado que forma
parte de la familia. Siguiendo el ejemplo de la UI, Button y Checkbox
serían las interfaces de producto abstracto. Estas interfaces definen las
1
operaciones comunes que todos los productos de ese tipo deben realizar,
como un método
paint().
4. ConcreteProduct (Producto Concreto): Son las implementaciones
específicas de las interfaces de producto abstracto, agrupadas por
variante. Por ejemplo, WindowsButton y MacButton son productos
concretos que implementan la interfaz Button. De manera similar,
WindowsCheckbox y MacCheckbox implementan Checkbox. Cada
producto concreto es creado por su fábrica concreta correspondiente. 1
55
5. Client (Cliente): Es la clase que utiliza los objetos creados por la fábrica.
El aspecto crucial es que el cliente interactúa exclusivamente a través de
las interfaces AbstractFactory y AbstractProduct. Nunca hace referencia a
las clases concretas de fábricas o productos. El cliente recibe un objeto de
1
fábrica concreto en tiempo de ejecución (generalmente a través de
inyección de dependencias) y lo utiliza para crear los productos que
necesita, sin saber ni preocuparse por la variante específica que está
utilizando.
Esta estructura desacopla eficazmente al cliente de la implementación. El
cliente puede funcionar con cualquier familia de productos sin necesidad de
modificar su código; el cambio de una familia de productos a otra se logra
simplemente instanciando y proporcionando una fábrica concreta diferente al
cliente.
Sección 5.3: Contraste Esencial: Abstract Factory vs. Factory Method
Una de las confusiones más persistentes en el estudio de los patrones de
diseño es la distinción entre Abstract Factory y Factory Method. Aunque ambos
son patrones creacionales y sus nombres son similares, resuelven problemas
diferentes y operan a distintos niveles de abstracción. Su diferenciación se
puede analizar a través de dos ejes principales: la intención y el mecanismo
estructural.
8
Diferencia de Intención y Alcance
La distinción más fundamental radica en el alcance de lo que crean:
Factory Method: Su propósito es definir una interfaz para crear un único
objeto, pero permitiendo que las subclases decidan qué clase concreta
instanciar. Es un patrón que delega la responsabilidad de la instanciación
11
de un tipo de producto a las subclases. Por ejemplo, una clase
Logistics podría tener un método fábrica createTransport(), y sus subclases
RoadLogistics y SeaLogistics lo implementarían para devolver un Truck o
un Ship, respectivamente. El foco está en la creación de un solo producto a
la vez.
Abstract Factory: Su propósito es crear familias de objetos
relacionados o dependientes. No se trata de crear un solo objeto, sino
1
un conjunto de productos que están diseñados para funcionar juntos y ser
coherentes. Una
GUIFactory no solo crea botones, sino también checkboxes, menús, etc., y
garantiza que todos pertenezcan a la misma variante (ej. todos estilo
Windows).
Diferencia de Mecanismo Estructural
56
La implementación de cada patrón también difiere fundamentalmente en su
enfoque estructural:
Factory Method: Generalmente se implementa utilizando herencia. El
cliente suele ser una subclase (ConcreteCreator) que hereda de una clase
base (Creator) y sobrescribe el método de fábrica para proporcionar una
implementación específica. La decisión sobre qué objeto crear está ligada
9
a la clase del creador.
Abstract Factory: Se implementa utilizando composición. El cliente no
hereda de la fábrica; en su lugar, contiene una referencia a un objeto
AbstractFactory. Esta fábrica se le proporciona al cliente en tiempo de
9
ejecución. Esto permite cambiar toda la familia de productos que el cliente
utiliza simplemente pasándole una instancia de una fábrica concreta
diferente, sin necesidad de cambiar la clase del cliente.
Esta distinción revela que los patrones operan en diferentes niveles de
abstracción creacional. La instanciación directa con new representa el nivel
más bajo de abstracción, con un acoplamiento máximo. Un nivel por encima se
encuentra el Factory Method, que abstrae la clase concreta del producto, pero
el creador sigue estando directamente involucrado en la creación de un tipo de
producto. En el nivel más alto se sitúa la Abstract Factory, que abstrae la
propia fábrica. El cliente no solo desconoce la clase del producto, sino también
la clase de la fábrica. Se programa contra una interfaz de fábrica, que es
intercambiable.
Es común que las implementaciones de AbstractFactory utilicen Factory
Method para cada uno de sus métodos de creación. Esto demuestra que los
15
patrones no son mutuamente excluyentes, sino que pueden componerse, con
Abstract Factory actuando como un orquestador para un conjunto de Factory
Methods.
La siguiente tabla resume estas diferencias clave para proporcionar una
referencia clara:
Criterio Factory Method Abstract Factory
Intención Crear un único objeto, Crear familias de objetos
Principal delegando la instanciación a relacionados y compatibles.
subclases.
Alcance (Nº Un tipo de producto a la vez. Múltiples tipos de productos que
de Productos) pertenecen a una familia.
Mecanismo Herencia. El cliente es una Composición. El cliente tiene una
Principal subclase que sobrescribe el referencia a un objeto fábrica.
método fábrica.
Relación con El cliente es un creador El cliente usa un creador (fábrica).
el Cliente (subclase).
Casos de Uso Frameworks donde el código Sistemas que necesitan soportar
Típicos base necesita crear objetos múltiples "temas" o "plataformas" (ej.
57
que son definidos por los UI para Windows/macOS, kits de
usuarios del framework. bases de datos).
Sección 5.4: Implementación de Referencia: Creación de Interfaces de
Usuario Multiplataforma
Para solidificar la comprensión del patrón Abstract Factory, se presenta una
implementación completa en Java. El ejemplo aborda un problema clásico: la
creación de un kit de interfaz de usuario (UI) que debe funcionar en diferentes
sistemas operativos, como Windows y macOS, manteniendo la coherencia
visual en cada plataforma. 2
Paso 1: Definir las Interfaces de Producto Abstracto (AbstractProduct)
Primero, se definen las interfaces para cada elemento de la UI que formará
parte de nuestra familia de productos. Estas interfaces declaran las
operaciones comunes que todos los productos concretos deben implementar.
Java
// Interfaces para los productos abstractos: Button y Checkbox
// Definen el contrato común para todas las variantes.
// [2, 8]
public interface Button {
void paint();
}
public interface Checkbox {
void paint();
}
Paso 2: Crear los Productos Concretos (ConcreteProduct)
A continuación, se crean las implementaciones concretas para cada sistema
operativo. Cada clase implementa una de las interfaces de producto abstracto y
proporciona la lógica de renderizado específica de su plataforma.
Java
// Implementaciones concretas para la familia "Windows"
// [2, 7]
public class WindowsButton implements Button {
@Override
public void paint() {
58
System.out.println("Renderizando un botón estilo Windows.");
}
}
public class WindowsCheckbox implements Checkbox {
@Override
public void paint() {
System.out.println("Renderizando una casilla de verificación estilo Windows.");
}
}
// Implementaciones concretas para la familia "macOS"
public class MacOSButton implements Button {
@Override
public void paint() {
System.out.println("Renderizando un botón estilo macOS.");
}
}
public class MacOSCheckbox implements Checkbox {
@Override
public void paint() {
System.out.println("Renderizando una casilla de verificación estilo macOS.");
}
}
Paso 3: Definir la Fábrica Abstracta (AbstractFactory)
Se declara la interfaz de la fábrica abstracta. Esta interfaz contiene un método
de creación para cada tipo de producto abstracto en la familia.
Java
// Interfaz de la fábrica abstracta
// Declara los métodos para crear cada tipo de producto.
// [2, 7]
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
Paso 4: Implementar las Fábricas Concretas (ConcreteFactory)
Ahora, se implementan las fábricas concretas, una para cada familia de
productos (Windows y macOS). Cada fábrica implementa los métodos de la
interfaz GUIFactory para instanciar y devolver los productos concretos de su
respectiva familia.
59
Java
// Fábrica concreta para la familia "Windows"
// [2, 7]
public class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
// Fábrica concreta para la familia "macOS"
public class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
Paso 5: Crear el Cliente (Client)
El cliente, en este caso una clase Application, se diseña para trabajar
únicamente con las interfaces abstractas GUIFactory, Button y Checkbox.
Recibe una fábrica en su constructor y la utiliza para crear los elementos de la
UI.
Java
// El cliente que utiliza la fábrica para crear la UI.
// No tiene conocimiento de las clases concretas.
// [2]
public class Application {
private Button button;
private Checkbox checkbox;
60
{
public Application(GUIFactory factory)
button = factory.createButton();
checkbox = factory.createCheckbox();
}
{
public void paint()
button.paint();
checkbox.paint();
}
}
Paso 6: Configuración y Uso en Tiempo de Ejecución
Finalmente, el punto de entrada de la aplicación determina qué fábrica concreta
instanciar, basándose en alguna configuración del entorno, como el nombre del
sistema operativo. Esta fábrica se pasa luego a la aplicación cliente.
Java
// Punto de entrada que configura la aplicación con la fábrica correcta.
// [2]
public class ApplicationRunner {
public static void main(String args) {
GUIFactory factory;
String osName = System.getProperty("os.name").toLowerCase();
// Decide qué fábrica usar en tiempo de ejecución
if (osName.contains("win")) {
factory = new WindowsFactory();
} else {
factory = new MacOSFactory();
}
// El cliente es configurado con la fábrica seleccionada
Application app = new Application(factory);
app.paint();
}
}
Al ejecutar este código en un sistema Windows, la salida será la renderización
de elementos de Windows. En un Mac, producirá la de macOS. Lo más
importante es que la clase Application permanece sin cambios, demostrando el
poder de desacoplamiento del patrón.
Sección 5.5: Aplicabilidad y Consideraciones Estratégicas
61
La decisión de implementar el patrón Abstract Factory debe basarse en una
evaluación cuidadosa de los requisitos del sistema. Es una solución poderosa,
pero su complejidad adicional solo se justifica en ciertos escenarios.
Cuándo Usar el Patrón:
Cuando un sistema debe ser independiente de cómo se crean,
componen y representan sus productos. El patrón oculta los detalles de
2
implementación de las clases de producto del cliente.
Cuando un sistema debe ser configurado con una de varias familias de
productos. Como se vio en el ejemplo de la UI, el patrón permite cambiar
2
fácilmente entre familias (Windows, macOS) en tiempo de ejecución.
Cuando se necesita garantizar la compatibilidad entre los productos de
una familia. El patrón asegura que los objetos creados por una fábrica
1
concreta están diseñados para funcionar juntos.
Ventajas:
Aislamiento de Clases Concretas: El cliente opera a un nivel de
abstracción, utilizando solo interfaces. Esto evita un acoplamiento fuerte
entre el cliente y las clases de producto concretas, lo que mejora la
mantenibilidad.1
Facilidad de Intercambio de Familias de Productos: Cambiar la familia
de productos utilizada por la aplicación es tan simple como instanciar una
clase de fábrica concreta diferente. Esto puede hacerse con una sola línea
de código en el punto de inicialización de la aplicación.
1
Promoción de la Consistencia: Al forzar el uso de productos de una sola
familia a la vez, el patrón garantiza la coherencia en todo el sistema. Por
ejemplo, previene la mezcla de elementos de UI de diferentes temas. 1
Desventajas:
Dificultad para Añadir Nuevos Tipos de Productos: La principal
desventaja del patrón es su rigidez a la hora de extender la familia de
productos. Si se necesita añadir un nuevo tipo de producto (por ejemplo,
un TextField a la GUIFactory), se debe modificar la interfaz
AbstractFactory. Este cambio, a su vez, obliga a modificar todas las clases
ConcreteFactory existentes para implementar el nuevo método de
creación. Esto viola el Principio de Abierto/Cerrado, que establece que el
software debe estar abierto a la extensión pero cerrado a la modificación.
2
Por esta razón, el patrón es más adecuado cuando la familia de productos
es estable y es poco probable que cambie.
62
Parte II: Capítulo 6: El Patrón Builder: Construcción de
Objetos Complejos Paso a Paso
Sección 6.1: Propósito e Intención Fundamentales
El patrón de diseño Builder es una solución creacional que permite construir
objetos complejos paso a paso. Su intención principal es separar el proceso
17
de construcción de un objeto de su representación final. Esta separación
permite que el mismo proceso de construcción pueda ser utilizado para crear
diferentes representaciones del objeto. 17
A diferencia de otros patrones creacionales que producen un objeto en un solo
paso, el Builder ofrece un control más fino sobre el proceso de instanciación.
Es particularmente útil cuando un objeto requiere la configuración de
numerosos parámetros, muchos de los cuales pueden ser opcionales. Al 19
extraer la lógica de construcción a un objeto
Builder dedicado, el patrón simplifica el código del cliente, mejora la legibilidad
y aumenta la robustez del proceso de creación de objetos.
Sección 6.2: La Problemática de los Constructores Múltiples:
Telescópicos y JavaBeans
Para apreciar plenamente el valor del patrón Builder, es crucial analizar las
deficiencias de las alternativas más comunes para la creación de objetos
complejos. 23
Antipatrón del Constructor Telescópico (Telescoping Constructor)
Este enfoque consiste en proporcionar una serie de constructores
sobrecargados. El primero toma solo los parámetros obligatorios, y cada
constructor subsiguiente añade un parámetro opcional más, llamando al
constructor más largo de la cadena. 21
Problemas Fundamentales:
o Ilegibilidad y Propensión a Errores: Cuando el número de parámetros
crece, el código cliente se vuelve difícil de escribir y, sobre todo, de
leer. Una llamada como new Pizza(12, true, false, true, 0, 100) es
prácticamente ininteligible. Es extremadamente fácil confundir el orden
de los parámetros, especialmente si son del mismo tipo (por ejemplo,
múltiples boolean o int), lo que puede introducir errores sutiles que son
difíciles de depurar.
24
o Escalabilidad y Mantenimiento deficientes: Este patrón escala muy
mal. Añadir un nuevo parámetro opcional puede requerir la creación de
varios constructores nuevos para cubrir las combinaciones relevantes,
lo que lleva a una explosión de código repetitivo y difícil de mantener. 21
63
Patrón JavaBeans
Una alternativa es utilizar el patrón JavaBeans. En este enfoque, se
proporciona un constructor sin argumentos y se establecen los parámetros del
objeto a través de métodos setter individuales. 24
Problemas Fundamentales:
o Estado Inconsistente: El principal defecto de este enfoque es que la
construcción del objeto se divide en múltiples llamadas. El objeto se
crea primero en un estado potencialmente incompleto o inválido, y solo
se vuelve consistente después de que se hayan invocado todos los
setters necesarios. Durante este intervalo, otras partes del programa
podrían acceder al objeto en un estado inconsistente, lo que puede
causar comportamientos inesperados o errores. 23
o Falta de Inmutabilidad: El uso de setters requiere que el objeto sea
mutable. Esto impide la creación de objetos inmutables, que son
altamente deseables por su simplicidad, seguridad en entornos
concurrentes y predictibilidad. Un objeto mutable puede cambiar su
estado en cualquier momento, lo que complica el razonamiento sobre
el código. 24
El patrón Builder surge como una solución que combina la seguridad del
constructor telescópico con la legibilidad del patrón JavaBeans, sin heredar sus
desventajas.
Sección 6.3: La Solución del Builder: Flexibilidad, Legibilidad e
Inmutabilidad
El patrón Builder, especialmente en la variante popularizada por Joshua Bloch
en su libro "Effective Java", resuelve de manera elegante los problemas de las
alternativas al proporcionar un mecanismo de construcción que es a la vez
flexible, legible y seguro.
21
Legibilidad y API Fluida (Fluent API): El Builder utiliza una serie de
métodos encadenados que hacen que el código de creación sea auto-
descriptivo. En lugar de una lista de parámetros críptica, el cliente escribe
una secuencia de llamadas a métodos con nombres claros, como new
Pizza.Builder(12).withCheese(true).withPepperoni(true).build(). Esta
sintaxis fluida mejora drásticamente la legibilidad y el mantenimiento del
código.21
Flexibilidad con Parámetros Opcionales: Manejar parámetros
opcionales se vuelve trivial. Si un parámetro no es necesario, simplemente
no se invoca el método correspondiente en el builder. No hay necesidad de
pasar valores null o valores por defecto ficticios, lo que limpia el código del
cliente.
25
Consistencia e Inmutabilidad: El objeto final se construye en un único y
atómico paso a través de la llamada al método build(). Este método es el
lugar ideal para realizar validaciones y asegurar que el objeto se cree en
un estado consistente. Más importante aún, el patrón permite la creación
64
de objetos inmutables. El constructor del objeto producto se hace privado,
de modo que solo el Builder puede llamarlo. Una vez que el método build()
ha sido invocado, el objeto resultante no puede ser modificado, lo que
garantiza su integridad y lo hace seguro para su uso en entornos
concurrentes. 26
Este enfoque transforma la creación de objetos en un proceso más formal y
seguro. El Builder actúa como un contrato de construcción explícito. El
constructor del propio Builder puede requerir los parámetros obligatorios,
mientras que la API fluida maneja los opcionales. Esto combina lo mejor de
ambos mundos: la garantía de los constructores para los campos requeridos y
la flexibilidad de los setters para los opcionales. El método build() funciona
como un punto de "commit" transaccional: antes de su llamada, la
configuración es mutable y está en progreso; después de su llamada, el objeto
resultante es atómico, consistente e inmutable. Este proceso formalizado
mejora drásticamente la robustez y fiabilidad del software.
La siguiente tabla compara las tres técnicas de construcción de objetos:
Criterio Constructor Patrón JavaBeans Patrón Builder
Telescópico (Bloch)
Legibilidad del Baja. Larga lista Alta. Llamadas a Muy Alta. API
Código Cliente de parámetros sin setters explícitos. fluida y auto-
nombre. descriptiva.
Manejo de Pobre. Requiere Bueno. Se omiten las Excelente. Se
Parámetros múltiples llamadas a setters. omiten las
Opcionales constructores. llamadas a
métodos del
builder.
Garantía de Posible. El objeto Imposible. Requiere Excelente. Facilita
Inmutabilidad puede ser setters, por lo tanto, la creación de
inmutable. mutabilidad. objetos inmutables.
Consistencia del Garantizada. El No garantizada. El Garantizada. El
Estado objeto se crea en objeto puede estar en objeto se crea de
un estado válido. un estado forma atómica en
inconsistente durante el método build().
la construcción.
Complejidad de Baja. Solo se Baja. Solo se Media. Requiere
Implementación escriben escriben setters. una clase Builder
constructores. adicional.
Sección 6.4: Implementación de Referencia: Construcción de un
HttpRequest
Un ejemplo canónico y moderno del patrón Builder se encuentra en la API de
cliente HTTP de Java (introducida en Java 11). La clase
java.net.http.HttpRequest.Builder es una implementación perfecta del patrón de
65
Bloch, diseñada para construir objetos HttpRequest inmutables con una
multitud de parámetros opcionales. 30
El siguiente ejemplo práctico en Java demuestra cómo construir una solicitud
HTTP POST compleja:
Paso 1: Obtener una instancia del Builder
La construcción comienza obteniendo una nueva instancia del Builder.
Java
// [31, 32]
import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;
// Se obtiene una instancia del constructor
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
Paso 2: Configurar parámetros mediante la API fluida
A continuación, se configuran los diversos parámetros de la solicitud, tanto
obligatorios (como la URI) como opcionales (headers, timeout, cuerpo de la
solicitud), encadenando las llamadas a los métodos del builder.
Java
// [31, 32]
try {
// Se encadenan las llamadas para configurar la solicitud
requestBuilder.uri(new URI("https://api.example.com/data"))
.header("Content-Type", "application/json")
.header("X-Auth-Token", "your-secret-token")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString("{ \"key\": \"value\" }"));
} catch (java.net.URISyntaxException e) {
e.printStackTrace();
}
En este fragmento, cada método como .uri(), .header() o .timeout() modifica el
estado interno del requestBuilder y devuelve la misma instancia (this), lo que
permite el encadenamiento fluido.
66
Paso 3: Construir el objeto inmutable final
Una vez que todos los parámetros han sido configurados, se invoca el método
build() para crear el objeto HttpRequest final.
Java
// [31, 32]
// Se construye el objeto HttpRequest inmutable
HttpRequest request = requestBuilder.build();
// El objeto 'request' ya está listo para ser enviado.
System.out.println("Método: " + request.method());
System.out.println("URI: " + request.uri());
System.out.println("Headers: " + request.headers().map());
El objeto request resultante es inmutable. Sus propiedades no pueden ser
modificadas después de su creación, lo que lo hace seguro para ser reutilizado
o compartido entre hilos. Si se necesita una solicitud ligeramente diferente, se
puede modificar el requestBuilder original (o una copia) y llamar a build() de
nuevo para generar una nueva instancia de HttpRequest.
Sección 6.5: Variantes y Consideraciones de Diseño
Aunque el patrón Builder de Bloch es el más comúnmente encontrado en las
APIs modernas, es importante distinguirlo de la variante clásica descrita por el
"Gang of Four" (GoF), que introduce un componente adicional: el Director.
Builder de GoF con Director
En la versión original del patrón, la estructura es más compleja e incluye:
Builder (Interfaz): Define los pasos de construcción (ej. buildWalls(),
buildDoor()).
ConcreteBuilder: Implementa la interfaz Builder para una representación
específica (ej. WoodHouseBuilder, StoneHouseBuilder).
Product: El objeto complejo que se está construyendo.
Director: Una clase que conoce el algoritmo o la secuencia de pasos
necesarios para construir un producto. El cliente configura el Director con
una instancia de ConcreteBuilder y luego le pide que construya el objeto. 17
El propósito del Director es encapsular y reutilizar un proceso de construcción
complejo. Por ejemplo, un Director podría saber cómo construir una casa
(fundación, paredes, techo). Se le podría pasar un WoodHouseBuilder para
construir una cabaña de madera, o un StoneHouseBuilder para construir un
67
castillo de piedra. El mismo Director (proceso) puede usarse con diferentes
Builders para crear diferentes representaciones del producto. 20
Builder de Bloch (sin Director)
La variante de Bloch, como se vio en el ejemplo de HttpRequest, omite la clase
Director. El cliente asume la responsabilidad de llamar a los métodos del
builder en el orden que desee. El propósito principal aquí no es abstraer un
21
algoritmo de construcción complejo, sino proporcionar una alternativa legible y
segura a los constructores telescópicos y al patrón JavaBeans para objetos con
muchos parámetros opcionales.
Esta distinción es crucial para una comprensión experta del patrón. Aunque
comparten el nombre "Builder", las variantes de GoF y Bloch resuelven
problemas fundamentalmente diferentes:
1. El GoF Builder se centra en el proceso de construcción. Responde a la
pregunta: "¿Cómo puedo reutilizar un algoritmo de construcción para crear
diferentes tipos de objetos?". La flexibilidad reside en intercambiar el
Builder mientras el Director mantiene el mismo proceso. Es un patrón de
abstracción de procesos.
2. El Bloch Builder se centra en la conveniencia del constructor.
Responde a la pregunta: "¿Cómo puedo crear un objeto con muchos
parámetros opcionales de una manera legible, segura e inmutable?". Es un
patrón de instanciación fluida.
Debido a su simplicidad y su enfoque directo en la mejora de la legibilidad y la
seguridad de la API, la variante de Bloch se ha convertido en la implementación
predominante en la práctica moderna del desarrollo de software.
Conclusión
Los patrones Abstract Factory y Builder, aunque ambos creacionales, abordan
desafíos de diseño distintos y operan a diferentes niveles de abstracción.
Abstract Factory actúa como un "kit de creación" cohesivo. Su principal
fortaleza es garantizar la coherencia y compatibilidad entre una familia de
objetos relacionados. Es la elección correcta cuando el sistema necesita
operar con diferentes "temas" o "plataformas" (como kits de UI para distintos
sistemas operativos) y el principal desafío es asegurar que no se mezclen
componentes de familias incompatibles. Su debilidad es la rigidez a la hora de
añadir nuevos tipos de productos a la familia.
Builder, por otro lado, funciona como un "asistente de ensamblaje" para un
único objeto. Su propósito es simplificar la construcción de un único objeto
complejo con múltiples parámetros opcionales. Sobresale en mejorar la
legibilidad del código, garantizar la consistencia del objeto en su creación y
facilitar la inmutabilidad. Es la solución ideal para reemplazar los problemáticos
constructores telescópicos y el patrón JavaBeans.
68
La elección estratégica entre ellos es clara: si el problema de diseño gira en
torno a qué variante de un conjunto de objetos crear, la solución es Abstract
Factory. Si el problema se centra en cómo configurar un único objeto complejo
de forma legible y segura, la respuesta es Builder. Comprender esta distinción
fundamental permite a los arquitectos y desarrolladores de software aplicar la
herramienta creacional correcta para el problema correcto, resultando en
sistemas más robustos, mantenibles y flexibles.
Obras citadas
1. Abstract Factory - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/es/design-patterns/abstract-factory
2. Patrón Abstract Factory | Creación de Objetos Flexibles y Escalables ...,
fecha de acceso: junio 29, 2025, https://codeandosimple.com/design-
patterns-abstract-factory.html
3. Patrones creacionales - Refactoring.Guru, fecha de acceso: junio 29,
2025, https://refactoring.guru/es/design-patterns/creational-patterns
4. More - O'Reilly Media, fecha de acceso: junio 29, 2025,
https://www.oreilly.com/search/?q=*&type=*&order_by=_oreilly_popularity
&topics=PHP
5. Abstract Factory - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/design-patterns/abstract-factory
6. Abstract Factory Pattern - GeeksforGeeks, fecha de acceso: junio 29,
2025, https://www.geeksforgeeks.org/system-design/abstract-factory-
pattern/
7. Abstract Factory Design Pattern in C++: Before and after - SourceMaking,
fecha de acceso: junio 29, 2025,
https://sourcemaking.com/design_patterns/abstract_factory/cpp/before-
after
8. Factory method Pattern and Abstract Factory Pattern | by Sumit Sagar -
Medium, fecha de acceso: junio 29, 2025, https://medium.com/@sumit-
s/factory-method-pattern-and-abstract-factory-pattern-89fdfb8c364e
9. What are the differences between Abstract Factory and Factory ..., fecha
de acceso: junio 29, 2025,
https://stackoverflow.com/questions/5739611/what-are-the-differences-
between-abstract-factory-and-factory-design-patterns
10. Difference between factory method and abstract factory pattern? - Reddit,
fecha de acceso: junio 29, 2025,
https://www.reddit.com/r/learnprogramming/comments/1bikubq/difference
_between_factory_method_and_abstract/
11. Factory Method - Refactoring.Guru, fecha de acceso: junio 28, 2025,
https://refactoring.guru/es/design-patterns/factory-method
12. Patrón Factory Method | Flexibiliza la Creación de Objetos - Codeando
Simple, fecha de acceso: junio 29, 2025,
https://codeandosimple.com/design-patterns-factory-method.html
13. Factory Method Design Pattern - SourceMaking, fecha de acceso: junio
29, 2025, https://sourcemaking.com/design_patterns/factory_method
14. Differentiating between Factory Method and Abstract Factory - Software
Engineering Stack Exchange, fecha de acceso: junio 29, 2025,
69
https://softwareengineering.stackexchange.com/questions/234942/differen
tiating-between-factory-method-and-abstract-factory
15. Abstract Factory Design Pattern - SourceMaking, fecha de acceso: junio
29, 2025, https://sourcemaking.com/design_patterns/abstract_factory
16. Abstract Factory - Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@bromanv/abstract-factory-4612ff65d074
17. Builder - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/es/design-patterns/builder
18. Diagrama de secuencia del patrón de diseño Builder - Scribd, fecha de
acceso: junio 29, 2025,
https://es.scribd.com/document/652076954/Diagrama-de-secuencia
19. Patrones de diseño en Kotlin - Parte 1 - Carrion.dev, fecha de acceso:
junio 29, 2025, https://carrion.dev/es/posts/design-patterns-1/
20. Builder - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/design-patterns/builder
21. Exploring Joshua Bloch's Builder design pattern in Java - Oracle Blogs,
fecha de acceso: junio 29, 2025,
https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-
builder-design-pattern-in-java
22. Builder Pattern in Java: Crafting Custom Objects with Clarity, fecha de
acceso: junio 29, 2025, https://java-design-patterns.com/patterns/builder/
23. The Builder Pattern for Constructors - Samuel Khongthaw, fecha de
acceso: junio 29, 2025, https://samuelkhongthaw.vercel.app/blog/builder-
pattern
24. medium.com, fecha de acceso: junio 29, 2025,
https://medium.com/@muradhajiyev/why-builder-pattern-not-telescoping-
or-javabeans-6daa689f418
25. Builder Design Pattern in Java | DigitalOcean, fecha de acceso: junio 29,
2025, https://www.digitalocean.com/community/tutorials/builder-design-
pattern-in-java
26. Effective Java! The Builder Pattern! - Scaled Code, fecha de acceso: junio
29, 2025, https://blog.scaledcode.com/blog/effective-java/effective-java-
builder-pattern/
27. Refactor to use Builder pattern or Telescoping constructor pattern - Stack
Overflow, fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/5603108/refactor-to-use-builder-
pattern-or-telescoping-constructor-pattern
28. Implement the Builder Pattern in Java | Baeldung, fecha de acceso: junio
29, 2025, https://www.baeldung.com/java-builder-pattern
29. Exploring Joshua Bloch's Builder design pattern in Java - Oracle Blogs,
fecha de acceso: junio 29, 2025,
https://blogs.oracle.com/content/published/api/v1.1/assets/CONT2B4CDA
10B349472DBEB5B390B11F5A85/native?cb=_cache_3c41&channelTok
en=4d6a6a00a153413e9a7a992032379dbf
30. HttpRequest.Builder - cr, fecha de acceso: junio 29, 2025,
https://cr.openjdk.org/~prappo/8087113/javadoc.01/java/net/httpclient/Http
Request.Builder.html
31. HttpRequest.Builder (Java SE 21 & JDK 21) - Oracle Help Center, fecha
de acceso: junio 29, 2025,
70
https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/h
ttp/HttpRequest.Builder.html
32. HttpRequest.Builder (Java SE 11 & JDK 11 ) - Oracle Help Center, fecha
de acceso: junio 29, 2025,
https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/h
ttp/HttpRequest.Builder.html
33. Builder Pattern - Spring Framework Guru, fecha de acceso: junio 29,
2025, https://springframework.guru/gang-of-four-design-patterns/builder-
pattern/
71
Capítulo 7: El Patrón Prototype: Creación de Objetos a
través de la Clonación
Fundamentos Conceptuales del Patrón Prototype
Intención y Principio Fundamental
El patrón de diseño Prototype es un patrón creacional cuyo propósito
fundamental es proporcionar un mecanismo para crear nuevos objetos
mediante la copia o clonación de una instancia existente, denominada
"prototipo". A diferencia de los métodos de creación convencionales que
1
instancian una clase desde cero utilizando el operador
new, el patrón Prototype delega el proceso de creación al propio objeto que se
va a duplicar. Este enfoque permite que un sistema genere nuevos objetos sin
2
acoplar el código cliente a las clases concretas de los productos que necesita.2
La intención principal es desacoplar el acto de creación del objeto de su uso. El
cliente no necesita conocer los detalles de la instanciación; simplemente
solicita a un objeto prototipo que genere una copia de sí mismo. Este principio
puede ilustrarse eficazmente con la analogía de la división celular mitótica: una
célula original (el prototipo) actúa como plantilla y participa activamente en la
creación de una copia idéntica, resultando en dos células con el mismo
genotipo. De manera similar, un objeto prototipo sirve como un molde para
2
generar nuevas instancias con un estado inicial idéntico o similar.
El Problema Solucionado: Eficiencia y Flexibilidad
El patrón Prototype aborda dos problemas críticos en el diseño de software: la
ineficiencia en la creación de objetos y la falta de flexibilidad en sistemas
dinámicos.
En primer lugar, su aplicación es particularmente valiosa cuando la creación de
un objeto es una operación costosa en términos de tiempo y recursos
computacionales. Ciertos objetos pueden requerir, para su inicialización,
5
accesos a bases de datos, llamadas a servicios de red, o la ejecución de
algoritmos complejos. Repetir estos procesos para cada nueva instancia puede
1
degradar significativamente el rendimiento de una aplicación. El patrón
Prototype mitiga este problema al permitir la clonación de un prototipo ya
inicializado, evitando así la necesidad de ejecutar repetidamente estas
operaciones costosas. 4
En segundo lugar, el patrón introduce un alto grado de flexibilidad,
especialmente en sistemas donde las clases a instanciar se especifican en
72
tiempo de ejecución. Permite añadir y eliminar productos dinámicamente,
3
simplemente registrando o desregistrando instancias prototípicas en un gestor
central. Este dinamismo es fundamental para aplicaciones que deben
9
adaptarse a configuraciones o contextos cambiantes sin necesidad de
recompilación.
Una de las implicaciones más profundas de este patrón es que ofrece una
alternativa a la herencia para gestionar variaciones de un objeto. Un enfoque
tradicional para manejar múltiples configuraciones (por ejemplo, un coche rojo y
un coche azul) podría implicar la creación de subclases como CocheRojo y
CocheAzul, lo que puede llevar a una proliferación de clases si las
combinaciones de atributos son numerosas. El patrón Prototype transforma
este problema de diseño estático en un problema de configuración dinámica.
En lugar de crear subclases, se instancia un único objeto Coche y se configura
como un prototipo (con color = "rojo"). Para obtener un coche rojo, se clona
este prototipo. Para un coche azul, se puede clonar el mismo prototipo y
simplemente modificar el atributo de color. Este enfoque permite definir "nuevos
tipos" de objetos mediante la composición y variación de valores en tiempo de
ejecución, en lugar de a través de la herencia en tiempo de compilación, lo que
resulta en sistemas más flexibles y con una jerarquía de clases más simple.2
Estructura y Participantes del Patrón
La estructura canónica del patrón Prototype es relativamente simple y se
compone de los siguientes participantes clave, como se ilustra en su diagrama
de clases UML :3
Prototype (Interfaz o Clase Abstracta): Declara la interfaz para la
clonación. Generalmente, esta interfaz contiene un único método, como
clone() o copy(), que las clases concretas deben implementar. 4
ConcretePrototype (Clase Concreta): Implementa la operación de
clonación definida en la interfaz Prototype. Es responsable de crear una
copia de sí misma, transfiriendo su estado al nuevo objeto.2
Client: Crea un nuevo objeto solicitando a una instancia de prototipo que
se clone. El cliente interactúa con los objetos a través de la interfaz
Prototype, por lo que no necesita conocer la clase concreta del objeto que
está clonando. 2
Para mejorar aún más el desacoplamiento y la gestión, a menudo se introduce
un componente opcional pero muy recomendado: el Registro de Prototipos
(Prototype Registry). Se trata de una clase gestora que mantiene una colección
de prototipos pre-configurados, típicamente en una estructura de datos como
un Map. El cliente puede solicitar un clon a este registro utilizando un
identificador (por ejemplo, una cadena de texto como
"CONFIG_PRODUCCION"), en lugar de mantener una referencia directa al
prototipo. Esto centraliza la gestión de prototipos y simplifica su acceso por
parte del cliente. 8
73
Implementación en Java: La Interfaz Cloneable y sus
Particularidades
En Java, la implementación canónica del patrón Prototype a menudo involucra
el uso de la interfaz java.lang.Cloneable y el método Object.clone(). Sin
embargo, este mecanismo presenta una serie de particularidades y críticas de
diseño que deben ser comprendidas en profundidad.
El Rol de java.lang.Cloneable
La interfaz Cloneable, introducida en JDK 1.0, es una interfaz marcadora
(marker interface). Esto significa que no declara ningún método o constante.
15
Su único propósito es "marcar" una clase para indicar a la Máquina Virtual de
Java (JVM) que es legal que el método
Object.clone() realice una copia campo por campo de sus instancias. 17
Si una clase no implementa Cloneable, cualquier intento de invocar el método
clone() sobre una de sus instancias (heredado de Object) resultará en el
lanzamiento de una CloneNotSupportedException. Este comportamiento
16
convierte la clonación en Java en un mecanismo de "opt-in", donde el
desarrollador debe declarar explícitamente la intención de que una clase sea
clonable.
El Método Object.clone(): Comportamiento y Contrato
El método clone() está definido como protected en la clase Object, lo que
significa que no es accesible directamente desde otras clases. Su
17
implementación nativa realiza una
copia superficial (shallow copy), copiando los valores de los campos de tipos
primitivos y las referencias de los campos de tipo objeto, pero no los objetos a
los que apuntan dichas referencias. 15
La documentación oficial de Java establece un contrato informal para el método
clone() :
18
La expresión x.clone()!= x debe ser verdadera, indicando que el clon es un
objeto distinto.
La expresión x.clone().getClass() == x.getClass() debe ser verdadera,
asegurando que el clon es del mismo tipo que el original.
La expresión x.clone().equals(x) es típicamente verdadera, aunque no es
un requisito absoluto.
74
Por convención, una clase que implementa Cloneable debe sobrescribir el
método clone() para hacerlo public, permitiendo así que los clientes lo
invoquen.16
Análisis Crítico del Diseño de Cloneable
El diseño del mecanismo de clonación en Java es uno de los aspectos más
criticados de su API estándar. La principal debilidad radica en que traslada la
18
verificación del contrato de clonación del tiempo de compilación al tiempo de
ejecución.
En un diseño ideal, una interfaz como Clonable<T> podría haber definido un
método público T clone(). Esto permitiría al compilador verificar estáticamente
que cualquier clase que implemente la interfaz proporcione correctamente el
método de clonación. Sin embargo, el diseño real de Java es diferente:
Cloneable es una interfaz vacía y Object.clone() es protected. Esto obliga al
desarrollador a realizar varias tareas propensas a errores: implementar la
interfaz marcadora, sobrescribir clone() para cambiar su visibilidad, manejar la
CloneNotSupportedException (incluso en casos donde lógicamente no debería
ocurrir), y realizar un casting del resultado de Object al tipo correcto.
19
Este enfoque establece un contrato implícito en lugar de uno explícito, lo cual
es contrario a los principios de un buen diseño de API. Debido a esta
"fragilidad", muchos expertos desaconsejan su uso y proponen alternativas
más robustas y seguras, como los constructores de copia (e.g., public
MiClase(MiClase original)) o los métodos de fábrica de copia (e.g., public
static MiClase newInstance(MiClase original)). Estas alternativas ofrecen
seguridad de tipos en tiempo de compilación y un contrato mucho más claro y
explícito para la creación de copias.
10
La Distinción Crítica: Copia Superficial vs. Copia Profunda
(Shallow vs. Deep Copy)
Una de las decisiones más importantes al implementar el patrón Prototype es la
estrategia de copia a utilizar. La elección entre una copia superficial y una
profunda tiene implicaciones directas sobre el aislamiento y la independencia
del estado entre el objeto original y su clon.
Comprendiendo la Copia Superficial (Shallow Copy)
Una copia superficial, que es el comportamiento por defecto del método
super.clone(), copia únicamente los campos de nivel superior de un objeto. Si
un campo es de un tipo de dato primitivo (como int o boolean), su valor se
75
copia directamente. Sin embargo, si un campo es una referencia a otro objeto
(como un ArrayList o una clase personalizada), lo que se copia es la dirección
de memoria (la referencia), no el objeto en sí.
12
El principal riesgo de este enfoque es que el objeto original y su clon terminan
compartiendo los mismos objetos internos. Cualquier modificación realizada en
un objeto referenciado a través del clon afectará también al objeto original, y
viceversa. Esto puede introducir efectos secundarios inesperados y errores
7
sutiles que son difíciles de rastrear y depurar. Por ejemplo, si se clona un
objeto
Pedido que contiene una lista de Items, una copia superficial resultaría en dos
objetos Pedido que apuntan a la misma lista de Items. Agregar un ítem a través
del clon lo agregaría también al original.
Dominando la Copia Profunda (Deep Copy)
Una copia profunda, por el contrario, duplica todo el grafo de objetos. No solo
se crea una nueva instancia del objeto principal, sino que también se crean
nuevas instancias de cada objeto mutable referenciado, de forma recursiva. El
resultado es un clon que es completamente independiente del original en
términos de estado. 12
Para implementar una copia profunda, es necesario sobrescribir el método
clone() de manera que, después de llamar a super.clone() para obtener la copia
superficial inicial, se clonen explícitamente cada uno de los campos que son
objetos mutables. Esto asegura que el nuevo objeto tenga sus propias copias
5
de todos los componentes internos.
A pesar de su aparente simplicidad en ejemplos básicos, la implementación de
una copia profunda robusta puede ser considerablemente compleja. Un desafío
clave surge al tratar con grafos de objetos que contienen referencias
circulares (por ejemplo, un objeto A que referencia a B, y B que a su vez
referencia a A). Un algoritmo de clonación profundo e ingenuo que intente
clonar recursivamente cada objeto referenciado entraría en un bucle infinito.
10
Para manejar correctamente estos casos, una implementación avanzada de
clone() necesitaría un mecanismo para rastrear los objetos que ya han sido
clonados durante la operación actual, por ejemplo, utilizando un Map<Object,
Object> que mapee las instancias originales a sus clones correspondientes.
Esta complejidad oculta es una desventobernaja significativa del patrón que a
menudo se pasa por alto en las discusiones introductorias.
Tabla Comparativa: Estrategias de Clonación
76
La siguiente tabla resume las diferencias clave entre las dos estrategias de
clonación para ayudar a decidir cuál es la más adecuada para un caso de uso
específico.
Característica Copia Superficial (Shallow Copia Profunda (Deep Copy)
Copy)
Objetos Se copian las referencias; el Se clonan recursivamente; el clon
Referenciados clon y el original comparten y el original tienen grafos de
los mismos objetos objetos completamente
internos.15 independientes.15
Rendimiento Más rápido y menos Más lento y costoso en recursos
intensivo en memoria.21 debido a la creación de nuevos
objetos.21
Aislamiento de Bajo. Cambios en los Alto. El clon y el original son
Estado objetos referenciados del completamente independientes.20
clon afectan al original.20
Implementación super.clone() realiza una Requiere la sobrescritura manual
por Defecto copia superficial.15 del método clone() para clonar
cada objeto mutable.5
Caso de Uso Ideal Objetos que contienen solo Objetos que contienen referencias
tipos de datos primitivos y/o a objetos mutables donde se
referencias a objetos requiere independencia total.
inmutables.
Aplicación Práctica: Estudio de Caso sobre Gestión de
Configuración
Para ilustrar la aplicación práctica del patrón Prototype, se analizará el
escenario de un sistema que debe gestionar múltiples objetos de configuración.
Escenario del Problema
Considere una aplicación que necesita operar en diferentes entornos, como
desarrollo, pruebas y producción. Cada entorno requiere un objeto de
configuración que, si bien comparte una base común de propiedades (por
ejemplo, timeout, logLevel, cacheEnabled), difiere en valores clave como la
URL de la base de datos o las credenciales de un servicio externo. Crear cada
22
uno de estos objetos de configuración desde cero sería un proceso repetitivo y
propenso a errores. Cargar un archivo de configuración base para cada
instancia sería ineficiente. En este contexto, el patrón Prototype ofrece una
solución elegante y eficaz.22
77
El Prototipo de Configuración Base
El primer paso es definir una clase base para la configuración, por ejemplo,
BaseConfiguration, que implemente Cloneable. Esta clase contendrá todas las
22
propiedades comunes y sus valores por defecto. Es crucial que su método
clone() realice una copia profunda, especialmente si alguna de sus propiedades
es un objeto mutable (como un Map para configuraciones personalizadas), para
garantizar que los clones sean totalmente independientes.
Java
// Interfaz para el prototipo de configuración
public interface ConfigurationPrototype extends Cloneable {
ConfigurationPrototype clone() throws CloneNotSupportedException;
void setProperty(String key, String value);
String getProperty(String key);
}
// Implementación concreta del prototipo
import java.util.HashMap;
import java.util.Map;
public class SystemConfiguration implements ConfigurationPrototype {
private String version;
private int timeout;
private Map<String, String> customProperties;
public SystemConfiguration(String version, int timeout) {
this.version = version;
this.timeout = timeout;
this.customProperties = new HashMap<>();
// Carga de propiedades por defecto
this.customProperties.put("logLevel", "INFO");
this.customProperties.put("cacheEnabled", "true");
}
// Getters y Setters
public void setProperty(String key, String value) {
this.customProperties.put(key, value);
}
public String getProperty(String key) {
return this.customProperties.get(key);
}
@Override
78
public ConfigurationPrototype clone() throws CloneNotSupportedException {
SystemConfiguration clonedConfig = (SystemConfiguration) super.clone();
// Implementación de copia profunda para el mapa de propiedades
clonedConfig.customProperties = new HashMap<>(this.customProperties);
return clonedConfig;
}
@Override
public String toString() {
return "SystemConfiguration{" +
"version='" + version + '\'' +
", timeout=" + timeout +
", customProperties=" + customProperties +
'}';
}
}
Implementación de un Registro de Prototipos (ConfigurationRegistry)
A continuación, se desarrolla una clase ConfigurationRegistry que actúa como
un gestor centralizado de los prototipos de configuración, similar al concepto
discutido en. Esta clase encapsula la creación y el acceso a los prototipos.
8
Java
import java.util.HashMap;
import java.util.Map;
public class ConfigurationRegistry {
private Map<String, ConfigurationPrototype> prototypes = new HashMap<>();
public void addPrototype(String key, ConfigurationPrototype prototype) {
prototypes.put(key, prototype);
}
public ConfigurationPrototype getClone(String key) throws CloneNotSupportedException {
ConfigurationPrototype prototype = prototypes.get(key);
if (prototype == null) {
throw new IllegalArgumentException("Prototype with key '" + key + "' not
found.");
}
return prototype.clone();
}
}
79
Generación de Configuraciones Específicas mediante Clonación
Finalmente, el código cliente utiliza el ConfigurationRegistry para generar de
manera eficiente las configuraciones específicas para cada entorno.
Java
public class Application {
public static void main(String args) throws CloneNotSupportedException {
ConfigurationRegistry registry = new ConfigurationRegistry();
// Crear y registrar un prototipo base
SystemConfiguration baseConfig = new SystemConfiguration("1.0", 5000);
baseConfig.setProperty("database.url", "jdbc:mysql://default-db:3306/default");
registry.addPrototype("BASE_CONFIG", baseConfig);
// Crear configuración para el entorno de desarrollo
ConfigurationPrototype devConfig = registry.getClone("BASE_CONFIG");
devConfig.setProperty("database.url", "jdbc:mysql://dev-db:3306/dev_db");
devConfig.setProperty("logLevel", "DEBUG");
// Crear configuración para el entorno de producción
ConfigurationPrototype prodConfig = registry.getClone("BASE_CONFIG");
prodConfig.setProperty("database.url", "jdbc:mysql://prod-db:3306/prod_db");
prodConfig.setProperty("timeout", "10000"); // Un timeout más largo para producción
System.out.println("Dev Config: " + devConfig);
System.out.println("Prod Config: " + prodConfig);
}
}
Este ejemplo demuestra cómo, a partir de una única plantilla (BASE_CONFIG),
se pueden generar múltiples configuraciones personalizadas de manera
eficiente y segura. El cliente solicita un clon y solo necesita modificar las
propiedades que difieren, reduciendo la duplicación de código y el riesgo de
errores de configuración. 22
Consideraciones Estratégicas y Mejores Prácticas
80
Análisis de Ventajas
El patrón Prototype ofrece beneficios significativos en los escenarios
adecuados:
Eficiencia: Reduce drásticamente el coste de creación de objetos al eludir
procesos de inicialización complejos y repetitivos, como accesos a bases
de datos o llamadas de red. 1
Reducción de Subclases: Proporciona una alternativa elegante a la
herencia para manejar múltiples variaciones de un objeto, evitando la
"explosión de clases" y manteniendo la jerarquía del sistema más limpia y
manejable. 2
Flexibilidad en Tiempo de Ejecución: Permite que un sistema sea
altamente dinámico. Los prototipos pueden ser añadidos, eliminados o
modificados en tiempo de ejecución, alterando el comportamiento del
sistema sin necesidad de recompilar el código. 1
Desacoplamiento: Oculta las clases concretas de los productos al cliente,
que solo necesita interactuar con la interfaz del prototipo. Esto reduce las
dependencias y aumenta la modularidad del sistema. 2
Análisis de Desventajas y Advertencias
A pesar de sus ventajas, el patrón Prototype no está exento de inconvenientes
y debe aplicarse con criterio:
Complejidad de la Clonación: La implementación de un método clone()
robusto puede ser muy compleja, especialmente cuando se requiere una
copia profunda de objetos con jerarquías intrincadas o referencias
circulares.
10
Dependencia de Cloneable: Al utilizar el mecanismo nativo de Java, se
hereda la fragilidad y la naturaleza poco intuitiva de la interfaz Cloneable y
el método Object.clone(). 18
Inicialización del Clon: El patrón a menudo requiere una operación
adicional de "inicialización" o "reseteo" en el objeto clonado si no se desea
que herede todo el estado del prototipo. Esto puede añadir una capa de
complejidad al proceso.
Riesgo de Sobreuso: Como cualquier patrón, puede ser utilizado en
exceso. Para objetos simples cuya creación no es costosa, la instanciación
directa con el operador new es a menudo una solución más clara y
eficiente.
Conclusión y Recomendaciones Finales
Síntesis del Patrón Prototype
81
El patrón Prototype se presenta como una poderosa herramienta creacional
que prioriza la eficiencia y la flexibilidad dinámica sobre los mecanismos de
creación de objetos estáticos. Su principal fortaleza reside en la capacidad de
generar nuevas instancias a partir de la clonación de plantillas pre-
configuradas, lo cual es ideal para sistemas donde la creación de objetos es un
proceso costoso o donde se requiere una configuración dinámica en tiempo de
ejecución. Sin embargo, su implementación, particularmente en Java, exige
una atención meticulosa a los detalles de la estrategia de copia (superficial vs.
profunda) y a las peculiaridades inherentes al diseño de la interfaz Cloneable.
Recomendaciones de Implementación
Para aplicar el patrón Prototype de manera efectiva, se deben considerar las
siguientes recomendaciones:
Cuándo usarlo: El patrón es más beneficioso cuando la creación de
objetos es computacionalmente cara o cuando el sistema necesita crear
dinámicamente instancias a partir de un conjunto de plantillas
configurables.
Mejores Prácticas:
1. Preferir la Copia Profunda: Para objetos que contienen estado
mutable, se debe implementar siempre una copia profunda para
garantizar un verdadero aislamiento entre el clon y el original, evitando
efectos secundarios no deseados.
2. Considerar Alternativas a Cloneable: Dada la naturaleza problemática
del mecanismo de clonación nativo de Java, se debe considerar
seriamente el uso de alternativas como los constructores de copia o los
métodos de fábrica de copia estáticos. Estos enfoques proporcionan un
código más seguro, legible y mantenible.
3. Utilizar un Registro de Prototipos: Implementar un Prototype Registry
centraliza la gestión de los prototipos, desacopla aún más al cliente de
las instancias específicas y simplifica la lógica de la aplicación.
4. Documentar el Comportamiento: Es fundamental documentar
claramente si el método de clonación de una clase realiza una copia
superficial o profunda para evitar un uso incorrecto por parte de los
clientes de la clase.
Obras citadas
1. Mastering the Prototype Design Pattern: Efficient and Flexible Object
Creation, fecha de acceso: junio 29, 2025,
https://curatepartners.com/blogs/skills-tools-platforms/mastering-the-
prototype-design-pattern-efficient-and-flexible-object-creation/
2. Prototype - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/design-patterns/prototype
3. Prototype pattern - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Prototype_pattern
82
4. Prototype Design Pattern - Definition & Examples | Belatrix Blog -
Globant, fecha de acceso: junio 29, 2025, https://belatrix.globant.com/us-
en/blog/tech-trends/prototype-design-pattern/
5. Prototype Design Pattern in Java | DigitalOcean, fecha de acceso: junio
29, 2025, https://www.digitalocean.com/community/tutorials/prototype-
design-pattern-in-java
6. Prototype Design Pattern - DEV Community, fecha de acceso: junio 29,
2025, https://dev.to/zeeshanali0704/prototype-design-pattern-3i95
7. Introduction to Prototype Design Pattern | by Irushinie Muthunayake |
Nerd For Tech, fecha de acceso: junio 29, 2025,
https://medium.com/nerd-for-tech/introduction-to-prototype-design-pattern-
39407a57550f
8. Prototype Design Pattern - DEV Community, fecha de acceso: junio 29,
2025, https://dev.to/kurmivivek295/prototype-design-pattern-3743
9. Prototype, fecha de acceso: junio 29, 2025,
https://www.cs.unc.edu/~stotts/GOF/hires/pat3dfs.htm
10. Prototype Pattern in Java | Baeldung, fecha de acceso: junio 29, 2025,
https://www.baeldung.com/java-pattern-prototype
11. Prototype Pattern | C++ Design Patterns - GeeksforGeeks, fecha de
acceso: junio 29, 2025, https://www.geeksforgeeks.org/system-
design/prototype-pattern-c-design-patterns/
12. Prototype Design Pattern - Scaler Topics, fecha de acceso: junio 29,
2025, https://www.scaler.com/topics/design-patterns/prototype-design-
pattern/
13. Understanding Prototype Pattern. In some situations, creating a new… |
by Jalitha Dewapura | Design Patterns with Java | Medium, fecha de
acceso: junio 29, 2025, https://medium.com/design-patterns-with-
java/understanding-prototype-pattern-377e93dd93d8
14. Prototype Design Pattern: A Quick Guide - ScholarHat, fecha de acceso:
junio 29, 2025,
https://www.scholarhat.com/tutorial/designpatterns/prototype-design-
pattern
15. Understanding the Cloneable Interface, Shallow Copy, and Deep Copy in
Java | by Pratik T, fecha de acceso: junio 29, 2025,
https://medium.com/@pratik.941/understanding-the-cloneable-interface-
shallow-copy-and-deep-copy-in-java-73c45066ecb1
16. Cloneable Interface in Java - GeeksforGeeks, fecha de acceso: junio 29,
2025, https://www.geeksforgeeks.org/java/cloneable-interface-in-java/
17. Cloneable (Java Platform SE 8 ) - Oracle Help Center, fecha de acceso:
junio 29, 2025,
https://docs.oracle.com/javase/8/docs/api/java/lang/Cloneable.html
18. Cloneable, a Java design gotcha - A Java geek, fecha de acceso: junio
29, 2025, https://blog.frankel.ch/cloneable-java-design-gotcha/
19. Understanding the Cloneable Interface in Java: A Guide, fecha de acceso:
junio 29, 2025, https://javalessons.com/cloneable-interface-in-java/
20. General | Creational Design Patterns | Prototype Pattern - Codecademy,
fecha de acceso: junio 29, 2025,
https://www.codecademy.com/resources/docs/general/creational-design-
patterns/prototype-pattern
83
21. Difference between Shallow and Deep copy of a class ..., fecha de
acceso: junio 29, 2025, https://www.geeksforgeeks.org/difference-
between-shallow-and-deep-copy-of-a-class/
22. Prototype Design Patterns Use Case: Config Management | by Mehar
Chand | Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@mehar.chand.cloud/prototype-design-patterns-use-
case-config-management-dee7d837bd00
84
Capítulo 8: Adapter: Un Puente Arquitectónico para la
Interoperabilidad de Sistemas
Introducción: El Desafío de la Interoperabilidad en
Sistemas de Software
En el vasto y dinámico ecosistema del desarrollo de software, la integración de
componentes dispares no es una excepción, sino la norma. Los sistemas de
software modernos rara vez se construyen en un vacío monolítico; son, por
naturaleza, ensamblajes complejos de módulos internos, bibliotecas de
terceros, servicios externos y, con frecuencia, sistemas heredados (legacy)
que, a pesar de su antigüedad, siguen siendo críticos para el negocio. Este
1
panorama heterogéneo presenta un desafío fundamental y recurrente: la
incompatibilidad de interfaces. Un componente puede ofrecer la funcionalidad
exacta que se necesita, pero presentarla a través de una interfaz (un conjunto
de métodos y firmas) que es completamente ajena al resto del sistema. Esta
disonancia puede detener el progreso, forzando a los equipos a considerar
alternativas costosas y arriesgadas, como la reescritura de componentes
estables o la modificación de bibliotecas de terceros, lo cual a menudo es
inviable o imposible. 3
Es en este contexto de fricción integradora donde el patrón Adaptador
(Adapter) emerge como una solución arquitectónica de notable elegancia y
pragmatismo. Su propósito es actuar como un intermediario, un traductor que
permite la colaboración fluida entre objetos con interfaces que, de otro modo,
serían incompatibles. La analogía más citada y efectiva es la del adaptador de
4
corriente universal o el cargador de un teléfono móvil. Un viajero con un
6
dispositivo (el
Cliente) que requiere un tipo de enchufe específico puede conectarlo a una
toma de corriente completamente diferente (el Adaptado) en otro país, gracias
a un adaptador. Este dispositivo intermediario no modifica ni el aparato ni la
toma de corriente; simplemente convierte la interfaz de uno para que sea
compatible con la del otro. De manera similar, en el software, el patrón
Adaptador envuelve a un objeto para "traducir" su interfaz a una que el cliente
espera, permitiendo que sistemas dispares colaboren sin necesidad de alterar
su código fuente original. 3
Clasificado como un patrón de diseño estructural, el Adaptador se centra en
cómo las clases y los objetos se componen para formar estructuras más
grandes y flexibles. Su existencia y prevalencia son un testimonio de una
12
verdad fundamental en la ingeniería de software: los sistemas raramente son
"limpios" o diseñados desde una visión unificada y perfecta. Son ecosistemas
en evolución donde el pragmatismo a menudo supera a la pureza teórica. El
Adaptador es, por tanto, un patrón de practicidad, una herramienta reactiva
diseñada para resolver la fricción del mundo real. No se utiliza típicamente en
proyectos "greenfield" donde todas las interfaces pueden diseñarse de forma
85
cohesiva desde el principio, sino que responde a la realidad de los sistemas
heterogéneos que surgen de la evolución del software, la reutilización de
código y la necesidad de mantener y modernizar sistemas heredados. La
habilidad de un arquitecto de software no reside solo en diseñar sistemas
perfectos, sino también en diseñar formas de integrar con gracia sistemas
imperfectos o dispares. El patrón Adaptador es una herramienta primordial para
esta "evolución gestionada", permitiendo que nueva funcionalidad se incorpore
sin la necesidad de una refactorización a gran escala y de alto riesgo del
código existente y estable.
Sección 1: Deconstruyendo el Patrón Adaptador
Para dominar el uso del Adaptador, es imperativo deconstruir su anatomía,
comprender su intención precisa y analizar los principios de diseño que lo
sustentan. Solo a través de este análisis se puede apreciar su rol como una
pieza clave en la construcción de software robusto y mantenible.
1.1 Intención y Principios Fundamentales
La intención principal del patrón Adaptador, según fue definido por el "Gang of
Four" (GoF), es convertir la interfaz de una clase en otra interfaz que los
clientes esperan. El patrón permite que clases que de otro modo no podrían
3
colaborar debido a interfaces incompatibles, trabajen juntas. Es crucial
entender que el Adaptador logra esto sin modificar el código fuente de la clase
que está siendo adaptada (el Adaptee); en su lugar, la "envuelve" en un nuevo
objeto que presenta la interfaz deseada. 1
Esta aproximación se alinea perfectamente con varios principios de diseño
S.O.L.I.D., que son la base del buen diseño orientado a objetos:
Principio de Responsabilidad Única (SRP): El patrón Adaptador respeta
el SRP al aislar la lógica de conversión. La responsabilidad de traducir las
llamadas de una interfaz a otra se encapsula completamente dentro de la
clase Adapter. Esto mantiene la lógica de negocio del Client limpia y
enfocada en su tarea, y deja al Adaptee intacto, enfocado en su propia
funcionalidad. El Adapter tiene una única razón para cambiar: si la interfaz
del Target o del Adaptee cambia. 2
Principio de Abierto/Cerrado (OCP): El sistema se vuelve abierto a la
extensión pero cerrado a la modificación. Se pueden introducir nuevos
tipos de Adaptee en el sistema sin necesidad de modificar el código del
Client. Para cada nueva clase con una interfaz incompatible, simplemente
se crea un nuevo Adapter concreto. El Client sigue operando a través de la
interfaz Target, ajeno a las nuevas implementaciones que se han
integrado.14
86
1.2 Anatomía Arquitectónica: Participantes y Colaboraciones
La estructura del patrón Adaptador se define por la interacción de cuatro
participantes clave. Comprender el rol de cada uno es esencial para su correcta
implementación. 5
Target (Objetivo): Es la interfaz específica del dominio que el Client utiliza
y espera. Define el conjunto de operaciones que el cliente puede invocar.
En la práctica, suele ser una interfaz o una clase abstracta.
Client (Cliente): Es la clase que tiene una dependencia con la interfaz
Target y necesita interactuar con un objeto que la implemente. El Client
invoca métodos en una instancia de Adapter sin ser consciente de que, en
realidad, está comunicándose indirectamente con un Adaptee.
Adaptee (Adaptado): Es la clase existente que posee la funcionalidad
deseada pero presenta una interfaz incompatible con la interfaz Target. Es
la clase que necesita ser "adaptada".
Adapter (Adaptador): Es la clase que actúa como puente. Implementa la
interfaz Target y, simultáneamente, mantiene una referencia al Adaptee
(en el caso del Adaptador de Objeto) o hereda de él (en el caso del
Adaptador de Clase). Su función es traducir las llamadas del Client
(recibidas a través de la interfaz Target) en llamadas a los métodos
correspondientes del Adaptee.
Diagrama de Clases UML
El siguiente diagrama UML ilustra las relaciones estructurales entre los
participantes en las dos variantes principales del patrón: Adaptador de Objeto y
Adaptador de Clase.
87
Figura 1: Diagrama de Clases UML del Patrón Adaptador. La variante de
Objeto utiliza la composición, mientras que la de Clase utiliza la herencia. El
Cliente interactúa exclusivamente con la interfaz Target.
Diagrama de Secuencia
Para visualizar el comportamiento dinámico, un diagrama de secuencia es
ideal. Muestra el flujo de una llamada típica a través del adaptador.
1. El Client invoca un método request() en el objeto Adapter, al que conoce
solo a través de la interfaz Target.
2. El Adapter recibe la llamada. Dentro de su implementación del método
request(), traduce la solicitud.
3. El Adapter invoca el método specificRequest() en la instancia del Adaptee
que contiene, posiblemente pasando datos transformados.
4. El Adaptee ejecuta la operación y, si es necesario, devuelve un resultado
al Adapter.
5. El Adapter puede realizar una transformación adicional en el resultado
antes de devolverlo al Client como la respuesta esperada por la interfaz
Target.
Figura 2: Diagrama de Secuencia que ilustra cómo una llamada del Cliente es
redirigida y traducida por el Adaptador hacia el Adaptado.
El mecanismo central que hace funcionar al patrón Adaptador es la
indirección. Esta capa adicional es, simultáneamente, su mayor fortaleza y su
principal coste, representando un clásico trade-off arquitectónico. El Client no
invoca directamente al Adaptee, sino que pasa a través del Adapter. El3
beneficio inmediato de esta indirección es un
desacoplamiento total: el Client está completamente aislado del Adaptee y
solo conoce la interfaz Target. Esto permite que el Adaptee sea sustituido por
88
otro, o que su implementación interna cambie drásticamente, sin que el Client
se vea afectado en lo más mínimo. Sin embargo, esta capa de indirección
1
tiene un coste en términos de
complejidad y rendimiento. El sistema ahora cuenta con una clase adicional
que debe ser mantenida, lo que puede dificultar el seguimiento del código y la
depuración. Además, existe una ligera sobrecarga de rendimiento debido a la
1
llamada a método adicional. Esto implica que el patrón Adaptador debe usarse
de manera juiciosa. Si la interfaz del
Adaptee puede modificarse directamente y de forma segura, esa suele ser una
solución más simple. El Adaptador es más valioso cuando modificar el Adaptee
es imposible o indeseable, como en el caso de una biblioteca de terceros, un
componente heredado estable o cuando un cambio podría romper otras partes
del sistema. La decisión de usar un Adaptador es, por lo tanto, una decisión
estratégica que equilibra la necesidad inmediata de integración con el coste a
largo plazo de la complejidad añadida.
Sección 2: Las Dos Caras de la Adaptación: Adaptadores
de Objetos vs. de Clases
El patrón Adaptador se manifiesta canónicamente en dos formas distintas, cada
una con sus propios mecanismos, ventajas y limitaciones. La elección entre
ellas depende de los requisitos del problema y las capacidades del lenguaje de
programación utilizado. La distinción entre estas dos variantes es una
manifestación práctica del principio de diseño "favorecer la composición sobre
la herencia".
2.1 El Adaptador de Objetos: Adaptación mediante Composición
Esta es la implementación más común, flexible y recomendada del patrón. Su
mecanismo se basa en el principio de composición de objetos.
Mecanismo: El Adapter implementa la interfaz Target que el cliente
espera. Internamente, el Adapter contiene una instancia (una referencia)
del Adaptee. Cuando el Client invoca un método en el Adapter, este delega
la llamada al objeto Adaptee que envuelve, realizando cualquier traducción
de datos o de firma de método necesaria en el proceso. Esta es una
10
relación "tiene-un" (
has-a).
Ventajas Clave:
o Alta Flexibilidad: Un único Adapter puede funcionar no solo con la
clase Adaptee específica, sino también con cualquiera de sus
subclases. Dado que la relación se basa en la composición, el Adapter
puede contener una referencia a cualquier objeto que se ajuste a la
89
clase Adaptee o a sus descendientes. Esto permite adaptar una
jerarquía completa de clases con un solo adaptador. 12
o Soporte Universal de Lenguajes: Este enfoque funciona en cualquier
lenguaje de programación orientado a objetos, ya que no depende de
características como la herencia múltiple, que no está disponible en
lenguajes populares como Java o C#. 12
Ejemplo de Código (Java):
Consideremos un sistema de reproducción de medios. El Client es un
AudioPlayer que solo entiende la interfaz MediaPlayer. Queremos que
pueda reproducir formatos avanzados como VLC y MP4, cuya lógica está
en una clase AdvancedMediaPlayer (Adaptee) con una interfaz diferente.
Java
// Target Interface
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee Interface and Concrete Classes
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Do nothing
}
}
public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Do nothing
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: " + fileName);
}
}
// Object Adapter
{
public class MediaAdapter implements MediaPlayer
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
90
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}
// Client
public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + fileName);
} else if (audioType.equalsIgnoreCase("vlc") |
| audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
```
En este ejemplo 13,
MediaAdapter es un Adaptador de Objeto. Implementa MediaPlayer y contiene
una instancia de AdvancedMediaPlayer, delegando la llamada play al método
apropiado del Adaptee.
2.2 El Adaptador de Clases: Adaptación mediante Herencia
Esta implementación es menos común y se basa en la herencia,
específicamente en la herencia múltiple.
Mecanismo: El Adapter hereda simultáneamente de la clase Adaptee
(para reutilizar su implementación) y de la interfaz Target (para cumplir con
el contrato que el cliente espera). Esta es una relación "es-un" (is-a). 10
91
Ventajas y Limitaciones:
o Ventaja - Sobrescritura de Comportamiento: Al heredar directamente
del Adaptee, el Adapter puede sobrescribir los métodos del Adaptee.
Esto permite no solo traducir la interfaz, sino también modificar el
comportamiento subyacente, algo que el Adaptador de Objeto no
puede hacer sin crear una subclase adicional del Adaptee. 19
o Limitación - Acoplamiento Fuerte: El Adapter está fuertemente
acoplado a una clase Adaptee concreta y no puede adaptar a las
subclases de esta. La herencia es una relación estática definida en
tiempo de compilación, lo que la hace menos flexible. 20
o Limitación - Soporte de Lenguaje: Su aplicabilidad está restringida a
lenguajes que soportan herencia múltiple de clases (como C++). En
lenguajes como Java o C#, que no lo permiten, esta variante solo es
posible si el Adaptee es una clase y el Target es una interfaz, ya que
una clase puede heredar de una sola clase pero implementar múltiples
interfaces.17
2.3 Análisis Comparativo y Recomendaciones
La elección entre las dos implementaciones se puede resumir en la siguiente
tabla, que destaca sus diferencias fundamentales.
Criterio Adaptador de Objetos Adaptador de Clases (Herencia)
(Composición)
Mecanismo Composición (has-a). El Herencia (is-a). El adaptador
adaptador contiene una hereda del adaptado.
instancia del adaptado.
Flexibilidad Alta. Puede adaptar la clase Baja. Solo puede adaptar la clase
Adaptee y todas sus subclases. Adaptee específica, no sus
subclases.
Acoplamiento Débil. El cliente y el adaptado Fuerte. El adaptador está
están desacoplados. fuertemente acoplado a la
implementación del Adaptee.
Sobrescritura No puede sobrescribir el Puede sobrescribir métodos del
comportamiento del Adaptee Adaptee.
directamente.
Soporte de Universal. Funciona en todos Limitado. Requiere herencia
Lenguaje los lenguajes orientados a múltiple, no disponible en Java o
objetos. C# para clases.
Complejidad Ligeramente más complejo al Más simple de implementar si el
requerir la instanciación y lenguaje lo permite, ya que la
delegación explícita. herencia es implícita.
Tabla 1: Adaptador de Objetos vs. Adaptador de Clases - Un Análisis
Comparativo.
92
La decisión entre el Adaptador de Objeto y el de Clase es una manifestación
práctica de uno de los principios más importantes del diseño orientado a
objetos: favorecer la composición sobre la herencia. La composición ofrece
una flexibilidad significativamente mayor en tiempo de ejecución, ya que el
objeto compuesto (Adaptee) puede ser intercambiado dinámicamente. La
herencia, por otro lado, es una relación estática, fijada en tiempo de
compilación. Las ventajas del Adaptador de Objeto se alinean directamente con
las de la composición: es más flexible y promueve un acoplamiento más débil. 12
Las desventajas del Adaptador de Clase reflejan las de la herencia: es rígido y
crea un acoplamiento más fuerte. Su único beneficio distintivo, la capacidad de
sobrescribir el comportamiento del
Adaptee, es una consecuencia directa del mecanismo de herencia. 19
Por estas razones, la recomendación experta es clara: el Adaptador de
Objetos es casi siempre la opción preferida en el desarrollo de software
moderno. Su flexibilidad, su menor acoplamiento y su compatibilidad universal
con los lenguajes orientados a objetos lo convierten en la implementación por
defecto y más robusta del patrón.18
Sección 3: El Patrón Adaptador en la Práctica: Casos de
Uso e Implementaciones del Mundo Real
La verdadera utilidad de un patrón de diseño se revela en su aplicación a
problemas concretos. El patrón Adaptador es omnipresente en el software
moderno, a menudo operando discretamente para permitir la coexistencia de
sistemas dispares.
3.1 Integración de Sistemas Heredados y APIs de Terceros
Este es quizás el caso de uso más emblemático del patrón Adaptador. Las
organizaciones dependen con frecuencia de sistemas heredados que, aunque
funcionales, utilizan tecnologías o formatos de datos anticuados. Del mismo
modo, la integración con APIs de terceros es una práctica estándar, pero estas
APIs vienen con sus propias interfaces que no se pueden modificar.
Escenario: Imagine una aplicación de análisis de datos moderna que
opera internamente con objetos JSON. Esta aplicación necesita consumir
datos de un servicio heredado que expone su información a través de una
API que solo devuelve respuestas en formato XML. El Client (la lógica de
análisis) espera una interfaz, digamos JsonDataProvider, que le entregue
93
los datos como objetos JSON. El Adaptee es el servicio antiguo que
devuelve un String XML. 1
Implementación: Se crearía una clase XmlToJsonAdapter que
implemente la interfaz JsonDataProvider. En su método fetchData(), este
adaptador realizaría los siguientes pasos:
1. Invocar la API del servicio heredado para obtener la cadena XML.
2. Utilizar una biblioteca de análisis XML (como JAXB en Java) para
deserializar la cadena XML en un conjunto de objetos de transferencia
de datos (DTOs) que representen la estructura XML.
3. Mapear estos DTOs XML a los objetos de dominio JSON que el Client
espera.
4. Devolver los objetos de dominio, cumpliendo así con el contrato de la
interfaz Target.
De esta manera, toda la complejidad de la comunicación con el sistema
heredado y la transformación de datos queda encapsulada dentro del
adaptador, manteniendo el código cliente limpio y ajeno a los detalles de la
integración.
3.2 Unificación de Interfaces de Bibliotecas
Otro caso de uso poderoso es la creación de una capa de abstracción sobre
bibliotecas con funcionalidades similares pero interfaces diferentes.
Escenario: Un ejemplo clásico es un adaptador de base de datos. Una
aplicación empresarial podría necesitar la flexibilidad de cambiar de
proveedor de base de datos (por ejemplo, de MySQL a Oracle o
PostgreSQL) con un impacto mínimo en el código. Cada proveedor de
base de datos ofrece su propio driver JDBC con una API específica para la
conexión y ejecución de consultas. 1
Implementación:
1. Se define una interfaz Target común, por ejemplo, IDatabaseConnector,
con métodos genéricos como connect(connectionString),
executeQuery(query), y disconnect().
2. Se crean adaptadores concretos para cada proveedor: MySqlAdapter,
OracleAdapter, PostgreSqlAdapter.
3. Cada adaptador implementa la interfaz IDatabaseConnector.
Internamente, el MySqlAdapter contendrá una referencia al driver de
MySQL (Adaptee) y traducirá la llamada a executeQuery() en las
llamadas específicas de la API de MySQL. Lo mismo ocurrirá con los
otros adaptadores.
4. El código de la aplicación se escribe para depender únicamente de la
interfaz IDatabaseConnector. Para cambiar de base de datos, solo se
necesita cambiar la instanciación del adaptador en un único punto
(idealmente, gestionado por un contenedor de inyección de
dependencias), sin modificar el resto del código de la aplicación.
94
3.3 Ejemplos Canónicos en Bibliotecas Centrales (Java)
El patrón Adaptador es tan fundamental que se encuentra en el núcleo de las
bibliotecas estándar de Java, demostrando su utilidad y robustez.
java.io.InputStreamReader y OutputStreamWriter: Estos son ejemplos
perfectos del patrón Adaptador. El ecosistema de I/O de Java se divide en
dos jerarquías principales: flujos de bytes (InputStream, OutputStream) y
flujos de caracteres (Reader, Writer). InputStreamReader actúa como un
adaptador que "envuelve" un InputStream (el Adaptee, que lee bytes) y
expone la interfaz Reader (el Target, que lee caracteres). Realiza la
traducción de bytes a caracteres según una codificación específica (por
ejemplo, UTF-8). De manera análoga, OutputStreamWriter adapta un
OutputStream a la interfaz Writer. Esto permite que el código que trabaja
con caracteres pueda operar sobre fuentes de datos que originalmente
solo proporcionan bytes, como un archivo o una conexión de red. 8
java.util.Arrays.asList(): Este método es un ejemplo sutil pero poderoso.
Un array en Java tiene una interfaz muy básica y no es compatible con la
rica API de la interfaz java.util.List. El método Arrays.asList(T... a) toma un
array (Adaptee) y devuelve un objeto que implementa la interfaz List
(Target). Este objeto devuelto es una vista de adaptador sobre el array
original. Permite que los arrays sean tratados como listas y pasados a
métodos que esperan una Collection o List. Sin embargo, es un adaptador
con limitaciones: la lista resultante tiene un tamaño fijo y operaciones como
add() o remove() lanzarán una UnsupportedOperationException, lo cual
ilustra perfectamente que un adaptador traduce una interfaz, pero no
necesariamente puede replicar toda la semántica de la interfaz Target si el
Adaptee subyacente no lo permite. 8
La aplicación del patrón Adaptador en estos contextos revela una conexión
más profunda con conceptos arquitectónicos estratégicos. En el campo del
Diseño Guiado por el Dominio (Domain-Driven Design o DDD), el Adaptador es
la piedra angular para la implementación de una Capa Anticorrupción (Anti-
Corruption Layer o ACL). Una ACL es un patrón de diseño estratégico que se
utiliza para aislar el dominio central de una aplicación de las complejidades,
inconsistencias y "corrupción" potencial de sistemas externos. El objetivo de
una ACL es traducir los datos y comandos del modelo del sistema externo al
modelo del dominio central, garantizando que el núcleo del dominio
permanezca puro, consistente y enfocado en su propia lógica de negocio.
La implementación principal de una ACL se realiza a través del patrón
Adaptador. En este escenario, el Adaptee es la API del sistema externo. El
Target es una interfaz definida por el dominio central, que representa el servicio
que necesita en sus propios términos. El Adapter implementa esta interfaz
Target y encapsula toda la lógica de traducción, comunicación y mapeo de
datos hacia y desde el Adaptee. Esta perspectiva eleva al Adaptador de ser un
simple "envoltorio" a un "guardián estratégico", un componente crucial para
mantener la integridad y la longevidad de aplicaciones empresariales complejas
y centradas en el dominio.
95
96
Sección 4: Una Evaluación Crítica: Beneficios Estratégicos
y Posibles Inconvenientes
Como toda herramienta de diseño, el patrón Adaptador no es una solución
universal. Su aplicación conlleva un conjunto de beneficios estratégicos
significativos, pero también introduce compromisos y posibles inconvenientes
que un arquitecto de software debe sopesar cuidadosamente.
4.1 Ventajas Estratégicas
Las ventajas del patrón Adaptador van más allá de la simple conexión de
código; ofrecen beneficios estructurales y de mantenimiento a largo plazo.
Reutilización de Código: La ventaja más directa es la capacidad de
reutilizar clases o componentes existentes que ya han sido probados y son
funcionalmente valiosos, incluso si sus interfaces no se alinean con los
requisitos del sistema actual. Esto evita la necesidad de reescribir lógica
compleja y reduce el tiempo de desarrollo.1
Flexibilidad y Mantenibilidad: Al desacoplar el código del cliente de las
implementaciones concretas del Adaptee, el sistema gana una flexibilidad
considerable. El componente adaptado puede ser sustituido por otro con
una implementación completamente diferente, siempre y cuando se
proporcione un nuevo adaptador. El código del cliente permanece
inalterado, lo que simplifica enormemente el mantenimiento y la evolución
del sistema. 1
Integración Transparente: Desde la perspectiva del cliente, la integración
es transparente. El cliente opera consistentemente a través de la interfaz
Target, sin necesidad de conocer los detalles de la adaptación que ocurre
tras bambalinas. Esto conduce a un código cliente más limpio y fácil de
entender. 3
4.2 Inconvenientes y Consideraciones de Diseño
A pesar de sus fortalezas, la introducción de adaptadores no está exenta de
costes.
Aumento de la Complejidad del Código: La desventaja más evidente es
el incremento en el número de clases e interfaces en el sistema. Cada
adaptación requiere al menos una nueva clase Adapter. En sistemas
simples, esto puede percibirse como una sobreingeniería, añadiendo una
capa de indirección que hace que el flujo del código sea más difícil de
seguir.6
Sobrecarga de Rendimiento (Overhead): La indirección inherente al
patrón introduce una pequeña penalización en el rendimiento. Cada
llamada del cliente al adaptador implica una llamada de método adicional
97
para delegar la solicitud al adaptado. Aunque en la mayoría de las
aplicaciones empresariales esta sobrecarga es insignificante, puede ser
una consideración importante en sistemas de alto rendimiento o de baja
latencia.
6
Riesgo de "Enmascaramiento" de Mal Diseño: El patrón puede ser
utilizado como un "parche" para integrar código heredado que está mal
diseñado o que no sigue las buenas prácticas. Si bien el adaptador permite
que el código funcione dentro del nuevo sistema, no soluciona los
problemas de diseño subyacentes del componente adaptado. Esto puede
llevar a la perpetuación de la deuda técnica, donde el mal diseño queda
oculto detrás de una interfaz limpia, pero sigue siendo una fuente de
problemas y un lastre para el mantenimiento. 1
El coste de un Adaptador no es solo la clase extra que se añade al proyecto; es
también la carga cognitiva y el riesgo de crear un "infierno de envoltorios"
(wrapper hell) si se abusa del patrón. Si un sistema utiliza numerosos
adaptadores para corregir muchas pequeñas incompatibilidades, el código
base puede convertirse en un laberinto de envoltorios, dificultando
enormemente el seguimiento de una llamada desde el Client hasta el Adaptee
final. Esto sugiere un principio de diseño importante: los adaptadores son más
efectivos cuando la interfaz Target es estable y bien definida, y el Adaptee es
un componente significativo y cohesivo, como una biblioteca completa o un
servicio heredado principal. Usar un adaptador para corregir una discrepancia
menor en el nombre de un método entre dos clases que están bajo nuestro
control es, probablemente, un antipatrón. La solución más simple y correcta en
ese caso sería refactorizar una de las clases para hacerlas directamente
compatibles. Por lo tanto, el patrón Adaptador debe ser visto como una
herramienta para la integración estratégica, no para correcciones tácticas a
pequeña escala. Su uso indica una decisión deliberada de construir un puente
sobre una brecha significativa entre dos sistemas que, de otro modo, son
independientes. El abuso del patrón para problemas menores indica un fallo en
el mantenimiento de un diseño consistente dentro de los límites de un mismo
sistema.
98
Sección 5: El Adaptador en Contexto: Análisis
Comparativo con Patrones Estructurales Relacionados
Una de las claves para alcanzar la maestría en patrones de diseño es no solo
entender cada patrón de forma aislada, sino también comprender sus
relaciones y diferencias con otros patrones. El Adaptador a menudo se
confunde con otros patrones estructurales que también utilizan una forma de
"envoltura" o indirección. Aclarar estas distinciones es fundamental.
5.1 Adaptador vs. Decorador
La confusión entre Adaptador y Decorador es común porque ambos patrones
envuelven a un objeto. Sin embargo, su intención es diametralmente opuesta.
Intención: El propósito del Adaptador es cambiar la interfaz de un objeto
para que sea compatible con lo que un cliente espera. El propósito del
Decorador es añadir responsabilidades o funcionalidades a un objeto
de forma dinámica, sin cambiar su interfaz.
4
Analogía: Un Adaptador es un convertidor de enchufe de viaje; cambia la
forma del enchufe (la interfaz) para que encaje en una toma de corriente
diferente. Un Decorador es un multicontacto con protección contra
sobretensiones; utiliza la misma interfaz de enchufe, pero añade la
funcionalidad de protección y más salidas.
Estructura: El Decorador se adhiere estrictamente a la misma interfaz que
el objeto que decora, lo que permite apilar múltiples decoradores de forma
recursiva. Los adaptadores no están diseñados para ser apilados de esta
manera.
5.2 Adaptador vs. Puente (Bridge)
Ambos patrones desacoplan una abstracción de una implementación, pero lo
hacen con propósitos y en momentos del ciclo de vida del software muy
diferentes.
Intención y Temporalidad: El patrón Puente se diseña de antemano
(proactivo). Su objetivo es desacoplar una jerarquía de abstracciones de
una jerarquía de implementaciones, permitiendo que ambas evolucionen
de forma independiente. Se utiliza cuando se prevé que tanto la
abstracción como la implementación necesitarán variar. El Adaptador, en
cambio, se aplica después del hecho (reactivo). Se utiliza para hacer que
clases existentes, que no fueron diseñadas para trabajar juntas, puedan
colaborar.4
Problema que Resuelven: El Puente está diseñado para evitar una
"explosión cartesiana" de clases cuando existen múltiples dimensiones de
variación (por ejemplo, formas y colores). El Adaptador está diseñado para
99
resolver una incompatibilidad de interfaz descubierta durante la
integración. En resumen: "El Adaptador hace que las cosas funcionen
después de que fueron diseñadas; el Puente hace que funcionen antes de
serlo".
28
5.3 Adaptador vs. Fachada (Facade)
Ambos patrones envuelven a otros objetos y simplifican la interacción para un
cliente, pero sus intenciones son distintas.
Intención: La Fachada tiene como objetivo proporcionar una interfaz
nueva y simplificada a un subsistema complejo. Su propósito es ocultar la
complejidad y proporcionar un punto de entrada fácil de usar. El 29
Adaptador, por el contrario, no busca simplificar, sino
reutilizar una interfaz existente que el cliente ya conoce y espera. Su
propósito es la traducción, no la simplificación. 4
Alcance: Una Fachada típicamente envuelve a todo un subsistema,
coordinando múltiples objetos para realizar una tarea. Un Adaptador
generalmente envuelve a un único objeto (Adaptee). 4
Para cristalizar estas distinciones, la siguiente tabla ofrece una comparación
directa basada en la intención y el caso de uso.
Patrón Intención Principal Analogía Cuándo Utilizar
Adaptador Convertir una interfaz Adaptador de enchufe Cuando se necesita
en otra para hacerla de viaje. integrar una clase
compatible. existente con una
interfaz
incompatible.
Decorador Añadir funcionalidades Añadir toppings a una Cuando se necesita
a un objeto pizza. extender el
dinámicamente. comportamiento de
un objeto sin usar
herencia y de forma
apilable.
Puente Desacoplar una Un control remoto Cuando se diseña
abstracción de su (abstracción) para un sistema desde el
implementación para diferentes televisores principio y se prevén
que ambas puedan (implementaciones). múltiples variantes
variar tanto en la
independientemente. abstracción como en
la implementación.
Fachada Proporcionar una El botón "Ver Película" Cuando se quiere
interfaz simplificada a de un sistema de cine ocultar la
un subsistema en casa. complejidad de un
complejo. subsistema y ofrecer
100
un punto de acceso
simple.
Tabla 2: Comparativa de Patrones Estructurales de Envoltura.
El análisis comparativo revela que, aunque estos cuatro patrones utilizan una
forma de envoltura o indirección, su propósito dicta su estructura y aplicación.
Un desarrollador que observe sus diagramas UML podría encontrar similitudes
estructurales, como un objeto que mantiene una referencia a otro y delega
llamadas. La confusión surge de esta similitud superficial. La clave para
4
diferenciarlos no reside en
cómo se estructuran, sino en por qué existen. La intención es lo que impulsa el
patrón. Entender esto es pasar de simplemente memorizar diagramas a
desarrollar un verdadero pensamiento de diseño arquitectónico, permitiendo al
desarrollador hacer la pregunta fundamental: "¿Qué problema estoy tratando
de resolver?" antes de elegir una solución.
Conclusión: El Adaptador como Herramienta Indispensable
para la Integración de Sistemas
El patrón Adaptador se erige como una solución estructural, pragmática y
fundamental en la ingeniería de software. Su propósito principal, resolver la
omnipresente fricción causada por la incompatibilidad de interfaces, lo
convierte en una herramienta esencial para cualquier desarrollador o arquitecto
que trabaje en sistemas complejos y en evolución. A través de un intermediario
que traduce las llamadas de una interfaz esperada (Target) a una interfaz
existente (Adaptee), el Adaptador construye un puente que permite la
colaboración entre componentes que de otro modo estarían aislados.
Hemos explorado sus dos variantes principales: el Adaptador de Objetos,
basado en la composición, y el Adaptador de Clases, basado en la herencia. La
clara superioridad del Adaptador de Objetos en términos de flexibilidad,
desacoplamiento y compatibilidad con lenguajes modernos lo posiciona como
la implementación preferida en la gran mayoría de los escenarios, reafirmando
el principio de diseño de "favorecer la composición sobre la herencia".
El rol estratégico del Adaptador se manifiesta en su capacidad para facilitar la
evolución de los sistemas. Es la clave para integrar sin problemas bibliotecas
de terceros, conectar con APIs externas y, de manera crucial, modernizar y
extender la vida útil de los sistemas heredados sin necesidad de costosas y
arriesgadas reescrituras. Su función como pilar en la implementación de Capas
1
Anticorrupción en el Diseño Guiado por el Dominio subraya su importancia no
solo como una táctica de código, sino como una estrategia arquitectónica para
mantener la integridad y la longevidad de los sistemas empresariales.
En última instancia, aunque el patrón Adaptador introduce una capa adicional
de complejidad y una mínima sobrecarga de rendimiento, su valor es
101
incuestionable. Es una herramienta indispensable en el arsenal de un
arquitecto de software. Su aplicación juiciosa y deliberada permite que
sistemas dispares colaboren de manera eficaz, fomentando la reutilización, la
mantenibilidad y la flexibilidad en un panorama tecnológico que se define por el
cambio y la integración constantes.
Obras citadas
1. Adapter Design Pattern - Definition and Examples | Belatrix Blog -
Globant, fecha de acceso: junio 29, 2025, https://belatrix.globant.com/us-
en/blog/tech-trends/adapter-design-pattern/
2. The Adapter Design Pattern: Bridging the Gap Between Incompatible
Interfaces - Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@CodeWithTech/the-adapter-design-pattern-
bridging-the-gap-between-incompatible-interfaces-a69a6314eb66
3. Adapter pattern - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Adapter_pattern
4. Adapter - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/design-patterns/adapter
5. Understanding the Adapter Design Pattern - DEV Community, fecha de
acceso: junio 29, 2025, https://dev.to/syridit118/understanding-the-
adapter-design-pattern-4nle
6. Adapter Pattern - Take The Notes, fecha de acceso: junio 29, 2025,
https://takethenotes.com/adapter-pattern/
7. Adapter Design Pattern in Java - Medium, fecha de acceso: junio 29,
2025, https://medium.com/@contactkumaramit9139/adapter-design-
pattern-in-java-22b1588b85dd
8. Adapter Design Pattern in Java - Medium, fecha de acceso: junio 29,
2025, https://medium.com/@akshatsharma0610/adapter-design-pattern-
in-java-fa20d6df25b8
9. Beginner's guide to Adapter Design Pattern in Java | by Neha Gupta |
Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@ngneha090/beginners-guide-to-adapter-design-
pattern-in-java-278ae8d435a4
10. Unraveling the Adapter Design Pattern: Class Adapter vs. Object Adapter
in Dart | by Ahmad Hassan | Stackademic, fecha de acceso: junio 29,
2025, https://blog.stackademic.com/unraveling-the-adapter-design-
pattern-class-adapter-vs-object-adapter-in-dart-587d2a35b568
11. Adapter Design Pattern (Episode-4) | by Gaurav Swarankar - Medium,
fecha de acceso: junio 29, 2025,
https://medium.com/@gauravswarankar/adapter-design-pattern-episode-
4-bcfc87c22811
12. Adapter Design Pattern in Java - DigitalOcean, fecha de acceso: junio 29,
2025, https://www.digitalocean.com/community/tutorials/adapter-design-
pattern-java
13. Design Patterns - Adapter Pattern - Tutorialspoint, fecha de acceso: junio
29, 2025,
https://www.tutorialspoint.com/design_pattern/adapter_pattern.htm
102
14. The Adapter Pattern - PMI, fecha de acceso: junio 29, 2025,
https://www.pmi.org/disciplined-agile/the-design-patterns-repository/the-
adapter-pattern
15. Understanding the Adapter Design Pattern: Bridging Incompatible
Interfaces, fecha de acceso: junio 29, 2025,
https://dev.to/bilelsalemdev/understanding-the-adapter-design-pattern-
bridging-incompatible-interfaces-413m
16. Handling changes in the system design using Adapter Design Pattern -
Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@rahulmora007/handling-changes-in-the-system-
design-using-adapter-design-pattern-bf96984fffbd
17. The Adapter Pattern, fecha de acceso: junio 29, 2025,
https://ima.udg.edu/~sellares/EINF-ES1/AdapterToni.pdf
18. Things you should know about : Class and Object Adapters | by Sourabh
Kumar, fecha de acceso: junio 29, 2025,
https://sourabhkr.medium.com/things-you-should-know-about-class-and-
object-adapters-c054b1edcdad
19. Adapter Pattern: Class Adapter vs Object Adapter - Stack Overflow, fecha
de acceso: junio 29, 2025,
https://stackoverflow.com/questions/5467005/adapter-pattern-class-
adapter-vs-object-adapter
20. Adapter Design Pattern - GitHub Pages, fecha de acceso: junio 29, 2025,
http://stg-tud.github.io/sedc/Lecture/ws13-14/5.7-Adapter.html
21. Adapter Pattern in Java: Seamless Integration of Incompatible Systems,
fecha de acceso: junio 29, 2025, https://java-design-
patterns.com/patterns/adapter/
22. Design-pattrns/Adapter-Pattern - GitHub, fecha de acceso: junio 29, 2025,
https://github.com/Design-pattrns/Adapter-Pattern
23. Adapter Design Pattern - Semih Kırdinli - Medium, fecha de acceso: junio
29, 2025, https://semihkirdinli.medium.com/adapter-design-pattern-
b55f9f13f41c
24. Decorator pattern - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Decorator_pattern
25. Decorator Design Pattern in Java Example - DigitalOcean, fecha de
acceso: junio 29, 2025,
https://www.digitalocean.com/community/tutorials/decorator-design-
pattern-in-java-example
26. Bridge - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/design-patterns/bridge
27. stackoverflow.com, fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/1425171/difference-between-bridge-
pattern-and-adapter-
pattern#:~:text=Adapter%20is%20used%20when%20you,interface%20an
d%20the%20underlying%20implementation.
28. Difference between Bridge pattern and Adapter pattern - Stack Overflow,
fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/1425171/difference-between-bridge-
pattern-and-adapter-pattern
103
29. en.wikipedia.org, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Facade_pattern#:~:text=Developers%20often
%20use%20the%20facade,simpler%20interface%20to%20the%20client.
30. Facade pattern - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Facade_pattern
31. Facade Design Pattern in Java - DigitalOcean, fecha de acceso: junio 29,
2025, https://www.digitalocean.com/community/tutorials/facade-design-
pattern-in-java
32. What is the difference between the Facade and Adapter Pattern? - Stack
Overflow, fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/2961307/what-is-the-difference-
between-the-facade-and-adapter-pattern
33. Design Patterns Part 4 – Adapter, Facade, and Memento - Coding Blocks
– Podcast, fecha de acceso: junio 29, 2025,
https://www.codingblocks.net/podcast/design-patterns-adapter-facade-
memento/
104
Capítulo 9: Análisis Exhaustivo del Patrón de Diseño
Decorator
Introducción al Patrón Decorator
Propósito y Definición Formal
El patrón Decorator es un patrón de diseño estructural cuyo propósito
fundamental es permitir la adición de nuevas funcionalidades o
responsabilidades a un objeto de manera dinámica. A menudo conocido por su
1
alias,
Wrapper (Envoltorio), su esencia radica en la capacidad de modificar el
comportamiento de una instancia individual en tiempo de ejecución, sin alterar
el comportamiento de otras instancias de la misma clase. 1
Este patrón logra su objetivo envolviendo el objeto original, denominado
componente, dentro de uno o más objetos "decoradores". Un aspecto crucial
6
de su diseño es que tanto el componente original como los decoradores que lo
envuelven comparten una interfaz común. Esta conformidad de interfaz
asegura que el uso de un objeto decorado sea transparente para el código
cliente; el cliente interactúa con el objeto decorado de la misma manera que lo
haría con el objeto original sin decorar. La funcionalidad adicional se
implementa en los decoradores, que pueden realizar tareas propias antes o
después de delegar la llamada al objeto que envuelven. 1
El Decorator como Alternativa Estratégica a la Herencia
El patrón Decorator se presenta como una alternativa poderosa y flexible a la
herencia para extender la funcionalidad de una clase. La distinción entre
1
ambos enfoques es fundamental para comprender el valor del patrón. La
herencia es un mecanismo estático; añade comportamiento en tiempo de
compilación, y este cambio afecta a
todas las instancias de la nueva subclase creada. Por el contrario, el Decorator
3
permite añadir responsabilidades en tiempo de ejecución y aplicarlas
selectivamente a
instancias específicas.
3
Este enfoque se alinea con uno de los principios más importantes del diseño
orientado a objetos: "favorecer la composición sobre la herencia". Mientras que
5
la herencia establece una relación "es un" (un
105
Gato es un Animal), la composición del Decorator establece una relación "tiene
un" (un BufferedInputStream tiene un InputStream). Esta diferencia permite una
mayor flexibilidad, ya que las responsabilidades pueden ser combinadas y
apiladas de formas que la herencia estática no permite.
Adherencia a Principios de Diseño Fundamentales (SOLID)
La elegancia del patrón Decorator también reside en su alineación con
principios de diseño de software robustos, como los principios SOLID.
Principio de Responsabilidad Única (SRP): El patrón promueve el SRP
al permitir que la funcionalidad se divida entre clases con áreas de interés
bien definidas. En lugar de tener una clase monolítica con múltiples
responsabilidades opcionales, la clase base mantiene su responsabilidad
principal, y cada nueva funcionalidad se encapsula en su propio
decorador.1
Principio Abierto/Cerrado (OCP): El Decorator es un ejemplo canónico
del OCP, que postula que las entidades de software deben estar "abiertas
para la extensión, pero cerradas para la modificación". Con este patrón, es
posible introducir nuevas funcionalidades creando nuevas clases de
decoradores sin alterar el código existente del componente base o de otros
decoradores. 5
Más allá de ser una mera alternativa a la herencia, el Decorator habilita un
paradigma de personalización dinámica de objetos. No se limita a "añadir" una
función, sino que permite que la identidad funcional de una instancia sea fluida
y evolucione durante su ciclo de vida. Por ejemplo, un objeto Car puede
"convertirse" en un SportsCar al ser envuelto por un SportsCarDecorator en
tiempo de ejecución. Esta capacidad de transformación dinámica lo convierte
2
en una herramienta estratégica para sistemas donde los objetos deben
adaptarse a contextos cambiantes, como la personalización de productos en
comercio electrónico o la aplicación de políticas de ejecución variables (como
7
logging o caching) a un objeto de servicio.
El Problema Fundamental: Rigidez Jerárquica y Explosión
de Clases
Para apreciar plenamente la solución que ofrece el patrón Decorator, es
imperativo analizar los problemas inherentes al enfoque tradicional de
extensión de funcionalidades: la herencia de clases.
Las Limitaciones de la Herencia Estática
106
La herencia, si bien es un pilar de la programación orientada a objetos,
introduce por su naturaleza un acoplamiento fuerte y una estructura rígida.
Cuando se define una subclase, su conjunto de características y
comportamientos queda fijado en tiempo de compilación. Esta naturaleza
1
estática se convierte en una limitación significativa cuando se necesita
flexibilidad.
Si una funcionalidad es opcional o solo se requiere para un subconjunto de
instancias, la herencia obliga a la creación de una nueva subclase completa.
Por ejemplo, si se desea añadir una funcionalidad de "borde rojo" a una forma
geométrica, se debe crear una clase RedBorderedCircle. Cualquier cliente que
necesite un círculo con borde rojo debe instanciar esta clase específica. No es
posible tomar un objeto Circle existente y añadirle un borde rojo en tiempo de
ejecución; su "identidad" como Circle sin borde es inmutable desde el punto de
vista de la jerarquía de clases.
8
El Fenómeno de la "Explosión de Clases"
El problema de la rigidez se agrava exponencialmente cuando un sistema
requiere múltiples combinaciones de funcionalidades independientes. Este
escenario conduce a un fenómeno conocido como "explosión de clases". 8
Considérese un sistema de notificaciones que puede enviar mensajes a través
de Email, SMS o Push. Ahora, supongamos que se desean añadir dos
funcionalidades ortogonales (independientes): Encrypted (cifrado) y HighPriority
(prioridad alta). Utilizando la herencia, para cubrir todas las combinaciones
posibles, se necesitaría una proliferación de clases:
EncryptedEmailNotification
HighPriorityEmailNotification
EncryptedHighPriorityEmailNotification
EncryptedSmsNotification
HighPrioritySmsNotification
EncryptedHighPrioritySmsNotification
Y así sucesivamente para Push y cualquier otro canal futuro.
Este problema no es meramente un inconveniente de codificación; es una
consecuencia matemática de intentar modelar características composicionales
con una herramienta jerárquica. La herencia establece una taxonomía "es un".
Sin embargo, características como el canal de entrega, el cifrado y la prioridad
son ortogonales; no forman una jerarquía natural. Un Email no "es un"
Encrypted por naturaleza. Al forzar estas características en una jerarquía, se
debe crear una clase para cada punto en el espacio de combinaciones. Con N
características binarias (presente/ausente), se necesitarían hasta 2^N
subclases para cubrir todas las posibilidades. El crecimiento es exponencial y
13
rápidamente se vuelve inmanejable.
Esto demuestra que la herencia es una herramienta de modelado inadecuada
para la composición de funcionalidades. El patrón Decorator, al utilizar la
107
composición en lugar de la herencia, modela esta relación de una manera
mucho más natural y escalable. En lugar de un crecimiento exponencial de
clases, solo se necesita un crecimiento lineal: una clase de decorador por cada
nueva funcionalidad.
Anatomía del Patrón Decorator: Estructura y Participantes
La estructura del patrón Decorator es elegante y se basa en la composición
recursiva para lograr su objetivo. Aunque no se pueden usar diagramas
visuales, su estructura puede describirse con precisión a través de sus
participantes y sus interacciones.
Los Cuatro Participantes Clave
El patrón se define por la interacción de cuatro roles distintos :
4
1. Component (Componente): Es la interfaz o clase abstracta que sirve
como tipo común tanto para los objetos que serán decorados como para
los decoradores mismos. Define la firma de las operaciones que pueden
ser extendidas. Ejemplos canónicos incluyen la interfaz Shape , la interfaz
6
Coffee , o la clase abstracta
18
InputStream. El cliente interactúa con los objetos a través de esta interfaz,
19
lo que le permite tratar a los objetos simples y a los decorados de manera
uniforme.
2. ConcreteComponent (Componente Concreto): Es la clase del objeto
original al que se le añadirán responsabilidades. Implementa la interfaz
Component y representa el final de la cadena de envoltura. Es el objeto
base que contiene la funcionalidad esencial. Ejemplos incluyen Circle ,6
SimpleCoffee , o 18
FileInputStream. 19
3. Decorator (Decorador): Es una clase abstracta que también implementa
la interfaz Component. Su rol estructural es clave: mantiene una referencia
a un objeto de tipo Component a través de la composición. La
implementación por defecto de las operaciones definidas en la interfaz
Component consiste simplemente en delegar la llamada al objeto
Component que envuelve. Esta delegación es el mecanismo que permite
pasar la llamada a través de las capas de decoración. Ejemplos de este rol
son ShapeDecorator , 6
CarDecorator , y 2
FilterInputStream. 19
4. ConcreteDecorator (Decorador Concreto): Son las clases que
implementan la funcionalidad adicional. Heredan del Decorator abstracto y
sobrescriben los métodos para realizar su tarea específica. Típicamente,
un ConcreteDecorator ejecutará su propia lógica antes o después de
delegar la llamada al objeto que envuelve (usando super.operation() o
wrappedComponent.operation()). Esto permite añadir comportamiento sin
108
modificar al componente envuelto. Ejemplos notables son
RedShapeDecorator , 6
MilkDecorator , y
18
BufferedInputStream. 19
Mecánica de Funcionamiento: Envoltura y Delegación Recursiva
El poder del patrón reside en su mecanismo de "apilamiento" o "envoltura"
(wrapping). Un cliente puede tomar un ConcreteComponent y envolverlo con un
ConcreteDecorator. El resultado es un objeto que sigue siendo de tipo
Component, por lo que puede ser envuelto por otro ConcreteDecorator, y así
sucesivamente. 3
Cuando un cliente invoca un método sobre el decorador más externo, se
desencadena una cascada de delegación. El decorador externo ejecuta su
comportamiento adicional y luego pasa la llamada al decorador interno que
envuelve. Este proceso se repite recursivamente a través de todas las capas
hasta que la llamada finalmente alcanza al ConcreteComponent original. Una
vez que el componente original completa su operación, el control puede
retornar por la misma cadena, permitiendo a los decoradores realizar acciones
adicionales después de la llamada delegada.
Implementación de Referencia: Decorando InputStream en
Java
El ejemplo más paradigmático y ampliamente reconocido del patrón Decorator
en la práctica se encuentra en el paquete java.io de la biblioteca estándar de
Java. Su uso en una API tan fundamental subraya la robustez y utilidad del
19
patrón para construir sistemas flexibles y extensibles.
El Ecosistema java.io como Paradigma del Decorator
El diseño de las clases de entrada/salida (I/O) en Java es un caso de estudio
sobre cómo el Decorator puede ser utilizado para añadir funcionalidades de
manera modular. En lugar de crear clases monolíticas como
GzipBufferedFileInputStream, el diseño separa cada responsabilidad en un
decorador distinto.
Mapeo de Clases java.io a los Participantes del Patrón
109
Las clases del paquete java.io se mapean directamente a los participantes del
patrón Decorator de la siguiente manera:
Componente: La clase abstracta java.io.InputStream. Define la operación
fundamental, int read(), que todas las clases de entrada de bytes deben
implementar.
Componente Concreto: La clase java.io.FileInputStream. Proporciona la
implementación base para leer bytes directamente desde un archivo en el
sistema de ficheros. Actúa como el objeto original que será decorado.
Decorador (Abstracto): La clase java.io.FilterInputStream. Esta clase
extiende InputStream y, crucialmente, contiene una referencia a otro objeto
InputStream (protected volatile InputStream in;). Su implementación por
defecto de los métodos de lectura es simplemente delegar las llamadas al
InputStream envuelto, cumpliendo perfectamente el rol de un decorador
base abstracto.
Decorador Concreto (Buffering): La clase java.io.BufferedInputStream.
Extiende FilterInputStream y añade la funcionalidad de buffering.
Sobrescribe el método read() para leer datos en un búfer interno de mayor
tamaño, mejorando significativamente el rendimiento al reducir el número
de llamadas costosas al sistema operativo.
Decorador Concreto (Descompresión): La clase
java.util.zip.GZIPInputStream. También extiende FilterInputStream y añade
la capacidad de descomprimir sobre la marcha un flujo de datos que ha
sido comprimido usando el algoritmo GZIP.
Ejemplo de Código Completo y Comentado
El siguiente bloque de código demuestra el apilamiento de estos decoradores
para lograr una tarea compleja: leer de manera eficiente un archivo de texto
que ha sido comprimido con GZIP.
Java
// Importaciones necesarias para el ejemplo
import java.io.*;
import java.util.zip.GZIPInputStream;
public class DecoratorExample {
public static void main(String args) {
// Objetivo: Leer un archivo de texto que ha sido comprimido con GZIP, de manera
eficiente.
String filePath = "datos_comprimidos.txt.gz";
try (
// 1. Componente Concreto: Se instancia FileInputStream para abrir el archivo físico.
// Esta clase proporciona la capacidad básica de leer bytes crudos desde el disco.
110
InputStream fileInputStream = new FileInputStream(filePath);
// 2. Primer Decorador (GZIP): Se envuelve el FileInputStream con un
GZIPInputStream.
// Este decorador añade la funcionalidad de descompresión. Al leer de
gzipInputStream,
// los bytes leídos del archivo se descomprimen automáticamente.
InputStream gzipInputStream = new GZIPInputStream(fileInputStream);
// 3. Segundo Decorador (Buffering): Se envuelve el GZIPInputStream con un
BufferedInputStream.
// Este decorador añade la funcionalidad de buffering. En lugar de realizar una llamada
// al sistema por cada byte leído, lee un gran bloque de datos en un búfer interno,
// lo que hace que las lecturas subsecuentes sean mucho más rápidas.
InputStream bufferedInputStream = new
BufferedInputStream(gzipInputStream);
// 4. Decoradores adicionales para conveniencia: Para leer texto en lugar de bytes,
// se puede seguir decorando. InputStreamReader actúa como un "puente" que
convierte
// un stream de bytes en un stream de caracteres.
Reader reader = new InputStreamReader(bufferedInputStream);
// BufferedReader es otro decorador que envuelve un Reader y añade la capacidad
// de leer líneas de texto completas de manera eficiente.
BufferedReader bufferedReader = new BufferedReader(reader)
){
String line;
System.out.println("Leyendo el archivo descomprimido y con buffer:");
// El cliente final interactúa con la interfaz más simple (readLine),
// sin tener conocimiento de las complejidades subyacentes.
while ((line = bufferedReader.readLine())!= null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error al leer el archivo: " + e.getMessage());
}
}
}
En este ejemplo, el código cliente final (while ((line =
bufferedReader.readLine())!= null)) es notablemente simple. No necesita saber
nada sobre la compresión GZIP, el buffering de memoria o la lectura de bytes.
Simplemente interactúa con la interfaz de alto nivel BufferedReader para leer
líneas de texto. Cada decorador ha añadido una capa de funcionalidad de
forma transparente, demostrando el poder y la elegancia del patrón. 22
Un aspecto fundamental que este ejemplo revela es la importancia de la
identidad de los objetos. En el código, fileInputStream, gzipInputStream, y
111
bufferedInputStream son tres objetos distintos en memoria. Si el cliente,
después de crear la cadena de decoradores, mantuviera y utilizara una
referencia al fileInputStream original, estaría leyendo los bytes comprimidos
directamente del disco, saltándose por completo la funcionalidad de
descompresión y buffering. Esto subraya una disciplina crítica que el patrón
20
impone al cliente: se debe trabajar exclusivamente con la referencia del
decorador más externo para asegurar que toda la cadena de responsabilidades
se ejecute correctamente. Un buen diseño de API que utilice este patrón
debería encapsular las capas intermedias para prevenir su mal uso.
Análisis de Consecuencias: Una Perspectiva Equilibrada
Como toda herramienta de diseño, el patrón Decorator presenta un conjunto de
ventajas y desventajas que deben ser cuidadosamente sopesadas en el
contexto de un problema específico.
Ventajas Clave
Flexibilidad en Tiempo de Ejecución: La principal ventaja es la
capacidad de añadir, quitar y combinar funcionalidades de forma dinámica
durante la ejecución del programa, en lugar de estar limitado a las
combinaciones definidas en tiempo de compilación. 3
Evita la Explosión de Clases: Como se discutió previamente, el patrón
evita el crecimiento exponencial de subclases que resulta de intentar
modelar combinaciones de características con herencia. El número de
clases solo crece linealmente con el número de características. 8
Cumplimiento de Principios de Diseño Sólidos: El patrón se alinea
naturalmente con el Principio de Responsabilidad Única y el Principio
Abierto/Cerrado, lo que conduce a un código más limpio, modular y
mantenible. 1
Favorece la Composición sobre la Herencia: Promueve un diseño más
desacoplado y robusto, que es una práctica recomendada en la ingeniería
de software moderna. 5
Capacidad de Prueba Mejorada: Cada decorador encapsula una única
responsabilidad, lo que permite probar cada funcionalidad de forma
aislada. Esto simplifica significativamente la creación de pruebas unitarias
en comparación con probar una clase monolítica con múltiples
funcionalidades entrelazadas. 25
Desventajas y Consideraciones Críticas
112
Proliferación de Pequeños Objetos: Una desventaja notable es que el
uso del patrón puede llevar a un sistema compuesto por una gran cantidad
de objetos pequeños que son muy similares en su estructura (todos
implementan la misma interfaz). Esto puede aumentar la complejidad inicial
del diseño y hacer que el código sea más difícil de entender para los
desarrolladores que no están familiarizados con el patrón. 2
Complejidad en la Depuración y Lógica Oculta: El flujo de ejecución de
una llamada a un método puede ser difícil de rastrear a través de múltiples
capas de decoradores. Una consecuencia más sutil y peligrosa es que una
lógica crítica puede estar "oculta" en un decorador. Un desarrollador que
examine el código cliente o el componente base podría no darse cuenta de
que se está aplicando un comportamiento adicional importante, lo que
puede llevar a errores difíciles de diagnosticar o a refactorizaciones
incorrectas. 29
Sobrecarga de Rendimiento: Cada capa de decoración introduce un nivel
de indirección. Una llamada al método en el decorador más externo
desencadena una cascada de llamadas delegadas a través de las capas
internas. A bajo nivel, esto puede traducirse en múltiples saltos en el
código ensamblador (JMP), lo que puede tener un impacto negativo en el
rendimiento, especialmente si el objeto decorado se utiliza dentro de
bucles de alta frecuencia o en secciones de código críticas para el
rendimiento. 30
Problemas de Identidad de Objetos: Como se mencionó anteriormente,
un decorador y su componente envuelto no son el mismo objeto
(decorator!= component). Si el código cliente depende de la identidad del
objeto (por ejemplo, mediante comparaciones con ==), puede encontrar
errores sutiles. El cliente debe ser disciplinado y utilizar consistentemente
la referencia al decorador más externo. 20
Tabla Comparativa: Decorator vs. Herencia
Para sintetizar la discusión, la siguiente tabla compara directamente el patrón
Decorator con el enfoque de herencia tradicional a través de varias
dimensiones clave.
Característica Herencia Patrón Decorator
Flexibilidad Baja. El comportamiento se fija Alta. El comportamiento se puede
en tiempo de compilación. añadir y modificar en tiempo de
ejecución.
Acoplamiento Alto. Las subclases están Bajo. El decorador y el
fuertemente acopladas a sus componente solo se conocen a
superclases. través de una interfaz común.
Mecanismo Relación "es un". Relación "tiene un"
(Composición).
113
Granularidad Gruesa. Afecta a toda la clase. Fina. Se aplica a instancias
individuales.
Escalabilidad Pobre. Conduce a una "explosión Buena. El número de clases crece
de clases" para múltiples linealmente con el número de
funcionalidades. funcionalidades.
Complejidad El código puede concentrarse en Puede resultar en muchos objetos
clases grandes y monolíticas. pequeños, lo que puede complicar
el diseño inicial.
Principio A menudo se viola, ya que añadir Se adhiere bien. Se pueden
OCP funcionalidad puede requerir añadir nuevos decoradores sin
modificar la clase base. modificar el código existente.
Conclusión y Recomendaciones de Experto
Síntesis Estratégica
El patrón Decorator es una solución estructural sofisticada y elegante para el
problema de la extensión de funcionalidades de una manera dinámica y
granular. Su poder emana del uso de la composición y la delegación recursiva,
lo que permite a los diseñadores de software crear sistemas flexibles,
mantenibles y escalables que se adhieren a principios de diseño fundamentales
como el SRP y el OCP. Al desacoplar la funcionalidad adicional del objeto
principal, el patrón ofrece una alternativa superior a la herencia estática cuando
se enfrentan a requisitos de comportamiento dinámico o combinatorio.
Directrices de Aplicación
La decisión de utilizar el patrón Decorator debe ser informada y contextual. No
es una solución universal, sino una herramienta específica para un conjunto de
problemas bien definidos.
Se recomienda usar el patrón Decorator cuando:
Se necesita añadir responsabilidades a objetos individuales de forma
dinámica y transparente, sin afectar a otras instancias. 4
La extensión de la funcionalidad mediante herencia es impráctica o
imposible debido a un gran número de combinaciones potenciales que
llevarían a una explosión de clases. 4
Las responsabilidades añadidas pueden necesitar ser retiradas en tiempo
de ejecución (aunque la implementación de la "eliminación" de
decoradores puede añadir complejidad adicional).
Se debe ser cauteloso o considerar alternativas cuando:
114
El sistema es muy simple y el patrón podría introducir una complejidad
innecesaria, convirtiéndose en una sobreingeniería. 30
El objeto decorado se va a utilizar en secciones de código de muy alto
rendimiento (por ejemplo, dentro de bucles anidados muy ajustados),
donde la sobrecarga de las llamadas delegadas a través de múltiples
capas podría convertirse en un cuello de botella medible.
La lógica que se va a añadir en los decoradores es muy compleja o
interdependiente. En tales casos, la "lógica oculta" puede volverse difícil de
mantener y depurar, y un patrón de diseño diferente o un rediseño más
explícito podría ser más claro y seguro. 29
Reflexión Final
El dominio del patrón Decorator trasciende el simple conocimiento de su
estructura UML. Requiere una comprensión profunda de sus consecuencias: la
complejidad jerárquica que evita, la disciplina que impone al código cliente en
el manejo de referencias de objetos, y el delicado equilibrio entre la inmensa
flexibilidad que ofrece y la complejidad estructural que puede introducir.
Cuando se aplica juiciosamente, el patrón Decorator es una de las
herramientas más efectivas en el arsenal de un arquitecto de software para
construir sistemas que son, a la vez, robustos y adaptables al cambio.
Obras citadas
1. Decorator pattern - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Decorator_pattern
2. Decorator Design Pattern in Java Example - DigitalOcean, fecha de
acceso: junio 29, 2025,
https://www.digitalocean.com/community/tutorials/decorator-design-
pattern-in-java-example
3. Software Design Patterns: Decorator in a Nutshell | by Javier Gonzalez |
Javarevisited, fecha de acceso: junio 29, 2025,
https://medium.com/javarevisited/software-design-patterns-decorator-in-a-
nutshell-eefd2025ea3c
4. Design Patterns: Decorator - Carlos Caballero, fecha de acceso: junio 29,
2025, https://www.carloscaballero.io/design-patterns-decorator/
5. Decorator Design Pattern Demystified: A Comprehensive Guide to
Decorating Your Code | Belatrix Blog, fecha de acceso: junio 29, 2025,
https://belatrix.globant.com/us-en/blog/tech-trends/decorator-design-
pattern/
6. Decorator Pattern in Design Patterns - Tutorialspoint, fecha de acceso:
junio 29, 2025,
https://www.tutorialspoint.com/design_pattern/decorator_pattern.htm
7. Decorator Pattern Explained: Basics to Advanced - Daily.dev, fecha de
acceso: junio 29, 2025, https://daily.dev/blog/decorator-pattern-explained-
basics-to-advanced
115
8. Decorator pattern versus sub classing - Stack Overflow, fecha de acceso:
junio 29, 2025, https://stackoverflow.com/questions/4842978/decorator-
pattern-versus-sub-classing
9. Decorator Pattern Vs Inheritance - Coderanch, fecha de acceso: junio 29,
2025, https://coderanch.com/t/503582/engineering/Decorator-Pattern-
Inheritance
10. Design Pattern : difference between composition and inheritance and
decorator., fecha de acceso: junio 29, 2025,
https://dev.to/elayachiabdelmajid/design-pattern-difference-between-
composition-and-inheritance-and-decorator-mji
11. Explained Design Patterns: Decorator and Composite | by Mosharraf
Hossain - Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@mail2mhossain/explained-design-patterns-
decorator-and-composite-c3054d5d5956
12. Decorator design pattern - by Adnan taşdemir - Medium, fecha de acceso:
junio 29, 2025, https://medium.com/@adnan.mehrat/decorator-design-
pattern-8c9925dcc85b
13. Decorator design pattern vs. inheritance? - c++ - Stack Overflow, fecha de
acceso: junio 29, 2025,
https://stackoverflow.com/questions/12379848/decorator-design-pattern-
vs-inheritance
14. Decorator Design Pattern - DEV Community, fecha de acceso: junio 29,
2025, https://dev.to/tkarropoulos/decorator-design-pattern-5g8n
15. Decorator pattern: a pattern for dynamic class expansion - IONOS, fecha
de acceso: junio 29, 2025,
https://www.ionos.com/digitalguide/websites/web-development/what-is-
the-decorator-pattern/
16. Decorator Design Pattern, fecha de acceso: junio 29, 2025,
https://jhumelsine.github.io/2024/02/08/decorator-design-pattern.html
17. Decorator Pattern | Object Oriented Design, fecha de acceso: junio 29,
2025, https://www.oodesign.com/decorator-pattern
18. Patrones de diseño en Kotlin - Parte 1 - Carrion.dev, fecha de acceso:
junio 29, 2025, https://carrion.dev/es/posts/design-patterns-1/
19. Decorator Pattern - kymr tech blog, fecha de acceso: junio 29, 2025,
https://kymr.github.io/2016/11/27/Decorator-Pattern/
20. Decorator Design Pattern - GitHub Pages, fecha de acceso: junio 29,
2025, http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html
21. Implementing Decorator Pattern in Java - Coderanch, fecha de acceso:
junio 29, 2025, https://coderanch.com/t/633854/java/Implementing-
Decorator-Pattern-Java
22. Use Cases and Examples of GoF Decorator Pattern for IO - Stack
Overflow, fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/6366385/use-cases-and-examples-of-
gof-decorator-pattern-for-io
23. The Decorator Pattern. I've started to go through the Head… | by Priya
Patil | Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@priya104/the-decorator-pattern-b28709b58481
24. Decorating IO Streams - Dev.java, fecha de acceso: junio 29, 2025,
https://dev.java/learn/java-io/reading-writing/decorating/
116
25. A decorator vs. a subclass - Justin Weiss, fecha de acceso: junio 29,
2025, https://www.justinweiss.com/articles/a-decorator-vs-a-subclass/
26. Is Inheritance Dead? A Detailed Look Into the Decorator Pattern - DZone,
fecha de acceso: junio 29, 2025, https://dzone.com/articles/is-inheritance-
dead
27. Memento Design Pattern - by Irushinie Muthunayake - Medium, fecha de
acceso: junio 29, 2025, https://medium.com/geekculture/memento-design-
pattern-df23d8351ff7
28. Decorator Design Pattern | Implementation and Disadvantages | Clean
Code Series, fecha de acceso: junio 29, 2025,
https://www.youtube.com/watch?v=vqy8BL0xV0c
29. The Decorator Pattern, Why We Stopped Using It, and the Alternative :
r/coding - Reddit, fecha de acceso: junio 29, 2025,
https://www.reddit.com/r/coding/comments/t9k0ki/the_decorator_pattern_
why_we_stopped_using_it_and/
30. What are the pros and cons of the decorator pattern? - Stack Overflow,
fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/14526518/what-are-the-pros-and-
cons-of-the-decorator-pattern
117
Capítulo 10: Patrón de Diseño Facade - Simplificando la
Complejidad en la Arquitectura de Software
Introducción: El Principio de la Interacción Simplificada
En la evolución de cualquier sistema de software, la complejidad es un
resultado casi inevitable. A medida que las aplicaciones crecen para satisfacer
nuevas funcionalidades y requisitos, el número de clases y sus
interdependencias tiende a aumentar exponencialmente. Esta proliferación de
componentes puede transformar un sistema inicialmente manejable en un
subsistema enrevesado, donde las clases están tan estrechamente acopladas
que su uso se vuelve engorroso y su mantenimiento, una tarea hercúlea. Los1
clientes que necesitan interactuar con dicho subsistema se ven forzados a
comprender y gestionar una red de objetos y dependencias, lo que incrementa
la fragilidad del código y dificulta su reutilización.
Para abordar este desafío fundamental, la ingeniería de software ofrece una
solución estructural elegante: el patrón de diseño Facade (Fachada). El
propósito central del patrón Facade es proporcionar una interfaz unificada y
simplificada a un conjunto de interfaces dentro de un subsistema complejo. 3
Actúa como un punto de entrada de alto nivel que enmascara la complejidad
subyacente, permitiendo que los clientes interactúen con el sistema de una
manera mucho más directa y sencilla. 5
Una analogía efectiva para comprender este patrón es el control remoto de un
sistema de cine en casa. Un sistema de este tipo puede incluir un televisor, un
6
proyector, un sistema de sonido envolvente, un reproductor de streaming y
luces ambientales. Cada uno de estos componentes representa un objeto en el
subsistema, con su propia interfaz y conjunto de operaciones complejas
(encender, seleccionar entrada, ajustar volumen, atenuar luces, etc.). Para ver
una película, un usuario tendría que interactuar con cada componente en una
secuencia específica. El patrón Facade introduce un "control remoto universal"
con un único botón: "Ver Película". Al presionar este botón, el control remoto (la
fachada) se encarga de orquestar todas las operaciones necesarias en el orden
correcto: enciende el proyector, baja las luces, activa el amplificador y
comienza la reproducción. El usuario final solo interactúa con la interfaz simple
del control remoto, completamente ajeno a la coreografía de interacciones que
ocurre detrás de escena. De esta manera, la intención del patrón Facade se
hace evidente: hacer que un subsistema complejo sea más fácil de usar. 1
Sección 1: La Anatomía del Patrón Facade
1.1. El Dominio del Problema: Domando el Subsistema Estrechamente
Acoplado
118
El problema que el patrón Facade resuelve de manera directa es la gestión de
la complejidad inherente a los subsistemas de software. Cuando un cliente
necesita utilizar una funcionalidad proporcionada por un conjunto de clases, la
interacción directa crea un fuerte acoplamiento entre el cliente y cada uno de
los componentes del subsistema. Este acoplamiento se manifiesta de varias
1
maneras problemáticas:
1. Carga Cognitiva para el Cliente: El cliente debe conocer la lógica de
inicialización, la secuencia de llamadas y las dependencias entre los
múltiples objetos del subsistema. Esto hace que el código del cliente sea
más propenso a errores y más difícil de escribir y entender.
2. Fragilidad ante los Cambios: Si la implementación interna del
subsistema cambia (por ejemplo, se reemplaza una clase o se modifica el
flujo de interacción), todos los clientes que dependen directamente de él
deben ser actualizados. Esto viola el principio de encapsulación y hace
que el sistema sea frágil y costoso de mantener. 1
3. Dificultad de Reutilización: El código del cliente se vuelve tan específico
para la implementación actual del subsistema que es prácticamente
imposible reutilizarlo en otros contextos.
El ejemplo propuesto por el usuario sobre un sistema de procesamiento de
video ilustra perfectamente este dominio del problema. Para convertir un video
de un formato a otro, un cliente no debería necesitar ser un experto en la
manipulación de códecs, la sincronización de flujos de audio, el renderizado de
fotogramas o la gestión de tasas de bits. Un cliente que tuviera que instanciar y
coordinar objetos como CodecFactory, AudioMixer, y BitrateReader estaría
fuertemente acoplado a los detalles internos de la biblioteca de conversión de
video. El patrón Facade interviene para proporcionar una capa de abstracción,
ofreciendo un método simple como convertirVideo(fichero, formato) que oculta
toda esa complejidad. 4
1.2. Participantes Estructurales y sus Colaboraciones
La estructura del patrón Facade es conceptualmente simple y se compone de
tres participantes clave:
El Subsistema (Subsystem): Representa la colección de clases,
interfaces y objetos que implementan la funcionalidad compleja. Puede ser
una biblioteca de terceros, un framework o un módulo dentro de la propia
aplicación. Los componentes del subsistema realizan el trabajo real, pero
sus interfaces suelen ser detalladas y de bajo nivel. Un aspecto
fundamental es que las clases del subsistema no conocen la existencia de
la fachada; no tienen ninguna referencia a ella, lo que garantiza que el
subsistema permanezca independiente y reutilizable. 9
La Fachada (Facade): Es una única clase que actúa como punto de
entrada simplificado al subsistema. Su responsabilidad es doble:
1. Conocer los componentes del subsistema que son necesarios para
satisfacer una solicitud del cliente.
2. Delegar las llamadas del cliente a los objetos apropiados del
subsistema, orquestando su colaboración.
119
La fachada traduce una llamada a un método de alto nivel en múltiples
acciones sobre los componentes del subsistema.1
El Cliente (Client): Son los objetos que necesitan utilizar la funcionalidad
del subsistema. En lugar de interactuar directamente con la multitud de
clases del subsistema, el cliente se comunica exclusivamente a través de
la interfaz pública de la fachada. Esto reduce drásticamente el número de
objetos con los que el cliente necesita interactuar y lo desacopla de la
implementación interna del subsistema. 1
La colaboración sigue un flujo claro y unidireccional:
1. El Cliente invoca un método simple en la instancia de la Fachada (por
ejemplo, facade.verPelicula("Avatar")).
2. La Fachada recibe esta solicitud de alto nivel.
3. La Fachada, que conoce la estructura y las interacciones del Subsistema,
realiza una secuencia de llamadas a los objetos pertinentes dentro del
mismo (enciende el Proyector, ajusta el Amplificador, etc.).
4. El Subsistema ejecuta las operaciones y el resultado se gestiona a través
de la Fachada, aunque a menudo la interacción es de tipo "dispara y
olvida" desde la perspectiva del cliente.
1.3. La Fachada como una Capa de Conveniencia Opcional
Un matiz crucial del patrón Facade, que lo distingue de otros patrones
estructurales, es que su propósito principal es la simplificación, no
necesariamente la restricción. La fachada no siempre oculta por completo las
interfaces del subsistema. En muchos casos, actúa como una "capa de
conveniencia" que ofrece una ruta más fácil para las tareas comunes, pero no
impide que un cliente, con necesidades más avanzadas, acceda directamente
a los componentes del subsistema si es necesario.
Esta característica se desprende de la descripción del patrón como un
"ayudante" para las aplicaciones cliente. La decisión de usar la fachada
9
depende completamente del cliente. Un documento de IONOS refuerza esta
idea al señalar que un cliente puede "eludir la fachada si es necesario".2
Este carácter opcional tiene profundas implicaciones arquitectónicas,
especialmente en la evolución y el refactorizado de sistemas existentes. Se
puede introducir una fachada en una base de código grande y compleja sin
romper el código cliente que ya interactúa directamente con el subsistema. Los
nuevos desarrollos pueden beneficiarse inmediatamente de la interfaz
simplificada que ofrece la fachada, mientras que las partes heredadas del
sistema pueden ser refactorizadas de forma incremental para utilizar la fachada
con el tiempo. Esto convierte al patrón Facade en una herramienta estratégica
invaluable para la gestión de la deuda técnica y la mejora gradual de la
mantenibilidad del software, ya que no exige una reescritura masiva y
disruptiva del código existente.
120
Sección 2: Implementación en la Práctica: Análisis Guiado
por Código
Para consolidar la comprensión teórica, es fundamental analizar
implementaciones concretas. A continuación, se presentan dos ejemplos en
Java: el caso canónico del cine en casa y una implementación que aborda
directamente el contexto de conversión de video propuesto por el usuario.
2.1. El Ejemplo Canónico: Una Fachada de Cine en Casa en Java
Este es el ejemplo clásico para ilustrar el patrón Facade, ya que encapsula de
manera intuitiva una secuencia de operaciones complejas detrás de una
interfaz simple. 6
El Subsistema Complejo
Primero, definimos las clases que componen nuestro subsistema de cine en
casa. Cada clase tiene su propia lógica y métodos específicos.
Java
// Subsistema: Parte 1 - El Amplificador
public class Amplifier {
public void on() { System.out.println("Amplificador encendido"); }
public void off() { System.out.println("Amplificador apagado"); }
public void setStreamingPlayer(StreamingPlayer player) {
System.out.println("Amplificador configurado para reproductor de streaming"); }
public void setSurroundSound() { System.out.println("Sonido envolvente activado"); }
public void setVolume(int level) { System.out.println("Volumen del amplificador ajustado
a " + level); }
}
// Subsistema: Parte 2 - El Proyector
public class Projector {
public void on() { System.out.println("Proyector encendido"); }
public void off() { System.out.println("Proyector apagado"); }
public void wideScreenMode() { System.out.println("Proyector en modo de pantalla
ancha"); }
}
// Subsistema: Parte 3 - Las Luces del Teatro
public class TheaterLights {
public void dim(int level) { System.out.println("Luces del teatro atenuadas a " + level +
"%"); }
public void on() { System.out.println("Luces del teatro encendidas"); }
121
}
// Subsistema: Parte 4 - El Reproductor de Streaming
public class StreamingPlayer {
public void on() { System.out.println("Reproductor de streaming encendido"); }
public void off() { System.out.println("Reproductor de streaming apagado"); }
public void play(String movie) { System.out.println("Reproduciendo película: \"" +
movie + "\""); }
}
La Clase Fachada
Ahora, creamos la clase HomeTheaterFacade. Esta clase contendrá
referencias a todos los componentes del subsistema y expondrá métodos
simples de alto nivel.
Java
public class HomeTheaterFacade {
// Referencias a los componentes del subsistema
private Amplifier amp;
private Projector projector;
private TheaterLights lights;
private StreamingPlayer player;
// El constructor de la fachada recibe los componentes del subsistema
public HomeTheaterFacade(Amplifier amp, Projector projector, TheaterLights lights,
StreamingPlayer player) {
this.amp = amp;
this.projector = projector;
this.lights = lights;
this.player = player;
}
/**
* El método watchMovie() simplifica una secuencia compleja de operaciones.
* El cliente solo necesita llamar a este método, y la fachada orquesta
* todo el subsistema.
*/
public void watchMovie(String movie) {
System.out.println("Preparando todo para ver una película...");
lights.dim(10);
projector.on();
projector.wideScreenMode();
amp.on();
amp.setStreamingPlayer(player);
122
amp.setSurroundSound();
amp.setVolume(5);
player.on();
player.play(movie);
}
/**
* El método endMovie() simplifica el proceso de apagado.
*/
public void endMovie() {
System.out.println("Apagando el cine en casa...");
player.off();
amp.off();
projector.off();
lights.on();
}
}
El Código Cliente
Finalmente, el código cliente demuestra la simplicidad obtenida. En lugar de
gestionar cuatro objetos y nueve llamadas a métodos, el cliente solo interactúa
con la fachada.
Java
public class Client {
public static void main(String args) {
// Inicialización de los componentes del subsistema
Amplifier amp = new Amplifier();
Projector projector = new Projector();
TheaterLights lights = new TheaterLights();
StreamingPlayer player = new StreamingPlayer();
// Creación de la fachada con los componentes del subsistema
HomeTheaterFacade homeTheater = new HomeTheaterFacade(amp,
projector, lights, player);
// Uso de la interfaz simplificada
homeTheater.watchMovie("Inception");
System.out.println("\n--- La película ha terminado ---\n");
homeTheater.endMovie();
}
}
123
Análisis: Este ejemplo demuestra de manera concluyente cómo la fachada
encapsula la complejidad. El cliente está completamente desacoplado de las
clases Amplifier, Projector, etc. Si en el futuro se añade un nuevo componente
(por ejemplo, una máquina de palomitas), solo habría que modificar la clase
HomeTheaterFacade, sin afectar al código cliente.
2.2. El Contexto del Usuario: Una Fachada de Conversión de Video en
Java
Abordando directamente el ejemplo del usuario, podemos diseñar una fachada
para un sistema de conversión de video. Este caso de uso es muy común, ya
que las fachadas son ideales para envolver bibliotecas o APIs complejas.
El Subsistema Complejo de Video
Definimos un subsistema hipotético con clases que representan las
complejidades de la manipulación de video.
Java
// Interfaces y clases del subsistema de conversión de video
// (Implementaciones simplificadas para el ejemplo)
// Representa un archivo de video
class VideoFile {
private String name;
public VideoFile(String name) { this.name = name; }
public String getName() { return name; }
}
// Interfaz para los códecs
interface Codec {}
class OggCompressionCodec implements Codec {}
class MPEG4CompressionCodec implements Codec {}
// Fábrica para extraer y determinar códecs
class CodecFactory {
public static Codec extract(VideoFile file) {
System.out.println("CodecFactory: extrayendo códec de " + file.getName());
// Lógica compleja para determinar el códec...
return new OggCompressionCodec();
}
}
// Lee y convierte la tasa de bits
class BitrateReader {
124
public static String convert(String buffer, Codec destinationCodec) {
System.out.println("BitrateReader: convirtiendo buffer a " +
destinationCodec.getClass().getSimpleName());
return "converted_buffer";
}
}
// Sincroniza el audio con el video
class AudioMixer {
public static String fix(String result) {
System.out.println("AudioMixer: sincronizando audio con el video.");
return "final_result";
}
}
// Representa un archivo resultante
class File {
private String content;
public File(String content) { this.content = content; }
public String getContent() { return content; }
}
La Fachada de Conversión de Video
La clase VideoConversionFacade proporcionará un único método para realizar
la conversión.
Java
public class VideoConversionFacade {
/**
* El método convertVideo simplifica un proceso de múltiples pasos que involucra
* varias clases del subsistema.
* @param fileName El nombre del archivo a convertir.
* @param format El formato de destino (ej. "mp4").
* @return Un objeto File que representa el video convertido.
*/
public File convertVideo(String fileName, String format) {
System.out.println("VideoConversionFacade: inicio de la conversión.");
// 1. Crear un objeto VideoFile
VideoFile file = new VideoFile(fileName);
// 2. Extraer el códec de origen usando una fábrica
Codec sourceCodec = CodecFactory.extract(file);
// 3. Determinar el códec de destino
125
Codec destinationCodec;
if (format.equals("mp4")) {
destinationCodec = new MPEG4CompressionCodec();
} else {
// En una implementación real, se manejarían más formatos o se lanzaría una
excepción.
System.out.println("VideoConversionFacade: formato '" + format + "' no
soportado.");
return null;
}
// 4. Leer y convertir el buffer (simulado)
String buffer = "raw_video_buffer"; // Simulación de lectura del archivo
String intermediateResult = BitrateReader.convert(buffer,
destinationCodec);
// 5. Sincronizar el audio
String finalResult = AudioMixer.fix(intermediateResult);
System.out.println("VideoConversionFacade: conversión completada.");
return new File(finalResult);
}
}
El Código Cliente
El cliente ahora puede convertir videos con una sola llamada, sin necesidad de
entender la lógica subyacente.
Java
public class Client {
public static void main(String args){
VideoConversionFacade converter = new VideoConversionFacade();
File mp4 = converter.convertVideo("mi_video_de_vacaciones.ogg", "mp4");
if (mp4!= null) {
System.out.println("Video convertido exitosamente: " + mp4.getContent());
}
}
}
Análisis: Este ejemplo demuestra el poder del patrón Facade para actuar
como una envoltura (wrapper) de APIs complejas. El cliente no necesita
instanciar CodecFactory, ni manejar BitrateReader o AudioMixer. Toda esa
126
lógica está encapsulada en la fachada. Si la biblioteca de conversión de video
se actualiza en el futuro, solo se necesita modificar VideoConversionFacade,
protegiendo al resto de la aplicación de los cambios.
Sección 3: Una Evaluación Crítica del Patrón Facade
Como toda herramienta de diseño, el patrón Facade ofrece ventajas
significativas, pero también presenta riesgos y desventajas que deben ser
considerados cuidadosamente durante la fase de diseño arquitectónico.
3.1. Ventajas Estratégicas: El Poder del Desacoplamiento y la
Simplificación
Las ventajas del patrón Facade se derivan directamente de su intención de
simplificar y desacoplar.
Desacoplamiento: La ventaja más importante es que la fachada
desacopla a los clientes de los componentes internos del subsistema. El 4
cliente solo depende de la interfaz estable de la fachada. Esto significa que
el subsistema puede ser modificado, refactorizado o incluso
completamente reemplazado sin que el código cliente se vea afectado,
siempre y cuando la firma de los métodos de la fachada se mantenga. Esta
flexibilidad es crucial para la mantenibilidad a largo plazo y la evolución de
un sistema.
Simplificación: El patrón proporciona una interfaz mucho más simple y de
más alto nivel para realizar tareas comunes. Esto reduce la curva de
aprendizaje para los nuevos desarrolladores que necesitan usar el
subsistema y disminuye la cantidad de código repetitivo (boilerplate) que
los clientes deben escribir para realizar operaciones estándar, lo que a su
vez reduce la probabilidad de errores. 1
Estructura en Capas: El patrón Facade es una herramienta natural para
la creación de arquitecturas en capas. Cada subsistema puede tener su
propia fachada, que actúa como el único punto de entrada a esa capa.
Esto ayuda a hacer cumplir las reglas de dependencia entre capas, ya que
una capa superior solo se comunicaría con la fachada de la capa inferior,
en lugar de tener dependencias dispersas con múltiples componentes
internos.1
3.2. Desventajas, Riesgos y Estrategias de Mitigación
A pesar de sus beneficios, una implementación ingenua o un uso inadecuado
del patrón Facade puede introducir problemas en la arquitectura.
El Anti-Patrón "God Object": El riesgo más significativo es que la clase
fachada se convierta en un "objeto dios" (God Object). Esto ocurre cuando
la fachada intenta simplificar demasiadas funcionalidades del subsistema,
acumulando una cantidad excesiva de métodos y responsabilidades. Una
127
fachada que se convierte en un monolito se vuelve, a su vez, difícil de
entender y mantener, traicionando el propósito original de simplificación.2
Sobrecarga de Rendimiento: La capa adicional de indirección que
introduce la fachada puede tener un impacto en el rendimiento. Cada
llamada a la fachada se traduce en una o más llamadas a los objetos del
subsistema. Aunque en la mayoría de las aplicaciones empresariales esta
sobrecarga es despreciable, en sistemas de muy alto rendimiento o de
baja latencia, podría ser un factor a considerar.
11
Simplificación Excesiva y Oscuridad: Al ocultar la complejidad, una
fachada puede también ocultar detalles importantes o funcionalidades de
bajo nivel que un cliente avanzado podría necesitar. Si la fachada es la
única forma de interactuar con el subsistema, puede limitar la flexibilidad y
dificultar la depuración de problemas que se originan en el interior del
subsistema. 11
Dependencia de la Interfaz de la Fachada: Si bien la fachada desacopla
al cliente del subsistema, crea una fuerte dependencia del cliente con la
propia interfaz de la fachada. Cualquier cambio en los métodos públicos de
la fachada puede tener un efecto dominó en todas las partes del sistema
que la utilizan. 2
3.3. El Arte de Comisariar la Interfaz de la Fachada
Una fachada bien diseñada no es simplemente una envoltura que expone todos
los métodos del subsistema. Su verdadero valor reside en ser una API
comisariada (curated). Esto significa que el diseñador de la fachada toma
decisiones deliberadas sobre qué funcionalidades exponer y cuáles ocultar.
La clave para evitar el anti-patrón del "God Object" es la limitación
intencionada. En lugar de crear un envoltorio de uno a uno, el diseñador debe
aplicar el principio 80/20: identificar el 20% de las funcionalidades del
subsistema que cubren el 80% de los casos de uso del cliente. La fachada
debe exponer únicamente estas operaciones comunes y de alto nivel. Como se
menciona en una de las fuentes, una fachada "podría proporcionar una
funcionalidad limitada" pero "incluye solo aquellas características que interesan
a los clientes".
4
Este enfoque de diseño tiene dos consecuencias importantes. Primero,
mantiene la fachada simple y enfocada en su propósito. Segundo, reconoce
que puede haber casos de uso avanzados o de borde que no son cubiertos por
la interfaz simplificada. Para estos escenarios, los clientes avanzados deberían
tener la opción de "eludir" la fachada y trabajar directamente con los
componentes del subsistema, como se discutió anteriormente.
Esto nos lleva a una estrategia de mitigación más sofisticada: en lugar de una
única fachada monolítica para un subsistema muy complejo, puede ser más
efectivo diseñar múltiples fachadas más pequeñas y basadas en roles. Por
ejemplo, para una biblioteca de gráficos compleja, podríamos tener una
SimpleDrawingFacade para tareas de dibujo básicas y una
AdvancedRenderingFacade para clientes que necesitan control sobre shaders
y texturas. Este enfoque mantiene cada fachada cohesiva y alineada con un
128
conjunto específico de necesidades del cliente, preservando la simplicidad sin
sacrificar por completo la flexibilidad.
Ventajas Desventajas
Reduce el acoplamiento cliente- Riesgo de convertirse en un "God Object"
subsistema. complejo.
Simplifica el código del cliente y reduce Puede ocultar detalles necesarios del
errores. subsistema.
Promueve una arquitectura en capas. Introduce una sobrecarga de rendimiento por
indirección.
Centraliza la lógica de interacción con el Crea una fuerte dependencia en la API de la
subsistema. fachada.
Sección 4: Contextualizando la Fachada entre Otros
Patrones de Diseño
Para definir con precisión el patrón Facade, es instructivo compararlo con otros
patrones estructurales, especialmente el patrón Adapter, con el que a menudo
se confunde. Además, es útil entender cómo Facade puede colaborar con
patrones de otras categorías, como los creacionales.
4.1. Fachada vs. Adaptador: Simplificación vs. Conversión
Aunque tanto el patrón Facade como el Adapter actúan como "envolturas"
(wrappers) alrededor de otros objetos, sus intenciones y contextos de
aplicación son fundamentalmente diferentes. 12
Intención: La diferencia clave radica en el propósito. La intención de una
Fachada es simplificar una interfaz, proporcionando una vista de más alto
nivel sobre un subsistema complejo. La intención de un Adaptador es
convertir una interfaz en otra, para hacer que dos interfaces incompatibles
puedan colaborar. 15
Analogía: La analogía de la fachada es el control remoto del cine en casa,
que simplifica un conjunto de operaciones. La analogía del adaptador es
un convertidor de enchufe de corriente, que permite que un aparato con un
enchufe europeo (una interfaz) funcione en una toma de corriente
americana (una interfaz incompatible). 16
Interfaz: Una Fachada define una interfaz nueva y más simple, que no
existía antes. Un Adaptador, por otro lado, implementa una interfaz
existente que el cliente ya espera utilizar.
Alcance: Una Fachada típicamente envuelve a múltiples objetos que
componen un subsistema. Un Adaptador generalmente envuelve a un
solo objeto (el Adaptee). 18
129
En resumen, se utiliza una Fachada cuando se quiere facilitar el uso de un
subsistema. Se utiliza un Adaptador cuando se quiere usar una clase existente,
pero su interfaz no es la que el cliente necesita. Una frase célebre resume la
diferencia en el ciclo de vida del software: "El Adaptador hace que las cosas
funcionen después de que fueron diseñadas; el Puente (y por extensión, la
Fachada) hace que funcionen antes". La Fachada se diseña a menudo desde
19
el principio para proporcionar una arquitectura limpia, mientras que el
Adaptador se usa comúnmente para integrar código heredado o de terceros.
Aspecto Patrón Facade Patrón Adapter
Intención Simplificar una interfaz Convertir una interfaz para que sea
compleja. compatible con otra.
Interfaz Define una interfaz nueva y de Implementa una interfaz existente
más alto nivel. que el cliente espera.
Alcance Envuelve a un subsistema Generalmente envuelve a un único
completo (múltiples objetos). objeto (el Adaptee).
Problema "Este subsistema es demasiado "No puedo usar este objeto porque
Resuelto difícil de usar". su interfaz es incorrecta".
4.2. Componiendo Patrones para Arquitecturas Avanzadas
El patrón Facade no existe en un vacío; a menudo se combina con otros
patrones para crear soluciones más robustas y limpias. Una de las sinergias
más poderosas es la composición de una Fachada (un patrón estructural) con
un patrón creacional como Factory Method o Abstract Factory.
El trabajo de una fachada es orquestar un proceso. A menudo, este proceso
requiere la creación de objetos auxiliares. Retomando el ejemplo de la
conversión de video, la fachada podría necesitar seleccionar un códec
específico (MPEG4CompressionCodec, WMVCompressionCodec, etc.)
basándose en el sistema operativo o en la configuración del usuario.
Si esta lógica de creación se implementa directamente dentro de la fachada, se
estaría violando el Principio de Responsabilidad Única. La responsabilidad de
la fachada es gestionar el proceso de conversión, no la creación de los códecs.
Introducir lógica condicional para la creación de objetos dentro de la fachada la
haría más compleja y menos mantenible.
Este es un escenario perfecto para delegar la creación a un patrón de fábrica. 9
La fachada no crearía el códec directamente con
new, sino que invocaría un método de una clase fábrica (por ejemplo,
CodecFactory.createCodec(format)). Esta fábrica encapsularía la lógica para
decidir qué códec concreto instanciar.
130
Esta composición crea una separación de responsabilidades muy elegante y en
capas:
1. El Cliente está protegido de la complejidad del proceso por la Fachada.
2. La Fachada está protegida de la complejidad de la creación por la
Fábrica.
Esta sinergia demuestra cómo los patrones de diseño no son soluciones
aisladas, sino bloques de construcción que pueden combinarse para resolver
problemas multifacéticos. La fachada actúa como un "gerente" que dirige el
flujo de trabajo, mientras que la fábrica actúa como un "especialista" al que se
le delega la tarea de crear las herramientas necesarias. Este nivel de
pensamiento arquitectónico eleva el uso de patrones de un simple ejercicio de
implementación a una estrategia de diseño de software sofisticada.
Conclusión: La Fachada como Herramienta Arquitectónica
Estratégica
El patrón de diseño Facade emerge como una solución estructural fundamental
para uno de los desafíos más persistentes en la ingeniería de software: la
gestión de la complejidad. Al proporcionar una interfaz simplificada y unificada
para un subsistema, no solo mejora la usabilidad del código, sino que también
establece un desacoplamiento crucial entre los clientes y las implementaciones
internas. Este desacoplamiento es la piedra angular de los sistemas
mantenibles y escalables.
A lo largo de este análisis, hemos visto que los beneficios principales del patrón
son la reducción de la complejidad para el código cliente y la promoción de una
arquitectura en capas más limpia. Sin embargo, su implementación no está
exenta de riesgos, siendo el más notable la posibilidad de que la propia
fachada evolucione hasta convertirse en un "God Object" que centraliza
demasiada lógica, traicionando su propósito original.
La clave para una aplicación exitosa del patrón Facade radica en un
entendimiento más profundo de su naturaleza. No se trata simplemente de
crear una envoltura, sino de un acto deliberado de diseño de API. Una fachada
efectiva es una interfaz opcional y comisariada, que expone conscientemente
solo los casos de uso más comunes y relevantes para el cliente. Para
necesidades más complejas, se puede permitir el acceso directo al subsistema,
lo que convierte a la fachada en una herramienta poderosa para la
refactorización gradual de sistemas heredados sin necesidad de cambios
disruptivos. Además, su capacidad para componerse con otros patrones, como
los de fábrica, demuestra su versatilidad como un componente dentro de una
estrategia arquitectónica más amplia.
En última instancia, dominar el patrón Facade es un paso significativo para
cualquier desarrollador. Implica pasar de simplemente escribir código que
funciona a diseñar sistemas que son robustos, flexibles y fáciles de mantener a
lo largo del tiempo. La capacidad de identificar la necesidad de una fachada y
de diseñarla de manera efectiva es una habilidad que distingue a un
131
programador competente de un arquitecto de software reflexivo. Dominar estos
conceptos es un desafío, pero es un paso alcanzable y profundamente
empoderador en la carrera de cualquier profesional del software.
Obras citadas
1. Facade pattern - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Facade_pattern
2. www.ionos.com, fecha de acceso: junio 29, 2025,
https://www.ionos.com/digitalguide/websites/web-development/whats-the-
facade-pattern/
3. en.wikipedia.org, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Facade_pattern#:~:text=Developers%20often
%20use%20the%20facade,simpler%20interface%20to%20the%20client.
4. Facade Design Pattern: Simplifying Complex Code Structures | Belatrix
Blog - Globant, fecha de acceso: junio 29, 2025,
https://belatrix.globant.com/us-en/blog/tech-trends/facade-design-pattern/
5. medium.com, fecha de acceso: junio 29, 2025,
https://medium.com/@kalanamalshan98/proxy-design-pattern-a-
comprehensive-guide-
73688bbd8e93#:~:text=The%20Proxy%20Design%20Pattern%20is,%2C
%20caching%2C%20or%20access%20control.
6. Facade design pattern:. The Facade pattern is a structural… | by Atul
Kumar | Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@kumar.atul.2122/facade-design-pattern-
f7d3aba2560b
7. The Facade Design Pattern: Simplifying Complex Systems | by Suresh -
Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@CodeWithTech/the-facade-design-pattern-
simplifying-complex-systems-1011b2a12def
8. Facade Pattern in Java: Simplifying Complex System Interfaces, fecha de
acceso: junio 29, 2025, https://java-design-patterns.com/patterns/facade/
9. Facade Design Pattern in Java - DigitalOcean, fecha de acceso: junio 29,
2025, https://www.digitalocean.com/community/tutorials/facade-design-
pattern-in-java
10. Facade Design Pattern in Java - DEV Community, fecha de acceso: junio
29, 2025, https://dev.to/mspilari/facade-design-pattern-in-java-57a1
11. The Facade Design Pattern in Angular: Pros, Cons, and Examples -
DevBySeb, fecha de acceso: junio 29, 2025,
https://www.devbyseb.com/article/the-facade-design-pattern-in-angular-
pros-cons-and-examples
12. Adapter - Refactoring.Guru, fecha de acceso: junio 29, 2025,
https://refactoring.guru/design-patterns/adapter
13. What is the difference between the Facade and Adapter Pattern? - Stack
Overflow, fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/2961307/what-is-the-difference-
between-the-facade-and-adapter-pattern
14. Design Patterns: Adapter and Façade - Learn Microsoft, fecha de acceso:
junio 29, 2025, https://learn.microsoft.com/en-us/shows/visual-studio-
toolbox/design-patterns-adapterfaade
132
15. stackoverflow.com, fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/1425171/difference-between-bridge-
pattern-and-adapter-
pattern#:~:text=Adapter%20is%20used%20when%20you,interface%20an
d%20the%20underlying%20implementation.
16. Adapter Pattern - Take The Notes, fecha de acceso: junio 29, 2025,
https://takethenotes.com/adapter-pattern/
17. Adapter Design Pattern in Java - Medium, fecha de acceso: junio 29,
2025, https://medium.com/@akshatsharma0610/adapter-design-pattern-
in-java-fa20d6df25b8
18. Design Patterns Part 4 – Adapter, Facade, and Memento - Coding Blocks
– Podcast, fecha de acceso: junio 29, 2025,
https://www.codingblocks.net/podcast/design-patterns-adapter-facade-
memento/
19. Difference between Bridge pattern and Adapter pattern - Stack Overflow,
fecha de acceso: junio 29, 2025,
https://stackoverflow.com/questions/1425171/difference-between-bridge-
pattern-and-adapter-pattern
133
Capítulo 11: Análisis del Patrón de
Diseño Composite
A. Propósito Fundamental: Jerarquías de Parte-
Todo
El patrón de diseño Composite (Compuesto) es un patrón estructural que
permite componer objetos en estructuras de árbol para representar
jerarquías de parte-todo. La característica más poderosa y definitoria de este
patrón es que permite a los clientes tratar a los objetos individuales (las "hojas"
de la estructura) y a las composiciones de objetos (los "contenedores" o
"ramas") de manera uniforme.
En muchos sistemas de software, nos encontramos con objetos que son
colecciones de otros objetos. Por ejemplo, una interfaz gráfica de usuario (GUI)
está compuesta por paneles, que a su vez contienen otros paneles, botones,
campos de texto, etc. Un sistema de archivos consiste en directorios que
contienen archivos y otros directorios. En estos casos, a menudo es deseable
poder realizar una misma operación sobre un elemento simple (un archivo) y
sobre un elemento complejo (un directorio que contiene muchos archivos) sin
que el código cliente tenga que hacer una distinción.
El patrón Composite resuelve este problema al definir una interfaz común que
es implementada tanto por los objetos simples (hojas) como por los objetos
contenedores (compuestos). De esta manera, un cliente puede invocar el
mismo método en cualquier objeto de la estructura, y la operación se propagará
recursivamente por el árbol si el objeto es un contenedor. Esto simplifica
enormemente el código del cliente y facilita la manipulación de estructuras
complejas.
B. Estructura y Participantes del Patrón
La clave de la estructura del Composite es la interfaz compartida que unifica los
objetos primitivos y los compuestos. Los participantes clave son:
Component (Componente):
o Es una interfaz o clase abstracta que declara las operaciones
comunes tanto para los objetos simples (Leaf) como para los
complejos (Composite).
o Define el contrato que todos los elementos de la jerarquía deben
seguir, asegurando que el cliente pueda tratarlos de manera
uniforme.
Leaf (Hoja):
o Representa los objetos "hoja" o terminales de la composición. Un
Leaf no tiene hijos.
134
o Implementa las operaciones definidas por la interfaz Component. Es
en las hojas donde generalmente se realiza el trabajo real de la
jerarquía (por ejemplo, dibujar un píxel, leer un archivo).
Composite (Compuesto):
o Representa los nodos "compuestos" que pueden tener hijos.
Almacena una colección de Component hijos (que pueden ser Leafs
u otros Composites).
o Implementa las operaciones de la interfaz Component,
generalmente delegando la solicitud a sus hijos y luego,
opcionalmente, realizando alguna operación adicional con los
resultados.
o Define métodos para gestionar a sus hijos, como add(Component),
remove(Component) y getChild(int).
Client (Cliente):
o Manipula los objetos de la composición a través de la interfaz
Component. Gracias a esta interfaz común, el cliente no necesita
saber si está tratando con una Leaf o un Composite, lo que
simplifica su lógica.
C. Ejemplo Práctico: Un Sistema de Archivos
Un sistema de archivos es el ejemplo canónico para ilustrar el patrón
Composite. Un directorio (Composite) puede contener archivos (Leaf) y otros
directorios. Tanto un archivo como un directorio pueden ser tratados de forma
similar en ciertas operaciones, como mostrar su nombre o calcular su tamaño
total.
C.1. El Component
Primero, definimos la interfaz común para todos los elementos del sistema de
archivos.
// package com.ejemplo.composite;
// La interfaz Component define las operaciones comunes para archivos y directorios.
public interface SistemaDeArchivosComponent {
// Muestra el nombre y la estructura del componente.
void mostrar(String indentacion);
// Devuelve el tamaño total del componente en bytes.
long getTamaño();
}
C.2. El Leaf
La clase Archivo representa una hoja en la jerarquía. No puede contener otros
componentes.
// package com.ejemplo.composite;
// La clase Leaf representa un objeto terminal.
135
public class Archivo implements SistemaDeArchivosComponent {
private String nombre;
private long tamaño;
public Archivo(String nombre, long tamaño) {
this.nombre = nombre;
this.tamaño = tamaño;
}
@Override
public void mostrar(String indentacion) {
System.out.println(indentacion + "- " + this.nombre + " (" + this.tamaño + " bytes)");
}
@Override
public long getTamaño() {
return this.tamaño;
}
}
C.3. El Composite
La clase Directorio representa un contenedor. Mantiene una lista de hijos, que
pueden ser Archivos u otros Directorios.
// package com.ejemplo.composite;
import java.util.ArrayList;
import java.util.List;
// La clase Composite representa un contenedor que puede tener hijos.
public class Directorio implements SistemaDeArchivosComponent {
private String nombre;
private List<SistemaDeArchivosComponent> hijos = new ArrayList<>();
public Directorio(String nombre) {
this.nombre = nombre;
}
public void agregar(SistemaDeArchivosComponent componente) {
hijos.add(componente);
}
public void remover(SistemaDeArchivosComponent componente) {
hijos.remove(componente);
}
@Override
public void mostrar(String indentacion) {
// Muestra su propio nombre.
System.out.println(indentacion + "+ " + this.nombre);
// Delega la operación de mostrar a sus hijos, aumentando la indentación.
for (SistemaDeArchivosComponent hijo : hijos) {
hijo.mostrar(indentacion + " ");
}
}
@Override
136
public long getTamaño() {
// Delega el cálculo del tamaño a sus hijos y suma los resultados.
long tamañoTotal = 0;
for (SistemaDeArchivosComponent hijo : hijos) {
tamañoTotal += hijo.getTamaño();
}
return tamañoTotal;
}
}
C.4. El Client
El cliente construye la estructura del árbol y opera sobre ella a través de la
interfaz SistemaDeArchivosComponent, sin necesidad de diferenciar entre archivos
y directorios.
// package com.ejemplo.cliente;
import com.ejemplo.composite.Archivo;
import com.ejemplo.composite.Directorio;
import com.ejemplo.composite.SistemaDeArchivosComponent;
public class Cliente {
public static void main(String[] args) {
// Crear la estructura del árbol de directorios y archivos.
Directorio raiz = new Directorio("raiz");
Directorio documentos = new Directorio("documentos");
Directorio imagenes = new Directorio("imagenes");
Directorio varios = new Directorio("varios");
Archivo reporte = new Archivo("reporte.docx", 256);
Archivo presentacion = new Archivo("presentacion.pptx", 1024);
Archivo foto_vacaciones = new Archivo("vacaciones.jpg", 2048);
Archivo logo = new Archivo("logo.png", 512);
Archivo musica = new Archivo("cancion.mp3", 4096);
// Construir la jerarquía.
raiz.agregar(documentos);
raiz.agregar(imagenes);
raiz.agregar(musica);
documentos.agregar(reporte);
documentos.agregar(presentacion);
documentos.agregar(varios);
imagenes.agregar(foto_vacaciones);
imagenes.agregar(logo);
varios.agregar(new Archivo("temporal.tmp", 128));
// El cliente puede ahora operar sobre cualquier parte de la estructura
// de forma uniforme.
System.out.println("Mostrando la estructura completa desde la raíz:");
raiz.mostrar("");
System.out.println("\n----------------------------------------\n");
137
System.out.println("Calculando tamaños:");
System.out.println("Tamaño total de la carpeta 'documentos': " + documentos.getTamaño()
+ " bytes");
System.out.println("Tamaño total de la carpeta 'raiz': " + raiz.getTamaño() + " bytes");
}
}
Salida del Programa:
Mostrando la estructura completa desde la raíz:
+ raiz
+ documentos
- reporte.docx (256 bytes)
- presentacion.pptx (1024 bytes)
+ varios
- temporal.tmp (128 bytes)
+ imagenes
- vacaciones.jpg (2048 bytes)
- logo.png (512 bytes)
- cancion.mp3 (4096 bytes)
----------------------------------------
Calculando tamaños:
Tamaño total de la carpeta 'documentos': 1408 bytes
Tamaño total de la carpeta 'raiz': 8064 bytes
Como se puede observar, el cliente invoca mostrar("") en el directorio raiz y la
operación se propaga recursivamente por toda la estructura, mostrando cada
elemento con la indentación correcta. De igual manera, al llamar a getTamaño(),
el cálculo se delega y se agrega recursivamente, todo de forma transparente
para el cliente.
D. Ventajas y Desventajas del Patrón Composite
Ventajas
Uniformidad y Simplicidad del Cliente: El cliente puede tratar a los
objetos compuestos y a los objetos individuales de manera uniforme. No
necesita escribir código condicional (if/else) para diferenciar entre una
hoja y un compuesto, lo que simplifica enormemente su lógica.
Principio de Abierto/Cerrado: Es fácil añadir nuevos tipos de
componentes (tanto Leaf como Composite) al sistema. Las nuevas clases
pueden ser introducidas sin necesidad de modificar el código cliente
existente, ya que este opera a través de la interfaz Component.
Estructura Jerárquica Clara: El patrón proporciona una arquitectura
flexible y natural para representar jerarquías de objetos de parte-todo.
Desventajas
Diseño "Demasiado General": A veces, el patrón puede hacer que el
diseño sea excesivamente general. Es posible que se quieran restringir
138
las operaciones o los tipos de componentes que se pueden añadir a un
compuesto, y la interfaz común puede dificultar la implementación de
estas restricciones.
Dilema de Transparencia vs. Seguridad: Existe un dilema de diseño
en dónde colocar los métodos de gestión de hijos (add, remove, etc.).
o Transparencia: Si se declaran en la interfaz Component, el cliente
puede tratar a todos los objetos de forma completamente
uniforme. Sin embargo, esto es "inseguro" porque una Leaf no
debería tener hijos, y tendría que implementar estos métodos
lanzando una excepción o no haciendo nada.
o Seguridad: Si se declaran solo en la clase Composite, el diseño es
más "seguro" porque una Leaf no tendrá métodos que no le
corresponden. Sin embargo, se pierde la transparencia, ya que el
cliente debe verificar si un objeto es un Composite antes de poder
agregarle hijos. La implementación en el ejemplo anterior
favorece la seguridad, ya que los métodos agregar y remover solo
existen en la clase Directorio.
Complejidad en el Código: En algunos lenguajes de programación,
implementar el patrón puede ser más difícil, y puede ser complicado
encontrar una interfaz común para clases que son conceptualmente muy
diferentes.
139
Capítulo 12: Análisis del Patrón de
Diseño Proxy
1. Propósito Fundamental: Un Sustituto Inteligente
El patrón de diseño Proxy, perteneciente a la categoría de patrones estructurales,
tiene como propósito fundamental proporcionar un sustituto o marcador de
posición (placeholder) para otro objeto con el fin de controlar el acceso a este.
En esencia, el Proxy actúa como un intermediario que se interpone entre un cliente y
el objeto real (denominado RealSubject). Esta capa de indirección permite al proxy
realizar tareas adicionales antes o después de delegar la solicitud al objeto real, como
verificar permisos, gestionar la creación de recursos costosos o manejar la
comunicación a través de una red.
La analogía más intuitiva es la de una tarjeta de crédito. La tarjeta (Proxy) es un
sustituto del dinero en efectivo que se encuentra en una cuenta bancaria
(RealSubject). Cuando realizamos una compra, no entregamos el dinero directamente.
En su lugar, usamos la tarjeta. El sistema de punto de venta (el Cliente) interactúa con
la tarjeta, la cual realiza comprobaciones de seguridad, verifica el saldo y autoriza la
transacción antes de acceder a los fondos reales. La tarjeta controla el acceso al
dinero, añadiendo una capa de seguridad y gestión que el efectivo por sí solo no tiene.
De manera similar, en el software, el patrón Proxy permite que un objeto "pretenda"
ser otro. El cliente interactúa con el proxy de la misma manera que lo haría con el
objeto real porque ambos comparten una interfaz común. Esta transparencia es clave,
ya que el cliente no necesita saber si está tratando con un proxy o con el objeto real, lo
que simplifica su lógica y desacopla el sistema.
2. Estructura y Participantes
El patrón Proxy se define por una estructura simple pero poderosa que involucra a
cuatro participantes clave:
Subject (Sujeto): Es una interfaz o clase abstracta que define las operaciones
comunes tanto para el RealSubject como para el Proxy. El cliente interactúa
con los objetos a través de esta interfaz, lo que garantiza que el proxy pueda
sustituir al objeto real de forma transparente.
RealSubject (Sujeto Real): Es la clase del objeto real que el proxy representa
y al que, en última instancia, el cliente desea acceder. Contiene la lógica de
negocio principal y realiza el trabajo pesado.
Proxy (Proxy): Mantiene una referencia que le permite acceder al RealSubject.
Implementa la misma interfaz Subject que el RealSubject, por lo que puede
sustituirlo. Su función es controlar el acceso al RealSubject y puede ser
responsable de crear, gestionar y eliminar el objeto real.
Client (Cliente): Es la clase que necesita interactuar con el RealSubject. En
lugar de hacerlo directamente, interactúa a través de la interfaz Subject, sin
saber si está comunicándose con el objeto real o con su proxy.
3. Tipos Comunes de Proxy y sus Aplicaciones
140
La verdadera versatilidad del patrón Proxy se manifiesta en sus diferentes variantes,
cada una diseñada para resolver un tipo específico de problema de control de acceso
o gestión de recursos.
3.1. Proxy Virtual (Virtual Proxy)
El Proxy Virtual se utiliza para gestionar la inicialización de objetos que son costosos
de crear en términos de memoria o tiempo de procesamiento. Su objetivo es retrasar
la creación del objeto real hasta el momento exacto en que es necesario, una técnica
conocida como carga perezosa (lazy loading o lazy initialization).
Mecanismo: El proxy virtual almacena la información necesaria para crear el
RealSubject, pero no lo instancia de inmediato. Cuando el cliente invoca por
primera vez un método en el proxy, este crea la instancia del RealSubject, la
guarda para futuras solicitudes y luego delega la llamada. En las llamadas
posteriores, el proxy simplemente delega la solicitud al objeto real ya creado.
Caso de Uso: El ejemplo clásico es la carga de imágenes de alta resolución en
un documento o una página web. Un ImageProxy puede mostrar inicialmente
un marcador de posición de baja resolución o simplemente almacenar las
dimensiones de la imagen. Solo cuando la imagen se vuelve visible en la
pantalla (es decir, cuando se llama a su método display()), el proxy carga la
imagen real desde el disco o la red, evitando el consumo de memoria y ancho
de banda para las imágenes que el usuario nunca ve.
3.2. Proxy de Protección (Protection Proxy)
El Proxy de Protección se utiliza para controlar el acceso a los métodos de un objeto
basándose en los permisos o credenciales del cliente que realiza la llamada. Actúa
como una capa de seguridad o autorización.
Mecanismo: Antes de delegar una llamada al RealSubject, el proxy de
protección verifica si el cliente tiene los derechos de acceso necesarios para
ejecutar esa operación. Si la verificación falla, el proxy puede lanzar una
excepción o devolver un error, impidiendo que la llamada llegue al objeto real.
Caso de Uso: En un sistema de gestión de documentos, un objeto Document
podría tener métodos como read(), write() y delete(). Un DocumentProxy podría
envolver este objeto. Si un usuario con rol de "lector" intenta llamar a delete(),
el proxy interceptaría la llamada, comprobaría los permisos del usuario y
denegaría la operación sin involucrar al objeto Document real. En cambio, si un
"administrador" realiza la misma llamada, el proxy la permitiría y la delegaría.
3.3. Proxy Remoto (Remote Proxy)
El Proxy Remoto se encarga de representar un objeto que reside en un espacio de
direcciones diferente, como en otro proceso en la misma máquina o, más
comúnmente, en un servidor remoto a través de la red.
Mecanismo: El proxy remoto se ejecuta en el espacio del cliente y actúa como
un representante local del objeto remoto. Su responsabilidad es ocultar toda la
complejidad de la comunicación de red (serialización de datos, manejo de
conexiones, gestión de timeouts, etc.). Cuando el cliente invoca un método en
el proxy remoto, este empaqueta la llamada y los argumentos, los envía a
141
través de la red al objeto real, espera la respuesta, la desempaqueta y la
devuelve al cliente como si la llamada hubiera sido local.
Caso de Uso: Las tecnologías de Invocación de Métodos Remotos (RMI) en
Java o los stubs generados en sistemas de RPC (Remote Procedure Call) son
ejemplos perfectos de proxies remotos. Permiten que el código cliente trabaje
con un objeto remoto de la misma manera que lo haría con un objeto local.
4. Ejemplo Práctico: Proxy Virtual para Carga de Imágenes
A continuación, se presenta una implementación en Java del ejemplo de Proxy Virtual
para cargar imágenes de alta resolución, que ilustra claramente el concepto de carga
perezosa.
Paso 1: Definir la interfaz Subject
Esta interfaz será compartida por la imagen real y su proxy.
// Subject: La interfaz común para el objeto real y el proxy.
public interface Image {
void display();
}
Paso 2: Crear la clase RealSubject
Esta clase representa el objeto costoso de crear. En este caso, simula la carga de una
imagen pesada desde el disco.
// RealSubject: El objeto que consume muchos recursos.
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
// Simula la costosa operación de cargar la imagen desde el disco.
loadFromDisk(fileName);
}
private void loadFromDisk(String fileName) {
System.out.println("Cargando la imagen: " + fileName);
// Aquí iría la lógica real de lectura del archivo.
}
142
@Override
public void display() {
System.out.println("Mostrando la imagen: " + fileName);
}
}
Paso 3: Crear la clase Proxy
El ImageProxy controla el acceso al RealImage. Solo lo creará cuando el cliente llame
a display().
// Proxy: El sustituto que controla el acceso al RealSubject.
public class ImageProxy implements Image {
private RealImage realImage; // Referencia al objeto real.
private String fileName;
public ImageProxy(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
// Carga perezosa (Lazy Loading): el RealImage se crea solo si es necesario.
if (realImage == null) {
System.out.println("Proxy: Creando la instancia de RealImage ahora.");
realImage = new RealImage(fileName);
}
// Una vez creado, simplemente se delega la llamada.
realImage.display();
}
}
Paso 4: El Client utiliza el Proxy
El cliente trabaja con el ImageProxy sin saber que la carga de la imagen se retrasa.
// Client: Utiliza el proxy para interactuar con el objeto.
143
public class Client {
public static void main(String[] args) {
// El cliente crea instancias del proxy, no de la imagen real.
// La creación del proxy es barata.
Image image1 = new ImageProxy("foto_vacaciones_HD.jpg");
Image image2 = new ImageProxy("documento_escaneado.png");
System.out.println("--- PRIMERA LLAMADA ---");
// La imagen real (image1) se carga y se muestra solo en este momento.
image1.display();
System.out.println("\n--- SEGUNDA LLAMADA ---");
// La imagen ya está cargada, el proxy solo delega la llamada.
image1.display();
System.out.println("\n--- TERCERA LLAMADA (OTRA IMAGEN) ---");
// La segunda imagen (image2) se carga y se muestra ahora.
image2.display();
}
}
Salida del Programa:
--- PRIMERA LLAMADA ---
Proxy: Creando la instancia de RealImage ahora.
Cargando la imagen: foto_vacaciones_HD.jpg
Mostrando la imagen: foto_vacaciones_HD.jpg
--- SEGUNDA LLAMADA ---
Mostrando la imagen: foto_vacaciones_HD.jpg
--- TERCERA LLAMADA (OTRA IMAGEN) ---
Proxy: Creando la instancia de RealImage ahora.
144
Cargando la imagen: documento_escaneado.png
Mostrando la imagen: documento_escaneado.png
Este resultado demuestra que la costosa operación loadFromDisk solo se ejecuta
cuando se invoca display() por primera vez en cada proxy.
5. Ventajas y Desventajas del Patrón
Ventajas
Control sobre el Objeto Real: El proxy permite gestionar el ciclo de vida y el
acceso al objeto real sin que el cliente lo sepa.
Funcionalidad Adicional: Se pueden añadir responsabilidades (como
caching, logging, seguridad) sin modificar el código del RealSubject,
respetando el Principio de Abierto/Cerrado.
Mejora del Rendimiento: El Proxy Virtual puede mejorar significativamente el
rendimiento de la aplicación al retrasar la creación de objetos costosos.
Transparencia de Red: El Proxy Remoto oculta la complejidad de la
comunicación en red, haciendo que el desarrollo de sistemas distribuidos sea
más sencillo.
Seguridad: El Proxy de Protección proporciona un mecanismo robusto para
implementar la lógica de autorización.
Desventajas
Aumento de la Complejidad: El patrón introduce una capa adicional de
indirección y nuevas clases en el sistema, lo que puede aumentar la
complejidad general del código.
Latencia de Respuesta: Las operaciones realizadas por el proxy
(comprobaciones de seguridad, comunicación de red) pueden introducir una
latencia adicional en la respuesta al cliente.
Posible "Desincronización": En implementaciones complejas, especialmente
con proxies de caché, existe el riesgo de que el estado del proxy se
desincronice con el del objeto real, lo que puede llevar a datos obsoletos.
145
Parte 4: Patrones de Comportamiento (Aprox. 50 páginas)
Capítulo 13: Análisis del Patrón de Diseño Observer
1. Propósito Fundamental: La Dependencia Uno a Muchos
El patrón de diseño Observer (Observador), también conocido como Publish-
Subscribe, es un patrón de comportamiento fundamental cuyo propósito es definir
una dependencia de uno a muchos entre objetos, de modo que cuando un objeto
(el "sujeto") cambia de estado, todos sus objetos dependientes (los
"observadores") son notificados y actualizados automáticamente. Este
mecanismo establece una forma de comunicación en la que los objetos no necesitan
conocerse explícitamente, promoviendo un bajo acoplamiento y una alta flexibilidad en
el sistema.
El problema que el patrón Observer resuelve es común en sistemas donde el estado
de un objeto es de interés para otros. Sin este patrón, el objeto cuyo estado cambia (el
sujeto) tendría que mantener referencias directas a todos los objetos que necesitan ser
informados, y llamar a sus métodos de actualización uno por uno. Esto crearía un
acoplamiento fuerte: el sujeto estaría íntimamente ligado a las clases concretas de sus
dependientes. Cualquier cambio, como añadir un nuevo tipo de dependiente,
requeriría modificar el código del sujeto, violando el Principio de Abierto/Cerrado.
El patrón Observer invierte esta lógica. En lugar de que el sujeto "empuje" activamente
las actualizaciones a dependientes específicos, establece un mecanismo de
suscripción. Los objetos interesados (observadores) se registran en el objeto que
desean monitorear (el sujeto). A partir de ese momento, cuando el sujeto experimenta
un cambio de estado relevante, simplemente notifica a todos los observadores de su
lista, sin necesidad de saber quiénes son o qué hacen. Lo único que el sujeto sabe es
que cada observador en su lista implementa una interfaz común (la interfaz Observer)
que tiene un método de actualización, como update().
Esta dinámica de publicación y suscripción es la piedra angular del diseño de sistemas
reactivos y controlados por eventos, desde las interfaces gráficas de usuario (donde
un clic de botón notifica a varios oyentes) hasta los sistemas de notificaciones en
tiempo real, como el mercado de valores o las subastas en línea.
2. Estructura y Participantes
La estructura del patrón Observer se define por la colaboración de cuatro participantes
clave, que forman dos jerarquías distintas: una para los sujetos y otra para los
observadores.
Subject (Sujeto): Es una interfaz o clase abstracta que define la interfaz para
gestionar a los observadores. Debe proporcionar métodos para que los
observadores puedan suscribirse (attach o register) y darse de baja (detach o
unregister). También declara un método para notificar a los observadores
(notify).
Observer (Observador): Es una interfaz o clase abstracta que define la
interfaz de actualización para los objetos que deben ser notificados de los
cambios en un sujeto. Típicamente, contiene un único método, como update().
ConcreteSubject (Sujeto Concreto): Implementa la interfaz Subject.
Almacena el estado que es de interés para los observadores y mantiene una
146
lista de sus observadores registrados. Cuando su estado cambia, invoca el
método notify() para informar a todos los observadores de su lista.
ConcreteObserver (Observador Concreto): Implementa la interfaz Observer.
Mantiene una referencia a un objeto ConcreteSubject para poder acceder a su
estado. Cuando su método update() es invocado por el sujeto, el observador
puede "jalar" (pull) el nuevo estado del sujeto y reaccionar en consecuencia.
3. Ejemplo Práctico: Sistema de Subastas en Tiempo Real
Para ilustrar el patrón, implementaremos el ejemplo de un sistema de subastas. En
este sistema, múltiples postores (ConcreteObserver) necesitan ser notificados
inmediatamente cada vez que se realiza una nueva oferta en un artículo de la subasta
(ConcreteSubject).
Paso 1: Definir las interfaces Subject y Observer
Primero, creamos las interfaces que definirán el contrato para nuestros sujetos y
observadores.
// Observer: Define la interfaz para los objetos que serán notificados.
public interface Bidder { // En este contexto, un postor es un observador.
void update(AuctionItem item, double newBid);
}
// Subject: Define la interfaz para el objeto que será observado.
public interface Auction { // La subasta es el sujeto.
void registerBidder(Bidder bidder);
void unregisterBidder(Bidder bidder);
void notifyBidders();
}
Paso 2: Crear el Sujeto Concreto (ConcreteSubject)
La clase AuctionItem representa el artículo que se está subastando. Almacena el
precio actual y la lista de postores registrados.
import java.util.ArrayList;
import java.util.List;
// ConcreteSubject: El artículo de la subasta.
public class AuctionItem implements Auction {
private String name;
private double currentBid;
147
private Bidder highestBidder;
private List<Bidder> bidders = new ArrayList<>();
public AuctionItem(String name, double startingPrice) {
this.name = name;
this.currentBid = startingPrice;
}
public String getName() {
return name;
}
public double getCurrentBid() {
return currentBid;
}
// Método que cambia el estado del sujeto
public void placeBid(Bidder bidder, double amount) {
if (amount > this.currentBid) {
System.out.println("\nNueva oferta de " + amount + " por " + bidder + " en '" +
this.name + "'");
this.currentBid = amount;
this.highestBidder = bidder;
// Notificar a todos los observadores del cambio de estado.
notifyBidders();
} else {
System.out.println("\n" + bidder + ", la oferta de " + amount + " es demasiado
baja.");
}
}
@Override
public void registerBidder(Bidder bidder) {
148
bidders.add(bidder);
System.out.println(bidder + " se ha unido a la subasta de '" + this.name + "'");
}
@Override
public void unregisterBidder(Bidder bidder) {
bidders.remove(bidder);
}
@Override
public void notifyBidders() {
System.out.println("Notificando a todos los postores sobre la nueva oferta
máxima...");
for (Bidder bidder : bidders) {
bidder.update(this, this.currentBid);
}
}
}
Paso 3: Crear el Observador Concreto (ConcreteObserver)
La clase AuctionBidder representa a un postor. Implementa la interfaz Bidder y
reacciona cuando es notificado de una nueva oferta.
// ConcreteObserver: Un postor que observa un artículo.
public class AuctionBidder implements Bidder {
private String name;
public AuctionBidder(String name) {
this.name = name;
}
@Override
public void update(AuctionItem item, double newBid) {
149
System.out.println("Hola " + this.name + "! La oferta actual por '" + item.getName()
+ "' es ahora: " + newBid);
// Aquí, el postor podría decidir si quiere realizar una contraoferta.
}
@Override
public String toString() {
return name;
}
}
Paso 4: El Cliente utiliza el sistema
El código cliente configura la subasta, registra a los postores y simula el proceso de
ofertas.
public class AuctionClient {
public static void main(String[] args) {
// 1. Crear el sujeto (el artículo de la subasta)
AuctionItem laptop = new AuctionItem("Laptop Gamer Pro", 800.00);
// 2. Crear los observadores (los postores)
Bidder bidder1 = new AuctionBidder("Carlos");
Bidder bidder2 = new AuctionBidder("Ana");
Bidder bidder3 = new AuctionBidder("Maria");
// 3. Registrar a los observadores en el sujeto
laptop.registerBidder(bidder1);
laptop.registerBidder(bidder2);
laptop.registerBidder(bidder3);
System.out.println("------------------------------------------");
// 4. Simular cambios de estado (nuevas ofertas)
laptop.placeBid(bidder1, 850.00);
150
laptop.placeBid(bidder2, 850.00); // Oferta demasiado baja
laptop.placeBid(bidder3, 900.00);
}
}
Salida del Programa:
Carlos se ha unido a la subasta de 'Laptop Gamer Pro'
Ana se ha unido a la subasta de 'Laptop Gamer Pro'
Maria se ha unido a la subasta de 'Laptop Gamer Pro'
------------------------------------------
Nueva oferta de 850.0 por Carlos en 'Laptop Gamer Pro'
Notificando a todos los postores sobre la nueva oferta máxima...
Hola Carlos! La oferta actual por 'Laptop Gamer Pro' es ahora: 850.0
Hola Ana! La oferta actual por 'Laptop Gamer Pro' es ahora: 850.0
Hola Maria! La oferta actual por 'Laptop Gamer Pro' es ahora: 850.0
Ana, la oferta de 850.0 es demasiado baja.
Nueva oferta de 900.0 por Maria en 'Laptop Gamer Pro'
Notificando a todos los postores sobre la nueva oferta máxima...
Hola Carlos! La oferta actual por 'Laptop Gamer Pro' es ahora: 900.0
Hola Ana! La oferta actual por 'Laptop Gamer Pro' es ahora: 900.0
Hola Maria! La oferta actual por 'Laptop Gamer Pro' es ahora: 900.0
Como muestra la salida, cada vez que se realiza una oferta válida, todos los postores
registrados son notificados del nuevo precio, demostrando el mecanismo de
comunicación uno a muchos en acción.
4. Variaciones y Modelos de Comunicación: Push vs. Pull
El patrón Observer admite dos modelos principales para la comunicación de datos
entre el sujeto y los observadores: el modelo Push y el modelo Pull.
Modelo Push (Empujar): El sujeto envía a los observadores toda la
información detallada sobre el cambio como parte de la notificación. El método
151
update tendría múltiples parámetros, por ejemplo, update(newState, oldState,
timestamp).
o Ventaja: Es simple para los observadores, ya que reciben todos los
datos que podrían necesitar sin tener que pedirlos.
o Desventaja: Puede ser ineficiente si los observadores solo necesitan
una pequeña parte de la información. El sujeto puede enviar datos
innecesarios, y está fuertemente acoplado a la cantidad de datos que
los observadores podrían querer.
Modelo Pull (Jalar): El sujeto envía una notificación mínima (a menudo, solo
una referencia a sí mismo), y es responsabilidad de cada observador "jalar" o
solicitar los datos que necesita del sujeto. Nuestro ejemplo de subasta utiliza
una versión híbrida, pero se inclina hacia el modelo Pull, ya que el observador
recibe una referencia al AuctionItem y puede consultar cualquier dato que
necesite (getName(), getCurrentBid(), etc.).
o Ventaja: Es más flexible y eficiente. Cada observador toma solo lo que
necesita, y el sujeto no necesita conocer los requisitos de datos de sus
observadores.
o Desventaja: Puede ser menos eficiente si la mayoría de los
observadores siempre necesitan los mismos datos, lo que resulta en
múltiples llamadas de "pull" al sujeto después de cada notificación.
La elección entre el modelo Push y Pull depende de los requisitos específicos de la
aplicación y del equilibrio deseado entre la simplicidad para el observador y la
flexibilidad y eficiencia del sistema en general.
5. Ventajas y Desventajas del Patrón
Ventajas
Bajo Acoplamiento: La ventaja más significativa es que promueve un bajo
acoplamiento entre el sujeto y los observadores. El sujeto no necesita conocer
la clase concreta de sus observadores, solo que implementan la interfaz
Observer.
Soporte para Comunicación de Difusión (Broadcast): El patrón permite
notificar a un número arbitrario de objetos interesados con una sola acción.
Relaciones Dinámicas: Los observadores pueden suscribirse y darse de baja
en tiempo de ejecución, lo que permite modificar las relaciones de dependencia
de forma dinámica.
Cumplimiento del Principio de Abierto/Cerrado: Se pueden introducir
nuevos tipos de observadores sin necesidad de modificar el código del sujeto.
Desventajas
Actualizaciones Inesperadas: Dado que los observadores están
desacoplados, no conocen la existencia de los demás. Una simple modificación
en el sujeto puede desencadenar una cascada de actualizaciones complejas y,
a veces, inesperadas en los observadores.
152
Riesgo de Fugas de Memoria (Lapsed Listener Problem): Si un observador
se registra en un sujeto pero no se da de baja explícitamente cuando ya no es
necesario, el sujeto mantendrá una referencia a él. Esto puede impedir que el
observador sea recolectado por el recolector de basura, causando una fuga de
memoria. Es crucial gestionar cuidadosamente el ciclo de vida de las
suscripciones.
Orden de Notificación no Garantizado: El patrón, en su forma básica, no
garantiza el orden en que los observadores serán notificados. Si el orden es
importante, se necesita una lógica adicional en el sujeto.
6. Conclusión
El patrón Observer es una piedra angular del diseño de software reactivo y basado en
eventos. Proporciona una solución elegante y eficaz para sincronizar el estado entre
objetos de una manera desacoplada y flexible. Al separar las preocupaciones del
"productor" de cambios (el sujeto) de las de los "consumidores" de esos cambios (los
observadores), el patrón fomenta la creación de sistemas modulares, mantenibles y
extensibles. A pesar de sus posibles inconvenientes, como la gestión del ciclo de vida
de los observadores y la posibilidad de actualizaciones en cascada, su capacidad para
construir sistemas dinámicos y reactivos lo convierte en una herramienta indispensable
en el arsenal de cualquier arquitecto o desarrollador de software.
153
Capítulo 14: Análisis del Patrón de
Diseño Strategy
1. Propósito Fundamental: Algoritmos como Componentes Intercambiables
El patrón de diseño Strategy (Estrategia) es un patrón de comportamiento que permite
definir una familia de algoritmos, encapsular cada uno de ellos en una clase
separada y hacerlos intercambiables. La esencia de este patrón es permitir que el
algoritmo utilizado por un objeto (el "contexto") pueda variar independientemente de
los clientes que lo utilizan. En lugar de implementar un comportamiento directamente
dentro de la clase que lo necesita, dicho comportamiento se extrae a un conjunto de
clases separadas, cada una representando una "estrategia" diferente.
El problema que el patrón Strategy resuelve es común en el desarrollo de software:
una clase necesita realizar una tarea de diferentes maneras, y la elección de la
manera específica puede depender de la configuración, del estado del sistema o de la
entrada del usuario. Un enfoque ingenuo para manejar esto sería utilizar una serie de
sentencias condicionales (if-else o switch) dentro de un método de la clase principal.
Sin embargo, este enfoque tiene serias desventajas:
Violación del Principio de Responsabilidad Única (SRP): La clase principal
se vuelve responsable tanto de su lógica principal como de la implementación
de múltiples algoritmos, lo que la hace compleja y difícil de mantener.
Violación del Principio de Abierto/Cerrado (OCP): Cada vez que se necesita
añadir una nueva variante del algoritmo, es necesario modificar la clase
principal, lo que aumenta el riesgo de introducir errores en el código existente.
Complejidad y Baja Legibilidad: A medida que el número de variantes crece,
el bloque condicional se vuelve masivo y difícil de leer y razonar.
El patrón Strategy resuelve estos problemas de una manera elegante. En lugar de que
el objeto "contexto" implemente el comportamiento directamente, delega esa
responsabilidad a un objeto "estrategia" que se le proporciona. El contexto mantiene
una referencia a una estrategia y se comunica con ella a través de una interfaz común.
Esto permite que la estrategia concreta sea reemplazada en tiempo de ejecución,
alterando dinámicamente el comportamiento del contexto sin cambiar su código.
2. Estructura y Participantes
La estructura del patrón Strategy se basa en la delegación y la composición,
involucrando tres participantes clave:
Strategy (Estrategia): Es una interfaz o clase abstracta que declara una
operación común para todos los algoritmos soportados. El Context utiliza esta
interfaz para invocar el algoritmo definido por una ConcreteStrategy.
ConcreteStrategy (Estrategia Concreta): Son las clases que implementan un
algoritmo específico, siguiendo la interfaz Strategy. Habrá una clase de
estrategia concreta para cada variante del algoritmo.
Context (Contexto): Es la clase cuyo comportamiento necesita variar.
Mantiene una referencia a un objeto Strategy. El Context no implementa el
154
algoritmo directamente, sino que delega esa tarea a su objeto Strategy. El
Context puede proporcionar un método para que el cliente pueda cambiar o
establecer la estrategia en tiempo de ejecución.
El flujo de trabajo es el siguiente: El Context se configura con una instancia de una
ConcreteStrategy. Cuando se requiere ejecutar el algoritmo, el Context invoca el
método definido en la interfaz Strategy en su objeto de estrategia actual. Dado que
todas las estrategias concretas implementan la misma interfaz, el Context no necesita
conocer los detalles de la estrategia que está utilizando, lo que lo desacopla de la
implementación del algoritmo.
3. Ejemplo Práctico: Estrategias de Cálculo de Envío en Comercio Electrónico
Un caso de uso clásico y muy ilustrativo para el patrón Strategy es un sistema de
comercio electrónico que necesita calcular los costos de envío. Los costos pueden
variar según diferentes factores, lo que da lugar a múltiples algoritmos de cálculo.
Escenario: Un carrito de compras necesita calcular el costo de envío de un pedido.
Las reglas de cálculo pueden ser:
1. Tarifa Fija: Un costo constante para todos los envíos.
2. Por Peso: El costo se calcula en función del peso total de los artículos.
3. Por Distancia: El costo depende de la distancia al destino del envío.
Paso 1: Definir la interfaz Strategy
Primero, definimos la interfaz común para todas nuestras estrategias de cálculo de
envío.
// Representa un pedido con datos relevantes para el cálculo.
class Order {
private double totalWeight;
private String destination;
public Order(double totalWeight, String destination) {
this.totalWeight = totalWeight;
this.destination = destination;
}
public double getTotalWeight() { return totalWeight; }
public String getDestination() { return destination; }
}
// Strategy: La interfaz para nuestros algoritmos de cálculo.
155
public interface ShippingStrategy {
double calculate(Order order);
}
Paso 2: Implementar las Estrategias Concretas (ConcreteStrategy)
A continuación, creamos una clase para cada algoritmo de cálculo, implementando la
interfaz ShippingStrategy.
// ConcreteStrategy 1: Tarifa Fija
public class FlatRateShipping implements ShippingStrategy {
private double rate;
public FlatRateShipping(double rate) {
this.rate = rate;
}
@Override
public double calculate(Order order) {
System.out.println("Calculando con tarifa fija.");
return rate;
}
}
// ConcreteStrategy 2: Por Peso
public class WeightBasedShipping implements ShippingStrategy {
private double ratePerKg;
public WeightBasedShipping(double ratePerKg) {
this.ratePerKg = ratePerKg;
}
@Override
public double calculate(Order order) {
System.out.println("Calculando por peso.");
156
return order.getTotalWeight() * ratePerKg;
}
}
// ConcreteStrategy 3: Por Distancia (simulado)
public class DistanceBasedShipping implements ShippingStrategy {
@Override
public double calculate(Order order) {
System.out.println("Calculando por distancia.");
// Lógica compleja para calcular la distancia y el costo...
// Aquí lo simulamos con un valor fijo para el ejemplo.
if (order.getDestination().equalsIgnoreCase("Local")) {
return 5.0;
}
return 20.0;
}
}
Paso 3: Crear la clase Context
La clase ShoppingCart actuará como el Context. Contendrá una referencia a una
ShippingStrategy y delegará el cálculo del costo a ella.
// Context: El carrito de compras que utiliza una estrategia de envío.
public class ShoppingCart {
private Order order;
private ShippingStrategy shippingStrategy;
public ShoppingCart(Order order) {
this.order = order;
}
// Permite al cliente establecer la estrategia en tiempo de ejecución.
public void setShippingStrategy(ShippingStrategy shippingStrategy) {
this.shippingStrategy = shippingStrategy;
157
}
// Delega el cálculo del costo a la estrategia actual.
public double calculateShippingCost() {
if (shippingStrategy == null) {
throw new IllegalStateException("La estrategia de envío no ha sido
establecida.");
}
return shippingStrategy.calculate(this.order);
}
}
Paso 4: El Código Cliente
El cliente crea un pedido, lo asocia a un carrito de compras y luego puede establecer y
cambiar la estrategia de cálculo de envío dinámicamente.
public class ECommerceClient {
public static void main(String[] args) {
// Crear un pedido
Order myOrder = new Order(15.5, "Internacional"); // 15.5 kg
// Crear el contexto (carrito de compras)
ShoppingCart cart = new ShoppingCart(myOrder);
// --- Escenario 1: Usar tarifa fija ---
cart.setShippingStrategy(new FlatRateShipping(10.0));
double cost1 = cart.calculateShippingCost();
System.out.println("Costo de envío (Tarifa Fija): $" + cost1); // $10.0
System.out.println("------------------------------------");
// --- Escenario 2: Cambiar a cálculo por peso ---
cart.setShippingStrategy(new WeightBasedShipping(2.5)); // $2.5 por kg
double cost2 = cart.calculateShippingCost();
158
System.out.println("Costo de envío (Por Peso): $" + cost2); // 15.5 * 2.5 = $38.75
System.out.println("------------------------------------");
// --- Escenario 3: Cambiar a cálculo por distancia ---
cart.setShippingStrategy(new DistanceBasedShipping());
double cost3 = cart.calculateShippingCost();
System.out.println("Costo de envío (Por Distancia): $" + cost3); // $20.0
}
}
Salida del Programa:
Calculando con tarifa fija.
Costo de envío (Tarifa Fija): $10.0
------------------------------------
Calculando por peso.
Costo de envío (Por Peso): $38.75
------------------------------------
Calculando por distancia.
Costo de envío (Por Distancia): $20.0
Como se puede ver, el ShoppingCart no cambia, pero su comportamiento para
calcular el costo de envío se modifica dinámicamente simplemente cambiando el
objeto de estrategia que utiliza.
4. Ventajas y Desventajas del Patrón Strategy
Ventajas
Flexibilidad y Desacoplamiento: El patrón desacopla la lógica del algoritmo
de la clase que lo utiliza (Context). Esto permite añadir, modificar o reemplazar
algoritmos sin afectar al Context.
Cumplimiento del Principio de Abierto/Cerrado: Es fácil introducir nuevas
estrategias sin modificar el código del Context o de las estrategias existentes.
El sistema está abierto a la extensión (nuevas estrategias) pero cerrado a la
modificación.
Elimina Sentencias Condicionales: Reemplaza una lógica condicional
compleja (if-else, switch) por una estructura polimórfica más limpia y
mantenible.
159
Elección de Algoritmos en Tiempo de Ejecución: Permite que un cliente o el
propio Context seleccionen o cambien la estrategia que se utilizará en tiempo
de ejecución.
Desventajas
Aumento del Número de Objetos: El patrón puede llevar a una proliferación
de clases y objetos en el sistema, ya que cada variante de un algoritmo
requiere su propia clase de estrategia.
Conocimiento del Cliente: En algunas implementaciones, el cliente debe
conocer las diferentes estrategias y entender cuál es la más apropiada para
cada situación para poder configurar el Context correctamente.
Comunicación entre Estrategia y Contexto: Si una estrategia necesita datos
del Context para funcionar, el Context debe pasárselos a través de la interfaz
Strategy. Esto puede complicar la interfaz si las diferentes estrategias tienen
requisitos de datos muy distintos.
5. Comparación con otros Patrones
Es útil comparar el patrón Strategy con otros patrones de comportamiento para
entender mejor su lugar en el diseño de software.
Strategy vs. State: Ambos patrones son estructuralmente muy similares
(delegan el comportamiento a un objeto separado). La diferencia clave está en
la intención. En Strategy, las diferentes estrategias representan diferentes
formas de hacer la misma cosa, y el cliente generalmente elige la estrategia.
En State, los diferentes estados representan diferentes comportamientos del
objeto en función de su estado interno, y el cambio de estado suele ser
gestionado por el propio Context o por los objetos de estado.
Strategy vs. Template Method: Ambos patrones tratan con algoritmos.
Template Method se basa en la herencia. Define el esqueleto de un algoritmo
en una superclase y permite que las subclases redefinan ciertos pasos, pero no
la estructura general del algoritmo. Strategy, en cambio, se basa en la
composición. Permite cambiar el algoritmo completo en tiempo de ejecución.
Se puede pensar en Template Method como una forma de personalizar partes
de un algoritmo, mientras que Strategy permite reemplazarlo por completo.
6. Conclusión
El patrón Strategy es una herramienta poderosa y fundamental en el diseño de
software orientado a objetos. Proporciona una solución limpia y flexible para gestionar
algoritmos que varían, promoviendo el bajo acoplamiento y la alta cohesión. Al
encapsular cada algoritmo en su propia clase, el patrón no solo simplifica el código del
objeto que utiliza estos algoritmos, sino que también crea un sistema más robusto,
mantenible y extensible. Aunque puede aumentar el número de clases en una
aplicación, los beneficios en términos de flexibilidad y cumplimiento de los principios
de diseño SOLID suelen superar con creces este inconveniente, convirtiéndolo en una
opción preferente sobre las complejas estructuras condicionales.
160
Capítulo 15: Análisis del Patrón de
Diseño Template Method
1. Propósito Fundamental: Definiendo el Esqueleto de un Algoritmo
El patrón de diseño Template Method (Método Plantilla) es un patrón de
comportamiento que se basa en la herencia para definir el esqueleto de un
algoritmo en una operación, difiriendo algunos de sus pasos a las subclases.
Este patrón permite que las subclases puedan redefinir ciertos pasos de un algoritmo
sin cambiar la estructura general del mismo.
La intención principal del Template Method es establecer una plantilla para un
algoritmo y permitir que las partes variables de ese algoritmo sean implementadas por
clases hijas. La clase base (abstracta) define la secuencia de los pasos y la estructura
fija, mientras que las subclases proporcionan los detalles concretos para los pasos que
pueden variar.
Este patrón encarna un principio de diseño fundamental conocido como "El Principio
de Hollywood": "No nos llames, nosotros te llamaremos". En este contexto, la clase
base (el "framework") llama a los métodos de las subclases (el código del "usuario") en
los momentos apropiados, pero no al revés. La superclase tiene el control del flujo del
algoritmo, y las subclases simplemente "completan los espacios en blanco"
proporcionando las implementaciones para los pasos diferidos.
El problema que resuelve es la duplicación de código en situaciones donde múltiples
clases implementan algoritmos que son muy similares pero no idénticos. En lugar de
que cada clase re-implemente toda la estructura del algoritmo, el Template Method
permite extraer la estructura común a una clase base, dejando solo las variaciones
para las subclases.
2. Estructura y Participantes
La estructura del patrón Template Method es sencilla y se basa en la herencia.
Involucra principalmente dos tipos de participantes:
AbstractClass (Clase Abstracta):
o Define el templateMethod(), que es el método que contiene el
esqueleto del algoritmo. Este método es típicamente declarado como
final para evitar que las subclases lo sobrescriban y alteren la secuencia
de pasos.
o El templateMethod() invoca una serie de métodos abstractos (o
concretos) que representan los pasos del algoritmo.
o Declara las operaciones primitivas (primitiveOperation()) como métodos
abstractos, que deben ser implementados por las subclases.
o Puede contener "hooks" (ganchos), que son métodos con una
implementación por defecto (a menudo vacía) que las subclases
pueden sobrescribir opcionalmente para proporcionar personalización
adicional.
ConcreteClass (Clase Concreta):
161
o Hereda de la AbstractClass.
o Implementa los métodos abstractos (las operaciones primitivas) que son
necesarios para completar el algoritmo.
o Puede sobrescribir los "hooks" para insertar comportamiento adicional
en puntos específicos del algoritmo.
El flujo es el siguiente: un cliente invoca el templateMethod() en una instancia de una
ConcreteClass. El control se transfiere a la AbstractClass, que ejecuta el algoritmo
paso a paso. Cuando la ejecución llega a un paso que corresponde a una operación
primitiva, la AbstractClass invoca el método correspondiente, que, gracias al
polimorfismo, será la implementación proporcionada por la ConcreteClass.
3. Ejemplo Práctico: Framework para Procesamiento de Datos
Un ejemplo excelente para ilustrar el patrón es un framework simple para procesar
datos. El framework define los pasos generales del procesamiento (leer datos,
procesarlos, escribir datos), pero deja que las implementaciones concretas decidan de
dónde leer los datos (por ejemplo, de un archivo CSV o JSON) y dónde escribirlos.
Paso 1: Crear la Clase Abstracta (AbstractClass)
La clase DataProcessor definirá la plantilla del algoritmo de procesamiento.
// AbstractClass: Define el esqueleto del algoritmo.
public abstract class DataProcessor {
// El template method, declarado final para que las subclases no puedan alterarlo.
public final void process() {
readData();
processData();
// Hook opcional
if (isLoggingEnabled()) {
logSummary();
}
writeData();
}
// Pasos abstractos que deben ser implementados por las subclases.
protected abstract void readData();
protected abstract void writeData();
// Un paso con una implementación concreta y común a todas las subclases.
162
protected void processData() {
System.out.println("Procesando los datos de forma genérica...");
// Lógica de procesamiento común...
}
// Un "hook". Las subclases pueden sobrescribirlo para añadir comportamiento.
// Por defecto, no hace nada (o está deshabilitado).
protected boolean isLoggingEnabled() {
return false;
}
private void logSummary() {
System.out.println("Resumen del procesamiento guardado en log.");
}
}
Análisis: El método process() es la plantilla. Define una secuencia inmutable: leer,
procesar, y luego escribir. También incluye un "hook" (isLoggingEnabled) que permite
a las subclases activar opcionalmente un paso de logging.
Paso 2: Crear las Clases Concretas (ConcreteClass)
Ahora, creamos subclases para manejar diferentes formatos de datos.
// ConcreteClass 1: Procesa datos desde un archivo CSV.
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Leyendo datos desde un archivo CSV.");
}
@Override
protected void writeData() {
System.out.println("Escribiendo datos a un archivo CSV.");
}
163
// Esta subclase decide activar el hook de logging.
@Override
protected boolean isLoggingEnabled() {
return true;
}
}
// ConcreteClass 2: Procesa datos desde un archivo JSON.
public class JsonDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Leyendo datos desde un archivo JSON.");
}
@Override
protected void writeData() {
System.out.println("Escribiendo datos a un archivo JSON.");
}
// Esta subclase utiliza la implementación por defecto del hook (deshabilitado).
}
Análisis: Cada clase concreta proporciona su propia implementación para leer y
escribir datos, personalizando el algoritmo sin alterar su estructura. CsvDataProcessor
además hace uso del hook para añadir un paso extra.
Paso 3: El Código Cliente
El cliente utiliza las clases concretas, pero invoca el mismo método plantilla.
public class FrameworkClient {
public static void main(String[] args) {
System.out.println("--- Procesando un archivo CSV ---");
DataProcessor csvProcessor = new CsvDataProcessor();
164
csvProcessor.process();
System.out.println("\n--- Procesando un archivo JSON ---");
DataProcessor jsonProcessor = new JsonDataProcessor();
jsonProcessor.process();
}
}
Salida del Programa:
--- Procesando un archivo CSV ---
Leyendo datos desde un archivo CSV.
Procesando los datos de forma genérica...
Resumen del procesamiento guardado en log.
Escribiendo datos a un archivo CSV.
--- Procesando un archivo JSON ---
Leyendo datos desde un archivo JSON.
Procesando los datos de forma genérica...
Escribiendo datos a un archivo JSON.
La salida muestra claramente cómo se sigue el mismo esqueleto de algoritmo en
ambos casos, pero los pasos concretos de lectura y escritura son diferentes, y el paso
de logging solo se ejecuta para el procesador CSV.
4. Ventajas y Desventajas del Patrón Template Method
Ventajas
Reutilización de Código: Evita la duplicación de código al centralizar la
estructura del algoritmo en una única clase base.
Control del Framework: Permite a los diseñadores de frameworks controlar
los puntos de extensión. La superclase tiene el control total sobre el algoritmo y
solo permite que las subclases modifiquen partes específicas.
Cumplimiento del Principio de Abierto/Cerrado: Se pueden introducir
nuevas variaciones del algoritmo creando nuevas subclases sin modificar el
código de la clase abstracta, que permanece "cerrada" a la modificación.
Desventajas
165
Rigidez de la Estructura: La estructura del algoritmo está fijada en la
superclase. Si se necesita una variación significativa en el esqueleto del
algoritmo, este patrón puede ser demasiado restrictivo.
Limitaciones de la Herencia: El patrón se basa en la herencia, lo que significa
que una subclase no puede heredar de otra clase si ya está extendiendo la
AbstractClass.
Complejidad en la Jerarquía: Con muchas implementaciones, la jerarquía de
clases puede volverse grande y difícil de manejar.
5. Comparación con el Patrón Strategy
El patrón Template Method a menudo se confunde con el patrón Strategy porque
ambos abordan la variación en los algoritmos. Sin embargo, lo hacen de maneras
fundamentalmente diferentes.
Criterio Template Method Patrón Strategy
Herencia. Las subclases Composición. El contexto contiene
sobrescriben partes de un una referencia a un objeto de
Mecanismo
algoritmo definido en la estrategia y delega la ejecución del
superclase. algoritmo a él.
Permite cambiar partes de un Permite cambiar el algoritmo
Granularidad
algoritmo. completo.
Estático. La variación se elige
en tiempo de compilación al Dinámico. La estrategia puede ser
Flexibilidad
seleccionar qué subclase cambiada en tiempo de ejecución.
instanciar.
Definir un esqueleto fijo para un
Definir una familia de algoritmos
algoritmo y dejar que las
Intención intercambiables y dejar que el cliente
subclases completen los
elija cuál usar.
detalles.
En resumen, se puede decir que Template Method se ocupa de cómo se hace algo,
definiendo la estructura pero permitiendo la personalización de los pasos. Strategy, en
cambio, se ocupa de qué se hace, permitiendo que se reemplace todo el enfoque
algorítmico.
6. Conclusión
El patrón Template Method es una herramienta elegante y simple para la reutilización
de código y la definición de frameworks. Al establecer un esqueleto de algoritmo en
una clase base y diferir la implementación de pasos específicos a las subclases,
promueve una estructura clara y reduce la duplicación. Aunque su dependencia de la
herencia lo hace más rígido que alternativas basadas en composición como el patrón
Strategy, es una solución excelente y directa cuando la estructura general de un
algoritmo es constante, pero sus detalles internos necesitan variar. Comprender
cuándo usar Template Method versus Strategy es una marca de madurez en el diseño
de software orientado a objetos.
166
Capítulo 16: Análisis del Patrón de Diseño Command
1. Propósito Fundamental: Encapsular una Solicitud como un Objeto
El patrón de diseño Command (Comando) es un patrón de comportamiento que
transforma una solicitud o una acción en un objeto independiente. Su propósito
fundamental es encapsular toda la información necesaria para realizar una acción
o desencadenar un evento en un objeto autocontenido. Este objeto, el "comando",
desacopla el objeto que invoca la operación (Invoker) del objeto que sabe cómo
realizarla (Receiver).
La analogía más clara es la de un restaurante. Un cliente (Client) le da una orden a un
camarero (Invoker). La orden en sí, escrita en una nota, es el objeto Command. El
camarero no necesita saber cómo preparar la comida; su única responsabilidad es
tomar la orden y ponerla en una cola de pedidos. El cocinero (Receiver) es quien
finalmente toma la nota de la cola, la lee y ejecuta la acción (preparar el plato).
Este desacoplamiento es la piedra angular del patrón y habilita una serie de
capacidades avanzadas:
Parametrizar Clientes con Solicitudes: El Invoker (el camarero) puede ser
configurado con diferentes objetos Command (diferentes órdenes). Por
ejemplo, un botón en una interfaz gráfica (Invoker) puede ejecutar diferentes
acciones (Commands) dependiendo del contexto.
Encolar o Registrar Solicitudes: Las órdenes (Commands) pueden ser
almacenadas en una cola para su ejecución posterior, como en el ejemplo del
restaurante. También pueden ser registradas en un log, lo que es útil para la
recuperación de fallos o para auditorías.
Soportar Operaciones que se Pueden Deshacer: Dado que la acción está
encapsulada en un objeto, este objeto puede también contener la información
necesaria para revertirla. Esto convierte al patrón Command en la solución
canónica para implementar funcionalidades de "Deshacer" (Undo) y "Rehacer"
(Redo).
2. Estructura y Participantes
El patrón Command se define por la interacción de cuatro roles principales, que
trabajan en conjunto para lograr el desacoplamiento:
Command (Comando): Es una interfaz que declara un método para ejecutar
una operación, comúnmente llamado execute(). Para soportar la funcionalidad
de deshacer, esta interfaz a menudo incluye también un método undo().
ConcreteCommand (Comando Concreto): Implementa la interfaz Command.
Un comando concreto vincula un objeto Receiver con una acción específica.
Almacena la referencia al Receiver y los parámetros necesarios para la acción.
Cuando se invoca execute(), el comando concreto llama al método apropiado
en su Receiver.
Receiver (Receptor): Es el objeto que posee el conocimiento sobre cómo
realizar la operación real. Contiene la lógica de negocio. Cualquier clase puede
actuar como un Receiver. En la analogía, es el cocinero.
167
Invoker (Invocador): Es el objeto que solicita la ejecución del comando.
Mantiene una referencia a un objeto Command pero no conoce nada sobre la
implementación concreta del comando ni sobre el Receiver. Su única
responsabilidad es invocar el método execute() en el momento apropiado.
Ejemplos de Invoker son botones de menú, barras de herramientas o gestores
de historial.
Client (Cliente): Es el responsable de crear una instancia de
ConcreteCommand y asociarla con su Receiver. El cliente luego pasa el objeto
de comando al Invoker.
3. Ejemplo Práctico: Implementación de "Deshacer" y "Rehacer" en un Editor de
Texto
El caso de uso más ilustrativo del patrón Command es la implementación de un
historial de operaciones que se pueden deshacer y rehacer en una aplicación como un
editor de texto.
Escenario: Se creará un editor de texto simple que permite insertar texto y
deshacer/rehacer esas inserciones.
Paso 1: Definir la interfaz Command
Esta interfaz define el contrato para todos los comandos, incluyendo los métodos para
ejecutar y deshacer la acción.
public interface Command {
void execute();
void undo();
}
Paso 2: Crear la clase Receiver
La clase TextEditor actúa como el receptor. Contiene la lógica real para manipular el
texto.
// Receiver: El objeto que sabe cómo realizar el trabajo.
public class TextEditor {
private StringBuilder text;
public TextEditor() {
this.text = new StringBuilder();
}
public void insertText(int position, String textToInsert) {
text.insert(position, textToInsert);
168
System.out.println("Estado actual: \"" + text + "\"");
}
public void deleteText(int position, int length) {
text.delete(position, position + length);
System.out.println("Estado actual: \"" + text + "\"");
}
public String getText() {
return text.toString();
}
}
Paso 3: Crear un ConcreteCommand
Se crea un comando concreto para la acción de inserción. Es crucial que este
comando almacene el estado necesario para poder deshacer la operación.
// ConcreteCommand: Encapsula una acción en el Receiver.
public class InsertTextCommand implements Command {
private TextEditor editor;
private String textToInsert;
private int position;
public InsertTextCommand(TextEditor editor, int position, String textToInsert) {
this.editor = editor;
this.position = position;
this.textToInsert = textToInsert;
}
@Override
public void execute() {
System.out.println("Ejecutando: Insertar '" + textToInsert + "'");
editor.insertText(position, textToInsert);
}
169
@Override
public void undo() {
System.out.println("Deshaciendo: Insertar '" + textToInsert + "'");
editor.deleteText(position, textToInsert.length());
}
}
Paso 4: Crear el Invoker con historial de Deshacer/Rehacer
El EditorApp actuará como Invoker. Gestionará dos pilas: una para el historial de undo
y otra para el de redo.
import java.util.Stack;
// Invoker: Mantiene los comandos y solicita su ejecución.
public class EditorApp {
private TextEditor editor;
private Stack<Command> undoHistory = new Stack<>();
private Stack<Command> redoHistory = new Stack<>();
public EditorApp() {
this.editor = new TextEditor();
}
public void executeCommand(Command command) {
command.execute();
undoHistory.push(command);
// Al ejecutar un nuevo comando, el historial de rehacer se limpia.
redoHistory.clear();
}
public void undo() {
if (!undoHistory.isEmpty()) {
Command command = undoHistory.pop();
170
command.undo();
redoHistory.push(command);
} else {
System.out.println("Nada que deshacer.");
}
}
public void redo() {
if (!redoHistory.isEmpty()) {
Command command = redoHistory.pop();
command.execute();
undoHistory.push(command);
} else {
System.out.println("Nada que rehacer.");
}
}
public String getCurrentText() {
return editor.getText();
}
}
Paso 5: El código del Client
El cliente configura el sistema y simula la interacción del usuario.
public class Client {
public static void main(String[] args) {
EditorApp app = new EditorApp();
// El usuario escribe "Hola"
app.executeCommand(new InsertTextCommand(app.editor, 0, "Hola"));
// El usuario añade " Mundo"
171
app.executeCommand(new InsertTextCommand(app.editor, 4, " Mundo"));
// El usuario añade "!"
app.executeCommand(new InsertTextCommand(app.editor, 10, "!"));
// Deshacer la última acción (quitar "!")
app.undo();
// Deshacer la acción anterior (quitar " Mundo")
app.undo();
// Rehacer la última acción deshecha (añadir " Mundo")
app.redo();
// Rehacer de nuevo (añadir "!")
app.redo();
// Deshacer todo
app.undo();
app.undo();
app.undo();
// Intentar deshacer cuando no hay nada en el historial
app.undo();
}
}
Análisis del Ejemplo: Este código demuestra cómo el patrón Command desacopla la
aplicación principal (EditorApp) de las acciones concretas (InsertTextCommand) y de
la lógica de edición (TextEditor). El Invoker puede gestionar un historial complejo de
operaciones sin conocer los detalles de ninguna de ellas. Si se quisiera añadir una
acción de "Borrar", solo sería necesario crear una nueva clase DeleteTextCommand
sin modificar el EditorApp ni el TextEditor.
4. Ventajas y Desventajas del Patrón Command
172
Ventajas
Desacoplamiento: Separa el objeto que invoca una operación del que sabe
cómo llevarla a cabo. Esta es la ventaja principal.
Extensibilidad (Principio de Abierto/Cerrado): Es muy fácil añadir nuevos
comandos al sistema creando nuevas clases que implementen la interfaz
Command, sin necesidad de modificar el código existente.
Composición de Comandos: Se pueden ensamblar secuencias de comandos
en un único "macro-comando" que los ejecuta en orden.
Soporte para Operaciones Asíncronas y en Cola: Los objetos de comando
pueden ser fácilmente almacenados y pasados entre hilos para su ejecución en
segundo plano o en un momento posterior.
Implementación de Deshacer/Rehacer: Como se ha demostrado, el patrón
proporciona una estructura natural y robusta para implementar estas
funcionalidades.
Desventajas
Aumento de la Complejidad del Código: El patrón puede introducir una gran
cantidad de clases pequeñas y similares, especialmente si hay muchas
acciones diferentes en el sistema. Esto puede hacer que el código base sea
más difícil de entender al principio.
Lógica de Comando Potencialmente Compleja: Si un comando necesita
gestionar mucho estado para su operación de undo, la clase de comando
puede volverse pesada y compleja.
Puede Haber Sobrecarga: Para aplicaciones muy simples con pocas
acciones, el uso del patrón Command puede ser una sobreingeniería.
5. Conclusión
El patrón Command es una herramienta de diseño de comportamiento increíblemente
poderosa y versátil. Su capacidad para encapsular acciones como objetos abre la
puerta a arquitecturas flexibles, extensibles y robustas. Aunque puede introducir una
capa de complejidad al aumentar el número de clases, los beneficios que ofrece,
especialmente en aplicaciones que requieren colas de tareas, ejecución asíncrona o
una funcionalidad de deshacer/rehacer, suelen superar con creces este coste. Es el
pilar fundamental para el diseño de sistemas interactivos modernos, desde editores de
texto y software de diseño gráfico hasta flujos de trabajo empresariales complejos.
Comprender el patrón Command es esencial para cualquier desarrollador que aspire a
construir aplicaciones interactivas y resilientes.
173
Capítulo 17: Análisis del Patrón de Diseño State
1. Propósito Fundamental: Alterar el Comportamiento a través del Estado Interno
El patrón de diseño State (Estado) es un patrón de comportamiento que permite a un
objeto alterar su comportamiento cuando su estado interno cambia. La
consecuencia de aplicar este patrón es que el objeto parecerá cambiar de clase. En
esencia, el patrón State extrae la lógica de comportamiento que depende del estado y
la encapsula en clases distintas, una para cada estado posible. El objeto principal,
conocido como el "Contexto", mantiene una referencia a uno de estos objetos de
estado y delega toda la ejecución de comportamiento dependiente del estado a él.
El problema que el patrón State resuelve de manera elegante es la gestión de objetos
cuyo comportamiento varía significativamente en función de su estado. Un enfoque
ingenuo para manejar esta situación sería utilizar grandes bloques de sentencias
condicionales (if/else o switch) dentro de los métodos del objeto principal. Por ejemplo,
un método manejarSolicitud() podría tener una estructura como:
public void manejarSolicitud() {
if (estado == "ESTADO_A") {
// Comportamiento para el estado A
} else if (estado == "ESTADO_B") {
// Comportamiento para el estado B
} else if (estado == "ESTADO_C") {
// Comportamiento para el estado C
}
}
Este enfoque tiene graves inconvenientes. Primero, viola el Principio de
Responsabilidad Única (SRP), ya que la clase principal se vuelve responsable de
implementar la lógica para todos los estados posibles. Segundo, viola el Principio de
Abierto/Cerrado (OCP), porque añadir un nuevo estado requiere modificar el código
existente en múltiples lugares, lo que aumenta la probabilidad de introducir errores. A
medida que el número de estados y comportamientos crece, este código se vuelve
masivo, difícil de leer y extremadamente frágil.
El patrón State soluciona esto aplicando un principio clave: en lugar de que el objeto
principal implemente todo el comportamiento, delega esa responsabilidad a una
jerarquía de clases de estado. Cada clase de estado representa un estado particular y
contiene la implementación del comportamiento correspondiente a ese estado. El
objeto principal (el Contexto) simplemente cambia su objeto de estado actual para
alterar su comportamiento.
2. Estructura y Participantes
La estructura del patrón State se basa en la composición y la delegación, y se define
por tres participantes clave:
174
Context (Contexto): Es la clase principal cuyo comportamiento depende de su
estado. Mantiene una instancia de una subclase de State que representa el
estado actual del objeto. El Context delega las solicitudes dependientes del
estado a este objeto de estado. También proporciona un método para que los
objetos de estado puedan cambiar el estado actual del Context.
State (Estado): Es una interfaz o clase abstracta que define una interfaz
común para todas las clases que representan los diferentes estados. Esta
interfaz declara los métodos de comportamiento que el Context delegará.
ConcreteState (Estado Concreto): Son las clases que implementan la interfaz
State. Cada clase ConcreteState implementa el comportamiento asociado con
un estado particular del Context. Una responsabilidad clave de un
ConcreteState es a menudo determinar y ejecutar la transición al siguiente
estado.
El flujo de trabajo es el siguiente: el Client interactúa con el Context. Cuando el Client
invoca un método en el Context, este delega la llamada al método correspondiente en
su objeto de estado actual. El objeto de estado ejecuta la lógica y, si es necesario,
cambia el estado del Context a un nuevo ConcreteState.
3. Ejemplo Práctico: El Ciclo de Vida de un Documento
Un excelente caso de uso para el patrón State es modelar el comportamiento de un
objeto Document a medida que avanza por un flujo de trabajo, por ejemplo, de
"Borrador" a "En Revisión" y finalmente a "Publicado". El comportamiento del
documento (por ejemplo, si puede ser editado o publicado) depende directamente de
su estado actual.
Paso 1: Definir la interfaz State
Esta interfaz definirá las acciones que pueden realizarse sobre un documento.
// State: La interfaz que encapsula el comportamiento asociado a un estado.
public interface DocumentState {
void edit(Document document, String content);
void review(Document document);
void publish(Document document);
}
Paso 2: Crear las clases ConcreteState
Cada clase representará un estado del documento e implementará el comportamiento
correspondiente.
// ConcreteState 1: Estado de Borrador
public class DraftState implements DocumentState {
@Override
public void edit(Document document, String content) {
175
document.setContent(content);
System.out.println("Documento en modo borrador. Contenido actualizado.");
}
@Override
public void review(Document document) {
System.out.println("Enviando documento a revisión.");
document.changeState(new InReviewState());
}
@Override
public void publish(Document document) {
System.out.println("Error: Un documento en borrador no puede ser publicado
directamente.");
}
}
// ConcreteState 2: Estado En Revisión
public class InReviewState implements DocumentState {
@Override
public void edit(Document document, String content) {
System.out.println("Error: No se puede editar un documento mientras está en
revisión.");
}
@Override
public void review(Document document) {
System.out.println("El documento ya está en revisión.");
}
@Override
public void publish(Document document) {
System.out.println("Publicando el documento.");
176
document.changeState(new PublishedState());
}
}
// ConcreteState 3: Estado Publicado
public class PublishedState implements DocumentState {
@Override
public void edit(Document document, String content) {
System.out.println("Error: No se puede editar un documento publicado.");
}
@Override
public void review(Document document) {
System.out.println("Error: Un documento publicado no puede ser enviado a
revisión.");
}
@Override
public void publish(Document document) {
System.out.println("El documento ya ha sido publicado.");
}
}
Paso 3: Crear la clase Context
La clase Document es el contexto. Mantiene el estado actual y delega las acciones a
él.
// Context: El objeto cuyo comportamiento cambia con el estado.
public class Document {
private DocumentState state;
private String content = "";
public Document() {
// El estado inicial es Borrador.
177
this.state = new DraftState();
System.out.println("Nuevo documento creado en estado de Borrador.");
}
// El contexto permite a los objetos de estado cambiar su estado.
public void changeState(DocumentState newState) {
this.state = newState;
}
public void setContent(String content) {
this.content = content;
}
// El contexto delega el comportamiento a su objeto de estado actual.
public void edit(String newContent) {
state.edit(this, newContent);
}
public void review() {
state.review(this);
}
public void publish() {
state.publish(this);
}
}
Paso 4: El Código Cliente
El cliente interactúa con el objeto Document sin conocer las clases de estado
concretas.
public class Client {
public static void main(String[] args) {
Document document = new Document();
178
// El usuario edita el borrador
document.edit("Este es mi primer borrador.");
// Intenta publicar directamente (debería fallar)
document.publish();
// Envía a revisión
document.review();
// Intenta editar mientras está en revisión (debería fallar)
document.edit("Un pequeño cambio.");
// Publica el documento
document.publish();
// Intenta editar después de publicar (debería fallar)
document.edit("Otro cambio.");
}
}
Salida del Programa:
Nuevo documento creado en estado de Borrador.
Documento en modo borrador. Contenido actualizado.
Error: Un documento en borrador no puede ser publicado directamente.
Enviando documento a revisión.
Error: No se puede editar un documento mientras está en revisión.
Publicando el documento.
Error: No se puede editar un documento publicado.
Este ejemplo demuestra cómo la lógica condicional se ha reemplazado por una
estructura de objetos limpia. Cada estado se encarga de su propio comportamiento y
179
de las transiciones válidas, lo que hace que el sistema sea mucho más fácil de
entender y extender.
4. Ventajas y Desventajas del Patrón State
Ventajas
Localiza el Comportamiento Específico del Estado: Agrupa el código
relacionado con un estado particular en una sola clase, lo que mejora la
cohesión.
Elimina Grandes Bloques Condicionales: Hace que el código del Context
sea más limpio y fácil de mantener al eliminar las sentencias if/else o switch.
Cumplimiento del Principio de Abierto/Cerrado: Es fácil introducir nuevos
estados sin cambiar las clases de estado existentes o el Context, simplemente
creando nuevas subclases de State.
Simplifica las Transiciones de Estado: Las transiciones entre estados se
vuelven explícitas. Un objeto de estado puede ser responsable de la transición
al siguiente, encapsulando las reglas del flujo de trabajo.
Desventajas
Aumento del Número de Clases: El patrón puede llevar a una proliferación de
clases si el objeto tiene muchos estados, lo que puede aumentar la
complejidad general del diseño.
Duplicación de Código: Si varios estados comparten un comportamiento
similar para ciertas acciones, puede haber duplicación de código entre las
clases de estado. Esto puede mitigarse utilizando una clase base abstracta
para los estados que implemente el comportamiento común.
5. Comparación: State vs. Strategy
El patrón State es estructuralmente muy similar al patrón Strategy, ya que ambos
delegan el comportamiento a objetos separados. Sin embargo, su intención es
diferente:
Intención: El patrón Strategy se centra en encapsular algoritmos
intercambiables para una tarea específica. El cliente generalmente es
consciente de las diferentes estrategias y elige una para configurar el contexto.
Las estrategias no suelen conocerse entre sí y no gestionan cambios entre
ellas.
Intención: El patrón State se centra en representar los diferentes estados de
un objeto. El cambio entre estados está predefinido por el flujo de trabajo, y
son los propios objetos de estado (o el contexto) quienes gestionan las
transiciones. El cliente no elige el estado; interactúa con el contexto y este
cambia de estado internamente.
En resumen, Strategy se ocupa de cómo un objeto hace algo, mientras que State
se ocupa de lo que es un objeto en un momento dado.
6. Conclusión
180
El patrón State es una solución poderosa y elegante para gestionar objetos cuyo
comportamiento cambia en función de su estado. Al transformar la lógica condicional
en una estructura de objetos cohesiva, el patrón promueve un diseño limpio, flexible y
mantenible que se alinea con los principios fundamentales del diseño orientado a
objetos. Es una herramienta esencial para modelar máquinas de estado y flujos de
trabajo complejos, permitiendo que los sistemas evolucionen de manera robusta y
predecible. Aunque puede aumentar el número de clases, los beneficios en términos
de claridad, cohesión y extensibilidad suelen superar con creces este coste en
aplicaciones con una lógica de estado no trivial.
181
Parte 5: Patrones de Arquitectura
Capítulo 18: Model-View-Controller (MVC)
1. Conceptos Fundamentales: La Separación de
Responsabilidades
El patrón Model-View-Controller (MVC) no es solo un patrón de diseño, sino
un paradigma arquitectónico fundamental en la ingeniería de software,
especialmente en el desarrollo de aplicaciones web y de escritorio. Su
propósito principal es organizar el código de una aplicación dividiéndolo en tres
componentes interconectados, cada uno con una responsabilidad clara y
distinta. Esta separación de intereses es la piedra angular del patrón, ya que
permite un desarrollo más modular, facilita el mantenimiento y mejora la
escalabilidad del sistema.1
El MVC fue concebido por Trygve Reenskaug en la década de 1970 y desde
entonces se ha convertido en la base de innumerables frameworks de
desarrollo. Los tres componentes que lo definen son:
4
1.1. Modelo (Model)
El Modelo es el cerebro de la aplicación. Su responsabilidad exclusiva es
gestionar los datos y la lógica de negocio. Esto incluye:
6
Representación de los datos: Contiene la estructura de datos de la
aplicación (por ejemplo, clases que representan usuarios, productos,
pedidos). En muchas aplicaciones web, el modelo se corresponde
directamente con las tablas de una base de datos. 5
Lógica de negocio: Implementa las reglas y operaciones que se aplican a
los datos (validaciones, cálculos, procesos).
Persistencia: Se comunica con la capa de persistencia (como una base
de datos) para recuperar y almacenar datos.
Un principio clave es que el Modelo es completamente independiente de la
interfaz de usuario. No sabe cómo se mostrarán los datos; su única función es
gestionar el estado y el comportamiento de los datos de la aplicación. 1
1.2. Vista (View)
182
La Vista es la cara de la aplicación; es todo lo que el usuario ve e con lo que
interactúa. Su única responsabilidad es la presentación de los datos que
recibe del modelo. 5
Renderización de la UI: Genera la interfaz de usuario, que en
aplicaciones web suele ser HTML, CSS y JavaScript.
Visualización de datos: Muestra los datos proporcionados por el modelo
en un formato específico (tablas, gráficos, formularios, etc.).
Captura de la interacción del usuario: Recibe las acciones del usuario
(clics, envíos de formularios) y las delega al Controlador para su
procesamiento.
La Vista debe ser "tonta" en el sentido de que no contiene lógica de negocio.
Su trabajo es simplemente mostrar lo que se le dice y comunicar las acciones
del usuario al Controlador. Pueden existir múltiples vistas para un mismo
8
modelo, cada una presentando los datos de una manera diferente. 6
1.3. Controlador (Controller)
El Controlador actúa como el intermediario o el director de orquesta entre el
Modelo y la Vista. Es el componente que recibe y procesa la entrada del
5
usuario.
Manejo de solicitudes: Recibe las acciones del usuario desde la Vista
(por ejemplo, una solicitud HTTP en una aplicación web).
Orquestación: Interpreta la entrada del usuario y decide qué hacer. Invoca
los métodos apropiados en el Modelo para actualizar su estado.
Selección de la Vista: Una vez que el Modelo ha sido actualizado, el
Controlador selecciona la Vista adecuada para mostrar el resultado al
usuario y le pasa los datos necesarios del Modelo.
El Controlador es el pegamento que une el Modelo y la Vista, asegurando que
ambos permanezcan desacoplados y puedan evolucionar de forma
independiente. 1
2. Flujo de la Solicitud y Responsabilidades de Cada
Componente
Comprender el flujo de una solicitud en una aplicación MVC es clave para
entender cómo estos tres componentes colaboran para responder a una
interacción del usuario. Aunque existen variaciones, el flujo típico en una
aplicación web es el siguiente :1
1. El Usuario Inicia la Solicitud: El ciclo comienza cuando el usuario
interactúa con la Vista (por ejemplo, haciendo clic en un enlace o enviando
un formulario). Esta acción genera una solicitud HTTP dirigida a una URL
específica.
2. El Controlador Recibe la Solicitud: Un mecanismo de enrutamiento
(común en los frameworks MVC modernos) examina la URL y dirige la
183
solicitud al Controlador apropiado y a uno de sus métodos de acción
(action method). El Controlador se convierte en el punto de entrada para
10
manejar la solicitud.
3. El Controlador Interactúa con el Modelo: El Controlador procesa la
entrada del usuario (por ejemplo, los datos de un formulario). Basándose
en esta entrada, invoca los métodos correspondientes en el Modelo para
realizar la lógica de negocio. Esto podría implicar consultar una base de
datos para obtener datos, actualizar un registro existente o realizar un
cálculo complejo. 5
4. El Modelo Actualiza su Estado: El Modelo realiza las operaciones
solicitadas, modificando su estado interno si es necesario. Por ejemplo, si
la solicitud era para crear un nuevo usuario, el Modelo crearía el objeto de
usuario y lo guardaría en la base de datos. Luego, devuelve los datos
actualizados o el resultado de la operación al Controlador.
5
5. El Controlador Selecciona la Vista: Una vez que el Controlador recibe la
respuesta del Modelo, selecciona la Vista apropiada que se debe
renderizar. A continuación, empaqueta los datos necesarios del Modelo (a
menudo en un objeto de modelo de vista) y se los pasa a la Vista
seleccionada.1
6. La Vista Renderiza la Respuesta: La Vista recibe los datos del modelo
del Controlador. Su única tarea es renderizar estos datos en un formato de
presentación (generalmente HTML). No realiza ninguna lógica de negocio;
simplemente muestra la información que se le ha dado.
7. La Respuesta se Envía al Usuario: La Vista renderizada (la página HTML
final) se devuelve al navegador del usuario, completando así el ciclo de la
solicitud.
Este flujo asegura una clara separación de responsabilidades: la Vista se
ocupa de la presentación, el Modelo de los datos y la lógica, y el Controlador
de la orquestación del flujo. Este desacoplamiento es lo que hace que las
8
aplicaciones MVC sean robustas, fáciles de probar (cada componente puede
probarse de forma aislada) y de mantener. 1
3. Ejemplo: Implementación de una Aplicación Web Simple
con Spring MVC
Spring MVC es un framework de Java que implementa el patrón Modelo-Vista-
Controlador para construir aplicaciones web. A continuación, se muestra un
ejemplo simple de "Hola Mundo" que utiliza Spring Boot con Thymeleaf como
motor de plantillas para la vista.
3.1. Configuración del Proyecto (Maven)
Para un proyecto Spring Boot, el pom.xml necesitaría las siguientes
dependencias clave para habilitar Spring Web MVC y Thymeleaf:
184
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
Spring Boot autoconfigura la mayor parte de lo necesario. Solo necesitamos
especificar en application.properties dónde encontrar nuestras plantillas. 14
Properties
# src/main/resources/application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
3.2. El Modelo (Model)
Para este ejemplo simple, no necesitamos una clase de modelo compleja. El
modelo será un simple String que pasaremos desde el controlador a la vista.
En aplicaciones más complejas, aquí tendríamos una clase POJO (Plain Old
Java Object), como User o Product. 14
3.3. El Controlador (Controller)
El controlador maneja las solicitudes entrantes. Creamos una clase
GreetingController que responderá a las solicitudes en la ruta /greeting.
185
Java
// src/main/java/com/example/mvc/GreetingController.java
package com.example.mvc;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class GreetingController {
// Mapea las solicitudes GET a /greeting
@GetMapping("/greeting")
public String greeting(@RequestParam(name="name", required=false,
defaultValue="Mundo") String name, Model model) {
// 1. Procesa la solicitud (recibe el parámetro 'name')
// 2. Prepara los datos para la vista (el Modelo)
// Añade un atributo llamado "message" al modelo.
model.addAttribute("message", "Hola, " + name + "!");
// 3. Devuelve el nombre lógico de la vista
// Spring buscará un archivo llamado "welcome.html" en la carpeta de plantillas.
return "welcome";
}
}
Análisis del Controlador:
@Controller: Marca esta clase como un controlador de Spring MVC. 17
@GetMapping("/greeting"): Mapea las solicitudes HTTP GET para la URL
/greeting a este método. 19
Model model: Spring inyecta un objeto Model en el método. Este objeto se
utiliza para pasar datos a la vista. 21
model.addAttribute("message",...): Añade un atributo al modelo. La vista
podrá acceder a este dato utilizando la clave "message". 21
return "welcome";: Devuelve el nombre lógico de la vista. Spring, a través
de su ViewResolver, buscará una plantilla llamada welcome.html. 23
3.4. La Vista (View)
La vista es una plantilla HTML que utiliza Thymeleaf para mostrar
dinámicamente los datos del modelo.
186
HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Bienvenida</title>
</head>
<body>
<h1 th:text="${message}">Mensaje por defecto</h1>
</body>
</html>
Análisis de la Vista:
th:text="${message}": Esta es una expresión de Thymeleaf. Le indica al
motor de plantillas que reemplace el texto dentro de la etiqueta <h1> con el
valor del atributo del modelo llamado message. 25
3.5. La Aplicación Principal (Punto de Entrada)
Finalmente, la clase principal de Spring Boot que inicia la aplicación.
Java
// src/main/java/com/example/mvc/MvcApplication.java
package com.example.mvc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MvcApplication {
public static void main(String args){
SpringApplication.run(MvcApplication.class, args);
}
}
Al ejecutar esta aplicación y navegar a
http://localhost:8080/greeting?name=Ana, el GreetingController manejará la
solicitud, añadirá el mensaje "Hola, Ana!" al modelo y devolverá la vista
welcome.html, que mostrará el mensaje en el navegador. Este ejemplo, aunque
simple, demuestra perfectamente la separación de responsabilidades que
define al patrón MVC. 24
187
Obras citadas
1. MVC Design Pattern - GeeksforGeeks, fecha de acceso: junio 29, 2025,
https://www.geeksforgeeks.org/system-design/mvc-design-pattern/
2. MVC: Model, View, Controller - Codecademy, fecha de acceso: junio 29,
2025, https://www.codecademy.com/article/mvc
3. Understanding the MVC Pattern in Software Design - Oshyn, fecha de
acceso: junio 29, 2025, https://www.oshyn.com/blog/mvc-pattern-
software-design
4. Understanding the Model-View-Controller (MVC) Pattern | by Richard
Nwonah | Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@nwonahr/understanding-the-model-view-controller-
mvc-pattern-97c6e057d96a
5. MVC Framework Introduction - GeeksforGeeks, fecha de acceso: junio
29, 2025, https://www.geeksforgeeks.org/software-engineering/mvc-
framework-introduction/
6. Model–view–controller - Wikipedia, fecha de acceso: junio 29, 2025,
https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controll
er
7. 6.1. Model-View-Controller (MVC) - Medium, fecha de acceso: junio 29,
2025, https://medium.com/@maheshmaddi92/6-1-model-view-controller-
mvc-52e7112d5fae
8. MVC Architecture: Simplifying Web Application Development - Ramotion,
fecha de acceso: junio 29, 2025, https://www.ramotion.com/blog/mvc-
architecture-in-web-application/
9. What is Model-View and Control? - Visual Paradigm, fecha de acceso:
junio 29, 2025, https://www.visual-paradigm.com/guide/uml-unified-
modeling-language/what-is-model-view-control-mvc/
10. EP04: How Requests Flow Through an MVC Application: A
Comprehensive Guide - Medium, fecha de acceso: junio 29, 2025,
https://medium.com/@sohailnazar4/ep04-how-requests-flow-through-an-
mvc-application-a-comprehensive-guide-6ee607c8ab18
11. Asp.net MVC Request Life Cycle, fecha de acceso: junio 29, 2025,
https://www.scholarhat.com/tutorial/mvc/aspnet-mvc-request-life-cycle
12. How Java Spring MVC Works: Spring MVC Request Flow Explained -
TutorialsPedia, fecha de acceso: junio 29, 2025,
https://tutorialspedia.com/how-java-spring-mvc-works-spring-mvc-request-
flow-explained-step-by-step/
13. Understanding the Model-View-Controller (MVC) Pattern: A Guide for
Software Developers, fecha de acceso: junio 29, 2025,
https://medium.com/@nikitinsn6/understanding-the-model-view-controller-
mvc-pattern-a-guide-for-software-developers-3530604d9c8d
14. Integrating Spring MVC with Thymeleaf for Server-Side Rendering -
GeeksforGeeks, fecha de acceso: junio 29, 2025,
https://www.geeksforgeeks.org/advance-java/spring-mvc-integrate-with-
thymeleaf-for-server-side-rendering/
15. Spring Boot Hello World Example - Thymeleaf - Mkyong.com, fecha de
acceso: junio 29, 2025, https://mkyong.com/spring-boot/spring-boot-hello-
world-example-thymeleaf/
188
16. Spring Boot - Thymeleaf with Example - GeeksforGeeks, fecha de
acceso: junio 29, 2025, https://www.geeksforgeeks.org/java/spring-boot-
thymeleaf-with-example/
17. Spring - MVC Framework - GeeksforGeeks, fecha de acceso: junio 29,
2025, https://www.geeksforgeeks.org/spring-mvc-framework/
18. Spring @RequestMapping Annotation with Example - GeeksforGeeks,
fecha de acceso: junio 29, 2025, https://www.geeksforgeeks.org/spring-
requestmapping-annotation-with-example/
19. Spring MVC @RequestMapping Annotation Example - centron GmbH,
fecha de acceso: junio 29, 2025, https://www.centron.de/en/tutorial/spring-
mvc-requestmapping-annotation-example/
20. Mapping Requests :: Spring Framework, fecha de acceso: junio 29, 2025,
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-
controller/ann-requestmapping.html
21. Model, ModelMap, ModelAndView in Spring MVC - GeeksforGeeks, fecha
de acceso: junio 29, 2025, https://www.geeksforgeeks.org/model-
modelmap-modelandview-in-spring-mvc/
22. Model, ModelMap, and ModelAndView in Spring MVC | Baeldung, fecha
de acceso: junio 29, 2025, https://www.baeldung.com/spring-mvc-model-
model-map-model-view
23. Spring MVC Hello World Example - Tutorialspoint, fecha de acceso: junio
29, 2025,
https://www.tutorialspoint.com/spring/spring_mvc_hello_world_example.ht
m
24. Spring MVC hello world tutorial - w3schools.blog, fecha de acceso: junio
29, 2025, https://www.w3schools.blog/hello-world-spring-mvc
25. Spring MVC and Thymeleaf: how to access data from templates, fecha de
acceso: junio 29, 2025,
https://www.thymeleaf.org/doc/articles/springmvcaccessdata.html
26. Spring MVC Tutorial | Baeldung, fecha de acceso: junio 29, 2025,
https://www.baeldung.com/spring-mvc-tutorial
27. Simplest Spring MVC Framework Tutorial - Hello World Example with UI
(JSP) Page, fecha de acceso: junio 29, 2025,
https://crunchify.com/simplest-spring-mvc-hello-world-example-tutorial-
spring-model-view-controller-tips/
28. Simple Spring Hello World program | TheServerSide, fecha de acceso:
junio 29, 2025, https://www.theserverside.com/video/Simple-Spring-Hello-
World-program
29. Spring MVC Hello World Example - Tutorialspoint, fecha de acceso: junio
29, 2025,
https://www.tutorialspoint.com/springmvc/springmvc_hello_world_example
.htm
30. Spring - Hello World Example - GeeksforGeeks, fecha de acceso: junio
29, 2025, https://www.geeksforgeeks.org/advance-java/spring-hello-world-
example/
189
Capítulo 19: Análisis de las
Evoluciones de MVC: Model-View-
Presenter (MVP) y Model-View-
ViewModel (MVVM)
1. La Evolución de MVC: De la Dependencia a la Abstracción
El patrón Model-View-Controller (MVC), aunque revolucionario y fundamental,
presenta en su implementación clásica una limitación que puede complicar las pruebas
y el mantenimiento, especialmente en el desarrollo de interfaces de usuario (UI) ricas y
complejas. Esta limitación radica en el acoplamiento directo que a menudo existe entre
la Vista (View) y el Modelo (Model).[1, 2, 3] En el patrón MVC clásico, la Vista puede
observar directamente al Modelo para actualizarse cuando los datos cambian, y el
Controlador actualiza el Modelo basándose en la entrada del usuario. Esto crea una
dependencia triangular donde la Vista conoce al Modelo, el Controlador conoce tanto a
la Vista como al Modelo, y en algunas variantes, la Vista conoce al Controlador.[3, 4]
Este acoplamiento hace que la Vista sea difícil de probar de forma aislada, ya que
depende de una instancia concreta del Modelo para funcionar.[1] Además, la lógica de
presentación (cómo formatear los datos para mostrarlos) a menudo termina residiendo
dentro de la propia clase de la Vista, mezclando el código de la UI (que puede ser
específico de una plataforma como Android, iOS o una web) con lógica que podría ser
reutilizable.
Para abordar estas deficiencias, surgieron dos patrones arquitectónicos principales
como evoluciones directas de MVC: Model-View-Presenter (MVP) y Model-View-
ViewModel (MVVM). Ambos patrones comparten un objetivo común: mejorar la
separación de responsabilidades y aumentar la testeabilidad de la aplicación,
especialmente desacoplando la Vista de la lógica de negocio y de
presentación.[2, 4, 5, 6]
2. Model-View-Presenter (MVP): El Presentador como Intermediario Activo
2.1. Conceptos Fundamentales y Flujo de Interacción
En el patrón MVP, el Controlador de MVC es reemplazado por un nuevo componente
llamado Presenter (Presentador). La diferencia fundamental es que el Presenter actúa
como el único intermediario entre el Modelo y la Vista. A diferencia de MVC, en MVP
la Vista y el Modelo están completamente desacoplados; no se conocen entre
sí.[2, 7, 8, 9]
Los componentes en MVP son:
190
Modelo (Model): Su responsabilidad es la misma que en MVC: gestionar los
datos y la lógica de negocio.[7]
Vista (View): Se convierte en un componente mucho más "pasivo". Su única
responsabilidad es mostrar datos y delegar todas las interacciones del usuario
al Presenter. Define una interfaz que el Presenter utiliza para comunicarse con
ella, sin que el Presenter conozca la implementación concreta de la Vista.[4, 8,
10]
Presentador (Presenter): Es el corazón del patrón. Contiene toda la lógica de
presentación y de la UI. Recibe los eventos de la Vista, interactúa con el
Modelo para obtener y actualizar datos, y luego invoca los métodos de la
interfaz de la Vista para que esta se actualice. El Presenter está en el medio,
orquestando todo.[7, 9, 11]
El flujo de una solicitud en MVP es el siguiente [7, 12]:
1. El usuario interactúa con la Vista.
2. La Vista delega la acción del usuario a un método en el Presenter. La Vista no
toma ninguna decisión lógica.
3. El Presenter recibe el evento, interactúa con el Modelo para obtener o
actualizar los datos necesarios.
4. El Modelo devuelve los datos al Presenter.
5. El Presenter formatea los datos para su visualización y luego invoca los
métodos de la interfaz de la Vista para que esta se actualice. Por ejemplo,
view.showUserName("John Doe") o view.showError("Contraseña incorrecta").
2.2. Diferencias Clave con MVC
La principal diferencia con MVC es la dirección de la comunicación y el rol de la
Vista [1, 2, 8, 13]:
Característica MVC (Clásico) MVP
Acoplamiento La Vista conoce y La Vista y el Modelo no se
Vista-Modelo observa directamente al conocen. Acoplamiento nulo.
Modelo. Acoplamiento
alto.
Rol de la Vista La Vista es activa; puede La Vista es pasiva; es una
tener lógica de interfaz controlada
presentación y se completamente por el Presenter.
actualiza a sí misma No tiene lógica.
observando el Modelo.
191
Punto de El Controlador es el El Presenter es el punto de
Entrada punto de entrada para la entrada, pero la interacción se
interacción del usuario. inicia en la Vista y se delega.
Testeabilidad La Vista es difícil de El Presenter es fácil de probar
probar de forma aislada unitariamente, ya que no
debido a su dependencia depende de frameworks de UI, y
del Modelo. la Vista puede ser simulada
(mocked) a través de su interfaz.
Alta testeabilidad.
Relación El Controlador puede Típicamente, hay una relación
interactuar con múltiples uno a uno entre una Vista y un
Vistas. Presenter.
3. Model-View-ViewModel (MVVM): La Magia del Data Binding
3.1. Conceptos Fundamentales y Flujo de Interacción
El patrón MVVM es una evolución adicional, popularizada por Microsoft en frameworks
como WPF y Xamarin, y adoptada masivamente en el desarrollo de aplicaciones
modernas (Android Jetpack, Angular, Vue.js, etc.). Introduce un nuevo componente, el
ViewModel, y se apoya fuertemente en un mecanismo llamado Data Binding (Enlace
de Datos).[2, 14, 15]
Los componentes en MVVM son:
Modelo (Model): Igual que en MVC y MVP, representa los datos y la lógica de
negocio.[14]
Vista (View): Al igual que en MVP, la Vista es responsable de la parte visual.
Sin embargo, en lugar de ser controlada activamente por otro componente, la
Vista se sincroniza automáticamente con el ViewModel a través del data
binding. Contiene muy poco o ningún "código subyacente" (code-behind).[4, 16]
ViewModel (Modelo de Vista): Es el intermediario. A diferencia del Presenter,
el ViewModel no tiene una referencia directa a la Vista. En su lugar, expone
datos y comandos (acciones) a los que la Vista puede "enlazar" (bind). El
ViewModel obtiene datos del Modelo y los transforma en un formato que es
fácil de consumir por la Vista (por ejemplo, convirtiendo un objeto Date en un
String formateado).[2, 14, 17]
El flujo en MVVM es más reactivo y menos imperativo [18]:
1. La Vista y el ViewModel están conectados a través del Data Binding. La Vista
se suscribe a los cambios en las propiedades del ViewModel.
192
2. El usuario interactúa con la Vista (por ejemplo, escribe en un campo de texto).
El data binding (bidireccional) actualiza automáticamente la propiedad
correspondiente en el ViewModel.
3. Si la interacción es una acción (por ejemplo, un clic de botón), la Vista invoca
un Comando expuesto por el ViewModel.
4. El ViewModel ejecuta la lógica de negocio, interactuando con el Modelo para
obtener o actualizar datos.
5. Cuando el Modelo devuelve los datos, el ViewModel actualiza sus propias
propiedades.
6. Gracias al data binding, cualquier cambio en las propiedades del ViewModel
se refleja automáticamente en la Vista, sin que el ViewModel tenga que
llamar a ningún método de la Vista.
3.2. Diferencias Clave con MVP y MVC
La diferencia más radical de MVVM es la eliminación de la manipulación manual de
la Vista [1, 2, 4, 19, 20]:
Característica MVP MVVM
Acoplamiento El Presenter tiene una El ViewModel no conoce la
Vista- referencia a la interfaz de Vista. La comunicación es a
Intermediario la Vista y la manipula través de data binding y
directamente comandos. Desacoplamiento
(view.setText(...)). máximo.
Mecanismo de Imperativo: El Presenter Declarativo/Reactivo: La Vista
Actualización "empuja" los datos a la se actualiza automáticamente
Vista. cuando los datos del
ViewModel cambian.
Lógica en la Prácticamente nula. Solo Cero lógica en el "code-
Vista delega eventos. behind". La lógica de la UI está
en el data binding (XML,
HTML).
Complejidad Conceptualemente más Requiere un framework con un
simple de entender. motor de data binding, lo que
puede tener una curva de
aprendizaje más alta.
193
4. Ejemplo Conceptual: Transformando MVC a MVP y MVVM
Imaginemos una aplicación MVC simple donde un usuario puede ver su nombre y
actualizarlo.
4.1. Escenario MVC
Modelo: class User { String name; }
Vista: Un archivo JSP o HTML. Muestra el nombre del usuario. Tiene un
formulario para cambiarlo. Podría acceder directamente a user.getName().
Controlador: UserController
o showUser(): Obtiene el User del Modelo, lo pasa a la Vista.
o updateUser(newName): Recibe la solicitud del formulario, llama a
user.setName(newName) en el Modelo y luego redirige o renderiza la Vista de
nuevo.
o Problema: La Vista está acoplada al objeto User. Para probar la Vista,
necesitas un User.
4.2. Transformación a MVP
Para transformar esto a MVP, introducimos una interfaz para la Vista y movemos la
lógica al Presenter.
Modelo: class User { String name; } (sin cambios)
Vista (Interfaz):
Java
interface UserView {
void setUserName(String name);
String getNewUserName();
void showUpdateSuccess();
}
Vista (Implementación): Una clase que implementa UserView. Los botones y
campos de texto ahora solo llaman a métodos del Presenter.
Java
public class UserActivity implements UserView {
private UserPresenter presenter;
// ...
public void onUpdateButtonClick() {
194
presenter.updateUserName();
}
}
Presentador:
Java
public class UserPresenter {
private UserView view;
private User model;
public UserPresenter(UserView view, User model) {
this.view = view;
this.model = model;
view.setUserName(model.getName()); // Configuración inicial
}
public void updateUserName() {
String newName = view.getNewUserName();
model.setName(newName);
view.setUserName(model.getName());
view.showUpdateSuccess();
}
}
Resultado de la Transformación:
La Vista (UserActivity) ya no conoce al Modelo (User).
La lógica de if/else, formateo, etc., ahora vive en el UserPresenter.
Se puede probar el UserPresenter por completo creando un "mock" (simulacro)
de UserView, sin necesidad de una UI real.
4.3. Transformación a MVVM
Para transformar a MVVM, eliminamos la referencia del Presenter a la Vista y nos
apoyamos en el data binding.
Modelo: class User { String name; } (sin cambios)
ViewModel:
Java
public class UserViewModel {
private User model;
// Estas propiedades serían "observables". Un framework se encargaría de
ello.
public String userName;
195
public String newUserNameInput;
public UserViewModel(User model) {
this.model = model;
this.userName = model.getName(); // Configuración inicial
}
// Este sería el "Comando" enlazado al botón de actualización.
public void updateUserNameCommand() {
model.setName(newUserNameInput);
// Simplemente actualizamos la propiedad observable.
this.userName = model.getName();
}
}
Vista (Conceptual - por ejemplo, en XML o HTML con un framework):
XML
<Label Text="{Binding userName}" />
<Entry Text="{Binding newUserNameInput, Mode=TwoWay}" />
<Button Command="{Binding updateUserNameCommand}" />
Resultado de la Transformación:
El ViewModel no tiene idea de qué es la Vista. No hay ninguna referencia
view.
Toda la lógica de actualización de la UI se maneja de forma declarativa a
través del data binding.
El ViewModel es aún más fácil de probar que el Presenter, ya que no tiene
dependencias de ninguna interfaz de Vista.
5. Conclusión: Eligiendo el Patrón Correcto
La elección entre MVC, MVP y MVVM depende en gran medida del framework, la
plataforma y la complejidad de la aplicación.
MVC sigue siendo un patrón muy válido, especialmente en el desarrollo web
del lado del servidor (como con Spring MVC o Ruby on Rails), donde el ciclo
solicitud-respuesta es claro y la "Vista" es a menudo un documento HTML
generado.
MVP es una excelente opción para aplicaciones donde se requiere un control
preciso y explícito sobre la Vista, o cuando no se dispone de un framework de
196
data binding robusto. Proporciona una gran mejora en la testeabilidad sobre
MVC.
MVVM es el estándar de facto en muchas plataformas de UI modernas
(Android, iOS con SwiftUI, frameworks de JavaScript como Vue/Angular/React)
porque aprovecha al máximo los mecanismos de data binding, reduciendo
drásticamente el código "boilerplate" para la manipulación de la UI y
permitiendo un desacoplamiento y una testeabilidad máximos.
En última instancia, MVP y MVVM no son reemplazos de MVC, sino
especializaciones que abordan sus limitaciones en el contexto del desarrollo de
interfaces de usuario ricas. Ambos patrones toman el principio de separación de
responsabilidades de MVC y lo llevan un paso más allá, creando arquitecturas más
limpias, más mantenibles y, sobre todo, mucho más fáciles de probar.
197
Capítulo 20: Arquitectura de
Microservicios
La arquitectura de microservicios es un enfoque para el desarrollo de software que
estructura una aplicación como una colección de servicios pequeños, autónomos y
débilmente acoplados. A diferencia de la arquitectura monolítica tradicional, donde
todos los componentes de la aplicación están interconectados en una única base de
código, los microservicios se construyen en torno a capacidades de negocio
específicas y se pueden desarrollar, desplegar y escalar de forma independiente.
1. Principios Fundamentales
La filosofía de los microservicios se sustenta en varios principios clave que guían su
diseño e implementación.
a. Descentralización
La descentralización es el principio más fundamental. En una arquitectura de
microservicios, no hay un punto central de control. Esto se manifiesta de varias
maneras:
Gobernanza Descentralizada: Cada equipo de servicio tiene la autonomía
para elegir la tecnología (lenguaje de programación, base de datos,
frameworks) que mejor se adapte a las necesidades de su servicio. Esto da
lugar a sistemas "políglotas".
Gestión de Datos Descentralizada: Cada microservicio es dueño de sus
propios datos y su propia base de datos. Se evita el uso de una única base de
datos centralizada compartida por todos los servicios, ya que esto crearía un
fuerte acoplamiento. La comunicación entre servicios para acceder a datos
ajenos se realiza a través de APIs bien definidas.
Lógica Descentralizada: La lógica de negocio se divide y se distribuye entre
los servicios individuales, cada uno enfocado en un dominio de negocio
específico (por ejemplo, gestión de usuarios, procesamiento de pagos).
b. Independencia
La independencia es el resultado directo de la descentralización. Cada microservicio
es una unidad autónoma que puede ser gestionada de forma independiente del resto
del sistema.
Despliegue Independiente: Se puede desplegar una nueva versión de un
servicio sin necesidad de volver a desplegar toda la aplicación. Esto permite
ciclos de lanzamiento mucho más rápidos y reduce el riesgo asociado a los
despliegues.
Escalabilidad Independiente: Si un servicio en particular experimenta una
alta carga (por ejemplo, el servicio de autenticación durante las horas pico),
solo ese servicio necesita ser escalado horizontalmente (añadiendo más
instancias), en lugar de tener que escalar toda la aplicación monolítica.
Aislamiento de Fallos: Si un servicio falla, y el sistema está bien diseñado
(utilizando patrones como el Circuit Breaker), el fallo no debería propagarse en
198
cascada y derribar toda la aplicación. Otros servicios pueden seguir
funcionando, aunque con una funcionalidad degradada.
c. Automatización
Debido a la complejidad inherente de gestionar un sistema distribuido compuesto por
docenas o cientos de servicios, la automatización no es una opción, sino una
necesidad absoluta.
Infraestructura como Código (IaC): El aprovisionamiento de servidores,
redes y almacenamiento se gestiona a través de scripts y herramientas
automatizadas, lo que garantiza la consistencia y la repetibilidad.
Integración Continua y Despliegue Continuo (CI/CD): Cada microservicio
tiene su propio pipeline de CI/CD automatizado. Cuando un desarrollador
confirma un cambio, se ejecutan automáticamente pruebas, se construye el
artefacto y se despliega en los diferentes entornos (desarrollo, pruebas,
producción) sin intervención manual. Esto es crucial para mantener la agilidad.
2. Ventajas y Desafíos
La adopción de microservicios ofrece beneficios significativos, pero también introduce
nuevos desafíos.
Ventajas
Escalabilidad Mejorada: Permite escalar los servicios de forma individual y
granular, optimizando el uso de recursos.
Agilidad y Velocidad de Desarrollo: Equipos más pequeños y autónomos
pueden desarrollar, probar y desplegar sus servicios de forma independiente y
más rápida.
Flexibilidad Tecnológica: Los equipos pueden adoptar nuevas tecnologías y
lenguajes para sus servicios sin afectar al resto del sistema.
Resiliencia y Aislamiento de Fallos: Un fallo en un servicio no tiene por qué
afectar a toda la aplicación.
Mantenibilidad: Las bases de código más pequeñas y enfocadas son más
fáciles de entender, mantener y modificar.
Desafíos
Complejidad Operacional: Gestionar, monitorizar y depurar un sistema
distribuido con muchos servicios es significativamente más complejo que un
monolito.
Complejidad de la Comunicación entre Servicios: La comunicación a través
de la red introduce latencia y posibles puntos de fallo. Se deben gestionar
problemas como la consistencia de datos entre servicios.
Descubrimiento de Servicios: En un entorno dinámico, los servicios
necesitan una forma de encontrarse unos a otros.
Pruebas Distribuidas: Realizar pruebas de integración y de extremo a
extremo (end-to-end) que abarquen múltiples servicios es más complicado.
199
Gestión de Datos: Asegurar la consistencia de los datos a través de múltiples
bases de datos (una por servicio) requiere el uso de patrones complejos como
Sagas.
3. Patrones Asociados
Para mitigar los desafíos de los microservicios, han surgido varios patrones de diseño
arquitectónico.
a. API Gateway
El API Gateway actúa como un único punto de entrada para todas las solicitudes de
los clientes externos (como aplicaciones web o móviles). En lugar de que el cliente
tenga que conocer las direcciones de docenas de servicios, simplemente se comunica
con el API Gateway. Este patrón tiene varias responsabilidades:
Enrutamiento (Routing): Dirige las solicitudes entrantes al microservicio
apropiado.
Composición: Puede agregar las respuestas de varios microservicios en una
única respuesta para el cliente.
Gestión de Asuntos Transversales (Cross-cutting concerns): Puede
manejar la autenticación, la autorización, la limitación de velocidad (rate
limiting) y el logging, liberando a los microservicios de estas tareas.
b. Service Discovery (Descubrimiento de Servicios)
En un entorno de microservicios, las instancias de servicio se crean y destruyen
dinámicamente. El patrón Service Discovery proporciona un mecanismo para que los
servicios se encuentren entre sí. Funciona de la siguiente manera:
1. Cada instancia de servicio, al iniciarse, se registra en un Registro de
Servicios (Service Registry), proporcionando su dirección de red.
2. Cuando un servicio (el cliente) necesita comunicarse con otro (el proveedor),
consulta el Registro de Servicios para obtener la dirección actualizada del
proveedor.
3. La instancia de servicio se da de baja del registro cuando se apaga.
Herramientas como Consul, Eureka o Zookeeper implementan este patrón.
c. Circuit Breaker (Cortocircuito)
El patrón Circuit Breaker es un patrón de resiliencia crucial para evitar fallos en
cascada. Envuelve las llamadas de red a otros servicios y monitoriza sus fallos.
Funciona como un interruptor eléctrico:
Cerrado (Closed): El estado normal. Las solicitudes fluyen hacia el servicio
remoto. Si el número de fallos supera un umbral, el interruptor se abre.
Abierto (Open): El interruptor está "abierto". Todas las llamadas al servicio
remoto fallan inmediatamente sin intentar la conexión, devolviendo un error o
un valor por defecto. Esto evita sobrecargar un servicio que ya está fallando.
Después de un tiempo de espera, el interruptor pasa al estado "entreabierto".
Entreabierto (Half-Open): El interruptor permite que una cantidad limitada de
solicitudes de prueba pasen. Si estas tienen éxito, el interruptor se cierra y
200
vuelve al estado normal. Si fallan, vuelve al estado abierto. Librerías como
Hystrix o Resilience4j implementan este patrón.
4. Ejemplo Conceptual: De Monolito a Microservicios
Consideremos una aplicación monolítica de comercio electrónico simple.
Aplicación Monolítica de E-Commerce:
Una única aplicación web que contiene toda la lógica de negocio.
Módulos: Gestión de Usuarios, Catálogo de Productos, Carrito de Compras,
Procesamiento de Pedidos y Notificaciones.
Base de Datos: Una única base de datos relacional grande que contiene las
tablas users, products, orders, order_items, etc.
Despliegue: Cualquier cambio, por pequeño que sea, en el módulo de
notificaciones requiere volver a probar y desplegar toda la aplicación. Si el
catálogo de productos necesita más recursos, se debe escalar toda la
aplicación.
Descomposición en Microservicios:
La aplicación se descompondría en los siguientes servicios independientes, cada uno
enfocado en una capacidad de negocio:
1. Servicio de Usuarios (User Service):
o Responsabilidad: Gestionar el registro, inicio de sesión, perfiles y
autenticación de usuarios.
o Base de Datos: Su propia base de datos con la tabla users.
o API: Expone endpoints como POST /users, GET /users/{id}, POST
/login.
2. Servicio de Catálogo (Product Catalog Service):
o Responsabilidad: Gestionar la información de los productos, precios e
inventario.
o Base de Datos: Su propia base de datos con tablas como products e
inventory.
o API: Expone endpoints como GET /products, GET /products/{id}.
3. Servicio de Pedidos (Order Service):
o Responsabilidad: Gestionar el ciclo de vida de un pedido, incluyendo
el carrito de compras, la creación de pedidos y el historial.
o Base de Datos: Su propia base de datos con tablas orders y
order_items.
o Comunicación: Necesita comunicarse con el Servicio de Usuarios
(para saber quién realiza el pedido) y el Servicio de Catálogo (para
obtener detalles y precios de los productos).
4. Servicio de Pagos (Payment Service):
201
o Responsabilidad: Integrarse con pasarelas de pago externas para
procesar las transacciones.
o Comunicación: Es llamado por el Servicio de Pedidos cuando un
pedido está listo para ser pagado.
5. Servicio de Notificaciones (Notification Service):
o Responsabilidad: Enviar correos electrónicos o notificaciones push a
los usuarios.
o Comunicación: Es llamado por otros servicios, por ejemplo, por el
Servicio de Pedidos después de una compra exitosa.
En esta nueva arquitectura, una aplicación web cliente (o una aplicación móvil) no
llamaría a cada servicio directamente. En su lugar, se comunicaría con un API
Gateway. El API Gateway recibiría una solicitud, por ejemplo, para "ver un pedido", y
luego orquestaría las llamadas necesarias al Servicio de Pedidos, al Servicio de
Usuarios y al Servicio de Catálogo para recopilar toda la información y devolver una
respuesta consolidada. Cada servicio sería un proyecto separado, con su propio
repositorio, su propio pipeline de CI/CD y la capacidad de ser desplegado y escalado
de forma totalmente independiente.
202