Patrones Estructurales (IV): Patrón Bridge


Objetivo:

«Desacoplar una abstracción de su implementación de modo que los dos puedan ser modificados de forma independiente.»

Design Patterns: Elements of Reusable Object-Oriented Software

El patrón Bridge o Puente es normalmente uno de los patrones que más cuesta entender, especialmente si nos ceñimos únicamente a su descripción. La idea tras este patrón, sin embargo, es sencilla: dado que cualquier cambio que se realice sobre una abstracción afectará a todas las clases que la implementan, Bridge propone añadir un nuevo nivel de abstracción entre ambos elementos que permitan que puedan desarrollarse cada uno por su lado.

Si le echamos un ojo al diagrama, es posible que de base no nos aclare demasiado. Nos centraremos en el elemento central: una clase abstracta Abstracción que contiene una referencia a una interfaz Implementor y un método operacion() que no hace más que invocar el método operacionOriginal() de dicha interfaz. Lo que hace esta clase Abstracción es, por tanto, encapsular a la interfaz Implementor exponiendo sus métodos.

Similitudes

Un momento… ¿no hemos visto ya esto antes? Nos suena, ¿verdad? ¿No es precisamente lo que realizaba también el patrón Adapter, tal y como vimos anteriormente?

No vamos por mal camino. La estructura de este patrón se parece mucho a la del patrón Adapter, ya que nuestra clase Abstracción hace las veces de «adaptador» entre nuestra clase cliente y la interfaz Implementor. Sin embargo, nos movemos por la sinuosa senda de la ingeniería, por lo que afirmar que Adapter y Bridge realizan lo mismo simplemente porque su estructura sea muy parecida es quedarnos en la superficie del problema que tratamos de resolver.

Como recordaremos, el patrón Adapter basaba su razón de ser en la necesidad de adaptar (valga la redundancia) la interfaz de una clase en otra, para lo cual encapsulaba dicha clase implementando la interfaz que el cliente requería. Hasta aquí, todo muy parecido, salvando el hecho de que nuestra clase Abstracción no implementa ninguna interfaz. ¿Por qué? Porque el objeto de este patrón no es realizar una adaptación. La clase cliente no espera utilizar la interfaz Implementor, sino que sabe que va a hacer uso de una clase derivada de la clase Abstracción. La razón de ser de esta estructura no es realizar una adaptación, sino separar una interfaz de su implementación. La diferencia básica entre ambos patrones es, por tanto, que Adapter se utiliza para unificar interfaces que ya existen, mientras que Bridge se utiliza cuando se sospecha que la implementación de una interfaz va a cambiar con el tiempo. Adapter intenta cerrar viejas heridas, mientras que Bridge intenta que los errores que cometieron los miembros que forman parte de Adapter no se vuelvan a repetir. Hay que aprender de la historia, amigos.

Otro detalle que quizás no quede claro al ver el diagrama UML es que Abstracción es una clase abstracta, por lo que el objeto que nuestra clase cliente utilizará será una instancia de RefinamientoAbstraccion o de cualquiera de sus posibles clases hermanas, que serán las que contendrán los métodos refinados. De este modo, las clases que implementan Implementor podrán evolucionar por un lado mientras las clases heredadas de Abstracción podrán hacerlo por otro.

¿Por qué «Bridge»?

Patrón tras patrón, vemos que los mismos conceptos se repiten una y otra vez. Minimizar el acoplamiento, hacer que las clases dependan de abstracciones en lugar de depender de implementaciones, preferir el uso de composición antes que el uso de herencia… El patrón Bridge no es una excepción. Sin embargo, hasta el momento todos los patrones que hemos visto tenían una relación entre su nombre y su funcionalidad. Un Adapter adapta. Una factoría fabrica objetos. Un Builder construye. Pero… ¿Bridge? ¿Qué tiene que ver un puente con todo esto?

Una de las razones por las que opino que este patrón es complicado de entender a la primera es, precisamente, que no existe una relación clara entre su nombre y su descripción. ¿Llamar puente a desligar una interfaz de la implementación? ¿Por qué? La razón no está tanto en este proceso sino en el camino que existe entre la clase que refina la abstracción y las implementaciones de la interfaz. Parte de nuestro código estará implementado dentro de nuestra clase AbstraccionRefinada, y parte estará fuera, por ejemplo en ImplementorConcretoA. Para acceder a ese código, se realiza un puente de modo que cuando parte del código de nuestra clase AbstraccionRefinada realice una operación cuyo código no dependa de sí misma, solicite la realización de esta al elemento que se encuentra al otro lado. En el ejemplo de un driver de un dispositivo, la clase AbstraccionRefinada podría representar operaciones comunes a una plataforma, como por ejemplo Windows 7. En un momento dado, nuestro driver necesita realizar un acceso a memoria, pero… ¡cuidado! Esta operación no dependerá únicamente de la plataforma, sino que será dependiente de la arquitectura del sistema.

Bueno, una solución será crear una clase para Windows7 32 bits y otra para Windows 7 64 bits. Así se solucionaría el problema, ¿verdad? Desde el punto de vista funcional, sí. Pero recordemos que siempre es preferible la composición antes de la herencia así que… ¿por qué no implementar las operaciones comunes a la plataforma Windows 7 en nuestra AbstraccionRefinada y dejarle a Implementor y sus implementaciones las tareas dependientes de la arquitectura concreta que estemos utilizando? De este modo, cuando sea necesario realizar una operación ligada a la arquitectura, bastará con solicitárselo al ImplementorConcreto que se encuentra al otro lado del puente y que está ligado a una arquitectura concreta y (más importante) puede ser incorporado a nuestro objeto en tiempo de ejecución.

Hablando en plata, y a modo de resumen, realizaremos la transformación del siguiente árbol de herencia:

En una composición como la siguiente:

Un ejemplo de patrón Bridge

Veamos la aplicación de nuestro patrón Bridge con un ejemplo en código C#. El ejemplo, como seguro que habréis adivinado, estará basado en vehículos. Nuestra abstracción simbolizará el vehículo en sí, mientras que la parte que se tenderá «al otro lado del puente» será el motor del mismo. Los tipos de vehículo podrán así evolucionar con independencia de los motores que éstos posean.

Comenzaremos codificando la interfaz Implementor, que en nuestro ejemplo estará representada por el motor. O más específicamente, por la interfaz IMotor.

IMotor


    // Implementor
    public interface IMotor
    {
        void InyectarCombustible(double cantidad);
        void ConsumirCombustible();
    }

Como vemos, nada complicado: nuestro Implementor expone dos métodos, InyectarCombustible y ConsumirCombustible, que deberán ser codificados en las clases que implementen la interfaz. Y dicho y hecho, añadiremos un par de clases cuyo papel en el patrón se corresponderá con ImplementorConcretoA e ImplementorConcretoB, y modelarán dos tipos de motores: diesel y gasolina.

Diesel


    // ImplementorConcretoA
    public class Diesel : IMotor
    {
        #region IMotor Members

        public void InyectarCombustible(double cantidad)
        {
            Console.WriteLine("Inyectando " + cantidad + " ml. de Gasoil");
        }

        public void ConsumirCombustible()
        {
            RealizarExplosion();
        }

        #endregion

        private void RealizarExplosion()
        {
            Console.WriteLine("Realizada la explosión del Gasoil");
        }
    }

Gasolina


    // ImplementorConcretoB
    public class Gasolina : IMotor
    {
        #region IMotor Members

        public void InyectarCombustible(double cantidad)
        {
            Console.WriteLine("Inyectando " + cantidad + " ml. de Gasolina");
        }

        public void ConsumirCombustible()
        {
            RealizarCombustion();
        }

        #endregion

        private void RealizarCombustion()
        {
            Console.WriteLine("Realizada la combustión de la Gasolina");
        }
    }

Con estas tres clases ya habríamos desarrollado el subárbol izquierdo del diagrama UML que mostramos al comienzo del artículo: la interfaz Implementor junto a sus implementaciones:

La siguiente parte será encapsular la interfaz dentro de nuestra abstracción Vehiculo, que dispondrá de una referencia a IMotor y de un método que hará uso de los métodos de nuestra interfaz, encapsulando su funcionalidad tal y como hacíamos en el patrón Adapter:

Vehiculo


    // Abstracción
    public abstract class Vehiculo
    {
        private IMotor motor;

        public Vehiculo(IMotor motor)
        {
            this.motor = motor;
        }

        // Encapsulamos la funcionalidad de la interfaz IMotor
        public void Acelerar(double combustible)
        {
            motor.InyectarCombustible(combustible);
            motor.ConsumirCombustible();
        }

        public void Frenar()
        {
            Console.WriteLine("El vehículo está frenando.");
        }

        // Método abstracto
        public abstract void MostrarCaracteristicas();
    }

Como venimos observando a lo largo de los últimos patrones, el objeto que implementará el motor es inyectado en el constructor, siguiendo el quinto de los principios SOLID: principio de inversión de dependencias. De este modo desacoplamos aún más la abstracción de la interfaz. Además, nuestra clase abstracta dispondrá de otros métodos que no estén necesariamente relacionados con el puente, como por ejemplo Frenar(). También incorporaremos un método abstracto que deberá ser implementado por cada una de las clases derivadas.

Por último, codificaremos la evolución de nuestra abstracción, que se corresponderá con RefinamientoAbstracciónA y RefinamientoAbstracciónB, representadas por dos tipos de vehículos: Berlina y Monovolumen.

Berlina


    // RefinamientoAbstraccionA
    public class Berlina : Vehiculo
    {
        // Atributo propio
        private int capacidadMaletero;

        // La implementacion de los vehículos se desarrolla de forma independiente
        public Berlina(IMotor motor, int capacidadMaletero) : base(motor)
        {
            this.capacidadMaletero = capacidadMaletero;
        }

        // Implementación del método abstracto
        public override void MostrarCaracteristicas()
        {
            Console.WriteLine("Vehiculo de tipo Berlina con un maletero con una capacidad de " + 
                capacidadMaletero + " litros.");
        }
    }

La clase refinada evoluciona de modo que incorpore sus propios métodos, atributos y constructores. En este caso, un atributo de tipo entero que definirá la capacidad en litros del maletero, atributo que no dispondrá, por ejemplo, su clase hermana Monovolumen.

Monovolumen


    public class Monovolumen : Vehiculo
    {
        // Atributo propio
        private bool puertaCorrediza;

        // La implementacion de los vehículos se desarrolla de forma independiente
        public Monovolumen(IMotor motor, bool puertaCorrediza)
            : base(motor)
        {
            this.puertaCorrediza = puertaCorrediza;
        }

        // Implementación del método abstracto
        public override void MostrarCaracteristicas()
        {
            Console.WriteLine("Vehiculo de tipo Berlina " + (puertaCorrediza ? "con" : "sin") + 
                " puerta corrediza.");
        }
    }

En su lugar, Monovolumen dispondrá de un atributo puertaCorrediza que indicará si la puerta trasera será corrediza o no.

Como vemos, cada refinamiento se desarrollará en una dirección, pero ambos elementos podrán seguir haciendo uso del motor por medio de los métodos codificados en la clase abstracta Vehiculo. La implementación del método abstracto MostrarCaracteristicas también será diferente. Por lo tanto, si invocamos este método, toda la funcionalidad contenida en él será parte dependiente de la propia abstracción (Vehiculo).


            IMotor motorDiesel = new Diesel();
            Vehiculo berlina = new Berlina(motorDiesel, 4);

            berlina.MostrarCaracteristicas();

Si, por el contrario, necesitáramos hacer uso de funcionalidad no dependiente del chasis del vehículo, deberíamos «cruzar el puente» y solicitarle la información (o la funcionalidad, en este caso) al motor. O más concretamente, a la implementación del motor, que será independiente del vehículo.


            IMotor motorDiesel = new Diesel();
            Vehiculo berlina = new Berlina(motorDiesel, 4);

            berlina.MostrarCaracteristicas();
            berlina.Acelerar(2.4d);

La invocación de Acelerar invocará, por tanto, los métodos InyectarCombustible y ConsumirCombustible que implementa la clase Diesel y expone la interfaz IMotor.

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

Un ejemplo típico de un patrón Bridge lo puede conformar cualquier familia de drivers de un dispositivo, tal y como vimos en el primer ejemplo.

Otro ejemplo típico suele ser el de las APIs de dibujo. Los elementos genéricos, tales como formas y figuras serían las abstracciones (por ejemplo, Forma sería el elemento Abstraction del que derivarían abstracciones refinadas como Circulo o Cuadrado), mientras que la parte «dependiente» del sistema sería la API concreta que se encargaría de dibujar en pantalla las formas genéricas definidas en la abstracción. Este funcionamiento puede observarse en los paquetes de java java.awt y java.awt.peer. (en Button y List, por ejemplo).

Las situaciones óptimas en los que se debe utilizar este patrón serán, por tanto:

  • Cuando se desea evitar un enlace permanente entre la abstracción y (toda o parte de) su implementación.
  • Cuando los cambios en la implementación de una abstracción no debe afectar a las clases que hace uso de ella.
  • Cuando se desea compartir una implementación entre múltiples objetos.

Fuentes:

7 comentarios

  1. Hola Daniel,

    La idea ha quedada clara como el agua. Y, efectivamente, veo ciertas similitudes con el patrón Adapter, pero, se pueden combinar los patrones?.

    Por ejemplo aplicar un patrón Adapter y sobre este un patrón Bridge?.

    Gracias.

    1. Los patrones pueden combinarse de cualquier forma. Pueden modificarse, adaptarse o no utilizarse en absoluto: dependerá de la situación de diseño concreta. Es más, abusar de ellos lo único que hace es aumentar la complejidad del código sin aportar nada positivo. De hecho, un error común al empezar a utilizarlos es ver patrones en todos lados.

      Como experiencia personal, cuando comencé a utilizarlos diseñé un programa que tenía que conectarse a un servicio web, realizar unas transformaciones y posteriormente ejecutar un par de accesos a base de datos. Lo que no debían de ser más de seis o siete clases se dispararon hasta las diecinueve clases y nueve interfaces. Y lo peor de todo: mis compañeros no entendían ni papa de lo que dentro de ese engendro se cocía.

      A medida que se adquiere experiencia utilizándolos, el propio diseño comienza a «pedirlos» (de ahí la subsección que siempre añado de «cuándo utilizar este patrón»). Es mejor realizar un diseño convencional y refactorizar una parte a través de un patrón que tener preparado el patrón en la mano y estar esperando a ver dónde lo podemos aplicar. Como dice el refrán, «cuando se tiene un martillo, todo parece un clavo».

      Centrándonos en tu pregunta, Adapter y Bridge tratan de solucionar problemas distintos. El caso que comentas es posible, pero si te he entendido bien, desde mi punto de vista un patrón Adapter debe ser simple y limitarse a realizar la transformación entre la interfaz de la clase cliente y la interfaz de la clase adaptada (a lo sumo, realizar también alguna transformación de datos). Añadir otra abstracción más al adaptador, que a su vez incorporará una referencia a una interfaz que a su vez será implementada por un conjunto de clases me parece abusar de la paciencia del equipo de desarrollo que tenga que mantener ese código. Además ¿evolucionará tu adaptador o únicamente servirá para ese caso concreto?

      En resumen: ¿Posible? ¡claro! ¿Aconsejable? Únicamente si la situación de diseño así lo precisa.

      Nuevamente, gracias por tu visita, José Miguel 😉

  2. La mejor explicación que he leído hasta ahora se este patrón. Por fin leo una definición decente de Bridge y lo he entendido todo. ¡Muchas gracias!

Deja una respuesta

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. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s