lunes, 30 de junio de 2014

[Patrones] Implementando Patrón adaptador - Adapter Pattern en C#

El patrón adaptador nos permite convertir o transformar una interface existente en otra interface esperada por un cliente, para realizar determinada operación, es decir en muchas ocasiones necesitamos hacer alguna operación desde nuestras aplicaciones (cliente), y en muchos casos ya tenemos esta operación implementada en una librería de clases por ejemplo. Pero también nos podemos encontrar que la interface que usa nuestro cliente no sea compatible con la interface que usa este componente genérico que tenemos. Es allí donde cobra importancia el patrón adaptador o adapter pattern, el cual nos permite a través de un componente intermedio entre las dos interfaces lograr que sean compatibles y lograr reutilizar nuestro código sin necesidad de repetirlo o acoplarlo fuertemente a alguna implementación en específico.

Veamos para comprender mejor en primera instancia el ejemplo más usado de este patrón con una imagen de la vida real:


Ahora veamos cómo sería el diagrama de clases de este patrón y analicémoslo un poco antes de implementarlo en C#:

Imagen tomada de Wikipedia (http://es.wikipedia.org/wiki/Adapter_(patr%C3%B3n_de_dise%C3%B1o))

Sin lugar a duda comprender en primera instancia un patrón de diseño suele ser algo complejo, por eso vamos a describir los diferentes componentes que vemos en el diagrama.

Target: Es una interface, la cual usa el cliente en el cual se quiere reutilizar el componente genérico que tenemos (Adaptee), es allí donde radica el problema que soluciona el patrón adaptador, ya que Target es diferente de la interface usada por el componente genérico que tenemos (Adaptee).

Adaptee: Es el componente genérico que tenemos, en el cual tenemos implementada cierta funcionalidad que queremos reutilizar desde nuestro cliente, al ser un componente genérico y pensado en cierta ocasión para algún escenario su interface es incompatible con Target, es decir la firma de su método puede retornar un valor diferente y recibir parámetros diferentes, es por esto que son incompatibles.

Adapter: Es nuestro adaptador o también conocido como Wrapper, el cual nos permite convertir en compatibles estas dos interfaces que son incompatibles por naturaleza, y como vemos en el diagrama, Adapter es una clase que hereda de Adaptee y que implementa la interface Target, de este modo puede cumplir con el contrato esperado por el cliente y a su vez utilizar la implementación existente en Adaptee.

Y tenemos un componente más que no se encuentra en el diagrama que es el Cliente, y es la aplicación u otro componente, desde la cual deseamos reutilizar la funcionalidad que tenemos en nuestro Adaptee, es por esto que anteriormente nos referimos mucho al cliente.

Ahora que conocemos bien que hace cada componente, comprendemos que nuestro Adapter invocará nuestro Adaptee y operará de modo que cumpla con el contrato esperado por nuestro cliente, es decir Target.

Probablemente lo primero que se nos venga a la cabeza es: ¿Y por qué no crear otro componente basado en el existente copiando el código que ya tengo? ó ¿por qué no envío un parámetro adicional a mi Adaptee de modo que pueda indicar cómo comportarse dependiendo del cliente que lo invoca? ó ¿Por qué no creo una sobrecarga de mi método en el Adaptee que si cumpla con el contrato esperado por mi nuevo cliente?

Pues bueno, la verdad es posible resolver el problema planteado a través de las alternativas anteriores, pero si nos detenemos a pensar, no estaríamos reutilizando componentes si no copiando y pegando, es decir replicando código que ya tenemos, también estaríamos acoplando fuertemente nuestro componente "genérico" a implementaciones concretas, o estaríamos modificando el comportamiento de nuestro componente creado inicialmente, lo cual iría en contra del principio solid "Open / Close" que nos indica que nuestro componente debería estar abierto a ser extendido y debería estar cerrado a ser modificado.

Es por esto que el patrón adaptador nos permite una solución, que sea desacoplada y que permita hacer extensible nuestro componente, sin tener que modificarlo directamente, y nos permite hacer múltiples adaptaciones de un componente para diferentes requerimientos de nuevos clientes.

Y bueno después de comprender el propósito y aplicación de este patrón, que en mi opinión es lo más complejo e importante, ahora vamos a ver como lo podemos implementar a través del lenguaje C#, aplicándolo en el siguiente escenario:

En nuestro ejemplo vamos a hacer una adaptación de un componente que nos permite guardar un error en archivo txt este es nuestro Adaptee, el cual en un principio solo fue pensado para esto. Pero lo deseamos reutilizar en otra aplicación que requiere otro comportamiento diferente.

    public class HelperLog
    {
        public void GuardarError(Exception ex)
        {
            using (var w = File.AppendText("log.txt"))
            {
                w.Write("\r\nLog Entry : ");
                w.WriteLine("{0} {1}"DateTime.Now.ToLongTimeString(), DateTime.Now.ToLongDateString());
                w.WriteLine("  :");
                w.WriteLine("  :{0}", ex.Message);
                w.WriteLine("-------------------------------");
                w.Write("\r\nError StackTrace : {0}", ex.StackTrace);
            }
        }
    }

Como vemos, en esta firma recibimos una excepción, la almacenamos en el archivo de log y no retornamos ningún valor, pero para nuestra nueva aplicación se requiere que se envíe solo el mensaje de error y un código de error personalizado, con el cual se obtendrá un mensaje amigable para el usuario de un archivo Xml y por último se le mostrará al mismo. Por lo tanto requerimos de un adaptador que nos ayude a reutilizar este componente volviendo compatibles las interfaces. Ahora veamos la interface que se requiere.

    public interface IClienteLog
    {
        string GuardarErrorLog(string error, string codigoError);
    }

Como vemos tenemos incompatibilidad, y es allí donde entra a jugar nuestro adaptador, el cual debe heredar de nuestra clase HelperLog e implementar nuestra interface IClienteLog.

    public class LogAdapter : HelperLogIClienteLog
    {
 
        public string GuardarErrorLog(string error, string codigoError)
        {
            GuardarError(new Exception(error));
 
            // Se obtiene el mensaje amigable de error basado en su código
            var root = XElement.Load("Mensajes.xml");
 
            return (from c in root.Elements("Mensaje")
                   where (string)c.Attribute("codigo"== codigoError
                   select c.Value).First();
        }
    }

Como vemos reutilizamos nuestro helper de log, y a la vez cumplimos con el contrato que requiere nuestro nuevo cliente, observemos como lo usamos desde una aplicación de consola por ejemplo:

    class Program
    {
        static void Main(string[] args)
        {
            IClienteLog clienteLog = new LogAdapter();
 
            try
            {
                Console.Write("Digite el número a convertir...");
                var cadena = Console.ReadLine();
                var numero = Convert.ToInt16(cadena);
            }
            catch (Exception ex)
            {
                Console.Write(clienteLog.GuardarErrorLog(ex.Message, "2"));
                Console.ReadLine();
            }
        }
    }

En este escenario utilizamos el la adaptación del helper log, para almacenar un error que corresponde a un error de conversión de tipos, el parámetro "2" corresponde al mensaje de este error en el xml, como podrás ver en la solución de código fuente que indico al final.

Bueno amigos eso es todo de esta muestra acerca del patrón de diseño adaptador o Adapter pattern, espero sea de utilidad y de interés para ustedes.

Este ejemplo lo puedes descargar de mi repositorio en GitHub



Saludos, y buena suerte!

4 comentarios:

  1. Excelente muy bien explicado, gracias.

    ResponderEliminar
    Respuestas
    1. Hola amigo, que bueno que te haya servido y gracias por leerme, saludos!

      Eliminar
  2. Gran post, excelente explicación, la verdad es que el ejemplo del enchufe es perfecto para explicar como la clase Adapter expone hacia el exterior (cliente) la implementación del interfaz, y en el otro lado del enchufe se encuentra la clase padre que queremos adaptar.

    Gracias.
    Saludos
    Josema.

    ResponderEliminar
    Respuestas
    1. Hola Josema, muchas gracias, que bueno que haya sido de utilidad, saludos!

      Eliminar