jueves, 20 de febrero de 2014

Patrones estructurales: Adapter

Este post corresponde a la serie Patrones de diseño - Patrones estructurales

Adapter

"Cambia la interfaz de una clase en otra interfaz que espera el cliente. El Adapter permite trabajar juntas a clases que de otra forma no podrían porque tienen interfaces incompatibles".

La definición del patrón Adapter dice que cambia la interfaz de una clase por otra interfaz para que podamos utilizarla desde un cliente. Hay que entender que la clase a la que queremos cambiar la interfaz tiene la funcionalidad que necesitamos pero no cumple la interfaz que requiere nuestra clase cliente.



A primera vista, cambiar la interfaz de una clase no parece que sea un gran problema ¿no? Si necesitamos que la clase cumpla una interfaz determinada; cambiamos la clase y todo solucionado. El problema viene cuando la clase que no cumple la interfaz que necesitamos  no podemos cambiarla porque no es nuestra y no tenemos su código fuente, o simplemente cambiar su interfaz no es viable.

El problema

Veamos un ejemplo de cuándo puede ser necesario utilizar un patrón Adapter.

Supongamos que hemos desarrollado una aplicación ERP (Enterprise Resource Planning). Este ERP nos permite entre otras cosas gestionar acciones comerciales dirigidas a clientes. Una parte importante de nuestro ERP es el módulo de informes. Este módulo genera informes muy potentes que permiten tomar mejores decisiones en diferentes ámbitos de la gestión de la empresa. Uno de estos informes se llama "Clientes dormidos" son clientes que llevan más de tres meses sin realizar una compra y es necesario llevar alguna acción comercial para animarles a volver a comprar.
Veamos cómo sería de forma simplificada el código que genera este informe:

public class Client
{
    public DateTime? LastPurchaseDate { get; set; }
}

public class ERP
{
    /// 
    /// Clientes almacenados en el Erp.
    /// Simula los clientes de la base de datos por ejemplo.
    /// 
    private List<Client> storedClients = null;

    public IEnumerable<Client> GetClientsLastPurchaseBeforeReport(int days)
    {
        return storedClients.Where(c => c.LastPurchaseDate != null && c.LastPurchaseDate < DateTime.Now.AddDays(days * -1));
    }
}

public class ReportsManager
{
    ERP internalErp;
    public ReportsManager(ERP erp)
    {
        internalErp = erp;
    }

    public IEnumerable<Client>GetSleepingClients()
    {
        // Pedimos al Erp los clientes que no han comprado en los 
        // últimos 90 dias.
        return internalErp.GetClientsLastPurchaseBeforeReport(90);
    }
}


Una vez vista la situación hasta aquí vayamos un paso más allá.

A nuestra empresa le está costando mucho vender el ERP. La mayoría de los clientes con los que contactamos ya tienen un ERP y convencerlos para cambiar a uno distinto es bastante complicado. Aun así, a los clientes en general les gusta mucho el módulo de informes ya que los informes de los ERP de la competencia son mucho más limitados.

Ante esta situación nuestra empresa decide, para facilitar introducirse en el mercado, comercializar el módulo de informes como una aplicación independiente que debe permitir conectarse con los ERP de la competencia y generar nuestros "fabulosos" informes sobre los datos de ERP de terceros. ¡Acaba de nacer la aplicación EnterpriseMagicReports! :o)

Lo primero que haremos será eliminar las referencias que hay en el módulo de informes a nuestro ERP refactorizando el código anterior. Definiremos una interfaz de proveedor de datos de clientes (IClientsDataProvider) para conseguir este primer paso.

public class Client
{
    public DateTime? LastPurchaseDate { get; set; }
}

public interface IClientsDataProvider
{
    IEnumerable<Client> GetClientsLastPurchaseBeforeReport(int days);
}

public class ERP : IClientsDataProvider
{
    /// 
    /// Clientes almacenados en el Erp.
    /// Simula los clientes de la base de datos por ejemplo.
    /// 
    private List<Client> storedClients = null;

    public IEnumerable<Client> GetClientsLastPurchaseBeforeReport(int days)
    {
        return storedClients.Where(c => c.LastPurchaseDate != null && c.LastPurchaseDate < DateTime.Now.AddDays(days * -1));
    }
}

public class ReportsManager
{
    IClientsDataProvider internalClientDataProcider;
    public ReportsManager(IClientsDataProvider clientDataProvider)
    {
        internalClientDataProcider = clientDataProvider;
    }

    public IEnumerable<Client> GetSleepingClients()
    {
        // Pedimos al Erp los clientes que no han comprado en los 
        // últimos 90 dias.
        return internalClientDataProcider.GetClientsLastPurchaseBeforeReport(90);
    }
}

Bien, ahora nuestro ERP sigue funcionando y generando informes pero el módulo de informes ya no tiene dependencias con nuestro ERP.

Nuestro problema ahora es que cada uno de los ERPs de la competencia tiene su propia interfaz para poder obtener datos de los clientes. Y excepto el nuestro, ninguno cumple con la interfaz IClientsDataProvider que espera nuestra nueva aplicación EnterpriseMagicReports.

Es decir, estamos en una situación tal que así:

El único ERP que cumple la interfaz que espera es "Nuestro ERP". Los ERP de la competencia cada uno expone su propia interfaz. Me refiero a interfaz en un sentido amplio, podría ser una API o incluso la propia base de datos del ERP de la competencia si no expone una API de forma explícita.

Si volvemos a repasar la definición que vimos al principio del patrón Adapter, "Cambia la interfaz de una clase en otra interfaz que espera el cliente. El Adapter permite trabajar juntas a clases que de otra forma no podrían porque tienen interfaces incompatibles", vemos que es esa justamente la situación en la que nos encontramos. Así que vamos a aplicar el patrón Adapter para llegar a la siguiente situación que resuelve nuestro problema:


Es decir, vamos a "interponer" un objeto adapter entre nuestra aplicación EnterpriseMagicReport y los ERP de la competencia; que por un lado exponga la interfaz IClientsDataProvider  que necesita EnterpriseMagicReport y por otro se comunique con el ERP de la competencia. De esta forma conseguimos mantener a nuestra aplicación EnterpriseMagicReport independiente del ERP al que está "atacando".

Dos formas de implementar Adapter

El patrón Adapter puede implementarse básicamente de dos formas distintas:

Implementación del patrón Adapter mediante composición de objetos


Este es el diagrama UML del patrón Adapter para implementarlo mediante la composición de objetos:


Los participantes en este patrón son:
  • Client (EnterpriseMagicReport)
  • Target (IClientsDataProvider) Define la interfaz que usa el cliente.
  • Adaptee (ERPs de la competencia) Definen una interfaz que necesita ser adaptada.
  • Adapter (Competencia ERP Adapters) Adapta la interfaz de Adaptee a la de Target.

La siguiente es la implementación de EnterpriseMagicReports incluyendo un adapter para un ERP de la competencia.

public class Client
{
    public DateTime? LastPurchaseDate { get; set; }
}

public interface IClientsDataProvider
{
    IEnumerable<Client> GetClientsLastPurchaseBeforeReport(int days);
}

public class ERP : IClientsDataProvider
{
    /// 
    /// Clientes almacenados en el Erp.
    /// Simula los clientes de la base de datos por ejemplo.
    /// 
    private Lis<Client> storedClients = null;

    public IEnumerable<Client> GetClientsLastPurchaseBeforeReport(int days)
    {
        return storedClients.Where(c => c.LastPurchaseDate < DateTime.Now.AddDays(days * -1));
    }
}

public class ReportsManager
{
    IClientsDataProvider internalClientDataProcider;
    public ReportsManager(IClientsDataProvider clientDataProvider)
    {
        internalClientDataProcider = clientDataProvider;
    }

    public IEnumerable<Client> GetSleepingClients()
    {
        // Pedimos al Erp los clientes que no han comprado en los 
        // últimos 90 dias.
        return internalClientDataProcider.GetClientsLastPurchaseBeforeReport(90);
    }
}

/// 
/// Adapter para el ERP de la marca ACME
/// Como el ERP de ACME no ofrece un API
/// El adapter ataca a la base de datos.
/// 
public class AcmeErpClientsDataAdapter: IClientsDataProvider
{
    SqlConnection acmeErpDatabaseConnection;

    public AcmeErpClientsDataAdapter(SqlConnection dataBaseConnection)
    {
        // Guardamos la conexión a la base de datos del ERP de ACME
        acmeErpDatabaseConnection = dataBaseConnection;
    }

    public IEnumerable<Client> GetClientsLastPurchaseBeforeReport(int days)
    {
        // Conectamos a la base de datos del ERP de ACME y 
        // Obtenemos los clientes filtrados.
        using (acmeErpDatabaseConnection)
        {
            acmeErpDatabaseConnection.Open();

            string commandText = "SELECT ... FROM Clients WHERE....";
            var command = new SqlCommand(commandText);
            using (SqlDataReader reader = command.ExecuteReader())
            {
                foreach (var acmeClient in reader)
                {
                    var client = new Client();
                    // Rellenamos las propiedades del cliente 
                    // desde el reader
                    yield return client;
                }
            }

            acmeErpDatabaseConnection.Close();
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var clientDataProvider = GetDataProvider();

        var reportManager = new ReportsManager(clientDataProvider);
        var sleepingClients = reportManager.GetSleepingClients();

        foreach (var client in sleepingClients)
        {
            // Escribimos los datos del cliente
            Console.WriteLine("Cliente...");
        }
    }

    private static IClientsDataProvider GetDataProvider()
    {
        // Lógica para decidir qué proveedor de datos
        // devolvemos. Nuesto ERP o el adapter para el 
        // ERP de ACME
        if (true)
        {
            SqlConnection acmeDbConnection = new SqlConnection();
            // Configuramos la conexión.

            return new AcmeErpClientsDataAdapter(acmeDbConnection);
        }

        return new ERP();

    }
}

Como podemos comprobar ahora hemos conseguido que nuestra aplicación de informes ataque tanto a nuestro propio ERP como al ERP del fabricante Acme, ya que hemos implementado un adapter que adapta la interfaz de ERP de Acme (en este caso la propia base de datos) a la interfaz IClientsDataProvider que es la interfaz que espera nuestra aplicación de informes.

Implementación del patrón Adapter mediante la herencia de clases.


Otra forma de implementar el patrón Adapter es mediante la herencia. Este es el diagrama UML para este caso:

Como podemos ver esta implementación se basa en la herencia múltiple, característica que no es soportada por todos los lenguajes de programación debido a los problemas de ambigüedad que conlleva su implementación. Entre los lenguajes que podríamos considerar más conocidos soportan herencia múltiple C++, Perl y Pyton. Estoy seguro que me olvidaré alguno. Si alguien quiere, que corrija. C# o VB.NET por ejemplo sólo soportan herencia simple.

Para los lenguajes que no soportan herencia múltiple una alternativa para implementar mediante herencia sería convertir la clase Target en una interfaz; y en vez de que nuestro adapter reciba el objeto a adaptar (AcmeErpClientDataProvider) por el constructor heredar de él.

Así sería la implementación de nuestro adapter AcmeErpClientDataProvider si optamos por la opción de implementarlo mediante herencia  en lugar de mediante composición de objetos:

/// 
/// Clase cliente de la API del ERP de ACME
/// 
public class AcmeClient
{
    public DateTime? LastPurchaseDate { get; set; }
}

/// 
/// API del ERP de ACME
/// 
public class AcmeErpClientApi
{
    public IEnumerable GetClientsNotPurchaseFrom(DateTime fromDate)
    {
        IEnumerable result = null;

        // Lógica para cargar el listado de clientes que no compran desde fromDate

        return result;
    }
}
public class Client
{
    public DateTime? LastPurchaseDate { get; set; }
}

public interface IClientsDataProvider
{
    IEnumerable GetClientsLastPurchaseBeforeReport(int days);
}

public class ReportsManager
{
    IClientsDataProvider internalClientDataProcider;
    public ReportsManager(IClientsDataProvider clientDataProvider)
    {
        internalClientDataProcider = clientDataProvider;
    }

    public IEnumerable GetSleepingClients()
    {
        // Pedimos al Erp los clientes que no han comprado en los 
        // últimos 90 dias.
        return internalClientDataProcider.GetClientsLastPurchaseBeforeReport(90);
    }
}


/// 
/// Adapter para el ERP de la marca ACME
/// Como el ERP de ACME no ofrece un API
/// El adapter ataca a la base de datos.
/// 
public class AcmeErpClientsDataAdapter : AcmeErpClientApi, IClientsDataProvider
{

    public IEnumerable GetClientsLastPurchaseBeforeReport(int days)
    {
        var dateFrom = DateTime.Now.AddDays(days * -1);
        var acmeClients= base.GetClientsNotPurchaseFrom(dateFrom);
        foreach (var acmeClient in acmeClients)
        {
            var client = new Client();
            // Transformar AcmeClient en Client
            yield return client;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var clientDataProvider = GetDataProvider();

        var reportManager = new ReportsManager(clientDataProvider);
        var sleepingClients = reportManager.GetSleepingClients();

        foreach (var client in sleepingClients)
        {
            // Escribimos los datos del cliente
            Console.WriteLine("Cliente...");
        }
    }

    private static IClientsDataProvider GetDataProvider()
    {
        // Lógica para decidir qué proveedor de datos
        // devolvemos. Nuesto ERP o el adapter para el 
        // ERP de ACME
        if (true)
        {
            SqlConnection acmeDbConnection = new SqlConnection();
            // Configuramos la conexión.

            return new AcmeErpClientsDataAdapter(acmeDbConnection);
        }

        return new ERP();

    }
}

En este caso, como ya dije antes, en lugar de realizar composición de objetos heredamos de la clase que pretendemos adaptar (AcmeErpClientApi) e implementamos la interfaz Target (IClientsDataProvider). La implementación de los métodos de la interfaz acaban llamando a los métodos de la clase base, la clase que queremos adaptar.

Hay que tener en cuenta que no siempre es posible heredar de la clase que queremos adaptar, por lo que no siempre es posible implementa el patrón Adapter mediante la herencia.

Desde mi punto de vista la elección principal debe ser implementar un Adapter mediante la composición de objetos ya que resulta en un diseño más flexible que con la herencia.

¿Cuánto debe adaptar un Adapter?

El hecho de implementar un Adapter no quiere decir de debemos adaptar toda la interfaz del objeto a adaptar. Es decir, en nuestro último ejemplo la clase AcmeErpClientApi podría tener diez métodos, pero para nuestro propósito sólo necesitamos adaptar uno; no es necesario adaptar todo el objeto. La cantidad de trabajo que adaptaremos lo definirá normalmente la clase Target (en nuestro caso la interfaz IClientsDataProvider).

Hay un sin fin de variaciones en cuanto a qué cantidad de la clase AcmeErpClientApi podemos adaptar. De hecho en una implementación de Adapter basada en la composición de objetos podríamos recibir en el constructor del adapter varios objetos a adaptar para cumplir la interfaz IClientsDataProvider. 

Incluso  podemos también crear diferentes Adapters para el mismo objeto de forma que cada adapter representa un aspecto diferente del objeto a adaptar. Por ejemplo, para el ERP de la marca Acme podríamos crear el adapter DemographicsAcmeErpAdapter que cumple la interfaz IDemographicsDataProvider con métodos para crear informes sobre la edad y el sexo de nuestros clientes.

Vamos a dejar aquí el patrón Adapter. Hay algunos matices y ejemplos más sobre los que podríamos seguir incidiendo, pero creo que con lo visto hasta ahora es suficiente para entender el patrón, implementarlo y reconocer los contextos donde puede ser útil.

En el próximo post veremos otro patrón estructural: Bridge.

6 comentarios:

  1. Era fiel de seguidor de tus artículos sobre patrones creacionales, y pienso seguir siéndolo en tu nueva serie sobre patrones estructurales.

    Me vienen de perlas ya que me cuesta mucho saber cuando utilizarlos, y viendo lo expones me clarifico bastante.

    ResponderEliminar
  2. Gracias MookieFumi por tu comentario.
    Me alegra que te estén siendo de ayuda los post.

    También yo voy visitando regularmente tu blog.

    Una cosa más:
    El otro día coincidí con tus compis en el CAM de ALM en Tajamar. Si no hay causas de fuerza mayor, espero verte el día 1 en el CAM de ALM avanzado.

    ResponderEliminar
  3. Hola Miguel,

    Muy buen artículo, desconocía la segunda parte de implementar Adapter. Mañana comentaré con MookieFumi una implementación con Adapter que vamos a montar en uno de los proyectos que tenemos aquí. Así le sacamos más jugo al artículo!!

    Nos vemos el día 1 en el CAM de ALM!!

    ResponderEliminar
  4. Gracias Antonio.
    Escribir y recibir la confirmación de que igual le sirve a alguien es un gustazo jeje.

    Ya te lo dije una vez, pero me repito sin problema. Si necesitáis algo en lo que pueda echar una mano ya sabes :o)

    Nos vemos el 1

    ResponderEliminar
  5. Gracias por los artículos, la serie de patrones me ha parecido bastante interesante.

    Tengo una duda con respecto a la implementación del ejemplo, según tengo entendido una de las características de este patrón es la reutilización del código de la clase base, es decir, la clase adaptee, debería estar inmersa en la clase adapter, y en el ejemplo lo que se está haciendo es dos implementaciones diferentes de una misma interfaz... No sé si mi concepto es errado o estoy analizando mal el ejemplo...

    Saludos.

    ResponderEliminar
    Respuestas
    1. Hola David.
      Gracias por leer el blog. Últimamente no he podido actualizarlo, pero prometo volver ya mismo. :o)

      El objetivo fundamental del patrón Adapter es conseguir cambiar la interfaz de un objeto (Adaptee) en una interfaz que nos interesa (Target). Lo habitual es que no podamos modificar el objeto Adaptee, por eso el Adapter.

      ¿Porqué queremos cambiar la interfaz en nuestro ejemplo?. La idea es poder tratar a todos los ERP's con los que queremos trabajar de la misma forma. De esta manera reducimos el riesgo de que nuestra aplicación se vea afectada por los cambios en los ERP's de la competencia o por la incorporación de nuevos ERP's.

      Si esto no responde a tu pregunta, por favor, especifica si te refieres al ejemplo implementado mediante composición o el implementado con herencia.

      Un saludo.

      Eliminar