Patrones Estructurales (I): Patrón Adapter (Wrapper)


Objetivo:

Convertir la interfaz de una clase en otra interfaz que el cliente espera. Adapter consigue que trabajen juntas clases que de otro modo no podrían.

Design Patterns: Elements of Reusable Object-Oriented Software

El patrón Adapter nos abre el camino hacia el segundo grupo de patrones propuestos por el Gang of Four: los patrones estructurales. Si bien los patrones de creación definían la forma en la que los objetos son instanciados, los patrones estructurales se basan en la forma en la que un conjunto de clases se relacionan entre sí para proporcionar una funcionalidad compleja, proporcionando una estructura para conseguir lograr ese objetivo.

La filosofía de Adapter, al igual que vimos con Prototype, es tan simple como autoexplicativa: establecer una capa intermedia que permita comunicarse a dos clases que de otro modo no podrían hacerlo, realizando una adaptación de la interfaz de la clase que proporciona el servicio a la que la solicita.

Representándolo de una forma visual, imaginemos que nuestro objeto pertenece a la clase Taladro y requiere hacer uso del método Flujo110V() de una clase que implemente la interfaz IEnchufeIngles. Por tanto, nuestro taladro está preparado para hacer uso de esa interfaz y espera recibir como valor de retorno un flujo con una diferencia de potencial de 110V:

Fuente: wikimedia.org

Sin embargo, resulta que la clase de la que disponemos en el otro extremo no implementa esta interfaz, sino otra llamada IEnchufeEuropeo() que dispone de un método capaz de devolver a nuestro taladro un flujo de 220V, cuyo nombre es Flujo220V(). La funcionalidad es parecida, pero no la esperada, y la interfaz que el taladro espera utilizar no es compatible con el elemento del subsistema. Nos encontramos, por tanto, con un claro problema de compatibilidad.

Fuente: wikimedia.org

Para que nuestro taladro pueda comunicarse con IEnchufeEuropeo, será necesario modificar uno de los dos extremos para que la interfaz de comunicación coincida, pero si hiciéramos esto, nuevamente estaríamos rompiendo el principio abierto/cerrado del que hablamos con anterioridad, amén de que cambiando esta interfaz haríamos que de repente dejasen de compilar todas aquellas clases que la estuvieran utilizando hasta el momento.

La solución adecuada consistirá en adaptar los requerimientos de la interfaz de la clase cliente (taladro) a los servicios que la clase servidora (sistema eléctrico europeo) puede ofrecer. Hablando en plata, y tal como indica el propio nombre del patrón, necesitaremos un adaptador que haga coincidir la entrada de un elemento con la salida del otro. Este será el objetivo del patrón Adapter.

Fuente: wikimedia.org

Por lo tanto, cuando nos encontremos con un problema similar a este, lo más adecuado será codificar una nueva clase que implemente la interfaz original (IEnchufeIngles) y que posea una propiedad cuyo tipo será el de la clase que se pretende adaptar. En este caso, nuestra clase adaptador contendrá una referencia a una instancia de una clase que implemente IEnchufeEuropeo. De este modo, nuestro taladro hará una llamada a la clase que espera (IEnchufeIngles.Flujo110V()) y de forma interna se invocará el método Flujo220V() mediante la instancia de EnchufeEuropeo.

Codificando un Adaptador

Un adaptador no sólo se encarga de transformar una interfaz en otra: también puede realizar otro tipo de operaciones, principalmente de transformación. En el ejemplo de los enchufes, además de transformar una interfaz en otra (los polos del enchufe), nuestro adaptador podía también realizar otras operaciones, como por ejemplo transformar la diferencia de potencial del flujo de salida de 220 a 110, ya que de lo contrario correríamos el riesgo de quemar nuestro dispositivo.

Comencemos proporcionando las interfaces IEnchufeIngles e IEnchufeEuropeo y dos clases que las implementen:

IEnchufeIngles


    public interface IEnchufeIngles
    {
        // Devuelve un array de enteros con un voltaje de unos 110V
        int[] Flujo110V();

        // Devuelve el numero de bornes del enchufe
        int getNumeroBornes();
    }

EnchufeBritanico


    public class EnchufeBritanico : IEnchufeIngles
    {
        #region IEnchufeIngles Members

        // Devuelve un array con voltajes en distintos momentos
        public int[] Flujo110V()
        {
            int[] arrayFlujo = new int[100];
            Random r = new Random();
            for (int i = 0; i < arrayFlujo.Length; i++)
            {
                // Calculamos la fluctuacion del voltaje
                int fluctuacion = r.Next(100) > 50 ? 1 : -1;    // Positiva o negativa
                fluctuacion = fluctuacion * (r.Next(7) + 1);    // El valor fluctuara entre -7 y +7

                // Valor total entre 103 y 117V
                arrayFlujo[i] = fluctuacion + 110;
            }

            return arrayFlujo;
        }

        // Devuelve el numero de bornes del enchufe
        public int getNumeroBornes()
        {
            return 3;
        }

        #endregion
    }

A continuación codificaremos la otra parte del programa: el enchufe europeo junto a la clase que la implementa, EnchufeEspanol.

IEnchufeEuropeo


    public interface IEnchufeEuropeo
    {
        // Devuelve un array de enteros con un voltaje de unos 220V
        int[] Flujo220V();

        // Devuelve el numero de bornes del enchufe
        int getNumeroBornes();
    }

Creamos una clase que implemente esta interfaz. Los enchufes españoles siguen la normativa europea, por lo que haremos que implementen esta interfaz.

EnchufeEspanol


    public class EnchufeEspanol : IEnchufeEuropeo
    {
        #region IEnchufeEuropeo Members

        // Devuelve un array con voltajes en distintos momentos
        public int[] Flujo220V()
        {
            int[] arrayFlujo = new int[100];
            Random r = new Random();
            for (int i = 0; i < arrayFlujo.Length; i++)
            {
                // Calculamos la fluctuacion del voltaje
                int fluctuacion = r.Next(100) > 50 ? 1 : -1;    // Positiva o negativa
                fluctuacion = fluctuacion * (r.Next(10) + 1);    // El valor fluctuara entre -0 y +10

                // Valor total entre 210 y 230V
                arrayFlujo[i] = fluctuacion + 220;
            }

            return arrayFlujo;
        }

        // Devuelve el numero de bornes del enchufe
        public int getNumeroBornes()
        {
            return 2;
        }

        #endregion
    }

Ahora codificaremos nuestra clase Taladro, que posee un enchufe inglés (es decir, posee una referencia a una interfaz IEnchufeIngles) para recibir la alimentación:


    public class Taladro
    {
        // Enchufe del taladro
        private IEnchufeIngles enchufe;

        // Constructor a traves del cual se inyecta el enchufe
        public Taladro(IEnchufeIngles enchufeTaladro)
        {
            this.enchufe = enchufeTaladro;
        }

        public void Encender()
        {
            // Obtenemos la alimentación a través de la interfaz.
            // Recordemos que nuestro enchufe requiere una alimentacion de 110V
            int[] voltaje100ms = enchufe.Flujo110V();

            // Mostramos por pantalla el resultado.
            for (int i = 0; i < voltaje100ms.Length; i++)
                Console.WriteLine("El taladro esta funcionando con voltaje de " + voltaje100ms[i] +" Voltios");
        }
    }

Lo ideal sería utilizar la clase EnchufeBritanico, que es lo que nuestro taladro espera.


            // Instanciamos enchufe y taladro
            IEnchufeIngles enchufe = new EnchufeBritanico();
            Taladro taladro = new Taladro(enchufe);

            // Encendemos el taladro
            taladro.Encender();

            Console.ReadLine();

Vemos que obtenemos un voltaje adecuado, entre 103 y 117 voltios, aceptable para nuestro taladro.

Sin embargo, recordemos que el patrón Adapter no tiene sentido en este escenario, sino cuando el objeto del que disponemos no cumple con la interfaz esperada, es decir: en nuestro sistema, la clase EnchufeBritanico no existe. La clase del subsistema al que necesitamos conectarnos no implementa la interfaz IEnchufeBritanico, sino que se trata de un objeto de la clase EnchufeEspanol. La situación, por lo tanto, es esta:


            // Instanciamos enchufe y taladro
            IEnchufeEuropeo enchufe = new EnchufeEspanol();
            Taladro taladro = new Taladro(enchufe);

            // Encendemos el taladro
            taladro.Encender();

Lo cual nos daría un clarísimo error de compilación, ya que nuestro taladro espera un enchufe inglés.

Por lo tanto, crearemos una clase adaptador (¡por fin!) que cumpla los preceptos que vimos previamente:

  • Implementa la interfaz IEnchufeIngles para ser compatible con la clase Taladro.
  • Incluya una referencia a un IEnchufeEuropeo
  • Implemente los métodos IEnchufeIngles invocando los métodos adecuados de IEnchufeEuropeo y realizando las operaciones de transformación adecuadas.

Un ejemplo sería el siguiente:


    // Implementamos la interfaz que espera recibir nuestra clase cliente
    public class AdapterInglesEuropeo : IEnchufeIngles
    {
        // Declaramos una referencia de la clase o interfaz a la que queremos conectarnos
        private IEnchufeEuropeo enchufe;

        // El constructor del adaptador recibirá como parámetro el objeto al que se quiere
        // adaptar nuestro cliente.
        public AdapterInglesEuropeo(IEnchufeEuropeo enchufe)
        {
            this.enchufe = enchufe;
        }

        #region IEnchufeIngles Members

        // Invocamos los métodos de la interfaz servidora, transformando los datos para que pueda
        // manejarlos la clase cliente.
        public int[] Flujo110V()
        {
            // Invocamos el método de la interfaz incompatible
            int[] voltajes220 = enchufe.Flujo220V();

            // Realizamos la adaptacion
            int[] voltajes110 = new int[voltajes220.Length];

            for (int i = 0; i < voltajes220.Length; i++)
                voltajes110[i] = (int)(voltajes220[i] / 2);

            // Devolvemos el resultado adaptado, que es el que espera el cliente
            return voltajes110;
        }

        public int getNumeroBornes()
        {
            return enchufe.getNumeroBornes() - 1;
        }

        #endregion
    }

Finalmente, instanciamos un enchufe español y usamos el adaptador para hacer que le sirva a nuestro taladro:


            // Instanciamos el enchufe español, que es el que existe en nuestro país
            IEnchufeEuropeo enchufeEuropeo = new EnchufeEspanol();

            // Instanciamos el adaptador pasándole el enchufe español como parámetro
            IEnchufeIngles adaptador = new AdapterInglesEuropeo(enchufeEuropeo);

            // Creamos el taladro pasándole nuestro adaptador
            Taladro taladro = new Taladro(adaptador);

            // Encendemos el taladro
            taladro.Encender(); 

Hecho esto, veremos que todo funciona como la seda, tal y como se esperaba:

Como detalle, debemos fijarnos en que tanto la clase cliente como la adaptada ignoran completamente su mutua existencia. Ambas clases están desacopladas por medio de esta clase Adaptador, por lo que por norma general es un patrón que sirve como ejemplo perfecto de buenas prácticas de programación.

Más adelante aprenderemos algo acerca de un patrón parecido y muy relacionado: el patrón Facade o Fachada, que más que adaptar, establece una capa o Wrapper de un conjunto de clases e interfaces concentrando un conjunto de llamadas.

¿Cuándo utilizar este patrón? Ejemplos reales

El escenario de este patrón es también bastante claro. Se utilizará en los casos en los que sea necesario utilizar una clase cuya interfaz no se adapte a los requerimientos de otra clase cliente. Podemos ver algunos ejemplos reales cuando realizamos transformaciones entre enumeraciones e iteradores, arrays y listas, etc.

El lenguaje Java, por ejemplo, ofrece los siguientes usos del patrón Adapter:

Fuentes:

Anuncios

6 comments

  1. Hola Daniel,

    Aunque me ha quedado clara la explicación de este patrón me ha surgido la siguiente duda. En el ejemplo estás suponiendo que la clase adaptada implementa una interfaz pero qué sucede si no es así?. Debemos crear esta interfaz + clase que obtenga la información que necesitamos de la clase adaptada?.

  2. Hola, José Miguel.

    La idea del adaptador es la de encapsular el objeto adaptado, proporcionando a la clase cliente la interfaz que ella espera. Por tanto, nuestro adaptador podría alojar directamente una referencia a la clase del objeto adaptado, no es necesario que sea una interfaz que esté implementada por ésta. Por lo tanto, los únicos requisitos que debe cumplir la clase que implementa este patrón son:

    – Implementa la interfaz esperada por el cliente.
    – Contiene una referencia a la clase que se pretende adaptar.

    En el ejemplo propuesto, nuestro adaptador AdapterInglesEuropeo debe implementar IEnchufeIngles, pero podría encapsular la clase adaptada mediante una referencia a EnchufeEspanol. El hecho de usar IEnchufeEuropeo en el adaptador proporciona mayor nivel de abstracción, permitiéndonos usar, por ejemplo, un hipotético EnchufeFrances sin necesidad de modificar nuestro código, pero no es un requisito del patrón: su objetivo es que una clase proporcione una funcionalidad a otra sin que ésta tenga que ser modificada.

    El uso de interfaces aumenta el nivel de abstracción y reduce el acoplamiento, pero al precio de aumentar la complejidad del sistema (y por tanto, su comprensión). Quizás he abusado un poco de ellas a la hora de explicar los patrones, y puesto que mi idea es la de simplificar su comprensión todo lo posible, a partir de ahora intentaré explicarlos prescindiendo de ellas en un principio e introduciéndolas al final para completar el ejemplo.

    Nuevamente, gracias por tus comentarios, espero haber aclarado tu duda 🙂

    1. Hola Daniel,

      Por favor, no varíes la forma en que explicas los patrones porque en mi opinión se entienden perfectamente y este supongo es el objetivo. Además uno de los principios SOLID se basa precisamente en esto de lo que hablas. Depender de abstracciones y no de concreciones.

      Mil gracias por tu explicación a mi duda.

      Voy a tratar de hacer mis propios ejemplos para machacar esto hasta que me los sepa de memoria. 🙂

  3. Buenos días,

    Lo primero darte las gracias por compartir con nosotros tu blog y sobre todo por ser tan claro en tus explicaciones, me esta ayudando mucho tu blog para comprender los patrones y para cambiar mi forma de pensar, trabajo con cobol y ahora estoy reciclandome hacia java.

    La verdad es que no tengo ninguna duda sobre este patrón, sólo comentarte que creo que en la clase del adaptador el número de bornes debe ser +1 y no -1, sino el enchufe ingles esperado sería de 1 borne.

    He aprovechado que he visto esto en el código para poder agrecederte tu labor de divulgación, hay pocos blogs de este tipo que sean tan claros y sobre todo que estén en castellano.

    un saludo y espero que sigas dándole caña al blog.

  4. Hola buen día, sin duda tu explicación es bastante entendible y agradezco por ello, soy novato en eso, podrías compartirme tu código fuente, ya que trato de hacerlo en NetBeans pero aun no me queda claro que archivos de clase tengo que generar.
    Saludos!!!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s