Patrón de Diseño Factory

Patrón de diseño Factory Method

El patrón de diseño Factory Method nos permite la creación de un subtipo determinado por medio de una clase de Factoría, la cual oculta los detalles de creación del objeto.

El objeto real creados es enmascarado detrás de una interface común entre todos los objetos que pueden ser creado, con la finalidad de que estos pueden variar sin afectar la forma en que el cliente interactúa con ellos. 

Es normal que un Factory pueda crear varios subtipos de una determinada interface y que todos los objetos concretos fabricados hagan una tarea similar pero con detalles de implementación diferentes. 

La intención del Factory Method es tener una clase a la cual delegar la responsabilidad de la creación de los objetos, para que no sea el mismo programador el que decida que clase instanciará, si no que delegará esta responsabilidad al Factory confiando en que este le regresará la clase adecuada para trabajar.

Lo anterior puede resultado un poco absurdo, pues para que necesitaría el programador ayuda de una clase para crear un objeto, en su lugar puede hacer simplemente un new y listo, tiene la instancia creada, sin embargo, el patrón Factory method se utiliza para casos en los que no se sabe en tiempo de diseño que subtipo vamos a necesitar, y en su lugar, definimos eso en la base de datos, en un archivo de configuración, o simplemente por medio de alguna condición en tiempo de ejecución podemos determinar que clase se utilizará.


Estructura del patrón Factory Method

El siguiente diagrama muestra los componentes que conforman el patrón de diseño:

  • IProduct: Representa de forma abstracta el objeto que queremos crear, mediante esta interface se definen la estructura que tendrá el objeto creado.
  • ConcreteProduct: Representa una implementación concreta de la interface IProduct, la cual es creada a través del ConcreteFactory.
  • AbstractFactory: Este componente puede ser opcional, sin embargo, se recomienda la creación de un AbstractFactory que define el comportamiento por default de los ConcreteFactory.
  • Concrete Factory: Representa una fábrica concreta la cual es utilizada para la creación de los ConcreteProduct, esta clase hereda el comportamiento básico del AbstractFactory.



Secuencia de ejecución

La siguiente sección describe la secuencia de ejecución del patrón

El diagrama se interpreta de la siguiente manera:

  1. El cliente le solicita al ConcreteFactory la creación del ProductA .
  2. El ConcreteFactory localiza la implementación concreta de ProductA y crea una nueva instancia.
  3. El ConcreteFactory regresa el ConcreteProductA creado.
  4. El cliente le solicita al ConcreteFactory la creación del ProductB.
  5. El ConcreteFactory localiza la implementación concreta del ProductB y crea una nueva instancia.
  6. El ConcreteFactory regresa el ConcreteProductB creado.



Ejemplo práctico

Para comprender mejor como funciona el patrón Factory Method analizaremos un ejemplo prácticos con el cual podemos presentarnos alguno de nuestros proyectos, el cual corresponde a una aplicación que se puede conectar a más de una base de datos, sin embargo, la base de datos a utilizar no se sabrá de antemano, si no que por medio de la configuración es como podremos determinar que base de datos estaremos utilizando.

Como vemos, en este caso, no está clara la base de datos que vamos a utilizar, por lo tanto no podemos crear una aplicación que se conecta a una base de datos determinada, si no que tenemos que esperar hasta conocer la configuración para poder establecer la conexión. Una posible solución es crear una serie de IF…ELSE IF para crear la conexión al momento de leer la configuración, sin embargo, esto no es para nada recomendable, pues crearemos una aplicación difícil de mantener y que además revuelva la lógica para determinar la base de datos junto con la lógica propia del sistema.

En su lugar, lo recomendable es crear un Factory Method que nos cree un objeto que nos permita conectarnos a la base de datos, y que este objeto oculte los detalles de implementación, por otro lado, el Factory Method ocultara los detalles de implementación para leer la configuración y crear el objeto adecuado según la configuración, de esta forma, todos los detalles son ocultados al cliente y este solo se preocupa por implementar la lógica.

Antes de comenzar, puedes ver todo el código fuente en GitHub: https://github.com/oscarjb1/blog-patterns-factorymethod


Lo primero será definir la interface que implementarán las clases que se conectarán a la base de datos:

package com.oscarblancarteblog;

import java.sql.Connection;

public interface IDBAdapter {
    public Connection getConnection();
}


Esta interface solo define el método getConnection, la cual deberá regresar una conexión abierta a la base de datos.

El siguiente paso es crear las clases concretas que se conectarán a las bases de datos Oracle, MySQL y SQLServer:

package com.oscarblancarteblog;

import java.sql.Connection;
import java.sql.DriverManager;

public class OracleDBAdapter implements IDBAdapter {
    
    static{
        try {
            //new oracle.jdbc.OracleDriver();
        } catch (Exception e) {
            throw new RuntimeException("Unexpected error on load Oracle Driver");
        }
    }

    @Override
    public Connection getConnection() {
        try {
            String user = ConfigLoader.getPropery("Oracle.user");
            String password = ConfigLoader.getPropery("Oracle.password");
            String host = ConfigLoader.getPropery("Oracle.host");
            String port = ConfigLoader.getPropery("Oracle.port");
            String db = ConfigLoader.getPropery("Oracle.db");

            String url = "jdbc:oracle:thin:@${host}:${port}:${db}"
                    .replace("${host}", host).replace("${port}", port).replace("${db}", db);

            Connection connection = DriverManager.getConnection(url, user, password); //connection to MySQL
            return connection;
        } catch (Exception e) {
            throw new RuntimeException("Oracle connection error " + e.getMessage());
        }
    }
    
}
package com.oscarblancarteblog;

import java.sql.Connection;
import java.sql.DriverManager;

public class MySQLDBAdapter implements IDBAdapter {

    static {
        try {
            new com.mysql.jdbc.Driver();
        } catch (Exception e) {
            throw new RuntimeException("Unexpected error on load MySQL Driver");
        }
    }

    @Override
    public Connection getConnection() {
        try {
            String user = ConfigLoader.getPropery("MySQL.user");
            String password = ConfigLoader.getPropery("MySQL.password");
            String host = ConfigLoader.getPropery("MySQL.host");
            String port = ConfigLoader.getPropery("MySQL.port");
            String db = ConfigLoader.getPropery("MySQL.db");

            String url = "jdbc:mysql://${host}:${port}/${db}?zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=false;"
                    .replace("${host}", host).replace("${port}", port).replace("${db}", db);

            Connection connection = DriverManager.getConnection(url, user, password); //connection to MySQL
            return connection;
        } catch (Exception e) {
            throw new RuntimeException("MySQL connection error " + e.getMessage());
        }
    }

}
package com.oscarblancarteblog;

import java.sql.Connection;
import java.sql.DriverManager;

public class SQLServerAdapter implements IDBAdapter {
    
    static{
        try {
            //new com.microsoft.jdbc.sqlserver.SQLServerDriver();
        } catch (Exception e) {
            throw new RuntimeException("Unexpected error on load SQLServer Driver");
        }
    }

    @Override
    public Connection getConnection() {
        try {
            String user = ConfigLoader.getPropery("SQLServer.user");
            String password = ConfigLoader.getPropery("SQLServer.password");
            String host = ConfigLoader.getPropery("SQLServer.host");
            String port = ConfigLoader.getPropery("SQLServer.port");
            String db = ConfigLoader.getPropery("SQLServer.db");

            String url = "jdbc:sqlserver://${host}:${port};databaseName=${db};"
                    .replace("${host}", host).replace("${port}", port).replace("${db}", db);

            Connection connection = DriverManager.getConnection(url, user, password); //connection to MySQL
            return connection;
        } catch (Exception e) {
            throw new RuntimeException("SQLServer connection error " + e.getMessage());
        }
    }
    
}


