20 de enero de 2022

Patrón de diseño Adapter



Adapter es un patrón de diseño estructural. Este es el único patrón que tiene los dos ámbitos, clases y objetos, ya que podemos implementarlos de dos formas diferentes. Este patrón se utiliza cuando tenemos dos elementos de una aplicación los cuales necesitamos que trabajen juntos, pero sus interfaces de comunicación no son compatibles. Para esos casos creamos una clase intermedia que facilita la comunicación entre ambas. Así, Adapter servirá como puente entre ambos elementos.

Algunas personas describen a este patrón con la frase: "Obtener la interface que quieres a partir de la interface que tienes" o "proporcionarle al cliente un objeto con la interface que necesita".

En este tutorial se mostraré la funcionalidad de este patrón, junto con dos estrategias diferentes de implementación, una para el ámbito de clases y otro para el ámbito de objetos.

Adapter facilita la reutilización de código, ya que adapta código existente dentro de una nueva interface, la cual muy probablemente era desconocida al momento de diseñar el código original. La idea es tomada directamente de los adaptadores de hardware donde tenemos un dispositivo con un tipo de puerto, digamos USB-C, y queremos conectarlo con otro elemento que tiene un tipo de conexión diferente, digamos micro USB. ¿Qué hacemos en ese caso? Usamos un tercer elemento (sí, un adaptador) que ayuda a conectar los dos elementos de forma que puedan entenderse. Internamente tal vez simplemente envíe directamente los datos, pero también es posible que realice algún tipo de conversión o modificación de estos.

Así, creamos una nueva clase que es responsable de unir las funcionalidades de dos componentes independientes o incompatibles.

Este patrón se usa mucho cuando trabajamos con código legado, ya que al adaptar el código existente (el cual probablemente ya ha sido probado y existe en producción desde hace algún tiempo) podemos usarlo con nuevas funcionalidades.


Objetivo o intención del patrón

Como podemos ver, el objetivo de este patrón es:
  • Adaptar la interface de una clase a la interface que espera el cliente (asumiendo que ambas interfaces son diferentes).
  • Permitir que clases con interfaces incompatibles trabajen juntas.

Cuando hablamos de "interfaces", nos referimos a los elementos o firmas que una clase expone para que otras puedan utilizarla. Esta es una palabra que leeras mucho en este tutorial.

Implementación

Antes de ver los detalles en código, un poco de teoría de cómo implementar el patrón.

Para este patrón debemos tener ciertos prerrequisitos:
  1. Debemos tener un Cliente, el cual delegue alguna funcionalidad importante a un objeto diferente. El Cliente debe tener una referencia al objeto usando un tipo abstracto (una interface o clase abstracta). A esta interface o tipo abstracto se le da el nombre de Target. Sé que esto suena muy general pero quedará claro con el ejemplo.
  2. Tener un objeto que implemente una funcionalidad, o tenga información, que pueda ser de interés para el Cliente, pero que no implemente la interface que este necesita. A este objeto se le da el nombre de Adaptee.

En otras palabras, debemos tener una clase que queramos usar para implementar una funcionalidad, pero esa clase debe tener una interface que no sea compatible.

No te preocupes por los nombres raros. Puedes echar un ojo al diagrama que está más abajo para que queden claros qué son estos objetos y cómo se relacionan entre ellos.

Ahora, implementar el patrón es en realidad muy sencillo.

Para el ámbito de objeto:
  1. Crear una clase que implemente la interface que el Cliente conoce y usa. Esta clase es el Adapter y la interface es el Target.
  2. Colocar en el Adapter una referencia al Adaptee. Esta referencia normalmente se obtiene en el constructor del Adapter.
  3. Delegar las peticiones recibicas por el Adapter hacia el Adaptee. Para esto depende de qué tan compleja sea la funcionalidad que estemos conectando; puede ser llamadas directas o algunas veces será necesario transformar los datos antes de enviarlos.
  4. Regresar al Cliente la respuesta recibida por el Adapter desde el Adaptee. Parecido al punto anterior, algunas veces regresaremos directamente la respuesta y en otras ocasiones habrá que transformarla.

Para el ámbito de clase:
  1. Crear una clase que implemente la interface que el Cliente conoce y usa. Esta clase es el Adapter y la interface es el Target.
  2. Indicar que la clase del Adapter extiende del Adaptee. Si el Adaptee recibe algún parámetro en su constructor, el Adapter debe enviarlo al momento de su construcción.
  3. Delegar las peticiones recibidas por el Adapter a su súper clase (al Adaptee). Para esto depende de qué tan compleja sea la funcionalidad que estemos conectando; puede ser llamadas directas o algunas veces será necesario transformar los datos antes de enviarlos.
  4. El Adapter recibe las respuestas y las regresa al Cliente.

Como puedes ver en ambas descripciones, las implementaciones son muy parecidas, con la excepción del punto 2. En la primera implementación usamos una referencia (composición) y en la segunda extendemos una clase (herencia).

Eso quiere decir que en el ámbito de clase tenemos una restricción importante: El Target debe ser una interface, no podemos usar una clase abstracta o concreta. ¿Por qué? Porque el Adapter debe extender del Adaptee y en Java no se permite la herencia múltiple. Además, el Adaptee no debe ser una clase final, de lo contrario no podremos extenderla.

Aunque también tiene una ventaja, podemos usar el Adapter en cualquier lugar en donde normalmente usamos al Adaptee y/o al Target.


Diagrama

Para este patrón tenemos dos diagramas, uno por cada alcance. Los dos son muy parecidos, pero en la diferencia radica el cambio del alcance.

Explicaré los elementos que participan en el patrón antes de pasar a los diagramas, de esta forma espero que sea más fácil entender las dos versiones. Los elementos que participan en este patrón son:
  • Target: El objeto o clase que tiene la interface que el cliente conoce y utiliza.
  • Adaptee: El objeto o clase existente y cuya funcionalidad queremos utilizar, pero su interface no es la que el cliente conoce o puede manejar.
  • Adapter: La clase u objeto intermedio que usamos para adaptar al Adaptee, para que use la misma interface que el Target.
  • Cliente: Objeto que usa a los objetos con la interface Target. Podemos decir que es el cliente o usuario del patrón.

En el primer diagrama podemos ver el patrón aplicado con el alcance de objeto:


En la imagen, la parte importante es la conexión que existe entre el Adapter y el Adaptee, ya que hacemos uso de la composición de objetos. Aquí, el Adapter tendrá una referencia al Adaptee (una variable declarada del tipo del Adaptee) y delega las llamadas a este. Esto quedará más claro cuando pasemos al ejemplo en código.

El segundo diagrama muestra cómo funciona este patrón en el ámbito de clase:


 



El Adapter y el Adaptee tienen una relación de herencia. Esto quiere decir que el Adapter extiende de la clase del Adaptee. Como puedes imaginar tenemos varias restricciones impuestas por el lenguaje Java. La primera y más importante es que nuestro Adapter solo podrá extender de una clase (el Adaptee), ya que Java no permite la herencia múltiple.

Otra restricción importante es que el Adaptee no debe ser una clase final, de lo contrario no podremos extender de esta. La buena noticia es que si no es posible extender del Adaptee siempre podemos regresar a la implementación mostrada en el primer diagrama.

Este es un patrón algo más variable en su implementación, ya que la funcionalidad y el tamaño del adaptar dependen mucho de la situación particular que estemos... bueno, adaptando. ¿A qué me refiero con esto? En algunos casos el Adapter solo se encargará de tomar las peticiones que el Cliente hace al Target, mandarlas al Adaptee, tomar la respuesta y enviarla de regreso. En ese caso solo funciona como un elemento de paso de mensajes. ¿Pero qué pasa si tiene que hacer alguna conversión? Digamos que tenemos un Adaptee que requiere de información cifrada usando cierto algoritmo propietario y un Target que trabaja con información en plano; en este caso particular el Adapter tendrá la responsabilidad de cifrar los mensajes que recibe del Target, para poder enviarlos al Adaptee y de descifrar las respuestas para poder enviarlas de regreso.

Así que la respuesta a la pregunta de "¿qué tanto debe hacer el Adapter?" es: tanto como sea necesario.

¿Dónde se usa este patrón? Dentro del JDK podemos ver dos ejemplos muy claros, InputStreamReader y OutputStreamWriter. En el primer caso podemos ver que InputStreamReader ayuda a conectar Clientes que requieren de una fuente de datos de tipo de carácter, con fuentes de datos de tipo bytes. InputStreamReader (nuestro Adapter) extiende de Reader (nuestro Target) y recibe en su constructor un InputStream.


 



El diagrama de InputStreamReader es parecido el siguiente, por lo que podemos ver que su implementación tiene el ámbito de objeto.


 




Ejemplo

Para los ejemplos uso Java 17 y Gradle. No uso ninguna característica particular de esta versión de Java, por lo que debe funcionar en cualquier versión (incluso en la 5). También, uso Lombok para evitar escribir código repetitivo de forma innecesaria.

Para validar el correcto funcionamiento de la aplicación usaré JUnit y AssertJ. Agrega las siguientes dependencias en el archivo build.gradle (si usas Maven, coloca las dependencias correspondientes en el archivo pom.xml).

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.22'
    annotationProcessor 'org.projectlombok:lombok:1.18.22'
 
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testImplementation 'org.assertj:assertj-core:3.21.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

Ahora sí, al código.

El problema que resolveremos es el siguiente: Tenemos una clase que se encarga de colorear los fondos de las imágenes, usando un color predeterminado. El funcionamiento de esta clase es muy sencilla, recibe la información del color que usará usando el formato RGB, toma ese color y colorea el fondo de la imagen. Para indicar el color hemos creado una interface RgbColor, la cual tiene un método para regresar cada uno de sus componentes (red, green, y blue).

Queremos ofrecerles a nuestros clientes la gama de colores más amplia posible, y hemos encontrado una librería (gratuita) con millones de colores, así que hemos decidido usar esa librería. Sin embargo existe un pequeño problema: esta librería no tiene la información de los colores en RGB, sino en hexadecimal.

Como no queremos modificar la clase original que colorea los fondos para adaptarla a que use los colores el hexadecimal (ya que además de ser una mala práctica podría romper algunas partes del sistema que ya se encuentran en funcionamiento, y tendríamos que volver a probar el funcionamiento completo del mismo) uno de nuestros mejores programadores ha sugerido una solución simple y elegante: usar el patrón de diseño Adapter (¡Qué conveniente!).

Presentaré dos implementaciones para este mismo problema, una con el ámbito de objeto, y una con el ámbito de clase. Como las diferencias son realmente pocas, daré la explicación usando la implementación del ámbito de objeto y posteriormente hablaré de las modificaciones necesarias para implementar el ámbito de clase.

Lo primero que haremos es definir la interface RgbColor. Esta interface representa al Target en el diagrama del patrón.
public interface RgbColor {
    int getRed();

    int getGreen();

    int getBlue();
}

Ahora, la clase que hará uso de RgbColor para colorear el fondo de las imágenes. Para este ejemplo, la clase cuenta con un método colorea, que recibe el objeto RgbColor, y regresa una cadena con la información de cada uno de los elementos obtenidos de la instancia anterior. Esta clase representa al Client.
public String colorea(RgbColor color) {
    return String.format("%d,%d,%d", color.getRed(), color.getGreen(), color.getBlue());
}

La clase que representa los colores de la librería ficticia que hemos decidido usar, usa los colores en formato hexadecimal. La clase recibe en su constructor la información del color que representa como un entero en formato hexadecimal, y contiene un solo método getter que regresa este mismo color. Una nota sobre la siguiente clase: el constructor y el método getter los estoy generando de forma explícita para que este ejemplo sea más fácil de entender, pero es mejor usar Lombok para generar ambos de manera automática.
public class HexColor {
    private final int colorCode;

    public HexColor(int colorCode) {
        this.colorCode = colorCode;
    }

    public int getColorCode() {
        return colorCode;
    }
}

Ahora crearemos el Adapter; la clase del Adapter se llama HexColorAdapter y, como lo indica el patrón, implementará la interface RgbColor:
public class HexColorAdapter implements RgbColor {

}

En la estrategia utilizada para el ámbito de objeto, HexColorAdapter recibe una referencia a un objeto de tipo HexColor, y delegará las peticiones recibidas por ColoreadorFondos a esta instancia. La forma más común de pasarle esta instancia es a través del constructor, de esta forma:
public class HexColorAdapter implements RgbColor {
    private final HexColor hexColor;

    public HexColorAdapter(HexColor hexColor) {
        this.hexColor = hexColor;
    }
}

Y ahora podemos implementar los métodos de RgbColor usando el código de color obtenido de la instancia de HexColor. El cómo hacer esta conversión es un método estándar, pero no te preocupes por los detalles ya que no son importantes para entender el patrón:
@Override
public int getRed() {
    return (hexColor.getColorCode() & 0xFF0000) >> 16;
}

@Override
public int getGreen() {
    return (hexColor.getColorCode() & 0xFF00) >> 8;
}

@Override
public int getBlue() {
    return (hexColor.getColorCode() & 0xFF);
}

La clase HexColorAdapter completa queda de la siguiente forma:
public class HexColorAdapter implements RgbColor {
    private final HexColor hexColor;

    public HexColorAdapter(HexColor hexColor) {
        this.hexColor = hexColor;
    }

    @Override
    public int getRed() {
        return (hexColor.getColorCode() & 0xFF0000) >> 16;
    }

    @Override
    public int getGreen() {
        return (hexColor.getColorCode() & 0xFF00) >> 8;
    }

    @Override
    public int getBlue() {
        return (hexColor.getColorCode() & 0xFF);
    }
}

Para validar que todo está funcionando de manera adecuada creamos una prueba unitaria, la prueba se encargará de validar que el color recibido en hexadecimal corresponda al color esperado en RGB. Lo primero que hacemos es crear una instancia de HexColor, pasándole como parámetro el código del color que deseamos. Iniciemos con una prueba simple: el color negro, en hexadecimal se representa como seis 0s:

HexColor colorNegro = new HexColor(0x000000);

Lo siguiente es crear una instancia del Adapter. Recuerda que esta instancia recibe en su constructor la instancia de HexColor que acabamos de crear:
HexColorAdapter hexColorAdapter = new HexColorAdapter(colorNegro);

Y con esta instancia ya podemos usar el método colorea de la clase ColoreadorFondos:
ColoreadorFondos coloreadorFondos = new ColoreadorFondos();
String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

Validemos que el color obtenido al final corresponde con negro en RGB, en donde los tres componentes deben estar en 0:
assertThat(colorFondo).isEqualTo("0,0,0");

El método de prueba completo queda de la siguiente forma:
@Test
void testColorRgbCuando_colorNegro() {
    HexColor colorNegro = new HexColor(0x000000);

    hexColorAdapter = new HexColorAdapter(colorNegro);

    String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

    assertThat(colorFondo).isEqualTo("0,0,0");
}

Si ejecutas la prueba puedes comprobar que esta se ejecuta de forma correcta, lo que quiere decir que el Adapter funciona.

Agregaré las validaciones algunos colores adicionales solo para estar seguro y refactorizaré el código:
class ColoreadorFondosTest {

    private ColoreadorFondos coloreadorFondos = new ColoreadorFondos();
    private HexColorAdapter hexColorAdapter;

    @Test
    void testColorRgbCuando_colorNegro() {
        HexColor colorNegro = new HexColor(0x000000);

        hexColorAdapter = new HexColorAdapter(colorNegro);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("0,0,0");
    }

    @Test
    void testColorRgbCuando_colorBlanco() {
        HexColor colorBlanco = new HexColor(0xFFFFFF);

        hexColorAdapter = new HexColorAdapter(colorBlanco);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("255,255,255");
    }

    @Test
    void testColorRgbCuando_colorVerde() {
        HexColor colorVerde = new HexColor(0x00FF00);

        hexColorAdapter = new HexColorAdapter(colorVerde);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("0,255,0");
    }

    @Test
    void testColorRgbCuando_colorMorado() {
        HexColor colorMorado = new HexColor(0xC678AA);

        hexColorAdapter = new HexColorAdapter(colorMorado);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("198,120,170");
    }
}

Si ejecutas los métodos de prueba anteriores, todos deben terminar con un resultado exitoso.

El diagrama de esta implementación queda de la siguiente forma:


 



Ahora, veamos qué cambios tenemos que hacer de la clase HexColorAdapter en el ámbito de clases.

Cuando implementamos este patrón usando esta estrategia, el Adapter extiende de la clase Adaptee, que en este caso es HexColor. También recuerda que HexColorAdapter debe implementar la interface RgbColor, por lo que la declaración de la clase queda de la siguiente forma:
public class HexColorAdapter extends HexColor implements RgbColor {

}

HexColor espera en su constructor el código del color en hexadecimal, así que debemos recibir este como parámetro en el constructor de HexColorAdapter y pasarlo al constructor de HexColor:
public HexColorAdapter(int colorCode) {
    super(colorCode);
}

El resto de la implementación es muy parecido a la que hicimos en la implementación anterior; así que la clase HexColorAdapter completa queda así:
public class HexColorAdapter extends HexColor implements RgbColor {

    public HexColorAdapter(int colorCode) {
        super(colorCode);
    }

    @Override
    public int getRed() {
        return (getColorCode() & 0xFF0000) >> 16;
    }

    @Override
    public int getGreen() {
        return (getColorCode() & 0xFF00) >> 8;
    }

    @Override
    public int getBlue() {
        return (getColorCode() & 0xFF);
    }
}

Para terminar, validemos que todo sigue funcionando de forma correcta. Las pruebas unitarias son muy parecidas a las anteriores, con la diferencia de que ahora HexColorAdapter recibe directamente el color en hexadecimal en su constructor. Quiero que veas algo en la línea 3 del siguiente bloque de código, y es que la variable la estamos declarando como un tipo RgbColor (el tipo esperado por ColoreadorFondos). La prueba del color negro queda de la siguiente forma:
@Test
void testColorRgbCuando_colorNegro() {
    RgbColor hexColorAdapter = new HexColorAdapter(0x000000);

    ColoreadorFondos coloreadorFondos = new ColoreadorFondos();
    String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

    assertThat(colorFondo).isEqualTo("0,0,0");
}

Y la clase completa de la prueba queda así:
class ColoreadorFondosTest {
    private ColoreadorFondos coloreadorFondos = new ColoreadorFondos();
    private RgbColor hexColorAdapter;

    @Test
    void testColorRgbCuando_colorNegro() {
        hexColorAdapter = new HexColorAdapter(0x000000);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("0,0,0");
    }

    @Test
    void testColorRgbCuando_colorBlanco() {
        hexColorAdapter = new HexColorAdapter(0xFFFFFF);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("255,255,255");
    }

    @Test
    void testColorRgbCuando_colorVerde() {
        hexColorAdapter = new HexColorAdapter(0x00FF00);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("0,255,0");
    }

    @Test
    void testColorRgbCuando_colorMorado() {
        hexColorAdapter = new HexColorAdapter(0xC678AA);

        String colorFondo = coloreadorFondos.colorea(hexColorAdapter);

        assertThat(colorFondo).isEqualTo("198,120,170");
    }
}

Al ejecutar las pruebas anteriores podemos validar que todo funciona de manera correcta, por lo que ambas implementaciones son exitosas.

El diagrama de la implementacion queda de la siguiente forma:


 




Ventajas y desventajas

  • ➕Permite utilizar código nuevo en sistemas existentes, agregando nuevas funcionalidades sin romper las que ya existen.
  • ➕Al tener dos ámbitos diferentes podemos usar el que más nos convenga dependiendo del problema que queremos resolver.


Otros nombres

  • Wrapper


Conclusión

Al igual que el resto de los patrones que hemos visto hasta ahora, Adapter aumenta mucha la flexibilidad de una aplicación al permitir conectar diferentes objetos a través de un tercer elemento, con lo cual evitamos modificar o alterar el código ya existente (lo cual es una excelente práctica).

Es el único patrón de diseño de los 23 definidos por la GoF que tiene tanto el ámbito de objeto como el de clase, lo que le da aún más flexibilidad al momento de usarlo en nuestras aplicaciones.

Como nota adicional, en el repositorio dejo, además del código, el archivo original en draw.io con los esquemas de los patrones.

Espero que este tutorial les sea de utilidad. Si tienen alguna duda, sugerencia, comentario o aclaración, pueden dejarla en la sección de comentarios o enviar un correo a programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También puedes seguir JavaTutoriales en las siguientes redes sociales: Saludos y gracias.

Descarga los archivos de este tutorial desde mi repositorio en GitHub:
Entradas relacionadas: