Design Patterns - Bridge

Propósito

El patrón Bridge tiene como objetivo desacoplar una abstracción de su implementación, de manera que ambas puedan variar de forma independiente.

Lo vuelve más flexible, cambia la herencia por composición.

Problema

El problema de tener una estructura rígida, donde la abstracción y su implementación están muy acopladas, lo que limita la flexibilidad y la capacidad de extensión porque cualquier cambio en la implementación puede requerir cambiar la abstracción, y viceversa. 

Solución

La solución que propone el Bridge es:

  • Separar Abstracciones e Implementaciones: Define dos jerarquías de clases separadas, una para las abstracciones y otra para las implementaciones.
  • Puente entre Abstracción e Implementación: Usa composición (en lugar de herencia) para conectar las abstracciones con sus implementaciones.

Estructura

bridge

Participantes

  • Abstraction: Define la interfaz de alto nivel y mantiene una referencia a un objeto de tipo Implementor.
  • RefinedAbstraction: Extiende o refina la interfaz definida por Abstraction.
  • Implementor: Define la interfaz para las implementaciones de clase.
  • ConcreteImplementor: Implementa la interfaz de Implementor.

Cuándo Usarlo

Este patrón es recomendable cuando:

  • queremos evitar un enlace permanente entre la abstracción y su implementación.
  •  tanto las abstracciones como sus implementaciones deben ser extensibles mediante subclases.
  • los cambios en la implementación de una abstracción no deben afectar a los clientes, es decir, no deben recompilar.

Ventajas

Principio de separación de intereses: Separa los aspectos de alto nivel (abstracciones) de los detalles de bajo nivel (implementaciones). La implementación puede configurarse en tiempo de ejecución. Cambiar una clase ya no requiere recompilar la abstracción y sus clientes.

Mejora la extensibilidad: Las jerarquías de abstracción e implementador se pueden extender de manera independiente.

Oculta detalles de implementación a los clientes: Podemos aislar a los clientes de los detalles de implementación.

Desventajas

Complejidad Aumentada: Introduce una capa adicional de abstracción, lo que puede complicar el diseño y entender el código.

Costo de Performance: La adición de una capa de abstracción puede resultar en una pequeña penalización en el rendimiento, sobre todo en sistemas muy críticos.

Ejemplo: Sistema para dibujar formas

Estamos desarrollando un software avanzado de dibujo, que debe ofrecer flexibilidad en la forma en que se dibujan distintas figuras geométricas.

El desafío es que cada figura geométrica (como círculos, rectángulos, etc.) puede ser representada de múltiples maneras, por ejemplo, renderizada en una pantalla o impresa en papel.

Además, es probable que en el futuro se agreguen más formas y métodos de representación.

Problema

Por un lado tenemos formas geométricas y por otro lado métodos de dibujo o representación.

Si codificamos cada combinación de forma geométrica y método de dibujo en las clases de las formas geométricas, vamos a tener un código rígido y difícil de mantener. Agregar un nuevo método de dibujo requeriría modificar todas las clases de las formas geométricas.

Esto va en contra de los principios de diseño de software como la modularidad y la escalabilidad.

Solución planteada

Para solucionarlo vamos a usar un Bridge. Este patrón nos permite separar la abstracción (las formas geométricas en sí) de su implementación (cómo se dibujan estas formas). Al hacerlo esto podremos variar y extender tanto las formas como sus métodos de dibujo de manera independiente.

La Abstracción, las Formas Geométricas, son las representaciones de alto nivel de las formas que queremos dibujar, como círculos o rectángulos.

La Implementación, los Métodos de Dibujo, son los distintos modos en que se pueden representar las formas, como en pantalla o en papel.

Definimos una interfaz para los métodos de dibujo (DrawAPI) y creamos dos implementaciones concretas para diferentes entornos (en pantalla y en papel).

Creamos una clase abstracta para las formas geométricas (Shape) y generamos dos implementaciones concretas para formas específicas (Circle y Rectangle), las cuales usan un objeto DrawAPI para realizar el dibujo.

En el cliente (Client), instanciamos nuestras formas concretas indicando la implementación deseada para el método de dibujo.

bridge

Código Java

Codificamos en Java lo que preparamos en el diagrama.

Definimos la Interfaz de Implementación (Métodos de Dibujo):


   // Define la interfaz de implementación para los métodos de dibujo
   interface DrawAPI {
         void drawCircle(double x, double y, double radius);
         void drawRectangle(double x, double y, double width, double height);
   }

Implementamos las clases de implementación concretas:


   // Implementación concreta para dibujar en pantalla
   class DrawOnScreen implements DrawAPI {
         public void drawCircle(double x, double y, double radius) {
            System.out.println("Dibujo círculo en pantalla con radio:" + radius);
         }
   
         public void drawRectangle(double x, double y, double width, double height) {
            System.out.println("Dibujo rectángulo en pantalla:" + width + " y " + height);
         }
   }
   
   // Implementación concreta para dibujar en papel
   class DrawOnPaper implements DrawAPI {
         public void drawCircle(double x, double y, double radius) {
            System.out.println("Dibujo círculo en papel en con radio:" + radius);
         }
   
         public void drawRectangle(double x, double y, double width, double height) {
            System.out.println("Dibujo rectángulo en papel:" + width + " y " + height);
         }
   }
            

Creamos la abstracción que representa las figuras geométricas:


   // Abstracción de la forma geométrica
   abstract class Shape {
      protected DrawAPI drawAPI;
   
      protected Shape(DrawAPI drawAPI) {
            this.drawAPI = drawAPI;
      }
   
      public abstract void draw();
   }
            

Implementamos abstracciones refinadas, cada forma geométrica concreta:

// Implementación concreta de la forma Círculo
   class Circle extends Shape {
      private double x, y, radius;
   
      public Circle(double x, double y, double radius, DrawAPI drawAPI) {
            super(drawAPI);
            this.x = x;
            this.y = y;
            this.radius = radius;
      }
   
      public void draw() {
            drawAPI.drawCircle(x, y, radius);
      }
   }
   
   // Implementación concreta de la forma Rectángulo
   class Rectangle extends Shape {
      private double x, y, width, height;
   
      public Rectangle(double x, double y, double width, double height, DrawAPI drawAPI) {
            super(drawAPI);
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
      }
   
      public void draw() {
            drawAPI.drawRectangle(x, y, width, height);
      }
   }

El cliente usa las abstracciones y las Implementaciones, las formas geómetricas y los métodos de dibujo:


   public class Client {
      public static void main(String[] args) {
            Shape circle = new Circle(100, 100, 10, new DrawOnScreen());
            circle.draw();
   
            Shape rectangle = new Rectangle(200, 200, 50, 30, new DrawOnPaper());
            rectangle.draw();
      }
   }

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: Implementor, ConcreteImplementor, Abstraction, RefinedAbstraction, Client:

  • DrawAPI (Implementor): Interfaz que define los métodos de dibujo.
  • DrawOnScreen, DrawOnPaper(ConcreteImplementor): Implementaciones concretas de DrawAPI, brinda los métodos específicos para dibujar en pantalla y en papel.
  • Shape(Abstraction): Clase abstracta para representar a las formas geómetricas que contiene una referencia a DrawAPI.
  • Circle, Rectangle(RefinedAbstraction): Abstracciones refinadas que extienden Shape y definen formas específicas.
  • Client (Client): Usa las abstracciones y las implementaciones.

Conclusiones

Vimos un ejemplo del patrón Bridge, nuestro software de dibujo se vuelve más flexible y fácil de mantener, ya que podemos extender nuestra gama de formas y métodos de dibujo de manera independiente, algo fundamental para un sistema que espera crecer a lo largo del tiempo.

Patrones relacionados

  • Abstract Factory

Puede crear y configurar un bridge.

  • Adapter

 Cambia la interfaz de una clase existente, mientras que Bridge separa la interfaz de la implementación.