Como acabamos de ver, estas clases tiene como única responsabilidad de crear la conexiones a las base de datos y registrar el driver JDBC correspondiente. Adicional, utilizan un archivo de configuración para recuperar los datos de conexión, el cual analizáramos más adelante.

Para recuperar la configuración se utiliza una clase de utilidad llamada ConfigLoader, la cual lee y carga en memoria el archivo dbconfig.properties:

package com.oscarblancarteblog;

import java.io.InputStream;
import java.util.Map;
import java.util.Properties;

public class ConfigLoader {

    private static Properties props;

    static {
        try {
            InputStream stream = ClassLoader.getSystemResourceAsStream("META-INF/dbconfig.properties");
            ConfigLoader.props = new Properties();
            props.load(stream);
        } catch (Exception e) {
            throw new RuntimeException("Faild to load configuration");
        }
    }
    
    public static String getDBType(){
        return props.getProperty("dbtype");
    }
    
    public static String getPropery(String propName){
        return props.getProperty(propName);
    }
    
    
}


El archivos de propiedades se ve de la siguiente manera:

dbtype MySQL
#Values -> MySQL | Oracle | SQLServer


MySQL.user root
MySQL.password 1234
MySQL.host localhost
MySQL.port 3306
MySQL.db factory

Oracle.user sys
Oracle.password 1234
Oracle.host localhost
Oracle.port 1521
Oracle.db factory

SQLServer.user sa
SQLServer.password 1234
SQLServer.host localhost
SQLServer.port 1433
SQLServer.db factory


Finalmente, tenemos la clase DBFactory, la cual se encarga de crear la instancia correcta de la interface IDBAdapter basado en el valor de la propiedad dbtype del archivo dbconfig.properties:

package com.oscarblancarteblog;

public class DBFactory {
    public static IDBAdapter getDBAdapter(){
        String dbType = ConfigLoader.getDBType();
        System.out.println("DBType => " + dbType);
        switch(dbType){
            case "MySQL":
                return new MySQLDBAdapter();
            case "Oracle":
                return new OracleDBAdapter();
            case "SQLServer":
                return new SQLServerAdapter();
            default:
                throw new RuntimeException("Unsupported db type");
        }
    }
}


Como podemos apreciar, esta clase tiene una lógica para determina la clase que deberá de regresar, por su parte, el cliente no tiene por que saber cual es la implementación concreta que creará el Factory, lo que permitiría que en el futuro cambiáramos de implementación sin afectar al cliente.


Probando la solución

Finalmente, ejecutaremos una prueba para comprobar que todo funciona según lo explicado, para eso, creamos la clase Main, la cual se ve de la siguiente manera:

package com.oscarblancarteblog;

import java.sql.Connection;

public class Main {

