Patrones de diseño: Adapter

En el anterior artículo hablábamos de qué son los patrones de diseño, para qué sirven y las ventajas que nos proporcionan a la hora de desarrollar nuestras aplicaciones.

Pero es hora de que nos pongamos manos a la obra y empecemos a destripar algunos de los patrones más comunes. Y para comenzar un patrón facilito, pero con mucha utilidad: el patrón Adapter.

Antes de empezar a explicar este patrón, dos puntualizaciones. La primera es que voy a utilizar el nombre del patrón en inglés. Soy defensor de utilizar nombres en español siempre que se pueda, pero en el caso de los patrones hay veces que la traducción no es tan directa como en este caso. Además la mayor parte de la literatura que encontraréis por internet estará en inglés, por lo que mejor conocer su nombre original. La segunda puntualización es que los ejemplos de código los escribiré en C#. No creo que esto suponga ningún problema para desarrolladores de otros lenguajes.

Descripción del patrón Adapter

Como se ilustra en la imagen que encabeza este artículo, el patrón Adapter sirve para hacer que dos interfaces, en principio diferentes, puedan comunicarse.

Para ello añadiremos un adaptador intermedio, que se encargará de realizar la conversión de una interface a otra.

Este patrón se utiliza generalmente cuándo ya existe una interface, pero no podemos (o no queremos) usarla. Por ejemplo cuando tenemos que utilizar librerías externas. Vamos a ver un ejemplo.

Describiendo el problema

En nuestra compañía CoolCorp, estamos desarrollando un software personalizado para controlar robots. Los robots no los construye nuestra compañía, si no que se los compramos a USRobots.

Los robots que nos proporcionan tienen su propia interface de comunicaciones. Es la siguiente:


    interface USRobotsInterface
    {
        float CurrentSpeedInMilesPerHour { get; set; }
        void EnablePartialFirstLawMode();   
        void Jump(float feet);       
    }

Un ejemplo de implementación de un robot de USRobots sería el siguiente:


    class USRobot:USRobotsInterface
    {   
        private float CurrentSpeed;      
        
        public float CurrentSpeedInMilesPerHour
        {
            get { return CurrentSpeed; }
            set { CurrentSpeed = value;}
        }

        public USRobot()
        {
            this.CurrentSpeed = 0;            
        }

        public void EnablePartialFirstLawMode()
        {            
            Console.WriteLine("Partial first law enabled");
        }

        public void Jump(float feet)
        {
            Console.WriteLine("Jump !!");
        }
    }

El problema que tenemos, es que nuestra compañía vende el software en España. Los clientes que van a trabajar con los robots, trabajan en Km/h y metros. Los robots de USRobots, compañía estadounidense, utilizan un extraño sistema métrico midiendo la velocidad en millas por hora y la altura en pies.

Además debido a la legislación europea, los robots no pueden modificar la primera ley de la robótica, algo que sí está permitido en EE.UU.

Así que en este caso, no podemos utilizar la interface proporcionada, porque es incompatible con nuestros requisitos.

Solución con el patrón Adapter

Está claro que los requisitos de nuestra aplicación nos obligan a hacer cambios. La solución chapucera sería la de instanciar objetos USRobot allí dónde los necesitemos. Cada vez que llamemos a uno de los métodos de USRobot, hacemos la conversión de unidades y listo.

El problema de este enfoque es que estaríamos dependiendo de una entidad externa, que no controlamos. Si USRobots actualiza sus implementaciones, añadiendo métodos o cambiando tipos, nosotros tendríamos que modificar una gran cantidad de código. Por no hablar de que estaremos incumpliendo el principio DRY (Don't Repeat Yourself).

Es mejor decidir qué es lo que necesitamos y encapsular USRobot para usarlo desde un único punto. Es decir, vamos a seguir el principio de encapsular lo que puede variar. Por tanto, pensando en nuestras necesidades, tendríamos la siguiente interface:


    interface CoolCorpInterface
    {
        float CurrentSpeedInKilometersPerHour { get; set; }
        void Jump(float meters);
    }

Todos los robots con los que trabajemos, implementarán dicha interface, ya que es la que mejor se adapta a nuestras necesidades. Ahora solo tenemos que desarrollar el adaptador en si, que en este caso tendrá el siguiente código:


    class CoolCorpRobot:CoolCorpInterface
    {
        private USRobot robot;

        public CoolCorpRobot()
        {
            this.robot = new USRobot();
        }

        public float CurrentSpeedInKilometersPerHour
        {
            get
            {
                return this.robot.CurrentSpeedInMilesPerHour * 1.6093f;
            }
            set
            {
                this.robot.CurrentSpeedInMilesPerHour = value * 0.62137f;
            }
        }

        public void Jump(float meters)
        {
            this.robot.Jump(meters * 0.3048f);
        }
    }

Como se puede ver, nuestro CoolCorpRobot implementa la interface que hemos definido antes. En el constructor, creamos un objeto USRobot, que al final es el que realiza las operaciones. La clase no implementa el método EnablePartialFirstLawMode porque no lo necesitamos.

Y ya está. Para usar el adaptador, bastará con utilizar algo similar a esto:


    class Program
    {
        static void Main(string[] args)
        {
            CoolCorpInterface robot = new CoolCorpRobot();
            robot.CurrentSpeedInKilometersPerHour = 3;
            robot.Jump(12);
            Console.ReadKey();
        }
    }

¿Y si USRobots modifica su interface? Pues nosotros solo tendremos que modificar el adaptador. Mientras que respetemos nuestra interface, no serán necesarios cambios en otras partes del código.

¿Y si USRobots saca a la venta un nuevo robot, pero hay que mantener en uso las versiones antiguas? En este caso lo que tendremos que hacer es crear un nuevo adaptador, que respete nuestra CoolCorpInterface. Así los clientes no se verán afectados. Y más si en lugar de utilizar new inyectamos la dependencia con el adaptador adecuado.

Otras cosas a tener en cuenta

El patrón Adapter se puede implementar de dos maneras: con objetos o con clases. Este artículo utiliza la primera opción. Para implementar el patrón con clases necesitaríamos un lenguaje que soportase multiherencia. No es el caso de C#.

Existe otra versión de este patrón denominada Two-Way Adapter. La idea es similar, pero en este caso, el adaptador implementa las interface original y la interface adaptada. Algo útil si necesitamos ambas funcionalidades.

Resumen para nuestro catálogo de patrones

  • Nombre del patrón: Adapter.
  • Tipo: patrón estructural.
  • Lo usaremos cuándo: necesitamos hacer compatibles dos interfaces, que de inicio no lo son. Muy útil cuando trabajamos con librerías externas. Una analogía con el mundo real sería la de un adaptador para utilizar un conector español en un enchufe americano.
  • Ventajas: hace que dos interfaces incompatibles, sean compatibles. Puede servir para encapsular clases que no controlamos, y que pueden cambiar.
  • Desventajas: como muchos patrones, añade complejidad al diseño. Hay quién dice que este patrón es un parche, utilizado en malos diseños.
  • Patrones similares o relacionados: Decorator, Facade y Strategy.

Imagen | Somos viajeros

En GenbetaDev | Diseño con patrones y fachadas, Patrones de diseño: Active Record VS DAO

Ver todos los comentarios en https://www.genbeta.com

VER 0 Comentario

Portada de Genbeta