Cidenet+Blog+Desarrollo limpio+Abstract Factory

Abstract Factory

¿Qué es?, ¿Qué problema resuelve?

Abstract Factory provee una interfaz para la creación de objetos relacionados o dependientes sin especificar sus clases concretas. Abstract Factory es una especialización del patrón Factory, donde ya no se tiene un Factory que crea un producto concreto dado un parámetro, sino que ya existen múltiples “factories” que crean múltiples productos dado un parámetro. Por ejemplo, una aplicación que cuenta con campos de texto, formularios, etc., y cada uno de estos campos cuenta con diferentes interfaces dependiendo del sistema operativo donde esta corra, en este caso, existirán tantas factories como sistemas operativos existen y habrá un producto por cada elemento de la interfaz por cada factory.

  • Abstract Factory: declara una interfaz para operaciones de creación de productos abstractos.
  • Concrete Factory: implementa las operaciones para crear objetos de productos concretos.
  • Abstract Product: declara una interfaz para un tipo de producto.
  • Concrete Product: define un producto a ser creado por Concrete Factory.
  • Client: usa solo interfaces declaradas por Abstract Factory y Abstract Product.

Cómo aplicar Abstract Factory:

Para aplicar este patrón es importante conocer, en tiempo de diseño, qué objetos tendrá la aplicación, sus relaciones y qué funciones cumplirán cada uno de ellos.

Para este proceso se puede diseñar una matriz donde se puedan relacionar los productos con sus creadores (las factories).

Continuando con el ejemplo de la interfaz gráfica, se puede decir que el programa estará disponible para Windows y Linux. Que la interfaz contará con un campo de texto y con un botón. Se espera que la apariencia de estos campos esté conforme a la del sistema operativo.

De esto, se puede concluir que los productos serán los respectivos campos y que habrá un factory por cada sistema operativo. De este modo, cada factory crea un campo de acuerdo a una plataforma.

Campo de texto Botón
Linux createLinuxTextField() createLinuxButton()
Windows createWindowsTextField() createWindowsButton()

Como ejemplo de código se tiene una aplicación que hace los siguientes procesos:

  • Hacer el proceso de linting a un código específico.
  • Hacer el proceso de darle formato a un código (beautify y uglify).

Se debe tener en cuenta que la aplicación debe realizar este proceso tanto a código Java como JavaScript.
Teniendo esta información se puede realizar la siguiente relación:

 

Creadores / Productos Productos
Linter Formatter
Creadores Java createJavaLinter() createJavaFormatter();
Javascript createJSLinter() createJSLinter();

Crear Abstract Factory:

En el abstract factory se especifican cuáles son los tipos de productos que se desean crear. También se crea el método de selección del factory por el código cliente.