    public static void main(String[] args) {
        try {
            IDBAdapter adapter = DBFactory.getDBAdapter();
            Connection connection = adapter.getConnection();
            System.out.println("Is Open => " + (!connection.isClosed()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


Al ejecutar este prueba tenemos como resultado la siguiente salida:

DBType => MySQL
Is Open => true


Como vemos, DBType indica que estamos utilizando la conexión a MySQL y al propiedad Is Open nos indica que la conexión se realizo correctamente. Si cambiamos la propiedad dbtype del archivo dbconfig.properties veremos como nos podemos conectar a las demás base de datos.

¿Te gustaría aprender más patrones como este? te invito a que veas mi nuevo libro “Introducción a los patrones de diseño”, donde explico los principales patrones de diseño desde un enfoque práctico y con ejemplos del mundo real. Olvídate de aprender con los ejemplos típicos de Internet, como crear una pizza, clase de animales que ladren o maullen, o figuras geométricas.



Conclusiones

Como hemos pido observar, implementar el patrón de diseño Factory Method es muy simple y otorga grandes ventajas que son fáciles de apreciar, como separar la lógica de creación de objetos en una clase de factoria o la facilidad de variar dinamicamnte la implementación del objeto fabricado sin afectar al cliente.


14 thoughts to “Patrón de Diseño Factory”

  1. Saluton.
    Muy buen aporte, de gran utilidad para aprender la implementación de un patron Factory. En un principio yo tambien comence a utiliar los patrones Factory en conecciones a BD, pero en la actualidad he implementado entity framework, ya no he visto que se utilize la implementación de Factory en conecciones, muchos de mis colegas comentan que requiere tiempo desarrollar algo así, pero si lo he implementado en funcionalidad para usuarios,como control de roles en un sistema… es una buena plicación bueno creo yo.
    Gran aporte compañero Felicidades.

    1. Gracias amigo.
      Este solo es un ejemplo y no es exclusivo para conexiones a base de datos, sin embargo se me hace un ejemplo muy practico para que todos podamos entender de que se trata.
      Con respecto al tiempo de desarrollo, yo creo que el tiempo de desarrollo es algo absurdo cuando los beneficios son mucho mas grandes y es que los patrones de diseño están diseñados para que nuestra aplicación se mucho mas fácil de mantener.

      Saludos

  2. Hola.

    La definición UML del patrón de diseño y y el código no resultan consecuentes, pues una fábrica concreta debe tener una fábrica abstracta que implemente el método definido en la abstracta.

    1. Hola Walter, lo que dices tiene sentido, pues esperas que el código refleja exactamente el diagrama, sin embargo, el factory abstracto es opcional, ya que no es necesario para implementar el patrón. En el diagrama UML se muestra por que es un componente que puede estar, aun que no es una regla.

      El motivo por el cual tenemos un Factory Abstracto, es para poder tener funcionalidad predefinida y reutilizable en caso de que exista más de un Factory.

      Espero que esta respuesta sirva de algo.
      Saludos.

  3. Muy buen ejemplo, aunque ahora me surgen muchas dudas,
    Si quiero realizar un crud por ejemplo: para empleados, la implementación de las consultas para cada motor de bd varía, por ejemplo para llamar a un procedimiento almacenado en mysql, es distinto que en oracle, entonces lo que yo estoy pensando es en crear una clase para cada tipo de conexión y con otra factoría llamar a una clase o otra dependiendo del tipo de conexión que quiera usar:
    . EmpleadoDaoMysql
    . EmpleadoDaoOracle
    :’)

    1. Hola Jerson, en realidad si te apegas al estándar SQL 92 no tendrías que realizar muchos cambios, pues las sentencias son iguales para todos los motores, sin embargo, a medida que generes queys especifico para cada DB, estarás obligado a crear un query diferente para cada DB.

      saludos.

  4. Buena explicación del patrón..
    me surge una duda.. si se aplica un factory para la creación de varios objetos similares (productos, formas geométricas..), pero con algún atributo diferente.. la persistencia como se debe hacer? En una sola tabla con todos los campos(atributos) de todos los objetos, aunque algunos campos estén vacíos en función del tipo de objeto.. o en una tabla diferente por cada tipo de objeto?
    Muchas gracias y un saludo.

    1. Hola María, primero que nada debe de queda en claro que la persisitencia es un tema independiente de la fábrica, pero bueno, regresando a la DB, en realidad lo podrías hacer como quieras, podrías tener tablas con datos de más para cada implementación.

  5. Gracias por el artículo
    Me gustaría saber si este patron es genérico para poderlo usar en todo el proyecto o sólo se ocupa en ciertas circunstancias?
    Gracias de ante mano

    1. Los patrones de diseño pueden ser utilizados en muchas partes, siempre y cuando se adapte al problema que resuelven, también son independientes del lenguaje, por lo que los puedes aplicar en cualquier lenguaje POO

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *