Patrón de diseño de método de fábrica en Java

El patrón de método de fábrica es un patrón de diseño de creación utilizado en lenguajes OO. En este artículo, nos sumergiremos en la teoría y la implementación de The Factory Method/Template.

Introducción

Los patrones de diseño son una colección de metodologías de programación utilizadas en la programación del día a día. Representan soluciones a algunos problemas comunes en la industria de la programación, que tienen soluciones intuitivas.

Tarde o temprano, un programa de escritorio, una aplicación móvil o algún otro tipo de software inevitablemente se volverá complejo y comenzará a presentar ciertos tipos de problemas. Estos problemas suelen estar relacionados con la complejidad de nuestro código base, la falta de modularidad, la incapacidad de separar ciertas partes entre sí, etc.

Por esta razón, los patrones de diseño se han convertido en el estándar de facto en la industria de la programación desde su uso inicial hace algunas décadas debido a su capacidad para resolver muchos de estos problemas. En este artículo, profundizaremos en una de estas metodologías, a saber, el Patrón de método de fábrica.

Patrones de diseño creativo {#patrones de diseño creativo}

El patrón Factory Method es uno de varios Patrones de diseño creacional que usamos a menudo en Java. Su propósito es hacer que el proceso de creación de objetos sea más simple, más modular y más escalable.

Estos patrones controlan la forma en que definimos y diseñamos los objetos, así como también cómo los instanciamos. Algunos encapsulan la lógica de creación lejos de los usuarios y manejan la creación (Factory y Abstract Factory; ), algunos se centran en el proceso de construcción de los propios objetos (Constructor), algunos minimizan el costo de creación ([Prototipo](/patrones-de-diseño-creativo- in-java/)) y algunos controlan el número de instancias en toda la JVM (único).

Específicamente, Factory Method y Abstract Factory son muy comunes en el desarrollo de software Java.

El patrón del método de fábrica {#el patrón del método de fábrica}

El Patrón de método de fábrica (también conocido como Constructor virtual o Patrón de plantilla de fábrica) es un patrón de diseño de creación utilizado en lenguajes orientados a objetos.

La idea principal es definir una interfaz o clase abstracta (una fábrica) para crear objetos. Aunque, en lugar de instanciar el objeto, la instanciación se deja a sus subclases.

Cada objeto se crea a través de un método de fábrica disponible en la fábrica, que puede ser una interfaz o una clase abstracta.

Si la fábrica es una interfaz, las subclases deben definir sus propios métodos de fábrica para crear objetos porque no hay una implementación predeterminada.

Si la fábrica es una clase, las subclases pueden usar la implementación existente u, opcionalmente, anular los métodos de la fábrica.

Con Factory Pattern, la lógica de creación de objetos está oculta para el cliente. En lugar de conocer la clase de objeto exacta e instanciarla a través de un constructor, la responsabilidad de crear un objeto se aleja del cliente.

El cliente puede entonces crear objetos a través de una interfaz común que simplifica el proceso.

Este enfoque separa la creación de objetos de la implementación, lo que promueve un acoplamiento flexible y, por lo tanto, facilita el mantenimiento y las actualizaciones.

Motivación

Después de una introducción teórica, veamos el patrón de fábrica en la práctica.

Imagina que estamos tratando de construir nuestra propia nave espacial. Dado que este es un ejemplo simplificado, también simplificaremos la construcción y diremos que nuestra nave espacial consiste en un casco, un ‘Motor’ y un ‘Dish’ satelital:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class SpaceshipHangar {
    public Spaceship createSpaceship() {
        Spaceship ship = new Spaceship();
        Engine engine = new SublightEngine();
        Dish dish = new RoundDish();

        ship.setEngine(engine);
        ship.setDish(dish);

        return ship;
    }
}

Nota: SublightEngine y RoundDish son subclases de Engine y Dish, respectivamente.

Ahora imagina que le mostraste tu nueva nave espacial a un amigo y, de repente, él también quiere una nave espacial propia. Pero en lugar del SublightEngine quieren poner un HyperdriveEngine, y en lugar del RoundDish quieren poner un SquareDish:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class SpaceshipHangar {
    public Spaceship createSpaceship() {
        Spaceship ship = new Spaceship();
        Engine engine = new HyperdriveEngine();
        Dish dish = new SquareDish();

        ship.setEngine(engine);
        ship.setDish(dish);

        return ship;
    }
}

Dado que las instancias están codificadas, puede crear un duplicado de su método original o cambiar su código.

Si duplica el método cada vez que alguien más quiere hacer una pequeña modificación en la nave, esto se convierte rápidamente en un problema porque tendrá muchos métodos casi idénticos con una diferencia mínima.

Si cambia el código original, entonces el método en sí mismo pierde el punto porque necesita ser reescrito cada vez que alguien quiere hacer un pequeño cambio en el barco.

Esto continúa a medida que agrega más variaciones relacionadas de una colección lógica, por ejemplo, todas las naves espaciales.

Implementación

Para resolver este problema, podemos crear una Fábrica de naves espaciales y dejar los detalles (qué motor o plato se usa) a las subclases para que los definan.

En lugar de codificar la creación de objetos en el método createSpaceship() con operadores nuevos, crearemos una interfaz Spaceship y la implementaremos a través de un par de clases concretas diferentes.

Luego, usando una SpaceshipFactory como nuestro punto de comunicación con estos, crearemos instancias de objetos del tipo Spaceship, aunque implementados como clases concretas. Esta lógica permanecerá oculta para el usuario final, ya que especificaremos qué implementación queremos a través del argumento pasado al método SpaceshipFactory utilizado para la creación de instancias.

Comencemos con la interfaz Spaceship:

1
2
3
4
public interface Spaceship {
    void setEngine(Engine engine);
    void setDish(Dish dish);
}

Ya que estamos trabajando con las clases Engine y Dish, definámoslas rápidamente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Engine {
    private String model;

    public Engine(String model) {
        this.model = model;
    }

    // Getters and Setters
}

public class Dish {
    private String model;

    public Dish(String model) {
        this.model = model;
    }

    // Getters and Setters
}

Y ahora, implementemos la interfaz a través de dos implementaciones concretas, comenzando con SpaceshipMk1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SpaceshipMk1 implements Spaceship {
    private Engine engine;
    private Dish dish;

    public SpaceshipMk1(Engine engine, Dish dish) {
        this.engine = engine;
        System.out.println("Powering up the Mk.1 Raptor Engine");

        this.dish = dish;
        System.out.println("Activating the Mk.1 Satellite Dish");
    }

    @Override
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    @Override
    public void setDish(Dish dish) {
        this.dish = dish;
    }
}

Y el SpaceshipMk2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SpaceshipMk2 implements Spaceship {
    private Engine engine;
    private Dish dish;

    public SpaceshipMk2(Engine engine, Dish dish) {
        this.engine = engine;
        System.out.println("Powering up the Mk.2 Raptor Engine");

        this.dish = dish;
        System.out.println("Activating the Mk.2 Satellite Dish");
    }

    @Override
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    @Override
    public void setDish(Dish dish) {
        this.dish = dish;
    }
}

Ahora, en lugar de simplemente instanciarlos como lo haríamos normalmente, vamos a crear una SpaceshipFactory para ellos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class SpaceshipFactory {
    public Spaceship getSpaceship(Engine engine, Dish dish) {
        if (engine.getModel().equals("Mk.2") && dish.getModel().equals("Mk.2")) {
            return new SpaceshipMk2(engine, dish);
        } else if (engine.getModel().equals("Mk.1") && dish.getModel().equals("Mk.1")) {
            return new SpaceshipMk1(engine, dish);
        } else {
            System.out.println("Incompatible models of engine and satellite dish.");
        }
        return null;
    }
}

La fábrica normalmente tiene un único método llamado getTypeName() con los parámetros que desea pasar. Luego, a través de tantas declaraciones if requeridas, verificamos qué clase exacta debe usarse para atender la llamada.

Y con esta fábrica en su lugar, cuando nos gustaría crear una instancia de cualquiera de estas dos clases de naves espaciales, usamos la fábrica:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SpaceshipFactory factory = new SpaceshipFactory();

Engine engineMk1 = new Engine("Mk.1");
Dish dishMk1 = new Dish("Mk.1");

Engine engineMk2 = new Engine("Mk.2");
Dish dishMk2 = new Dish("Mk.2");

Spaceship spaceshipMk1 = factory.getSpaceship(engineMk1, dishMk1);
Spaceship spaceshipMk2 = factory.getSpaceship(engineMk2, dishMk2);
Spaceship spaceshipMkHybrid = factory.getSpaceship(engineMk1, dishMk2);

Aquí, en lugar de usar el operador nuevo para crear una instancia de cualquiera de las naves espaciales, recurrimos a la interfaz común Nave espacial y usamos la construcción de fábrica/creamos una instancia de los objetos. Ejecutar este código produciría:

1
2
3
4
5
Powering up the Mk.1 Raptor Engine
Activating the Mk.1 Satellite Dish
Powering up the Mk.2 Raptor Engine
Activating the Mk.2 Satellite Dish
Incompatible models of engine and satellite dish.

Nota: Idealmente, también tendríamos fábricas para motores y platos, especialmente si tenemos tipos derivados como HyperdriveEngine y SquareDish. Tener múltiples fábricas terminaría con múltiples palabras clave nuevas, lo que va en contra de lo que significa el método de fábrica.

¿Cuál es la solución entonces? ¿No acabamos de hacer una rotonda y terminamos con el mismo problema?

Ahí es donde salta el Patrón de diseño de fábrica abstracta. Es como una fábrica de fábricas que, utilizando el mismo enfoque, instanciaría todas las fábricas relacionadas con naves espaciales con solo una llamada “nueva” al comienzo.

Ventajas y desventajas

Ventajas

  • Permite código débilmente acoplado, lo que hace que los cambios sean menos perjudiciales
  • Fácil de realizar pruebas unitarias y simulacros ya que el código está desacoplado

Contras

  • Hace que el código sea menos legible ya que todo el código de creación de objetos está detrás de una capa de abstracción
  • Si se usa con Abstract Factory Pattern (una fábrica de fábricas), el código rápidamente se vuelve engorroso pero funcional

Conclusión

El método de fábrica y otros patrones de diseño son técnicas probadas y probadas para trabajar. Independientemente de si se usa en proyectos personales o bases de código industriales muy grandes. Ofrecen soluciones inteligentes a algunos problemas comunes y alientan a los desarrolladores y equipos completos a realizar primero el diseño de la arquitectura y luego la programación. Esto casi siempre conduce a un código de mayor calidad en lugar de saltar directamente a la programación.

Es un error pensar que los patrones de diseño son soluciones sagradas para todos los problemas. Los patrones de diseño son técnicas para ayudar a mitigar algunos problemas comunes, inventadas por personas que han resuelto estos problemas en numerosas ocasiones.