public static AbstractFactory getFactory(EnumPlatform platform) {
   switch (platform) {
       case JAVA:
           return new JavaFactory();
       case JAVASCRIPT:
           return new JavaScriptFactory();

Crear Abstract Product:

Se debe crear un abstract product por cada tipo de producto concreto que haya en la relación. En estas clases se definen las responsabilidades que va a tener el producto. En este caso hay dos Abstract product.

public abstract class Formatter {
   public abstract String beautify(String text);
   public abstract String uglify(String text);
}
public abstract class Linter {
   public abstract String lint(String text);
}

Crear Productos concretos:

Debe existir un producto concreto por cada factory/abstract factory. En este caso al tener 2 factory y 2 productos, se tendrán 4 productos concretos. Es en estas clases donde se crea la lógica por cada plataforma / producto abstracto.

En las siguientes 2 imágenes se muestra una implementación dummy de los métodos. En realidad, en la clase JavaFormatter se debe crear la lógica para embellecer una cadena texto a código Java y hacer el proceso contrario en el método contiguo. En la clase JavaScriptLinter, se debe escribir la lógica para correr el proceso de linting a un código JavaScript. Se debe hacer lo mismo para JavaScriptFormatter y JavaLinter, pero ya escribiendo la lógica para los mismos procesos, pero dependiendo del lenguaje en cuestión.

public class JavaFormatter extends Formatter {
   @Override
   public String beautify(String text) {
       return String.format("%s beautified with Java beautifier", text);
   }

   @Override
   public String uglify(String text) {
       return String.format("%s uglified with Java uglifier", text);
   }
}

public class JavaScriptLinter extends Linter {
   @Override
   public String lint(String text) {
       return String.format("%s linted with JavaScript linter", text);
   }
}

Crear concrete factory:

Finalmente se debe crear un factory por cada plataforma, véase sistema operativo, tipo de usuario, etc. En este caso la plataforma es el lenguaje. Hay un factory para el lenguaje Java y otra para JavaScript. Acá es donde se define qué tipo de producto debe ser instanciado.

Creadores / Productos Productos
Linter Formatter
Creadores Java createJavaLinter() createJavaFormatter();
Javascript createJSLinter() createJSLinter();
Creadores / Productos Productos
Linter Formatter
Creadores Java createJavaLinter() createJavaFormatter();
Javascript createJSLinter() createJSLinter();
public class JavaFactory extends AbstractFactory {
   @Override
   public Formatter createFormatter() {
       return new JavaFormatter();
   }

   @Override
   public Linter createLinter() {
       return new JavaLinter();
   }
}

public class JavaScriptFactory extends AbstractFactory {
   @Override
   public Formatter createFormatter() {
       return new JavaScriptFormatter();
   }

   @Override
   public Linter createLinter() {
       return new JavaScriptLinter();
   }
}

Llamando a los factories desde el cliente:

Gracias al desacoplamiento logrado por la separación de plataformas y productos, el llamado es muy sencillo. Símplemente se debe obtener una instancia del factory concreto que se desea usar y por medio de esta instancia llamar al producto concreto que se espera obtener.

@Test
public void shouldGetJavaFormatter() {
assertEquals("text beautified with Java beautifier", AbstractFactory.getFactory(EnumPlatform.JAVA).createFormatter().beautify("text"));
assertEquals("text uglified with Java uglifier", AbstractFactory.getFactory(EnumPlatform.JAVA).createFormatter().uglify("text"));
}

@Test
public void shouldGetJavaLinter() {
assertEquals("text linted with Java linter", AbstractFactory.getFactory(EnumPlatform.JAVA).createLinter().lint("text"));
}

@Test
public void shouldGetJavaScriptFormatter() {
assertEquals("text beautified with Javascript beautifier", AbstractFactory.getFactory(EnumPlatform.JAVASCRIPT).createFormatter().beautify("text"));
assertEquals("text uglified with Javascript uglifier", AbstractFactory.getFactory(EnumPlatform.JAVASCRIPT).createFormatter().uglify("text"));
}

@Test
public void shouldGetJavaScriptLinter() {
assertEquals("text linted with JavaScript linter", AbstractFactory.getFactory(EnumPlatform.JAVASCRIPT).createLinter().lint("text"));
}

Agregar un nuevo factory:

Suponga que hay un nuevo requerimiento de agregar el lenguaje de Python, con este patrón es muy fácil realizar esta adición. Se debe crear un factory concreto para este nuevo lenguaje

public class PythonFactory extends AbstractFactory {
   @Override
   public Formatter createFormatter() {
       return new PythonFormatter();
   }

   @Override
   public Linter createLinter() {
       return new PythonLinter();
   }
}

Una clase por cada producto concreto:

public class PythonLinter extends Linter {
   @Override
   public String lint(String text) {
       return String.format("%s linted with Python linter", text);
   }
}

Y agregar el nuevo factory en el abstract factory para poder ser usado por el cliente.

public static AbstractFactory getFactory(EnumPlatform platform) {
   switch (platform) {
       case JAVA:
           return new JavaFactory();
       case JAVASCRIPT:
           return new JavaScriptFactory();
       case PYTHON:
           return new PythonFactory();
       default:
           return new DefaultFactory();
   }
}

Conclusiones:

  • Si se puede representar por medio de una matriz relaciones entre plataformas y servicios, esto es un indicio de que se puede aplicar este patrón.
  • La clase de un factory concreto aparece solo una vez en la aplicación, en el abstract factory, que es donde se instancia. Esto hace fácil cambiar el factory concreto que usa una aplicación.
  • Se pueden usar diferentes productos simplemente cambiando el factory concreto, en el ejemplo se puede cambiar de un linter para Javascript a uno de Java simplemente cambiando al factory correspondiente a Java.

El código de ejemplo se puede encontrar en https://bit.ly/3bEn9SZ

Fuentes:
Design Patterns : Elements of Reusable Object-Oriented Software

Contáctanos

Déjanos tus datos

    Medellín - Colombia

  • Calle 47D #72-29
  • (+57) 4 3222567
  • comunicaciones@cidenet.com.co

    Estados Unidos

  • 1200 Colorado Blvd, Denver Colorado 80220
  • (+1) 7723619239
  • jceballos@cidenet.net
WhatsApp