Continuando con la serie sobre patrones de diseño en Java, hoy veremos un patrón que es especialmente útil para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas. Es útil cuando se necesita asegurar que los objetos creados pertenecen a una misma familia y que funcionen bien juntos.
Para explicar este patrón, lo aplicaremos a un sistema de fabricación de vehículos donde diferentes marcas producen distintos tipos de automóviles (sedán y SUV), cada uno con características específicas.
Qué es el patrón Abstract Factory?
El patrón Abstract Factory es una forma de organizar el código para crear familias de objetos relacionados sin preocuparte por las clases específicas que los generan. En lugar de crear los objetos directamente, le pides a una fábrica que los haga por ti.
Por ejemplo:
En una fábrica de autos y se necesitan producir sedanes y SUVs, puedes usar una fábrica para cada marca (Toyota, Nissan, etc.) y pedirle que cree los autos correspondientes. Así, el cliente no necesita saber cómo se construyen, solo qué tipo de auto necesita.
En resumen:
Es como un menú en un restaurante donde eliges un «combo» (familia de objetos) y el chef (fábrica) se encarga de prepararlo, sin que tú veas los detalles de la cocina.
Esto es útil para hacer cosas como:
- Familias de objetos relacionados: Cuando existen varios objetos que deben trabajar juntos y queremos segurarnos de que todos pertenezcan a la misma familia (por ejemplo, botones y menús con el mismo estilo en una interfaz gráfica).
- Sistemas multiplataforma: Si estás desarrollando software que debe funcionar en diferentes sistemas operativos (Windows, macOS, Linux) y cada uno tiene implementaciones específicas de ciertos componentes.
- Cambio de dependencias fácilmente: Cuando quieres cambiar entre diferentes implementaciones (por ejemplo, bases de datos como MySQL y PostgreSQL) sin modificar mucho el código.
- Modularidad y extensibilidad: Si prevés que en el futuro agregarás nuevas variantes de objetos relacionados, el patrón facilita esta expansión sin romper el código existente.
- Separación de lógica de creación: Cuando deseas mantener el código limpio al separar la lógica de creación de objetos del resto del sistema.
Y se puede aplicar en casos de uso como:
- Interfaces gráficas de usuario (GUIs): Múltilples temas en una aplicación ya que proporcionaría los componentes visuales adecuados para cada tema
- Sistemas de bases de datos: Abstraer la conexión y operaciones para trabajar con diferentes bases de datos
- Juegos: Crear familias de personajes, armas, y ambientes dependiendo del nivel o tema del juego
- Sistemas de reportes: Generar reportes en diferentes formatos (PDF, Excel, HTML) con un conjunto de componentes relacionados
- Aplicaciones de comercio electrónico: Crear familias de productos relacionados con diferentes regiones o monedas
- Aplicaciones en la nube: Para provisionar recursos según el proveedor de la nube (AWS, Azure, Google Cloud)
Cómo implementar Abstract Factory en Java
1. Escenario práctico
En una fábrica de automóviles, diferentes marcas (por ejemplo, Toyota y Nissan) producen vehículos que comparten categorías (sedán y SUV) pero tienen implementaciones específicas para cada marca.
Con el patrón Abstract Factory, podemos definir una interfaz que gestione las fábricas de estas marcas y permita la creación de automóviles sin depender de clases concretas.
2. Implementación del patrón Abstract Factory
a) Interfaz de la fábrica abstracta
Define los métodos para crear los diferentes tipos de vehículos.
public interface FabricaAbstractaVehiculos {
Automovil crearSedan();
Automovil crearSUV();
}b) Implementaciones concretas de la fábrica
Cada fábrica concreta representa una marca y define los detalles específicos de los vehículos que produce.
Fábrica Toyota:
public class FabricaToyota implements FabricaAbstractaVehiculos {
@Override
public Automovil crearSedan() {
return new Automovil.Builder()
.modelo("Toyota Corolla")
.color("Blanco")
.motor("1.8L")
.gps(true)
.build();
}
@Override
public Automovil crearSUV() {
return new Automovil.Builder()
.modelo("Toyota RAV4")
.color("Negro")
.motor("2.5L")
.gps(true)
.build();
}
}Fábrica Nissan:
public class FabricaNissan implements FabricaAbstractaVehiculos {
@Override
public Automovil crearSedan() {
return new Automovil.Builder()
.modelo("Nissan Sentra")
.color("Plateado")
.motor("2.0L")
.gps(true)
.build();
}
@Override
public Automovil crearSUV() {
return new Automovil.Builder()
.modelo("Nissan X-Trail")
.color("Rojo")
.motor("2.5L")
.gps(true)
.build();
}
}c) Clase Automóvil con Builder
Se utiliza el patrón Builder para personalizar los vehículos, como ya vimos anteriormente.
public class Automovil {
private String modelo;
private String color;
private String motor;
private boolean gps;
private Automovil(Builder builder) {
this.modelo = builder.modelo;
this.color = builder.color;
this.motor = builder.motor;
this.gps = builder.gps;
}
@Override
public String toString() {
return "Automovil{" +
"modelo='" + modelo + '\'' +
", color='" + color + '\'' +
", motor='" + motor + '\'' +
", gps=" + gps +
'}';
}
public static class Builder {
private String modelo;
private String color;
private String motor;
private boolean gps;
public Builder modelo(String modelo) {
this.modelo = modelo;
return this;
}
public Builder color(String color) {
this.color = color;
return this;
}
public Builder motor(String motor) {
this.motor = motor;
return this;
}
public Builder gps(boolean gps) {
this.gps = gps;
return this;
}
public Automovil build() {
return new Automovil(this);
}
}
}3. Uso del patrón Abstract Factory
El cliente utiliza la fábrica abstracta para crear los automóviles sin preocuparse por las implementaciones específicas de cada marca.
public class Main {
public static void main(String[] args) {
// Crear una fábrica para Toyota
FabricaAbstractaVehiculos fabricaToyota = new FabricaToyota();
Automovil sedanToyota = fabricaToyota.crearSedan();
Automovil suvToyota = fabricaToyota.crearSUV();
// Crear una fábrica para Nissan
FabricaAbstractaVehiculos fabricaNissan = new FabricaNissan();
Automovil sedanNissan = fabricaNissan.crearSedan();
Automovil suvNissan = fabricaNissan.crearSUV();
// Imprimir los vehículos creados
System.out.println(sedanToyota);
System.out.println(suvToyota);
System.out.println(sedanNissan);
System.out.println(suvNissan);
}
}4. Resultado esperado
Al ejecutar el programa, obtendrás algo similar a:
Automovil{modelo='Toyota Corolla', color='Blanco', motor='1.8L', gps=true}
Automovil{modelo='Toyota RAV4', color='Negro', motor='2.5L', gps=true}
Automovil{modelo='Nissan Sentra', color='Plateado', motor='2.0L', gps=true}
Automovil{modelo='Nissan X-Trail', color='Rojo', motor='2.5L', gps=true}Relación con otros patrones
- Builder: Se usa para personalizar los automóviles creados por las fábricas.
- Singleton: Puede usarse para asegurar que solo haya una instancia de cada fábrica concreta si es necesario.
- Prototype: Si necesitas clonar vehículos creados previamente por la fábrica, puedes implementar este patrón en la clase Automovil.
Ventajas del enfoque combinado
- Flexibilidad: Las fábricas pueden producir diferentes tipos de vehículos sin acoplarse a clases concretas.
- Extensibilidad: Es fácil agregar nuevas marcas o categorías de vehículos sin modificar el código existente.
- Modularidad: Los patrones se combinan para resolver problemas específicos en la creación y manejo de objetos.
Ventajas del patrón Abstract Factory
- Consistencia en familias de objetos:
Garantiza que los objetos creados por una fábrica pertenezcan a la misma familia y sean compatibles entre sí. - Facilita cambios futuros:
Cambiar de una familia de objetos a otra (por ejemplo, de un tema claro a uno oscuro) es sencillo porque solo necesitas cambiar la fábrica que usas. - Aísla la lógica de creación:
Separa la lógica de creación de objetos del código cliente, haciendo el sistema más limpio y modular. - Escalabilidad:
Es fácil agregar nuevas familias de objetos sin modificar el código existente, respetando el principio de abierto/cerrado. - Promueve la flexibilidad:
Permite que el cliente trabaje con interfaces o clases abstractas en lugar de depender de implementaciones concretas.
Desventajas del patrón Abstract Factory
- Complejidad adicional:
Introduce múltiples clases e interfaces, lo que puede hacer el diseño más complicado y difícil de entender para proyectos pequeños. - Dificultad para agregar nuevos productos:
Si necesitas agregar un nuevo tipo de producto en todas las familias, debes modificar todas las fábricas, lo que puede violar el principio abierto/cerrado. - Menor control sobre instancias:
Al delegar la creación a fábricas, el cliente tiene menos control directo sobre cómo se instancian los objetos, lo que podría ser problemático en ciertos casos específicos. - Sobrecarga en sistemas simples:
Para aplicaciones pequeñas o con pocos tipos de objetos, el patrón puede ser innecesariamente pesado.
En resumen, el patrón Abstract Factory es ideal para proyectos grandes y sistemas que requieren flexibilidad, pero puede ser excesivo para aplicaciones más simples.
