Programación Avanzada

Patrones de diseño

Patrón de diseño Método Factoría (Factory Method)

Introducción

Si una clase, cliente, crea instancias de otras, proveedoras, para trabajar con ellas, estamos relacionando estrechamente la clase cliente con las proveedoras.

Esta estrecha relación se llama acoplamiento.

Para que nuestras aplicaciones sean lo más flexibles posible frente a futuros cambios, debemos evitar el acoplamiento entre clases en el momento de creación de los objetos.

Un modo de hacerlo es mediante el uso del patrón de diseño Factory Method.

Bibliografía

Contenidos

  1. Echando un vistazo atrás.
  2. De nuevo, encapsulando lo que varía.
  3. Recapitulemos.
  4. Fábrica parametrizada (el mismo perro distinto collar).
  5. Definición del patrón de diseño Factory Method.
  6. Enumeraciones todo en uno.
  7. Resumen.

Echando un vistazo atrás

¿Recuerdas el patrón de diseño Strategy?

Cuando queríamos trabajar con él hacíamos algo como:

Astronauta astronauta = new Astronauta("Gagarin", 
    new ContadorAscendente());
System.out.println("Soy " + astronauta.getNombre() + 
    ": " + astronauta.cuenta());

astronauta.setContador(new ContadorDescendente());
System.out.println("Soy " + astronauta.getNombre() + 
    ": " + astronauta.cuenta());

Creábamos, con el operador new las instancias de los contadores.

Echando un vistazo atrás

Como hemos comentado alguna vez, que una clase haga un new para obtener una referencia de otra clase, crea una estrecha dependencia entre las dos clases.

El término que se suele utilizar es acoplamiento entre clases.

Como también hemos comentado, en la medida de lo posible, hay que relajar al máximo el acoplamiento entre clases.

Veamos cómo hacerlo en el caso de la creación de objetos.

Echando un vistazo atrás

Recordemos que con el patrón Strategy teníamos una interface, y algunas implementaciones de ella:

public interface Contador {
    String cuenta();
}
public class ContadorAscendente implements Contador {
    @Override public String cuenta() {
        return "Uno, dos, tres, cuatro, cinco, 
            seis, siete, ocho, nueve y diez";
    }
}
public class ContadorDescendente implements Contador {
    @Override public String cuenta() {
        return "Diez, nueve, ocho, siete, seis, 
            cinco, cuatro, tres, dos y uno.";
    }
}

De nuevo, encapsulando lo que varía

¿Qué pasa si queremos que nuestros candidatos cuenten en inglés?

Deberíamos crear nuevas clases que implementen Contador y que sobrescriban el método public String cuenta().

public class ContadorAscendenteIngles implements Contador {
    @Override
    public String cuenta() {
        return "One, two, three, four, five, 
            six, seven, eight, nine and ten";
    }
}

De nuevo, encapsulando lo que varía

De acuerdo, pero, ¿y desde el punto de vista del cliente?

Astronauta astronauta = new Astronauta("Gagarin", 
    new ContadorAscendenteIngles());
System.out.println("Soy " + astronauta.getNombre() + 
    ": " + astronauta.cuenta());

Vaya, tenemos que modificarlo, y utilizar, esta vez, una instancia de ContadorAscendenteIngles.

Intentemos encapsular, en una clase, la tarea de crear las instancias concretas. Recuerda que todas implementan la misma interface.

De nuevo, encapsulando lo que varía

Necesitamos una fábrica que cree las instancias concretas. Definamos una interface con este requerimiento.

public interface Fabrica {
    Contador getContadorAscendente();
    Contador getContadorDescendente();
}

Bien, ahora implementemos una fábrica que me devuelva instancias de contadores en castellano.

De nuevo, encapsulando lo que varía

public class FabricaCastellano implements Fabrica {
    public FabricaCastellano() {
        super();
    }
    @Override
    public Contador getContadorAscendente() {
        return new ContadorAscendente();
    }
    @Override
    public Contador getContadorDescendente() {
        return new ContadorDescendente();
    }
}

Perfecto, esta es la clase que se ocupa de instanciar las clases concretas.

Hemos encapsulado la creación de clases dentro de ella.

De nuevo, encapsulando lo que varía

Probemos la nueva idea:

Fabrica fabrica = new FabricaCastellano();
Contador contador = fabrica.getContadorAscendente();
System.out.println(contador.cuenta());
contador = fabrica.getContadorDescendente();
System.out.println(contador.cuenta());

Uno, dos, tres, cuatro, cinco, seis, siete, ocho, nueve y diez.
Diez, nueve, ocho, siete, seis, cinco, cuatro, tres, dos y uno.

El único new que tenemos es el de creación de la fábrica concreta.

De nuevo, encapsulando lo que varía

¿Y si queremos contadores en inglés?

Debemos crear la fábrica correspondiente.

public class FabricaIngles implements Fabrica {
    public FabricaIngles() {
        super();
    }
    @Override
    public Contador getContadorAscendente() {
        return new ContadorAscendenteIngles();
    }
    @Override
    public Contador getContadorDescendente() {
        return new ContadorDescendenteIngles();
    }
}

De nuevo, encapsulando lo que varía

Desde el punto de vista del cliente:

Fabrica fabrica = new FabricaIngles();
Contador contador = fabrica.getContadorAscendente();
System.out.println(contador.cuenta());
contador = fabrica.getContadorDescendente();
System.out.println(contador.cuenta());

One, two, three, four, five, six, seven, eight, nine and ten.
Ten, nine, eight, seven, six, five, four, three, two and one.

Sólo tenemos que cambiar de fábrica (proveedor)!!!

Recapitulemos

  1. Hemos encapsulado la creación de instancias en una clase.
  2. Podemos tener más de una fábrica para los mismos productos.
  3. Nuestros clientes eligen qué fábrica usar.
  4. Todos los productos se ven bajo lainterface común.

¿Cuantos principios SOLID está siguiendo esta implementación?

Single Responsability: Sí.

Open Close: No.

Liskov Substitution: Sí.

Interface Segregation: Sí.

Dependency Inversion: Sí

Recapitulemos

Fantástico, lo hemos resuelto, el cliente se desentiende de la creación de las instancias, y además, puede elegir entre varios proveedores.

Pero, un detalle de implementación, el cliente debe conocer los métodos para obtener la instancia concreta:

  1. getContadorAscendente();
  2. getContadorDescendente();

Recapitulemos

Supongamos que nuestro cliente es una pequeña aplicación que muestra un menú, solicita al usuario seleccionar un tipo de contador, y finalmente muestra el resultado de contar.

private void ejecuta() {
    int tipo;
    do {
        menu();
        tipo = pideOpcion();
        filtraOpcion(tipo);
    } while(true);
}

Recapitulemos

private void menu() {
    System.out.println("0.- Contador ascendente.");
    System.out.println("1.- Contador descendente.");
}
private void filtraOpcion(int tipo) {
    Contador contador = fabrica.getContadorAscendente();
    switch(tipo) {
        case 0:
            break;
        case 1:
            contador = fabrica.getContadorDescendente();
            break;
    }
    System.out.println(contador.cuenta());....

Si queremos incluir nuevas formas de contar, no nos queda más remedio que abrir nuestra clase cliente para tener en cuenta las nuevas modificaciones.

Recapitulemos

Intentemos que nuestra clase cliente sea más independiente a los cambios de las fábricas.

¿Cómo? Implementando el patrón de diseño como una fábrica parametrizada.

Fábrica parametrizada

Empecemos cambiando la interface Fabrica, para que en vez de una colección de métodos, tenga sólo un método que reciba el tipo del objeto que queremos crear:

public interface FabricaParametrizada {
    Contador getContador(TipoContador tipo);
}

Y TipoContador es una enumeración. Recuerda lo útiles que fueron las enumeraciones para representar opciones de menú: podíamos añadir nuevas opciones y el cliente de la enumeración veía los cambios sin necesidad de modificaciones.

Fábrica parametrizada

public enum TipoContador {
    ASCENDENTE("Contador ascendente"),
    DESCENDENTE("Contador descendente");
    private String descripcion;
    private TipoContador(String descripcion) {
        this.descripcion = descripcion;
    }
    public static String opciones() {
        StringBuilder sb = new StringBuilder();
        for(TipoContador tipo: values()) 
            sb.append(tipo.ordinal() + ".- " + tipo.descripcion + "\n");
        return sb.toString();
    }
    public static TipoContador enteroATipo(int posicion) {
        return values()[posicion];
    }
}

Fábrica parametrizada

Finalmente, aquí tenemos nuestra fábrica parametrizada:

public class FabricaCastellanoParametrizada 
       implements FabricaParametrizada {
    public Contador getContador(TipoContador tipo) {
        Contador contador = new ContadorAscendente();
        switch (tipo) {
            case DESCENDENTE:
                contador = new ContadorDescendente();
                break;
        }
        return contador;
    }
}

Hemos sustituido los métodos por un switch.

Fábrica parametrizada

¿Qué pinta tiene el cliente?

    private void menu() {
        System.out.println(TipoContador.opciones());
    }
    private void filtraOpcion(TipoContador tipo) {
        Contador contador = fabrica.getContador(tipo);
        System.out.println(contador.cuenta());
    }
    private void ejecuta() {
        TipoContador tipo;
        do {
            menu();
            tipo = pideOpcion();
            filtraOpcion(tipo);
        } while(true);
    }

Fábrica parametrizada

¿Y si tenemos un contador impares-pares?

Sólo tenemos que cambiar la fábrica (y la enumeración).

public class FabricaCastellanoParametrizada implements FabricaParametrizada {
    public Contador getContador(TipoContador tipo) {
        Contador contador = new ContadorAscendente();
        switch (tipo) {
            case DESCENDENTE:
                contador = new ContadorDescendente();
                break;
            case IMPARES_PARES:
                contador = new ContadorImparesPares();
                break;
        }
        return contador;
    }
}

Fábrica parametrizada

public enum TipoContador {
    ASCENDENTE("Contador ascendente"),
    DESCENDENTE("Contador descendente");
    IMPARES_PARES("Contador impares - pares");
    ...
}

El cliente es el mismo:

private void menu() {
    System.out.println(TipoContador.opciones());
}

private void filtraOpcion(TipoContador tipo) {
    Contador contador = fabrica.getContador(tipo);
    System.out.println(contador.cuenta());
}

Fábrica parametrizada

Estupendo!!! Y la fábrica parametrizada en inglés.

Bueno, no está mal como ejercicio ;)

Definición del patrón Factory Method

Define una interfaz para crear un objeto, pero deja que sean las subclases quienes decidan qué clase instanciar. Permite que una clase delegue en sus subclases la creación de objetos.
Patrones de diseño
Erich Gamma et al.

Definición del patrón Factory Method

Este es el diagrama UML de este patrón de diseño.

Definición del patrón Factory Method

En nuestro ejemplo:

Producto → Contador.

ProductoConcreto → ContadorAscendente, ContadorAscendenteIngles, etc.

Fábrica → Fabrica.

FábricaConcreta → FabricaCastellano, FabricaCastellanoParametrizada, etc.

Definición del patrón Factory Method

Y este es el diagrama UML para nuesto ejemplo.

Definición del patrón Factory Method

Ventajas:

  1. La creación de nuevas subclases, hijas de las que ya proporciona la fábrica, es transparente para los clientes.
  2. Conecta jerarquías de clases paralelas.

Enumeraciones todo en uno

Quizás, algo que no te guste es tener, por un lado la enumeración, y por otro la propia fábrica.

La enumeración está encargada de las opciones de menú, y la fábrica de crear las instancias de las clases.

Pero, no tiene sentido una sin la otra.

Tratemos de fusionar la fábrica y la enumeración es una enumeración capaz de devolverme instancias de contadores.

Enumeraciones todo en uno

Empecemos por el caso sencillo, la fábrica devuelve objetos sin estado, como en el caso de los contadores. Dicho de otro modo, los objetos no tiene atributos que pueden cambiar con el tiempo, o directamente no tiene atributos.

Queremos que cada elemento devuelva un objeto:

  • Añadamos un atributo a cada elemento de la enumeración de tipo Contador.
  • Añadamos un método que devuelve ese atributo.

Enumeraciones todo en uno

public enum FabricaEnumeracion {
    ASCENDENTE("Contador ascendente", new Ascendente()),
    SALIR("Salir", new Salir());

    private String descripcion;
    private Contador contador;

    private FabricaEnumeracion(String descripcion, Contador contador) {
        this.descripcion = descripcion;
        this.contador = contador;
    }

    public Contador getContador() {
        return contador;
    }
    .....
}

Enumeraciones todo en uno

La clase Salir es una clase de comodidad, aquí la tienes:

public class Salir implements Contador {
    @Override
    public String cuenta() {
        return "Adios.";
    }
}

Y el bucle del programa:

FabricaEnumeracion opcion;
do {
    menu();
    opcion = pideOpcion();
    filtraOpcion(opcion);
} while(opcion != FabricaEnumeracion.SALIR);

Enumeraciones todo en uno

Y esto es lo bueno, mira a qué queda reducido el método de filtrado:

private void filtraOpcion(FabricaEnumeracion tipo) {
    System.out.println(tipo.getContador().cuenta());
}

Sí, el switch ha desaparecido, recuerda que las enumeraciones nos servían para hacer desaparecer los switch.

Enumeraciones todo en uno

¿Y si las clases que devuelve la enumeración tienen estado y la fábrica debe devolver un nuevo objeto cada vez que se solicita?

Bonito ejercicio ;)

Resumen

El patrón de diseño Factory method nos permite encapsular la creación de objetos.

Desacopla las dependencias entre clases al no aparecer explícitamente el operador new cuando creamos objetos.

Nos permite tener varias fábricas que crean distintos conjuntos del mismo tipo de datos.

Recursos en Internet