8 de diciembre de 2021

Patrón de diseño Factory Method



El patrón de diseño Factory Method es un patrón de diseño creacional del ámbito de los objetos; es el único patrón de este tipo de los 24 definidos originalmente por la GoF.

Este patrón se usa mucho dentro del JDK y en frameworks como Spring, además de en un sin número de librerias y especificaciones, ya que uno de sus principales objetivos es poder ocultar los detalles de la implementación a través de una clase abstracta o interface para definir y mantener relaciones entre objetos.

En este tutorial veremos varias estrategias de implementación y una variación a las cual se les conoce simplemente como Factory o como Simple Factory y sus diferencias con Factory Method.

Factory puede aplicarse cuando tenemos varias clases diferentes que tienen elementos en común o las tratamos de la misma forma, pero con pequeñas diferencias. En estos casos hacemos uso de la herencia para crear una clase base (normalmente abstracta, aunque esto no es necesario), o una interface, de la cual heredan el resto de las clases, y, dependiendo de ciertas condiciones, queremos obtener una instancia una de estas clases sin que realmente nos importen los detalles de la clase concreta que se está usando o como se instanció.

Este patrón toma la responsabilidad de instanciar dicha clase (o sea, de crear el objeto concreto que necesitamos) y regresarnos la instancia sin que nos preocupemos de los detalles de cómo hizo la selección y como creó el objeto.

Objetivo o intención del patrón

Con la breve explicación anterior podemos decir que el objetivo o intención de este patrón es:
  1. Crear objetos sin exponer al cliente la lógica de cómo se crean esas instancias (este en realidad corresponde a Factory, no a Factory Method, pero me parece que ambos lo cumplen a la perfección).
  2. Hacer referencia al objeto de reciente creación usando una interface común.
  3. Definir una interface para crear objetos, pero dejar que sean las subclases quienes decidan qué clase concreta instanciar.
En las líneas anteriores, cliente se refiere a la parte de la aplicación que hace uso del patrón Factory Method para obtener la instancia que necesita.

También, esto ayuda a que el cliente no se preocupe de los detalles de cómo se inicializan los objetos que recibe; en ocasiones puede ser necesario conectarse a una base de datos, o a un servicio externo, si las regresa de un caché o un pool, si regresa la misma instancia cada vez que lo necesitamos o si siempre crea una nueva instancia y la devuelve; estos son detalles que no son importantes para el cliente. Al cliente solo le interesa obtener una instancia de una subclase (incluso, no es importante cuál es la clase concreta que se está usando).

📌 Lo anterior ayuda a genera un bajo acoplamiento al aplicar de forma correcta el polimorfismo.

¿Qué pasa si tu cliente necesita conocer los detalles de qué subclase se está usando? Digamos que quieres que, si la instancia es de un tipo particular hacer algo diferente como invocar un método o establecer el valor de una variable. Esto solo lo necesitas para este tipo particular pero no para los demás.

La anterior es en realidad es una pregunta con truco pero que he escuchado muchas veces. La respuesta tiene dos partes:
  1. La pregunta no tiene nada que ver con el patrón. El alcance del patrón está relacionado con la creación del objeto, no con lo que el cliente hace con ese objeto una vez que lo tiene.
  2. Hablando de "buenas prácticas", si necesitas conocer qué implementación particular estás usando para hacer algo diferente entonces estás aplicando mal los conceptos de herencia, polimorfismo y abstracción. Posiblemente necesites revisar y ajustar tu diseño o tu algoritmo.
Es válido refactorizar tu clase base para agregar métodos que necesites, aunque algunas implementaciones dejen ese método vacío. Podemos cambiar esto:
public abstract class Computadora{
    public int horasContinuasDeUso;
	
    public void incrementaHorasDeUso() {
        horasDeUso++;
    }
	
    public abstract void hazCosasDeComputadora();
}

public class Servidor extends Computadora {

    @Overrride
    public void hazCosasDeComputadora(){
		//cálculos muy complejos, multi hilos, trabajo de red, etc.
		incrementaHorasDeUso();
    }
	
    public boolean isMantenimientoNecesario() {
        return horasDeUso <= 1000;
    }
  
    public boolean darMantenimiento(){
        horasDeUso = 0;
    }
}

