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

P

Introducción

Los patrones de diseño son una colección de metodologías de programación que se utilizan en la programación diaria. Representan soluciones a algunos problemas que ocurren comúnmente 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 se volverá inevitablemente complejo y comenzará a presentar ciertos tipos de problemas. Estos problemas suelen estar relacionados con la complejidad de nuestra base de código, la no modularidad, la incapacidad de separar determinadas 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 creacional

El patrón de método de fábrica es uno de los varios patrones de diseño de creación 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 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 enfocan en el proceso de construcción de los objetos en sí mismos (Builder), algunos minimizan el costo de creación (Prototype) y algunos controlan el número de instancias en el JVM completo (Singleton).

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

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. Sin embargo, 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 fábrica.

Con Factory Pattern, la lógica de creación de objetos se oculta al cliente. En lugar de conocer la clase de objeto exacta y crear una instancia a través de un constructor, la responsabilidad de crear un objeto se aleja del cliente.

Luego, el cliente puede 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, un mantenimiento y actualizaciones más fáciles.

Motivación

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

Imagina que estamos intentando construir nuestra propia nave espacial. Dado que este es un ejemplo simplificado, también simplificaremos la construcción y diremos que nuestra nave espacial consta de un casco, un Enginey un satélite Dish:

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:

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 de forma rígida, 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 el barco, 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í 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, todas las naves espaciales, por ejemplo.

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 definir.

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

Luego, usando un SpaceshipFactory como nuestro punto de comunicación con estos, crearemos una instancia de objetos de Spaceship type, sin embargo, implementado como clases concretas. Esta lógica permanecerá oculta para el usuario final, ya que estaremos especificando qué implementación queremos a través del argumento pasado al SpaceshipFactory método utilizado para la instanciación.

Empecemos con el Spaceship interfaz:

public interface Spaceship {
    void setEngine(Engine engine);
    void setDish(Dish dish);
}

Dado que estamos trabajando con Engine y Dish clases, vamos a definirlas muy rápido:

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 el SpaceshipMk1:

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:

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, creemos un SpaceshipFactory para ellos:

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 método único llamado getTypeName() con los parámetros que le gustaría pasar. Entonces, a través de tantos if declaraciones 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:

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 new operador para instanciar cualquiera de las naves espaciales, recurrimos a la interfaz común Spaceship y usando la fábrica construir / instanciar los objetos. Ejecutar este código produciría:

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 de motores y platos, especialmente si tenemos tipos derivados como HyperdriveEngine y SquareDish. Tener múltiples fábricas terminaría con múltiples new palabras clave, que va en contra de lo que representa el método de fábrica.

Entonces, ¿cuál es la solución? ¿No hemos hecho solo una rotonda y acabamos con el mismo problema?

Ahí es donde interviene el Patrón de diseño de fábrica abstracta. Es como una fábrica de fábricas que, utilizando el mismo enfoque, crearía una instancia de todas las fábricas relacionadas con naves espaciales con un solo new llamar al inicio.

Pros y contras

Pros

  • Permite código acoplado libremente, lo que hace que los cambios sean menos disruptivos
  • 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 se vuelve engorroso pero funcional rápidamente

Conclusión

El método de fábrica y otros patrones de diseño se prueban y se prueban técnicas de trabajo. Independientemente de si se usa en proyectos personales o en bases de código industriales muy grandes. Ofrecen soluciones inteligentes a algunos problemas comunes y animan a los desarrolladores y equipos enteros a hacer 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.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad