23 de diciembre de 2021

Patrón de Diseño Abstract Factory



Abstract Factory es un patrón de diseño creacional del ámbito de objetos. Este patrón ayuda a crear grupos o familias de objetos que, aunque en esencia son diferentes, tienen una relación de herencia o dependencia entre ellos a niveles de padres e hijos, pero también a nivel de hermanos. De esta forma ayudan a que la construcción de esa familia de objetos mantenga una lógica y coherencia, manteniendo al mismo tiempo un alto nivel de abstracción y bajo acoplamiento a través del polimorfismo.

En algunas ocasiones este patrón es descrito como una fábrica de fábricas lo cual, en parte, tiene razón, pero esta descripción se queda corta con respecto al potencial que ofrece.

En este tutorial te explicaré más en detalle en qué consiste este patrón de diseño, así como una estrategia de implementación.

La esencia de Abstract Factory es similar a la de Factory Method en el sentido de que lo que buscamos conseguir es abstraer la forma en la que creamos los objetos de cierto tipo sin preocuparnos de los detalles de cómo se crear e inicializan, pero también sin preocuparnos del tipo concreto de estos objetos en ejecución.

La diferencia principal es que Factory Method se encarga de la creación de objetos únicos; es decir, se crea un objeto de varias posibles clases candidatas. En el caso de Abstract Factory, lo que queremos es crear familias de objetos que mantengan una relación entre ellos.

Veamos un ejemplo sencillo para entender el concepto anterior: Muebles.

Digamos que somos fabricantes de muebles y nos especializamos en hacer los siguientes tipos:
  • Mesas
  • Sillas
  • Sofás
  • Escritorios

Al inicio solo nos dedicábamos a estos tipos de muebles usando un solo estilo: muebles de madera de color natural, sin adornos ni accesorios adicionales. Cuando un cliente llega a nuestra tienda selecciona qué tipo de mueble quiere y compra una unidad de este mueble. Para la construcción del mueble nos basta con una Factory Method que recibe como parámetro el tipo de mueble que el cliente quiere y regresamos un Mueble. Una vez que tenemos esa instancia de Mueble, la empaquetamos y se la enviamos al cliente. Tan simple como eso. Todos los muebles los tratamos de la misma forma sin importar el tipo de mueble, por lo que nuestra abstracción funciona bien.

Como el negocio ha crecido y muchas personas han recomendado nuestros muebles de alta calidad, una gran empresa ha puesto sus ojos en nosotros y quiere comprar nuestra fábrica de muebles. La oferta que han hecho es bastante generosa pero solo nos ponen una condición: como sus clientes son normalmente dueños de enormes casas o mansiones, compran conjuntos de muebles del mismo estilo para las diferentes secciones de sus casas, por lo que no están interesados en la venta de muebles individuales, sino en conjuntos completos (una colección o familia de muebles). Así que nos piden ampliar nuestra producción, manteniendo los tipos de muebles que hacemos (al que los genios de marketing han llamado la “línea classic”) pero nos piden que los fabriquemos también en diferentes estilos.

Así que ahora tenemos que fabricar mesas, sillas, sofás y escritorios en estilo minimalista, rústico, y steampunk.

Claro, no es posible mezclar muebles de diferentes estilos en un solo conjunto, así que nunca tendremos una mesa minimalista con sillas rústicas y un sofá steampunk. Sino que el cliente seleccionará un estilo y nosotros fabricaremos los muebles.

Así que ahora nuestro Factory Method no es suficiente ya que no nos permite seleccionar el estilo de mueble que queremos, ni asegurar que todos los muebles serán del mismo estilo (o familia). Por lo tanto, debemos agregar un nivel extra de abstracción para cumplir con todos los requerimientos.

En este ejemplo, tenemos que cada uno de los estilos de muebles será creado con una fábrica diferente. Tendremos una especializada en muebles rústicos, otra en muebles minimalistas, otra en muebles steampunk, etc.

Otro ejemplo clásico es el de la interfaz de usuario. Si vamos a proporcionar una interfaz en modo obscuro y otra en modo claro, todos los componentes visuales de cada uno de los modos deben tener una coherencia entre ellos (todos claros o todos obscuros). O si vamos a tener una interfaz tipo Windows y otra tipo Mac, todos los controles de la familia Windows serán iguales entre ellos y todos los de la familia Mac también serán iguales entre ellos, aunque entre familias serán diferentes.


Objetivo o intención del patrón

Con la explicación anterior podemos entender que lo que buscamos lograr con este patrón es:
  • Tener una interface para crear una familia de objetos relacionados o dependientes, sin especificar sus clases de forma explícita.
En el ejemplo anterior, las clases que buscamos crear son Silla, Mesa, Sillón, etc. Y las clases concretas son SillaMinimalista, SillaRustica, SillaSteampunk, etc.

Esto ayudará a que cuando tengamos que integrar un nuevo estilo de muebles lo podamos hacer sin necesidad de modificar ninguno de los estilos que ya tenemos.


Implementación

Antes de ver los detalles en código, un poco de teoría de cómo implementar el patrón.
  1. Crear una familia de clases base. Estas serán las clases que representen los tipos regresados por la fábrica. Pueden ser clases abstractas o interfaces. En el ejemplo de los muebles, la familia de clases base serían Silla, Mesa, Escritorio, etc.
  2. Crear una serie de clases hija que extiendan (o implementen) la clase base. Debe haber una implementación por cada fábrica que tengamos. Estas clases serán los objetos creados por nuestras fábricas. Así que, si tenemos 5 tipos de muebles y 2 estilos, tendremos 10 clases hija concretas.
  3. Crear una clase base abstracta Factory. Esta puede ser una clase abstracta o una interface, con un método que regrese una instancia de cada uno de los tipos de la familia de clases. Eso quiere decir que habrá un método para obtener una Silla, otro para una Mesa, otro para un Escritorio, etc. Hay que mencionar que la fábrica abstracta solo declara la interface para la creación de los productos, y son las fábricas concretas las responsables de la creación de los objetos concretos.
  4. Crear una o más implementaciones de la clase Abstract Factory. Estas implementaciones serán quienes regresen los objetos concretos creados por la fábrica. Las clases concretas se encargarán de regresar los objetos de la familia.
  5. Crear una clase que se encargue de decidir qué instancia particular de la Abstract Factory se usará. A esta clase la conocemos como Factory Maker o Factory Producer. Esta clase puede recibir parámetros si es necesario para que decida cuál es la instancia que se debe regresar.
  6. Crear los objetos se regresarán al cliente. Las instancias pueden ser creada de inmediato, tomada de un pool de objetos, u obtenido de cualquier otra forma que sea necesaria.
  7. Regresar al cliente las instancias como referencias del tipo de la clase base.

El paso 5 es opcional, pero facilita mucho el seleccionar estas fábricas concretas.


Diagrama

Este es el diagrama general del patrón:



Como puedes ver es muy directo, nuestro Cliente usa objetos de tipo ClaseBaseA y ClaseBaseB. Cada una de las fábricas se encarga de crear los objetos concretos; en el diagrama la FabricaConcreta1 crea objetos de tipo ClaseHijaA1 y ClaseHijaB1 y la FabricaConcreta2 creo objetos de tipo ClaseHijaA2 y ClaseHijaB2.

El Cliente obtiene la instancia correspondiente de la AbstractFactory a través del FactoryMaker.

Para el ejemplo de los Muebles, el diagrama es:



Cada una de las fábricas tiene métodos para cada una de las clases concretas.

Este patrón no tiene ninguna variación real. Existe una modificación que en algunos lugares podrás encontrar como Abstract Factory (cuando en realidad no lo es), y que se representa con el siguiente diagrama.


¿Por qué esta no es una variación correcta del patrón? Si observas, ShapeFactory y ColorFactory son las dos fábricas concretas que extienden de la AbstractFactory y cada una se encarga de crear elementos de una familia de clases; hasta aquí todo bien. Sin embargo, en realidad ninguna implementa todos los métodos de la AbstractFactory, sino que implementan el método correspondiente a su familia de clases, ShapeFactory implementa getShape y ColorFactory implementa getColor. Esto ocurre así porque no tiene sentido que ShapeFactory implemente getColor y viceversa. Por lo tanto, en realidad no es una forma correcta de usar Abstract Factory.

El diagrama anterior tendría más sentido usando una relación de composición y un par de Factory Methods.

¿Dónde se usa Abstract Factory? Dentro del JDK podemos verlo en DocumentBuilderFactory. El método newInstance() busca y regresa la instancia apropiada de una subclase concreta que extienda DocumentBuilderFactory.

En este caso DocumentBuilderFactory funciona, además de como la AbstractFactory, como el FactoryMaker ya que ella misma se encarga de encontrar la instancia que regresará en lugar de delegarlo a una clase externa.

📌Muchas de las clases del JDK y de frameworks populares como Spring hacen esto mismo fusionando estos dos elementos en uno solo y usando reflexión para decidir en tiempo de ejecución qué instancia concreta crearán y regresarán.


Ejemplo

En el ejemplo no usaremos los muebles sino algo más interesante. Antes de saltar a la explicación unos detalles técnicos sobre el proyecto.

Para los ejemplos uso Java 17 y Gradle. No uso ninguna característica particular de esta versión de Java, por lo que debe funcionar en cualquier versión (incluso en la 5). También, uso Lombok para evitar escribir código repetitivo de forma innecesaria.

Para validar el correcto funcionamiento de la aplicación usaré JUnit y AssertJ. Agrega las siguientes dependencias en el archivo build.gradle (si usas Maven, coloca las dependencias correspondientes en el archivo pom.xml)
dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.22'
    annotationProcessor 'org.projectlombok:lombok:1.18.22'

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testImplementation 'org.assertj:assertj-core:3.21.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
Ahora sí, al código.

El problema que resolveremos es el siguiente: Tenemos una aplicación la cual distribuiremos a diferentes clientes. Una de las ventajas de nuestra aplicación es que los clientes pueden decidir el tipo de base de datos que quieren usar. Esto quiere decir que para nuestra aplicación da igual si usan MongoDB o si usan PostgreSQL.

Para que la lógica de nuestra aplicación no se preocupe de los detalles de la implementación del mecanismo de persistencia, hemos decidido que el acceso a datos se hará a través de una serie de clases abstractas Repository (o DAO si lo prefieres, pero para este ejemplo particular no importan los detalles sobre las diferencias entre estos dos).

Tenemos tres tablas (o entidades) principales: Usuario, Producto y Compra; así que tendremos un UsuarioRepository, un ProductoRepository y un CompraRepository. Estos repositorios representan nuestra familia de clases.

Para no complicar la implementación de los repositorios (ya que el objetivo del tutorial es aprender sobre el patrón de diseño, y no sobre el trabajo con la capa de persistencia) solo agregaremos un método para guardar la "entidad" correspondiente (la cual representaremos como una String) y regresaremos también una String que contendrá un texto indicando que la instancia correspondiente se guardó. Por lo tanto, tenemos las siguientes tres interfaces:
public interface CompraRepository {
    String guardaCompra(String compra);
}
public interface ProductoRepository {
    String guardaProducto(String producto);
}
public interface UsuarioRepository {
    String guardaUsuario(String usuario);
}

En la primera implementación proporcionaremos la habilidad de seleccionar entre dos almacenes diferentes una base datos relacional y MongoDB. Por lo tanto, tendremos una fábrica abstracta que tendrá tres métodos, cada uno de los cuales regresará el Repository correspondiente:
public interface AbstractRepositoryFactory {
    CompraRepository getCompraRepository();

    ProductoRepository getProductoRepository();

    UsuarioRepository getUsuarioRepository();
}

Además, tendremos dos implementaciones de la Abstract Factory, una para una base de datos relacional y otra para MongoDB:
public class RelacionalRepositoryFactory implements AbstractRepositoryFactory {
    @Override
    public CompraRepository getCompraRepository() {
        return new CompraRelacionalRepository();
    }

    @Override
    public ProductoRepository getProductoRepository() {
        return new ProductoRelacionalRepository();
    }

    @Override
    public UsuarioRepository getUsuarioRepository() {
        return new UsuarioRelacionalRepository();
    }
}
public class MongoRespositoryFactory implements AbstractRepositoryFactory {
    @Override
    public CompraRepository getCompraRepository() {
        return new CompraMongoRepository();
    }

    @Override
    public ProductoRepository getProductoRepository() {
        return new ProductoMongoRepository();
    }

    @Override
    public UsuarioRepository getUsuarioRepository() {
        return new UsuarioMongoRepository();
    }
}
📌 Un punto importante que hay que mencionar, es que la implementación de las fábricas normalmente es muy sencilla y podemos mejorar su implementación si las creamos como Singletons o usando el patrón Prototype.

Lo siguiente es crear las implementaciones de estas interfaces. Tendremos dos por interfaz, una para la base relacional y para MongoDB, así que en total tendremos 6 clases concretas. Estas son las implementaciones para la base relacional
public class CompraRelacionalRepository implements CompraRepository {

    @Override
    public String guardaCompra(String compra) {
        return String.format("Guardando %s en una base de datos relacional", compra);
    }
}
public class ProductoRelacionalRepository implements ProductoRepository {
    @Override
    public String guardaProducto(String producto) {
        return String.format("Guardando %s en una base de datos relacional", producto);
    }
}
public class UsuarioRelacionalRepository implements UsuarioRepository {
    @Override
    public String guardaUsuario(String usuario) {
        return String.format("Guardando %s en una base de datos relacional", usuario);
    }
}



Y estas para la MongoDB.
public class CompraMongoRepository implements CompraRepository {
    @Override
    public String guardaCompra(String compra) {
        return String.format("Guardando %s en MongoDB", compra);
    }
}
public class ProductoMongoRepository implements ProductoRepository {
    @Override
    public String guardaProducto(String producto) {
        return String.format("Guardando %s en MongoDB", producto);
    }
}
public class UsuarioMongoRepository implements UsuarioRepository {
    @Override
    public String guardaUsuario(String usuario) {
        return String.format("Guardando %s en MongoDB", usuario);
    }
}


Como puedes ver el código es muy sencillo. El valor de retorno de cada uno de los métodos nos ayudará a verificar que nuestra fábrica funciona de forma correcta cuando creemos nuestras pruebas unitarias.

Lo siguiente es una pieza esencial para este patrón y que puede implementarse de dos formas. Me refiero, por supuesto, al FactoryMaker. Esta es la clase que se encarga de seleccionar cuál fábrica concreta se regresará al cliente y desde la cual se crearán los repositorios correspondientes. La implementación de este también es muy sencilla. Usaremos un parámetro para indicar qué tipo de repositorio queremos: RELACIONAL o MONGO_DB. Colocaremos estos dos valores en una enumeración y usaremos una constante de esa enumeración para indicarle al FactoryMaker qué implementación debe regresar.

Así que primero crearemos la enumeración correspondiente:
public enum TipoRepositorio {
    RELACIONAL,
    MONGO_DB
}


Y ahora el FactoryMaker, el cual tendrá un solo método estático que recibirá como parámetro una constante de tipo TipoRepositorio y regresará un AbstractRepositoryFactory. Usaremos como valor por default RelacionalRepositoryFactory. Por lo tanto, la implementación queda de la siguiente forma:

public class RepositoryFactoryMaker {
    public static AbstractRepositoryFactory getRepositoryFactory(TipoRepositorio tipo) {
        if (TipoRepositorio.MONGO_DB.equals(tipo)) {
            return new MongoRespositoryFactory();
        }

        return new RelacionalRepositoryFactory();
    }
}


Y listo, ya tenemos todos los elementos de nuestra aplicación.

El diagrama de nuestra implementación se ve así:



Como nota adicional, hace unos momentos comenté que el FactoryMaker podíamos implementarlo de dos formas; la primera es la que acabamos de ver, como una clase public que podemos usar de forma independiente. En la segunda implementación podemos hacer algo parecido a lo que hace DocumentBuilderFactory, en donde esta misma clase es quien proporciona la clase concreta correspondiente. DocumentBuilderFactory hace uso de una clase, FactoryFinder, que es quien se encarga de obtener y regresar la clase concreta.



Siguiendo esta misma idea, RepositoryFactoryMaker podría ser una clase con visibilidad por default (sin modificador de acceso), con lo que se limitaría su uso al mismo paquete en el que se encuentra AbstractFactoryRepository, y esta última clase sería quién tuviera un método estático que internamente usara a RepositoryFactoryMaker para regresar la instancia correspondiente. Nuevamente, muy parecido a como lo hace DocumentBuilderFactory. Esta segunda implementación sería algo como esto:
public static AbstractRepositoryFactory(TipoRepositorio tipo){
	return RepositoryFactoryMaker.getRepositoryFactory(tipo);
}

📌 Para que lo anterior funcione AbstractFactoryRepository tendría que ser una clase abstracta y no una interface, que es como yo la implemente. Esto porque las interfaces no pueden tener métodos estáticos. ¿Cuál implementación es mejor? La que le haga más sentido a tu aplicación y la que mejor se acomode a tu forma de programar. No hay mucha diferencia entre ambas, así que no te preocupes mucho por esto.

La implementación que acabamos de hacer de FactoryMaker tiene un punto débil que vale la pena mencionar: cada vez que creemos una nueva implementación de AbstractFactoryRepository debemos actualizar dos clases: TipoRepository (agregando una nueva constante) y la propia FactoryMaker para agregar una nueva condición para regresar la implementación correspondiente de AbstractFactoryRepository.

¿Cómo podemos evitar esto? Realmente las mejores implementaciones que he visto de FactoryMaker hacen uno del mecanismo de reflección (reflection) de Java para encontrar y crear la instancia adecuada.

Si alguien está interesado en esto, podemos hacer un tutorial que hable más de este tema, mezclando reflection y anotaciones para una selección dinámica de las instancias y que no requiera de modificaciones al código de FactoryMaker.

Para terminar, validaremos el funcionamiento de la aplicación con un par de pruebas unitarias muy sencillas. En la primera solicitaremos repositorios para una base de datos relacional, y comprobaremos que los repositorios regresan el mensaje correspondiente al momento de invocar el método de guardado:

@Test
public void testRelacionalRepository_cuandoTipoEsRelacional() {
	AbstractRepositoryFactory repositoryFactory = RepositoryFactoryMaker.getRepositoryFactory(TipoRepositorio.RELACIONAL);

    CompraRepository compraRepository = repositoryFactory.getCompraRepository();
    ProductoRepository productoRepository = repositoryFactory.getProductoRepository();
    UsuarioRepository usuarioRepository = repositoryFactory.getUsuarioRepository();

    assertThat(compraRepository.guardaCompra("compra sencilla")).isEqualTo("Guardando compra sencilla en una base de datos relacional");
    assertThat(productoRepository.guardaProducto("producto elegante")).isEqualTo("Guardando producto elegante en una base de datos relacional");
    assertThat(usuarioRepository.guardaUsuario("usuario importante")).isEqualTo("Guardando usuario importante en una base de datos relacional");
}

Para la segunda validación haremos básicamente lo mismo, pero solicitaremos repositorios para MongoDB:

@Test
public void testMongoRepository_cuandoTipoEsMongo() {
	AbstractRepositoryFactory repositoryFactory = RepositoryFactoryMaker.getRepositoryFactory(TipoRepositorio.MONGO_DB);

    CompraRepository compraRepository = repositoryFactory.getCompraRepository();
    ProductoRepository productoRepository = repositoryFactory.getProductoRepository();
    UsuarioRepository usuarioRepository = repositoryFactory.getUsuarioRepository();

    assertThat(compraRepository.guardaCompra("compra sencilla")).isEqualTo("Guardando compra sencilla en MongoDB");
    assertThat(productoRepository.guardaProducto("producto elegante")).isEqualTo("Guardando producto elegante en MongoDB");
    assertThat(usuarioRepository.guardaUsuario("usuario importante")).isEqualTo("Guardando usuario importante en MongoDB");
}

Y esto es todo; al momento de ejecutar las pruebas unitarias podemos ver que ambas se ejecutan de forma exitosa, y por lo tanto nuestra Abstract Factory funciona de forma correcta.


Ventajas y Desventajas

  1. ➕ Aísla la creación la creación de los objetos y la oculta al cliente que necesita los objetos, dándole acceso a los mismos a través de una interface, lo que hace que manipularlos sea más fácil.
  2. ➕ Cambiar de una familia a otra es fácil, ya que solo hace falta cambiar la fábrica concreta que estamos usando.
  3. ➕ Si la fábrica está bien implementada, asegura que los objetos creados corresponden siempre a la misma familia.
  4. ➖ Agregar nuevos productos o clases derivadas a familias existentes es difícil, ya que hay que proporcionar una implementación para cada una de las fábricas. Esto muchas veces obliga a realizar cambios en la fábrica abstracta y en todas las implementaciones concretas. Además de que todas las familias deben tener una implementación de cada producto, aún si no hace mucho sentido para esa familiar en particular (¿qué pasa si uno de los estilos de nuestros muebles no requiere de un Sofá?)
  5. ➖ Aunado al punto anterior; se necesita mucho código para implementar la fábrica de forma correcta. Podemos ver en los diagramas del ejemplo de muebles, como al agregar un nuevo mueble o estilo nos obliga a generar muchas clases nuevas.

Otros nombres

  1. Factory Kit o simplemente Kit

Conclusión

Abstract Factory es un patrón muy útil y fácil de implementar y que ayuda a mantener una consistencia entre los objetos creados por cada una de las fábricas. Además de que facilita el intercambio de fábricas con tan solo la modificación de un parámetro. Con esto logramos tener un código más flexible y fácil de mantener.

Como nota adicional, en el repositorio dejo, además del código, el archivo original en draw.io con los esquemas de los patrones.

Espero que este tutorial les sea de utilidad. Si tienen alguna duda, sugerencia, comentario o aclaración, pueden dejarla en la sección de comentarios o enviar un correo a programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También pueden seguir JavaTutoriales en las siguientes redes sociales: Saludos y gracias.

Descarga los archivos de este tutorial desde mi repositorio en GitHub
Entradas relacionadas