public class Laptop extends Computadora {
     public void hazCosasDeComputadora(){
        //Hojas de cálculo, presentaciones, documentos y memes
		incrementaHorasDeUso();
     }
}

public  class Cliente {
    public static void main(String... args) {
        Computadora computadora = ComputadoraFactory.getComputadora(TIPO.TRABAJO_DE_HOY);

        if(computadora instanceOf Servidor servidor) {  //A partir de Java 14 podemos usar esta notación 
			
            if(servidor.isMantenimientoNecesario()){
				servidor. darMantenimiento();
			}
        }

		computadora. hazCosasDeComputadora();
    }
}

Por esto:
public abstract class Computadora {
    public int horasContinuasDeUso;
	
    public void incrementaHorasDeUso() {
        horasDeUso++;
    }
	
    public abstract void hazCosasDeComputadora();
    public abstract boolean isMantenimientoNecesario();
    public abstract boolean darMantenimiento();
}

public class Servidor extends Computadora {
    @Overrride
    public void hazCosasDeComputadora(){
        //cálculos muy complejos, multi hilos, trabajo de red, etc.
		incrementaHorasDeUso();
    }
	
    @Override
    public boolean isMantenimientoNecesario() {
        return horasDeUso <= 1000;
    }
  
    @Override
    public boolean darMantenimiento(){
		horasDeUso = 0;
    }
}

public class Laptop extends Computadora {
    public void hazCosasDeComputadora(){
        //Hojas de cálculo, presentaciones, documentos y memes
		incrementaHorasDeUso();
    }
	
    @Override
    public boolean isMantenimientoNecesario() {
        return false;
    }
  
    @Override
    public boolean darMantenimiento(){
    }
}

public  class Cliente {
    public static void main(String... args) {
		Computadora computadora = ComputadoraFactory.getComputadora(TIPO.TRABAJO_DE_HOY);

        if(computadora.isMantenimientoNecesario()){
			computadora.darMantenimiento();
		}

		computadora.hazCosasDeComputadora();
    }
}
Así, Cliente no necesita conocer detalles de la implementación y si en algún momento decides que Laptop también necesita mantenimiento, no tendrás que hacer ningún cambio en el código de Cliente, solo directamente en la clase Laptop o en alguna subclase especializada.

Nuevamente, esto no tiene nada qué ver con el patrón, pero es una pregunta que siempre sale al explicarlo.


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 clase base. Esta representa a los tipos de datos regresados por la fábrica. La clase puede ser concreta, pero lo más común y recomendable es que se use una clase abstracta o una interface para realmente facilitar un bajo acoplamiento.
  2. Crear una serie de clases hija que extiendan (o implementen) la clase base. Claro que al final las clases tendrán detalles diferentes en la implementación. Estas clases serán los objetos creados por nuestras fábricas.
  3. Crear una clase base Factory Method. Esta puede ser una clase abstracta o una interface, con un método que regrese una instancia de la clase base. Este método puede recibir parámetros si es necesario para que decida cuál es la instancia que se debe regresar.
  4. Crear una o más implementaciones de la clase Factory. Estas implementaciones serán quienes regresen los objetos concretos creados por la fábrica. Si necesitamos más de una forma de crear los objetos, podemos crear también más de una implementación de la fábrica. Sé que esto es poco claro o puede ser confuso, pero lo explicaré en un momento.
  5. Obtener la instancia que se regresará al cliente. La instancia puede ser creada de inmediato, tomada de un pool de objetos, u obtenido de cualquier otra forma que sea necesaria. La manera en la que el Factory Method cree la instancia dependerá de las necesidades particulares de tu aplicación.
  6. Regresar al cliente la instancia como una referencia del tipo de la clase base.
Los pasos 3 y 4 necesitan un poco de atención adicional. ¿Es necesario hacer una clase fábrica base y luego una implementación?, ¿podemos hacer una sola clase y olvidarnos de lo demás? Sí... y no.

Explicaré la respuesta anterior con un ejemplo reutilizando la fábrica de computadoras.

Si diagramamos los objetos del ejemplo anterior tendremos algo parecido a esto:



Observa que toda la lógica de la creación de las instancias de Computadora, está centrada en una sola clase: ComputadoraFactory. No nos interesa cómo es que esta clase obtiene las instancias de Computadora. Digamos que nuestro Cliente simplemente nos pide una computadora y no le interesa si vamos a comprar una nueva, si le vamos a dar una usada que esté en la bodega, si se la vamos a quitar a alguien más para dársela a él, etc. Lo único que le interesa es obtener una Computadora. Hasta aquí todo bien. La responsabilidad de obtener la Computadora es de ComputadoraFactory. Esto funciona bastante bien.

Lo anterior es algo que los estudiosos llaman: Simple Factory (o Factory a secas), una sola clase que se encarga de la creación de estos objetos. Pero, estos mismos estudiosos, dicen que Simple Factory NO es un patrón de diseño porque no es suficientemente flexible. Solo tenemos una forma de crear los objetos (una sola regla), si esa regla cambia entonces hay que modificar la lógica dentro de la clase, lo cual viola algunos de los principios de diseño S.O.L.I.D.

En mi opinión, Simple Factory es útil, sea defina o no como un patrón de diseño (tan útil es que se tomaron la molestia de darle un nombre). Entonces, lo que indican los pasos 3 y 4 es que, para tener una Factory que cumpla con el patrón, ComputadoraFactory debe ser abstracta y debe haber por lo menos una implementación concreta. Y no, aunque tenemos una Fabrica y esta es Abstracta, no es el patrón Abstract Factory. En el siguiente tutorial hablaré de la diferencia entre estos patrones.

Por cierto, yo sí considero a Simple Factory como un patrón de diseño.

Dentro de los siguientes ejemplos del tutorial no hablaré de Simple Factory ni daré un ejemplo... porque ya hablé de él y di un ejemplo 😉. Pero a lo que me refiero es que no proporcionaré un ejemplo de código.



Diagrama

Este es el diagrama de Factory Method:



Como ves, el patrón es muy sencillo de implementar. Para el problema particular que quieres resolver necesitas adecuar la plantilla anterior; la cual, por cierto, te dejo en GitHub junto con el código del ejemplo. Puedes abrir la plantilla usando draw.io.

En muchos lugares verás que lo que yo llamo ClaseBase lo presentan como Producto y ClaseHijaX lo represetan como ProductoConcreto; de forma parecida ClaseBaseAbstractFactory lo representan como Creador. Los nombres cambian pero la idea es exactamente la misma.

Existen tres variaciones o estrategias de implementación para este patrón (seguro hay más, pero estas son las más comunes). A la estrategia presentada anteriormente le llamaré Estrategia A.

En esta estrategia podemos tener una o más implementaciones de la Factory, cada una con una lógica diferente para crear los objetos concretos. Si en algún momento queremos cambiar la lógica solo debemos crear una nueva implementación de la fábrica y así no debemos tocar nada del código que ya existe. Incluso podemos definir algunas condiciones bajo las cuales se use una u otra fábrica.

Digamos que queremos dar un Regalo a los empleados de una empresa. Con una implementación de la fábrica podríamos hacer que el regalo sea seleccionado con base a la antigüedad del empleado, con otra fábrica podemos generarlo tomando en cuenta las horas de trabajo, y con otra podríamos generarlos completamente al azar.

En la Estrategia B tenemos un diagrama muy parecido al anterior, pero en donde la Factory base es abstracta y se tienen clases concretas que crean cada uno de los tipos concretos. La diferencia con las estrategia anterior es que cada fábrica concreta se encarga de genera solamente un tipo de objeto concreto. Así, por ejemplo, podemos tener una fábrica de teléfonos celulares, en donde una fábrica solo cree teléfonos Samsung y la otra solo teléfonos Motorola. Aunque la forma de fabricación pude ser parecida, los materiales y procesos seguro que son diferentes.



En la Estrategia C la ClaseBase también es la fábrica y es ella misma quien determina qué clase Hija concreta regresar.



Puede sonar un poco raro, pero de hecho esta es la estrategia del patrón Factory Method original definida por GoF. ¿La Estrategia C se usa en el mundo real? Sí, y mucho, sobre todo en la implementación del JDK y OpenJDK. El ejemplo más conocido es la clase Calendar. Esta es una clase abstracta que tiene una serie de métodos getInstance (línea 966 en la liga anterior) que a su vez usa un método privado createCalendar (línea 1016) y regresa una de 3 instancias.

El diagrama de implementación de Calendar sería algo parecido al siguiente.



También, se usa en la clase NumberFormat. Esta clase tiene una serie de métodos getInstance que regresan la instancia correspondiente para dar formato a los número. En el caso de NumberFormat usa una serie de Factory Methods por lo que es algo más complejo que el ejemplo anterior.





Si quieres divertirte un rato, puedes ver el código fuente de NumberFormat aquí: https://github.com/AdoptOpenJDK/openjdk-jdk12u/blob/master/src/java.base/share/classes/java/text/NumberFormat.java



Ejemplo

En este tutorial me centraré en la implementación de la Estrategia A, pero en el repositorio te dejo el código que también incluye la Estrategia B. No lo explico aquí ya que este tutorial se volvería eterno; pero con la explicación de la Estrategia A es suficiente para que entiendas la implementación de la Estrategia B. Hago lo mismo con la Estrategia C.

📌 Nota: Estos nombres (estrategia A, B y C) son nombres que yo le estoy dando para diferenciar las formas de implementar el patrón, no son nombres "oficiales" de las diferentes estrategias.
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 cafetería y el dueño ha decidido lanzar una nueva tarjeta de lealtad. Habrá tres tipos de tarjetas de lealtad: Bronce, Plata y Oro. Cada tarjeta otorgará un descuento diferente:

  • Bronce: 0%. Solo se usa para almacenar el número de compras que el cliente ha realizado en el establecimiento.
  • Plata: 5% de descuento sobre el costo total de la compra.
  • Oro: Si el total de la compra es menor o igual a $50, se considera una compra baja y se otorga un 10% de descuento; si es mayor a $50 se otorga un 5% de descuento.

El dueño aún no está seguro de cuál es el mejor criterio para otorgar una tarjeta a un cliente, si con base en el número de compras que ha realizado, la suma del total de estas compras, la edad del cliente, etc. Sin embargo, y como sabe que hay que iniciar por algún lado, quiere ver qué tan bien funciona el otorgar las tarjetas dependiendo del número de compras que ha realizado el cliente. Todos los clientes iniciarán sus compras en 0, ya que antes no se llevaba este registro. Las tarjetas se les darán a los clientes al momento de llegar al número de compras indicado y no es necesario que la soliciten (y tampoco pueden rechazarla).

Las tarjetas se otorgarán usando el siguiente criterio:
  • En la primera compra se les otorgará una tarjeta de Bronce.
  • Al tener 5 compras acumuladas se les otorgará la tarjeta de Plata.
  • Al tener 10 compras se les otorgará la tarjeta de Oro.
Si este criterio no funciona el dueño considerará una nueva forma de otorgar las tarjetas.

Como sabemos que en este caso la forma en la que se crearán las tarjetas (la lógica de creación de los objetos) puede cambiar en cualquier momento, queremos que esta parte de la aplicación sea flexible y que modificar la misma no sea difícil en caso de que el criterio cambie. Esto por varios motivos:
  • Cuando el criterio cambie el dueño querrá que se haga prácticamente al instante, sin esperar algunos días para que realicemos los cambios necesarios en la aplicación y volvamos a probar todo.
  • En general es una buena práctica el encapsular y aislar esta lógica de creación de los objetos de forma que, si cambia, solo tengamos que hacer ajustes en un archivo de código (o aún mejor, en algún archivo de configuración).
  • No queremos volver a probar toda la aplicación para verificar que el cambio realizado no ha tenido impactos no deseados en otras partes de la aplicación.
Comencemos entonces con el desarrollo de la aplicación.

Lo primero será modelar nuestra tarjeta de lealtad. La única función de la tarjeta será el calcular el descuento de la compra, y este descuento varía en función del tipo de tarjeta. Esto podemos hacerlo de dos formas:
  1. Modelando la tarjeta de lealtad como una clase abstracta con un método abstracto que obtenga el total del descuento.
  2. Modelando la tarjeta como una interface, con un método que obtenga el total del descuento.
Como no tenemos en realidad ningún atributo ni funcionalidad o métodos compartidos por los diferentes tipos de tarjetas, usaré la segunda forma. La clase base (interface base) TarjetaLealtad queda de la siguiente forma:
public interface TarjetaLealtad {
    float calculaDescuento(float totalVenta);
}
Y las implementaciones de la siguiente forma:
public class TarjetaBronce implements TarjetaLealtad {

    @Override
    public float calculaDescuento(float totalVenta) {
        return 0;
    }
}
public class TarjetaPlata implements TarjetaLealtad {

    private static final float PORCENTAJE_DESCUENTO = 5f / 100f; // 5% de descuento

    @Override
    public float calculaDescuento(float totalVenta) {
        return totalVenta * PORCENTAJE_DESCUENTO;
    }
}
public class TarjetaOro implements TarjetaLealtad {

    private static final float PORCENTAJE_DESCUENTO_PRECIO_BAJO = 10f / 100f; // 10% de descuento
    private static final float PORCENTAJE_DESCUENTO_PRECIO_ALTO = 5f / 100f; // 5% de descuento

    private static final float UMBRAL_PRECIO_ALTO = 50f;


    @Override
    public float calculaDescuento(float totalVenta) {
        return totalVenta > UMBRAL_PRECIO_ALTO ? totalVenta * PORCENTAJE_DESCUENTO_PRECIO_ALTO : totalVenta * PORCENTAJE_DESCUENTO_PRECIO_BAJO;
    }
}

Y el cliente queda representado de la siguiente forma (uso algunas anotaciones de Lombok para simplificar la escritura del código):
@Data
@Builder
public class Cliente {
    private int edad;
    private int numeroCompras;
    private float totalCompras;

    private TarjetaLealtad tarjetaLealtad;

    public void compra(float montoCompra) {
        totalCompras += montoCompra;
        numeroCompras++;
    }
}

Ahora sí veremos la implementación de la Estrategia A del patrón.

Sabemos que podemos tener diferentes formas de crear las instancias las tarjetas de lealtad, usando diferentes criterios, por lo que queremos encapsular esa lógica. Vemos nuevamente el diagrama de la Estrategia A:.



La ClaseBase es representada por TarjetaLealtad, y las clases hijas representadas por TarjetaOro, TarjetaPlata y TarjetaBronce.

Lo que haremos ahora será crear la representación de ClaseBaseAbstractFactory. Para ello crearemos una clase abstracta que tendrá un solo método, llamado getTarjetjetaLealtad, el cual recibirá la instancia de Cliente al que asignaremos la tarjeta. Siguiendo la misma lógica que usé para TarjetaLealtad, como esta será una clase abstracta sin atributos ni métodos concretos, solo métodos abstractos, cambiaré de una clase abstracta a una interface:
public interface TarjetaLealtadFactory {
    TarjetaLealtad getTarjetaLealtad(Cliente cliente);
}

El último paso será crear una implementación de esta interface que cumpla con los deseos actuales del dueño; esta será la representación de la FabricaConcreta en el diagrama del patrón. Como el criterio que usaré es el número de compras, llamaré a esta clase "TarjetaLealtadFactoryNumeroCompras":
public class TarjetaLealtadFactoryNumeroCompras implements TarjetaLealtadFactory {

}

Mantendremos el número de compras mínimo para obtener cada tarjeta en constantes para darles un significado más claro:
private static final int MINIMO_COMPRAS_PLATA = 5;
private static final int MINIMO_COMPRAS_ORO = 10;

Finalmente, sobre escribiremos el método getTarjetaLealtad usando la lógica descrita anteriormente:
@Override
public TarjetaLealtad getTarjetaLealtad(Cliente cliente) {

    if (cliente.getNumeroCompras() >= MINIMO_COMPRAS_ORO) {
        return new TarjetaOro();
    }

    if (cliente.getNumeroCompras() >= MINIMO_COMPRAS_PLATA) {
        return new TarjetaPlata();
    }

    return new TarjetaBronce();
}

Y esto es todo. Dependiendo del número de compras del cliente le asignaremos una tarjeta de Oro, Plata o Bronce.

El diagrama de la implementación queda de la siguiente forma:



Ahora probemos que la implementación funciona. Para eso haré uso de pruebas unitarias usando Junit 5 y AssertJ. Primero, el esqueleto de la clase para pruebas unitarias:
public class TarjetaLealtadFactoryNumeroComprasTest {

    TarjetaLealtadFactory tarjetaLealtadFactory = new TarjetaLealtadFactoryNumeroCompras();

}

Comencemos validando que la tarjeta de Bronce se asigna desde la primera compra (cuando el total de compras aún es 0). Para validar que estamos obteniendo la tarjeta de bronce lo haremos de dos formas: la primera comprobando que efectivamente estamos obteniendo una instancia de la clase TarjetaBronce; la segunda es comprobando que no se está otorgando ningún descuento a la compra.
@Test
public void testTarjetaBronce_cuandoComprasEsCero() {

    Cliente cliente = Cliente.builder().edad(15).numeroCompras(0).totalCompras(0).build();
    TarjetaLealtad tarjetaLealtad = tarjetaLealtadFactory.getTarjetaLealtad(cliente);

    final float montoCompra = 25f;

	assertThat(tarjetaLealtad).isOfAnyClassIn(TarjetaBronce.class);
	assertThat(tarjetaLealtad.calculaDescuento(montoCompra)).isEqualTo(0);
}

Ahora, verificaremos que seguimos teniendo la tarjeta de bronce y el descuento de 0% en la cuarta compra:
@Test
public void testTarjetaBronce_cuandoComprasEsCuatro() {

    Cliente cliente = Cliente.builder().edad(16).numeroCompras(4).totalCompras(120f).build();
    TarjetaLealtad tarjetaLealtad = tarjetaLealtadFactory.getTarjetaLealtad(cliente);

    final float montoCompra = 35.5f;

    assertThat(tarjetaLealtad).isOfAnyClassIn(TarjetaBronce.class);
    assertThat(tarjetaLealtad.calculaDescuento(montoCompra)).isEqualTo(0);
}

A partir de la quinta compra debemos tener la tarjeta de plata y un descuento de 5%:
@Test
public void testTarjetaPlata_cuandoComprasEsCinco() {
    Cliente cliente = Cliente.builder().edad(20).numeroCompras(5).totalCompras(155.5f).build();
    TarjetaLealtad tarjetaLealtad = tarjetaLealtadFactory.getTarjetaLealtad(cliente);

    final float montoCompra = 40f;

    assertThat(tarjetaLealtad).isOfAnyClassIn(TarjetaPlata.class);
    assertThat(tarjetaLealtad.calculaDescuento(montoCompra)).isEqualTo(2f);
}

Para la tarjeta de oro, validaremos los dos casos: cuando una compra es menor o igual a $50 y cuando es mayor:
@Test
public void testTarjetaOrocuandoComprasEs10YPrecioEsBajo() {
    Cliente cliente = Cliente.builder().edad(27).numeroCompras(10).totalCompras(255.5f).build();
    TarjetaLealtad tarjetaLealtad = tarjetaLealtadFactory.getTarjetaLealtad(cliente);

    final float montoCompra = 40f;

    assertThat(tarjetaLealtad).isOfAnyClassIn(TarjetaOro.class);
    assertThat(tarjetaLealtad.calculaDescuento(montoCompra)).isEqualTo(4f);
}

@Test
public void testTarjetaOrocuandoComprasEs10YPrecioEsAlto() {
    Cliente cliente = Cliente.builder().edad(27).numeroCompras(10).totalCompras(300f).build();
    TarjetaLealtad tarjetaLealtad = tarjetaLealtadFactory.getTarjetaLealtad(cliente);


    final float montoCompra = 80f;

    assertThat(tarjetaLealtad).isOfAnyClassIn(TarjetaOro.class);
    assertThat(tarjetaLealtad.calculaDescuento(montoCompra)).isEqualTo(4f);
}

Y listo, con esto podemos comprobar que la lógica de generación de tarjetas y de cálculo de descuentos funciona correctamente (si quieres, puedes generar una clase main para hacer alguna comprobación adicional).

¿Qué ocurre si el dueño decide que el número de compras no fue el criterio correcto para seleccionar la tarjeta de lealtad que se le otorgará al cliente? Fácil, solo tenemos que crear una nueva implementación de TarjetaLaltadFactory que contenga la nueva lógica de creación de tarjetas.

¿Podemos simplemente modificar la lógica que ya existe en TarjetaLealtadFactoryNumeroCompras? Sí... y no.

Explico la respuesta anterior. No hay algo que impida que hagamos una modificación de la clase ya existente, salvo las recomendaciones de los expertos. Existe una serie de guías y principios de diseño, de los cuales hablaré en otro tutorial, que indican que una vez que una clase está terminada y funcionando, esta no debería de ser modificada a menos que sea para corregir errores o realizar mejoras. El hacer un cambio completo de la lógica no es ni una corrección de errores ni una mejora. Es un algoritmo diferente que se usará para conseguir el mismo fin, obtener una tarjeta de lealtad, pero con un criterio diferente. Esto es una simplificación de los problemas reales que tenemos cuando empezamos a mover y a cambiar cosas de esta forma. Recuerda que además ya tenemos una serie de pruebas que validan el funcioamiento correcto de este algoritmo.

Por lo tanto, es mejor y más flexible crear una nueva clase con la nueva lógica, pero que implemente la misma interface.

¿Qué ocurre si el dueño decide que la nueva forma es aún peor que la anterior y quiere que regresemos de inmediato a al proceso anterior? Si simplemente modificamos la clase existente, no podremos hacer esto.

Por lo tanto, la implementación de este patrón ofrece una serie de ventajas en este caso.

Ocurre lo mismo con las estrategias B y C. No entraré en detalles de estas estrategias, pero creo que es importante explicar un poco lo que verás en el código.

En la Estrategia B, uso una Factory Method para Computadora. Tenemos dos tipos de computadoras: Laptop y Servidor, lo que queremos es obtener nuevas instancias de alguna de los dos tipos, pero con diferentes características de memoria RAM, velocidad del procesador, y Sistema Operativo. La fábrica se encargará de crear e inicializar la Computadora del tipo correspondiente, establecer los valores de sus atributos, y regresarlas al código que la necesita. Tendremos dos fábricas concretas, una para Laptops y otra para Servidores.

Recordemos que este es el diagrama de la estrategia B:



Y este es el de la implementación particular que estamos realizando.



Para la Estrategia C tengo una clase base abstracta Documento y dos clases derivadas: Reporte y Resumen. Documento sirve como la fábrica y regresará uno de los dos subtipos, de acuerdo con un parámetro proporcionado en el método. Cada tipo de documento contiene diferentes secciones o Paginas.

Este es el diagrama general de la estrategia C:



Y este el de nuestra implementación particular.



Ventajas y Desventajas

  • ➕Separa el código con la lógica de la construcción de objetos que pertenecen a una misma familiar, del código que hace uso de esos objetos y la encapsula en un solo punto. Esto ayuda a tener un bajo acomplamiento entre las clases, lo que beneficia al mantenimiento; si agregamos nuevas subclases, solo debemos agregar la lógica de la creación en un solo lugar.
  • ➕El cliente no necesita conocer los detalles de la subclase que está recibiendo, lo único que necesita conocer es la clase base. Esto ayuda a que proporcionemos una instancia diferente en caso de ser necesario o como parte de una mejora, sin que el código deba sufrir modificaciones por este cambio.
  • ➕Genera un bajo acoplamiento al hacer uso del polimorfismo.
  • ➖Debe ser usada para una familia de objetos. Si las clases no extienden una clase base común no pueden ser creadas usando Factory Method.


Otros nombres

Este patrón también podemos encontrarlo con los siguientes nombres:
  1. Factory (sin el Method).
  2. Simple Factory
  3. Factory Pattern.
  4. Virtual Constructor.
Recuerda que Factory (o Simple Factory) hacen referencia a una variación del patrón que expliqué al inicio del tutorial; pero muchas veces se usan (erróneamente) como sinónimos de Factory Method.



Conclusión

Como pudiste ver, este es un patrón de diseño muy fácil de implementar y muy útil. Al implementar cualquiera de las estrategias que vimos realmente ayuda a mantener el código de una forma más estructurado, flexible y menos acoplado.

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