El patrón adaptador es muy útil cuando necesitamos incluir (adaptar) código adicional a una aplicación que ya está funcionando. Casi siempre, es algún código hecho por algún tercero que necesitamos procesar dentro de nuestra aplicación y retornar un resultado. A veces, es necesario utilizarlo para poder seguir accediendo a ciertas características que ya están siendo consumidas actualmente.
El problema aquí es que el software actual ya está probado y funciona de manera correcta. No deberíamos de hacer cambios en ese código sólo para poder soportar la información de la compañía que queremos procesar. Además, ¿qué pasa si otra compañía también quiere utilizar nuestro software? De nuevo estaríamos en problemas. ¿Cómo podemos soportar código ajeno a nuestro programa sin modificar el código actual?
Para tener una mejor idea, veamos el problema en una imagen.
Para resolver este problema, podemos hacer uso de un adaptador. Ese adaptador entiende cómo funciona nuestro sistema actualmente y además, sabe cómo procesar el código nuevo que es ajeno a nuestra aplicación.
Podemos decir que un adaptador es el intermediario entre nuestro sistema y sistemas fuera de nuestra aplicación.
Como podemos observar en la imagen anterior, el adaptador logra procesar el código que recibe desde “afuera” y lo transforma para que pueda ser entendido por nuestra aplicación.
De esta manera, sólo necesitamos codificar el adaptador y así evitamos:
- Cambiar nuestro código actual.
- Cambiar el código creado por un tercero.
Así es como un cliente hace uso de un adaptador según el libro Head First Design Patterns:
- El cliente realiza una solicitud al adaptador (un método dentro del adaptador).
- El adaptador traduce la solicitud, en una o más solicitudes al código ajeno, a nuestro código actual.
- El cliente recibe los resultados sin saber que en realidad utilizó un adaptador.
Este patrón se puede implementar de dos maneras, una es Adaptador por Objeto y la otra Adaptador por Clases.
Adaptador por Objeto
Se utiliza en lenguajes de programación que no permiten herencia múltiple, como por ejemplo JAVA. Veamos el diagrama de clases según Head First Design Patterns:
En el diagrama anterior, podemos ver que el Adaptador sólo hereda de una interfaz llamada Target.
Adaptador por Clases
Se utiliza en lenguajes que sí permiten herencia múltiple, como por ejemplo C++ o Python. Este sería el diagrama:
En este caso, el adaptador hereda de las clases Target y Adaptee. Aquí, en lugar de utilizar Adaptee por composición, lo utilizamos por medio de herencia.
Head First Design Patterns define el patrón Adaptador de la siguiente forma:
Este patrón, convierte la interfaz de una clase, a otra interfaz que es la esperada por el cliente. Los adaptadores, le permiten trabajar juntas a varias clases que, de otra forma, no podrían debido a las incompatibilidades.
Desarrollemos el ejemplo para este patrón. Pensemos que creamos una aplicación se se encarga de calcular el impuesto de un precio y desplegar el resultado y el porcentaje de impuesto aplicado.
Creamos una interfaz llamada ITaxable con dos métodos: processTax() y getTaxValue(). Esta interfaz nos va a definir el comportamiento de cualquier impuesto en el futuro.
/**
*
* @author Carlos Marin
* Runnable Patterns (runnablepatterns.com)
*
* ITaxable interface used to make taxable classes
*
*/
public interface ITaxable {
/**
* Process the specific tax for the amount received as a parameter
* @param amount The amount to be taxed
* @return The tax value for the amount
*/
public String processTax(double amount);
/**
* Gets the current tax value applied.
* @return The tax applied in the operation
*/
public String getTaxValue();
}
La clase TaxA implementa esta interfaz y los métodos necesarios.
/**
*
* @author Carlos Marin
* Runnable Patterns (runnablepatterns.com)
*
* TaxA class used to create tax objects of type A
*
*/
public class TaxA implements ITaxable {
/**
* Current tax to be applied.
*/
private double taxValue;
/**
* Overloaded constructor used to set the current tax value
* @param _taxValue The tax value to use
*/
public TaxA(double _taxValue) {
this.taxValue = _taxValue;
}
public String processTax(double amount) {
return (amount*this.taxValue) + "";
}
public String getTaxValue() {
return (this.taxValue * 100) + "%";
}
}
Nuestra aplicación se encarga de crear impuestos, calcular los montos y desplegar el porcentaje aplicado.
/**
*
* @author Carlos Marin
* Runnable Patterns (runnablepatterns.com)
*
* Demo class for the Adapter Pattern
*
*/
public class AdapterPatternDemo {
public static void main (String[] args) {
// create taxes
ITaxable taxA = new TaxA(0.13);
ITaxable taxAA = new TaxA(0.1);
// create array to store taxes
ArrayList<ITaxable> taxList = new ArrayList();
// add taxes to list
taxList.add(taxA);
taxList.add(taxAA);
// define the amount to be taxed
double amountToTax = 3500.0;
System.out.println(String.format("Printing tax amounts for: %s \n", amountToTax) );
// iterate in the taxes
for(int index = 0; index < taxList.size(); index++) {
// get tax details
System.out.println(taxList.get(index).processTax(amountToTax));
System.out.println(taxList.get(index).getTaxValue());
System.out.println("***************************");
}
}
}
El resultado de ejecutar el programa es el siguiente.
Ahora bien, nuestra aplicación se vuelve famosa, y un tercero también cuenta con una versión de impuestos, pero no implementa nuestra interfaz. Ese tercero decidió crear una clase que hace algo similar a la que definimos en nuestra aplicación. La idea de esta compañía es utilizar nuestro programa para crear e imprimir impuestos de igual manera que procesamos los nuestros.
Esta compañía definió la siguiente clase que maneja los impuestos.
/**
*
* @author Carlos Marin
* Runnable Patterns (runnablepatterns.com)
*
* TaxClientB class created by a third party company
*
*/
public class TaxClientB {
/**
* Variable used to store current tax value
*/
private double currentTax;
/**
* Constructor to initialize tax value
*/
public TaxClientB() {
this.setTax(0.0);
}
/**
* Get the current tax value
* @return The current tax value
*/
public double getTax() {
return currentTax;
}
/**
* Set the current tax value
* @param currentTax Tax value to be set
*/
public void setTax(double currentTax) {
this.currentTax = currentTax;
}
/**
* Calculates the tax value for the amount received as a parameter
* @param amount The amount to be taxed
* @return The taxed amount
*/
public String calculateTax(double amount) {
return (this.getTax() * amount) + "";
}
}
En esta clase encontramos varias diferencias con la interfaz ITaxable. Por ejemplo, los métodos son diferentes, el método getTaxValue() no esta presente en esta clase. El método processTax() de nuestro código, es diferente al método calculateTax(), entre otros.
Apliquemos el patrón creando la clase TaxClientBAdapter.
/**
*
* @author Carlos Marin
* Runnable Patterns (runnablepatterns.com)
*
* TaxClientB Adapter class used to transform third party class TaxClientB into a ITaxable object
*
*/
public class TaxClientBAdapter implements ITaxable {
/**
* Defines a composition variable to store 3rd party class
*/
private TaxClientB taxClientB;
/**
* Overloaded constructor to force clients to provide instance of 3rd party class
* @param _taxClientB The 3rd party class used to perform the requests
*/
public TaxClientBAdapter(TaxClientB _taxClientB) {
this.taxClientB = _taxClientB;
}
@Override
public String processTax(double amount) {
// return the tax for the amount using 3rd party class
return this.taxClientB.calculateTax(amount);
}
@Override
public String getTaxValue() {
// return the tax value from the third party class
return (this.taxClientB.getTax() * 100) + "%";
}
}
Los métodos de esta clase en realidad están utilizando los métodos del objeto creado por terceros. Nuestra aplicación no se preocupa por cómo fueron implementados esos métodos y la clase creada por terceros no le interesa quién la utiliza, sólo le concierne que sea utilizada para desplegar la información.
Así sería nuestra aplicación actualizada utilizando el adaptador.
/**
*
* @author Carlos Marin
* Runnable Patterns (runnablepatterns.com)
*
* Demo class for the Adapter Pattern
*
*/
public class AdapterPatternDemo {
public static void main(String[] args) {
// create taxes
ITaxable taxA = new TaxA(0.13);
ITaxable taxAA = new TaxA(0.10);
// create 3rd party class
TaxClientB taxBClient = new TaxClientB();
// set the tax
taxBClient.setTax(0.05);
// wrap the 3rd party class into the adapter class
ITaxable taxB = new TaxClientBAdapter(taxBClient);
// create array to store taxes
ArrayList<ITaxable> taxList = new ArrayList();
// add taxes to list
taxList.add(taxA);
taxList.add(taxAA);
taxList.add(taxB);
// define the amount to be taxed
double amountToTax = 3500.0;
System.out.println(String.format("Printing tax amounts for: %s \n", amountToTax) );
// iterate in the taxes
for(int index = 0; index < taxList.size(); index++) {
// get tax details
System.out.println(taxList.get(index).processTax(amountToTax));
System.out.println(taxList.get(index).getTaxValue());
System.out.println("***************************");
}
}
}
Al utilizar la clase TaxClientB, si tratáramos de agregarla a la lista directamente, nos daría un error de compilación. Es decir, tendríamos que crear una nueva lista sólo para poder manejar esta clase, además de duplicar el código que se encarga de desplegar la información.
El patrón Adaptador nos soluciona este problema al utilizar TaxClientBAdapter, que hereda de ITaxable.
Este sería el diagrama de clases de este ejemplo.
Al ejecutar de nuevo nuestra aplicación, el resultado es el siguiente.
Así de simple, por medio de Adaptadores, podemos adoptar código externo en nuestras aplicaciones sin tener que modificar los procesos actuales, que ya fueron probados y sabemos que están funcionando correctamente.
El código de ejemplo del patrón adaptador lo pueden descargar aquí.
En el próximo post vamos a analizar el patrón Fachada.
Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.
Referencias:
Libro Head First Design Patterns. Versión digital.